diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3015169..1c3dbf7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,10 @@ name: test on: - push: - branches: - - master - pull_request: + push: + branches: + - master + pull_request: env: FOUNDRY_PROFILE: ci @@ -27,7 +27,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9 + version: 9 - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 diff --git a/.gitignore b/.gitignore index eb560657..ab00af0a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ cache/ out/ crates/**/target/ +# Invariant testing +echidna/ +crytic-export/ + # Ignores development broadcast logs broadcast/ diff --git a/.gitmodules b/.gitmodules index e69de29b..59285366 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/baseline-v2"] + path = lib/baseline-v2 + url = git@github.com:0xBaseline/baseline-v2.git diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e2b51cc5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +# Don't rewrite pnpm lockfile +pnpm-lock.yaml + +# Ignore artifacts: +build +coverage + +# also includes what is in .gitignore + +lib/**/* +dependencies/**/* + +test/lib/uniswap-v2/**/* +test/lib/uniswap-v3/**/* + +src/callbacks/liquidity/BaselineV2/lib/**/* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/.solhint.json b/.solhint.json index 911f8b30..f8787f3b 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,10 +1,7 @@ { "extends": "solhint:recommended", "rules": { - "compiler-version": [ - "error", - ">=0.7.0" - ], + "compiler-version": ["error", ">=0.7.0"], "avoid-low-level-calls": "off", "const-name-snakecase": "warn", "var-name-mixedcase": "warn", @@ -30,4 +27,4 @@ "reentrancy": "error", "state-visibility": "warn" } -} \ No newline at end of file +} diff --git a/.solhintignore b/.solhintignore index afa64c5b..28ebd765 100644 --- a/.solhintignore +++ b/.solhintignore @@ -1,7 +1,7 @@ -lib/** -dependencies/** +lib/**/* +dependencies/**/* -test/lib/uniswap-v2/** -test/lib/uniswap-v3/** +test/lib/uniswap-v2/**/* +test/lib/uniswap-v3/**/* -src/callbacks/liquidity/BaselineV2/lib/** +src/callbacks/liquidity/BaselineV2/lib/**/* diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d5fa10d5..f4f147b1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,4 +6,4 @@ "nomicfoundation.hardhat-solidity", "tamasfe.even-better-toml" ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 76ea6e90..ff9dbd20 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,20 +1,14 @@ { - "cSpell.words": [ - "ECIES", - "IPFS", - "keypairs" - ], + "cSpell.words": ["ECIES", "IPFS", "keypairs"], "[json]": { "editor.tabSize": 2 }, "json.schemas": [ { - "fileMatch": [ - "/script/deploy/sequences/*.json" - ], + "fileMatch": ["/script/deploy/sequences/*.json"], "url": "/script/deploy/sequence_schema.json" } ], "solidity.packageDefaultDependenciesContractsDirectory": "src", "solidity.packageDefaultDependenciesDirectory": "lib" -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d502fa49..7edd271c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Changelog -## 1.???.0 +## 1.0.0 -- Introduces a direct-to-liquidity callbacks contract to deploy auction proceeds into a Baseline pool (Axis-Fi/axis-core#156) +- Introduces direct-to-liquidity callbacks. Multi-use, permissionless versions are available for UniswapV2 and UniswapV3 pools. Permissioned launches are available for Baseline pools on Blast. +- Deployed to blast, base, arbitrum-one, mantle, and mode mainnets and testnets. ## 0.9.0 diff --git a/README.md b/README.md index 6555febb..8cfbe85b 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ When updating the version of a dependency provided through soldeer, the followin 1. Update the version of the dependency in `foundry.toml` or through `forge soldeer` 2. Re-run the [installation script](#first-run) 3. If the version number has changed: - - Change the existing entry in [remappings.txt](remappings.txt) to point to the new dependency version - - Update imports to use the new remapping + - Change the existing entry in [remappings.txt](remappings.txt) to point to the new dependency version + - Update imports to use the new remapping #### Updating axis-core diff --git a/deployments/.arbitrum-one-v0.9.0.json b/deployments/.arbitrum-one-v0.9.0.json new file mode 100644 index 00000000..1fef61fe --- /dev/null +++ b/deployments/.arbitrum-one-v0.9.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x9826F771e56Fc6623C8D47D63416F809Aa454D56", + "deployments.callbacks.BatchMerkleAllowlist": "0x98A30139c73B3DF755082b8790D3253674cC9DC2", + "deployments.callbacks.BatchTokenAllowlist": "0x980Ce05E482aB873C1E38725a5dE22F206afF862", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98836F2727E3Fe3c0067568d51ae60297525015f", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE6558832167221bcC80576BeA1dED4B5969185Ff", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE6E58B6D836725B9Df30054F2FC6EE84c6DE6886" +} diff --git a/deployments/.arbitrum-one-v1.0.0.json b/deployments/.arbitrum-one-v1.0.0.json new file mode 100644 index 00000000..7e351ce5 --- /dev/null +++ b/deployments/.arbitrum-one-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "deployments.callbacks.BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "deployments.callbacks.BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE67cB70883fBBf4BDAe501e73E9dC5E881E53452", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE6C7C76D480075658789f6a0ed87771a7179E0b3" +} diff --git a/deployments/.arbitrum-sepolia-v1.0.0.json b/deployments/.arbitrum-sepolia-v1.0.0.json new file mode 100644 index 00000000..1d9e32fc --- /dev/null +++ b/deployments/.arbitrum-sepolia-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x9859AcCA8a9afEbb9b3986036d4E0efc0246cEeA", + "deployments.callbacks.BatchMerkleAllowlist": "0x98d64E00D9d6550913E73C940Ff476Cf1723d834", + "deployments.callbacks.BatchTokenAllowlist": "0x9801e45362a2bb7C9F22486CC3F5cA9224e9CC55", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98C8ffFf24bcfC3A5B0b463c43F10932Cedb7B8F", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE63Ea32d7D1BF1cfe10801E6B7Aa7f3E6d21f2Cd", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE603A98566BA6ca4C898a759Ef13c7E6A7A26f4A" +} diff --git a/deployments/.base-sepolia-v1.0.0.json b/deployments/.base-sepolia-v1.0.0.json new file mode 100644 index 00000000..bd41557c --- /dev/null +++ b/deployments/.base-sepolia-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x9859AcCA8a9afEbb9b3986036d4E0efc0246cEeA", + "deployments.callbacks.BatchMerkleAllowlist": "0x98d64E00D9d6550913E73C940Ff476Cf1723d834", + "deployments.callbacks.BatchTokenAllowlist": "0x9801e45362a2bb7C9F22486CC3F5cA9224e9CC55", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98C8ffFf24bcfC3A5B0b463c43F10932Cedb7B8F", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE6546c03B1b9DFC4238f0A2923FdefD5E4af7659", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE68b21C071534781BC4c40E6BF1bCFC23638fF4B" +} diff --git a/deployments/.base-v0.9.0.json b/deployments/.base-v0.9.0.json new file mode 100644 index 00000000..2100d757 --- /dev/null +++ b/deployments/.base-v0.9.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x98B85ded86493cf5Fa058956447C5d19A1e1Fca8", + "deployments.callbacks.BatchMerkleAllowlist": "0x980EE91db19Dff91f95FFA9CB0825f2A028DF34A", + "deployments.callbacks.BatchTokenAllowlist": "0x980bFd44358F06562521aFD68DeE7160eaE66a88", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98c4648021C12a5b44C8549f71A293532533c3b9", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE6731E192421CA4197EAFC682220D3189c64fde0", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE68D9DeCC2F3a273f31C5a68F3a5715785e5307F" +} diff --git a/deployments/.base-v1.0.0.json b/deployments/.base-v1.0.0.json new file mode 100644 index 00000000..29f5d9f1 --- /dev/null +++ b/deployments/.base-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "deployments.callbacks.BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "deployments.callbacks.BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE6F93df14cB554737A26acd2aB5fEf649921D7F2", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE64d6e058dD5F76CCc8566c07b994090a24CCB75" +} diff --git a/deployments/.blast-sepolia-v1.0.0.json b/deployments/.blast-sepolia-v1.0.0.json new file mode 100644 index 00000000..5930015f --- /dev/null +++ b/deployments/.blast-sepolia-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x98Fa90B451171389a2132191a95a0d4109C16B36", + "deployments.callbacks.BatchMerkleAllowlist": "0x98bd3C7ddF2510553A858e3Fb636299BDD61992b", + "deployments.callbacks.BatchTokenAllowlist": "0x983df377a8c64F26f947312374e6859ebc00dA81", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98e37312E0Bdb9012Eb1F6b70fe5b1cB82AC07dc", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE6BFCC5C3f3e71b5A7a9F4c0C8952f084d649850", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE6531D363c29A453c2589D9CDEFcC02773f8ee95" +} diff --git a/deployments/.blast-v0.9.0.json b/deployments/.blast-v0.9.0.json new file mode 100644 index 00000000..a16fcee3 --- /dev/null +++ b/deployments/.blast-v0.9.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x986B25Cd77B5A9175eD40f26D3acE0bf6C7547Fe", + "deployments.callbacks.BatchMerkleAllowlist": "0x982FdCD97dcFb433977820814aa8D86Ef0dC320d", + "deployments.callbacks.BatchTokenAllowlist": "0x983155c5e8E08700D4aeda7b0A3417EA33a47918", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x984070D753B0b9e0544fC66b13EB9722d7310e97", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE6ad77d3637847C787369c53A37c3b41ee188cf5", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE6AA1A3001D65DbEDaA8Dc795866Edbe0613Db23" +} diff --git a/deployments/.blast-v1.0.0.json b/deployments/.blast-v1.0.0.json new file mode 100644 index 00000000..5746163f --- /dev/null +++ b/deployments/.blast-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x980997561d92D30Cae5bae01CCdfc6923E0c54a6", + "deployments.callbacks.BatchMerkleAllowlist": "0x9817F12e9461e9D68b0ecB7ff2e8E9da5cf0959C", + "deployments.callbacks.BatchTokenAllowlist": "0x989Dc1DAa66139ed26361cc3A24CdA372370a7dA", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x986827E5FC7ECE5704449aAA3B4A4b16b417b8e7", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE6512c5F23403585916C936a194D63880DAd7EDF", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE6B27B702DB07a3a7b6d5572f9fA5dc7F201a439" +} diff --git a/deployments/.mantle-sepolia-v1.0.0.json b/deployments/.mantle-sepolia-v1.0.0.json new file mode 100644 index 00000000..e8aef19a --- /dev/null +++ b/deployments/.mantle-sepolia-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x98a0826b19B412a159cedb38Bd38899930382972", + "deployments.callbacks.BatchMerkleAllowlist": "0x98E56d6466fC7B2c88acb39e9e4C6E7671e28CBd", + "deployments.callbacks.BatchTokenAllowlist": "0x98a16dF00DB1ea2bC4Cb05E8e2c06d43F56f8e62", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98b7402E399ff864a3277bd4c3e01Df8ef0234e8", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE60a178a2f5e86BF77fB1D6814Ed47790B0993f0", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE6bF04764268B46ddE10e9e8a13c3E7fF0a181d6" +} diff --git a/deployments/.mantle-v1.0.0.json b/deployments/.mantle-v1.0.0.json new file mode 100644 index 00000000..1d0e3e35 --- /dev/null +++ b/deployments/.mantle-v1.0.0.json @@ -0,0 +1,6 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "deployments.callbacks.BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "deployments.callbacks.BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69" +} diff --git a/deployments/.mode-sepolia-v1.0.0.json b/deployments/.mode-sepolia-v1.0.0.json new file mode 100644 index 00000000..ceeac89e --- /dev/null +++ b/deployments/.mode-sepolia-v1.0.0.json @@ -0,0 +1,8 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x98a0826b19B412a159cedb38Bd38899930382972", + "deployments.callbacks.BatchMerkleAllowlist": "0x98E56d6466fC7B2c88acb39e9e4C6E7671e28CBd", + "deployments.callbacks.BatchTokenAllowlist": "0x98a16dF00DB1ea2bC4Cb05E8e2c06d43F56f8e62", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98b7402E399ff864a3277bd4c3e01Df8ef0234e8", + "deployments.callbacks.BatchUniswapV2DirectToLiquidity": "0xE60c0BFa8DE1250eFDF46d80F98AE5eAe947e5E2", + "deployments.callbacks.BatchUniswapV3DirectToLiquidity": "0xE68d298ef59B3ddFc82d12aFC9B8Db67ac068266" +} diff --git a/deployments/.mode-v1.0.0.json b/deployments/.mode-v1.0.0.json new file mode 100644 index 00000000..1d0e3e35 --- /dev/null +++ b/deployments/.mode-v1.0.0.json @@ -0,0 +1,6 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "deployments.callbacks.BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "deployments.callbacks.BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69" +} diff --git a/foundry.toml b/foundry.toml index cf2432e8..ebf9cf68 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ fs_permissions = [ {access = "read-write", path = "./bytecode/"}, {access = "read", path = "./script/"}, {access = "read-write", path = "./deployments/"}, - {access = "read", path = "./dependencies/axis-core-1.0.0/script/env.json"}, + {access = "read", path = "./dependencies/axis-core-1.0.1/script/env.json"}, ] ffi = true solc_version = "0.8.19" @@ -26,21 +26,25 @@ quote_style = "double" number_underscore = "thousands" wrap_comments = false ignore = [ - "lib/**", - "src/lib/**", + "lib/**/*", + "dependencies/**/*", + "src/lib/**/*", + "test/lib/uniswap-v2/**/*", + "test/lib/uniswap-v3/**/*", ] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options [dependencies] forge-std = { version = "1.9.1" } -axis-core = { version = "1.0.0" } +axis-core = { version = "1.0.1" } "@openzeppelin-contracts" = { version = "4.9.2" } "@openzeppelin-contracts-upgradeable" = { version = "4.9.2" } "@uniswap-v2-core" = { version = "1.0.1" } "@uniswap-v3-core" = { version = "1.0.1-solc-0.8-simulate" } g-uni-v1-core = { version = "0.9.9", git = "git@github.com:Axis-Fi/g-uni-v1-core.git", rev = "d6bcb6e811e86d36bc836c002eb2e9a2c73d29ca" } "@uniswap-v2-periphery" = { version = "1.0.1", git = "git@github.com:Axis-Fi/uniswap-v2-periphery.git", rev = "19be650786731dfe43cac3aac7a2d1f0731d18e2" } +"@uniswap-v3-periphery" = { version = "1.4.2-solc-0.8", git = "git@github.com:Uniswap/v3-periphery.git", rev = "b325bb0905d922ae61fcc7df85ee802e8df5e96c" } solmate = { version = "6.7.0", git = "git@github.com:transmissions11/solmate.git", rev = "c892309933b25c03d32b1b0d674df7ae292ba925" } clones-with-immutable-args = { version = "1.1.1", git = "git@github.com:wighawag/clones-with-immutable-args.git", rev = "f5ca191afea933d50a36d101009b5644dc28bc99" } solady = { version = "0.0.124" } diff --git a/lib/baseline-v2 b/lib/baseline-v2 new file mode 160000 index 00000000..60bed78b --- /dev/null +++ b/lib/baseline-v2 @@ -0,0 +1 @@ +Subproject commit 60bed78b7bee28016321ddd8c590df6c61bae6e9 diff --git a/package.json b/package.json index ba347ac6..3a03fc60 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,16 @@ "scripts": { "build": "forge build", "deploy": "./script/deploy/deploy.sh", - "fmt:check": "forge fmt --check", - "fmt": "forge fmt", - "lint:all": "pnpm run fmt && pnpm run solhint:all", + "fmt:check": "forge fmt --check && prettier . --check", + "fmt": "forge fmt && prettier . --write", "lint:check": "pnpm run fmt:check && pnpm run solhint:check", "lint": "pnpm run fmt && pnpm run solhint", "postinstall": "./script/install.sh", "publish": "./script/publish.sh", "salts": "./script/salts/write_salt.sh", "size": "forge clean && forge build --sizes --skip test --skip '*/Mock*.sol'", - "solhint:all": "solhint --fix --config ./.solhint.json 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", - "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol'", - "solhint": "solhint --fix --config ./.solhint.json 'src/**/*.sol'", + "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", + "solhint": "solhint --fix --config ./.solhint.json 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", "test": "forge test --nmt largeNumberOf -vvv" }, "keywords": [], @@ -29,5 +27,8 @@ "license": "MIT", "dependencies": { "solhint-community": "^3.7.0" + }, + "devDependencies": { + "prettier": "3.3.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd2147f6..8ce809b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,10 @@ importers: solhint-community: specifier: ^3.7.0 version: 3.7.0 + devDependencies: + prettier: + specifier: 3.3.3 + version: 3.3.3 packages: @@ -224,6 +228,11 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -465,6 +474,8 @@ snapshots: prettier@2.8.8: optional: true + prettier@3.3.3: {} + punycode@2.3.1: {} require-from-string@2.0.2: {} diff --git a/remappings.txt b/remappings.txt index 5a87e55f..ba0a6edd 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,7 +1,7 @@ @forge-std-1.9.1=dependencies/forge-std-1.9.1/src -@axis-core-1.0.0=dependencies/axis-core-1.0.0/src -@axis-core-1.0.0-script=dependencies/axis-core-1.0.0/script -@axis-core-1.0.0-test=dependencies/axis-core-1.0.0/test +@axis-core-1.0.1=dependencies/axis-core-1.0.1/src +@axis-core-1.0.1-script=dependencies/axis-core-1.0.1/script +@axis-core-1.0.1-test=dependencies/axis-core-1.0.1/test @solmate-6.7.0=dependencies/solmate-6.7.0/src @clones-with-immutable-args-1.1.1=dependencies/clones-with-immutable-args-1.1.1/src @openzeppelin-contracts-4.9.2=dependencies/@openzeppelin-contracts-4.9.2 @@ -9,9 +9,11 @@ @uniswap-v2-core-1.0.1=dependencies/@uniswap-v2-core-1.0.1/contracts @uniswap-v2-periphery-1.0.1=dependencies/@uniswap-v2-periphery-1.0.1/contracts @uniswap-v3-core-1.0.1-solc-0.8-simulate=dependencies/@uniswap-v3-core-1.0.1-solc-0.8-simulate/contracts +@uniswap-v3-periphery-1.4.2-solc-0.8=dependencies/@uniswap-v3-periphery-1.4.2-solc-0.8/contracts @g-uni-v1-core-0.9.9=dependencies/g-uni-v1-core-0.9.9/contracts @uniswap/v2-core/contracts=dependencies/@uniswap-v2-core-1.0.1/contracts @uniswap/v3-core/contracts=dependencies/@uniswap-v3-core-1.0.1-solc-0.8-simulate/contracts @openzeppelin/contracts=dependencies/@openzeppelin-contracts-4.9.2 @openzeppelin/contracts-upgradeable=dependencies/@openzeppelin-contracts-upgradeable-4.9.2 -@solady-0.0.124=dependencies/solady-0.0.124/src \ No newline at end of file +@solady-0.0.124=dependencies/solady-0.0.124/src +@baseline=lib/baseline-v2/src \ No newline at end of file diff --git a/script/deploy/Deploy.s.sol b/script/deploy/Deploy.s.sol index b6a81242..f05a29e9 100644 --- a/script/deploy/Deploy.s.sol +++ b/script/deploy/Deploy.s.sol @@ -4,16 +4,20 @@ pragma solidity 0.8.19; // Scripting libraries import {Script, console2} from "@forge-std-1.9.1/Script.sol"; import {stdJson} from "@forge-std-1.9.1/StdJson.sol"; -import {WithEnvironment} from "./WithEnvironment.s.sol"; import {WithSalts} from "../salts/WithSalts.s.sol"; +import {WithDeploySequence} from "./WithDeploySequence.s.sol"; // axis-core -import {Keycode, keycodeFromVeecode} from "@axis-core-1.0.0/modules/Keycode.sol"; -import {Module} from "@axis-core-1.0.0/modules/Modules.sol"; -import {AtomicAuctionHouse} from "@axis-core-1.0.0/AtomicAuctionHouse.sol"; -import {BatchAuctionHouse} from "@axis-core-1.0.0/BatchAuctionHouse.sol"; -import {IFeeManager} from "@axis-core-1.0.0/interfaces/IFeeManager.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {Keycode, keycodeFromVeecode} from "@axis-core-1.0.1/modules/Keycode.sol"; +import {Module} from "@axis-core-1.0.1/modules/Modules.sol"; +import {AtomicAuctionHouse} from "@axis-core-1.0.1/AtomicAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; +import {IFeeManager} from "@axis-core-1.0.1/interfaces/IFeeManager.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; + +// Uniswap +import {IUniswapV2Router02} from "@uniswap-v2-periphery-1.0.1/interfaces/IUniswapV2Router02.sol"; +import {GUniFactory} from "@g-uni-v1-core-0.9.9/GUniFactory.sol"; // Callbacks import {UniswapV2DirectToLiquidity} from "../../src/callbacks/liquidity/UniswapV2DTL.sol"; @@ -22,6 +26,7 @@ import {CappedMerkleAllowlist} from "../../src/callbacks/allowlists/CappedMerkle import {MerkleAllowlist} from "../../src/callbacks/allowlists/MerkleAllowlist.sol"; import {TokenAllowlist} from "../../src/callbacks/allowlists/TokenAllowlist.sol"; import {AllocatedMerkleAllowlist} from "../../src/callbacks/allowlists/AllocatedMerkleAllowlist.sol"; +import {BaselineAxisLaunch} from "../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; import {BALwithAllowlist} from "../../src/callbacks/liquidity/BaselineV2/BALwithAllowlist.sol"; import {BALwithAllocatedAllowlist} from "../../src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol"; @@ -38,7 +43,7 @@ import { /// @notice Declarative deployment script that reads a deployment sequence (with constructor args) /// and a configured environment file to deploy and install contracts in the Axis protocol. -contract Deploy is Script, WithEnvironment, WithSalts { +contract Deploy is Script, WithDeploySequence, WithSalts { using stdJson for string; string internal constant _PREFIX_DEPLOYMENT_ROOT = "deployments"; @@ -64,33 +69,18 @@ contract Deploy is Script, WithEnvironment, WithSalts { // ========== DEPLOY SYSTEM FUNCTIONS ========== // function _setUp(string calldata chain_, string calldata deployFilePath_) internal virtual { - _loadEnv(chain_); - - // Load deployment data - string memory data = vm.readFile(deployFilePath_); + _loadSequence(chain_, deployFilePath_); - // Parse deployment sequence and names - bytes memory sequence = abi.decode(data.parseRaw(".sequence"), (bytes)); - uint256 len = sequence.length; - console2.log("Contracts to be deployed:", len); + // Get the sequence names + string[] memory sequenceNames = _getSequenceNames(); + console2.log("Contracts to be deployed:", sequenceNames.length); - if (len == 0) { - return; - } else if (len == 1) { - // Only one deployment - string memory name = abi.decode(data.parseRaw(".sequence..name"), (string)); - deployments.push(name); - - _configureDeployment(data, name); - } else { - // More than one deployment - string[] memory names = abi.decode(data.parseRaw(".sequence..name"), (string[])); - for (uint256 i = 0; i < len; i++) { - string memory name = names[i]; - deployments.push(name); + // Iterate through the sequence names and configure the deployments + for (uint256 i; i < sequenceNames.length; i++) { + string memory sequenceName = sequenceNames[i]; - _configureDeployment(data, name); - } + deployments.push(sequenceName); + _configureDeployment(_sequenceJson, sequenceName); } } @@ -110,19 +100,23 @@ contract Deploy is Script, WithEnvironment, WithSalts { for (uint256 i; i < len; i++) { // Get deploy deploy args from contract name string memory name = deployments[i]; - // e.g. a deployment named EncryptedMarginalPrice would require the following function: deployEncryptedMarginalPrice(bytes) - bytes4 selector = bytes4(keccak256(bytes(string.concat("deploy", name, "(bytes)")))); - bytes memory args = argsMap[name]; + // e.g. a deployment named EncryptedMarginalPrice would require the following function: deployEncryptedMarginalPrice(string memory) + bytes4 selector = bytes4(keccak256(bytes(string.concat("deploy", name, "(string)")))); + + console2.log(""); + console2.log("Deploying ", name); // Call the deploy function for the contract (bool success, bytes memory data) = - address(this).call(abi.encodeWithSelector(selector, args)); + address(this).call(abi.encodeWithSelector(selector, name)); require(success, string.concat("Failed to deploy ", deployments[i])); // Store the deployed contract address for logging - (address deploymentAddress, string memory keyPrefix) = - abi.decode(data, (address, string)); - string memory deployedToKey = string.concat(keyPrefix, ".", name); + (address deploymentAddress, string memory keyPrefix, string memory deploymentKey) = + abi.decode(data, (address, string, string)); + // e.g. "callbacks.EncryptedMarginalPrice" + // The deployment functions allow the deployment key to be overridden by the sequence or arguments + string memory deployedToKey = string.concat(keyPrefix, ".", deploymentKey); deployedToKeys.push(deployedToKey); deployedTo[deployedToKey] = deploymentAddress; @@ -269,21 +263,30 @@ contract Deploy is Script, WithEnvironment, WithSalts { // ========== DEPLOYMENTS ========== // - function deployAtomicUniswapV2DirectToLiquidity(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployAtomicUniswapV2DirectToLiquidity( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying UniswapV2DirectToLiquidity (Atomic)"); + // Get configuration variables address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); - address uniswapV2Factory = _getAddressNotZero("constants.uniswapV2.factory"); - address uniswapV2Router = _getAddressNotZero("constants.uniswapV2.router"); + address uniswapV2Factory = + _getEnvAddressOrOverride("constants.uniswapV2.factory", sequenceName_, "args.factory"); + address uniswapV2Router = + _getEnvAddressOrOverride("constants.uniswapV2.router", sequenceName_, "args.router"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); + + // Check that the router and factory match + require( + IUniswapV2Router02(uniswapV2Router).factory() == uniswapV2Factory, + "UniswapV2Router.factory() does not match given Uniswap V2 factory address" + ); // Get the salt bytes32 salt_ = _getSalt( - "UniswapV2DirectToLiquidity", + sequenceName_, type(UniswapV2DirectToLiquidity).creationCode, abi.encode(atomicAuctionHouse, uniswapV2Factory, uniswapV2Router) ); @@ -299,28 +302,35 @@ contract Deploy is Script, WithEnvironment, WithSalts { salt: salt_ }(atomicAuctionHouse, uniswapV2Factory, uniswapV2Router); console2.log(""); - console2.log( - " UniswapV2DirectToLiquidity (Atomic) deployed at:", address(cbAtomicUniswapV2Dtl) - ); + console2.log(" deployed at:", address(cbAtomicUniswapV2Dtl)); - return (address(cbAtomicUniswapV2Dtl), _PREFIX_CALLBACKS); + return (address(cbAtomicUniswapV2Dtl), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchUniswapV2DirectToLiquidity(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployBatchUniswapV2DirectToLiquidity( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying UniswapV2DirectToLiquidity (Batch)"); + // Get configuration variables address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); - address uniswapV2Factory = _getAddressNotZero("constants.uniswapV2.factory"); - address uniswapV2Router = _getAddressNotZero("constants.uniswapV2.router"); + address uniswapV2Factory = + _getEnvAddressOrOverride("constants.uniswapV2.factory", sequenceName_, "args.factory"); + address uniswapV2Router = + _getEnvAddressOrOverride("constants.uniswapV2.router", sequenceName_, "args.router"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); + + // Check that the router and factory match + require( + IUniswapV2Router02(uniswapV2Router).factory() == uniswapV2Factory, + "UniswapV2Router.factory() does not match given Uniswap V2 factory address" + ); // Get the salt bytes32 salt_ = _getSalt( - "UniswapV2DirectToLiquidity", + deploymentKey, type(UniswapV2DirectToLiquidity).creationCode, abi.encode(batchAuctionHouse, uniswapV2Factory, uniswapV2Router) ); @@ -336,28 +346,36 @@ contract Deploy is Script, WithEnvironment, WithSalts { batchAuctionHouse, uniswapV2Factory, uniswapV2Router ); console2.log(""); - console2.log( - " UniswapV2DirectToLiquidity (Batch) deployed at:", address(cbBatchUniswapV2Dtl) - ); + console2.log(" deployed at:", address(cbBatchUniswapV2Dtl)); - return (address(cbBatchUniswapV2Dtl), _PREFIX_CALLBACKS); + return (address(cbBatchUniswapV2Dtl), _PREFIX_CALLBACKS, deploymentKey); } - function deployAtomicUniswapV3DirectToLiquidity(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployAtomicUniswapV3DirectToLiquidity( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying UniswapV3DirectToLiquidity (Atomic)"); + // Get configuration variables address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); - address uniswapV3Factory = _getAddressNotZero("constants.uniswapV3.factory"); - address gUniFactory = _getAddressNotZero("constants.gUni.factory"); + address uniswapV3Factory = _getEnvAddressOrOverride( + "constants.uniswapV3.factory", sequenceName_, "args.uniswapV3Factory" + ); + address gUniFactory = + _getEnvAddressOrOverride("constants.gUni.factory", sequenceName_, "args.gUniFactory"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); + + // Check that the GUni factory and Uniswap V3 factory are consistent + require( + GUniFactory(gUniFactory).factory() == uniswapV3Factory, + "GUniFactory.factory() does not match given Uniswap V3 factory address" + ); // Get the salt bytes32 salt_ = _getSalt( - "UniswapV3DirectToLiquidity", + deploymentKey, type(UniswapV3DirectToLiquidity).creationCode, abi.encode(atomicAuctionHouse, uniswapV3Factory, gUniFactory) ); @@ -373,28 +391,36 @@ contract Deploy is Script, WithEnvironment, WithSalts { salt: salt_ }(atomicAuctionHouse, uniswapV3Factory, gUniFactory); console2.log(""); - console2.log( - " UniswapV3DirectToLiquidity (Atomic) deployed at:", address(cbAtomicUniswapV3Dtl) - ); + console2.log(" deployed at:", address(cbAtomicUniswapV3Dtl)); - return (address(cbAtomicUniswapV3Dtl), _PREFIX_CALLBACKS); + return (address(cbAtomicUniswapV3Dtl), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchUniswapV3DirectToLiquidity(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployBatchUniswapV3DirectToLiquidity( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying UniswapV3DirectToLiquidity (Batch)"); + // Get configuration variables address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); - address uniswapV3Factory = _getAddressNotZero("constants.uniswapV3.factory"); - address gUniFactory = _getAddressNotZero("constants.gUni.factory"); + address uniswapV3Factory = _getEnvAddressOrOverride( + "constants.uniswapV3.factory", sequenceName_, "args.uniswapV3Factory" + ); + address gUniFactory = + _getEnvAddressOrOverride("constants.gUni.factory", sequenceName_, "args.gUniFactory"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); + + // Check that the GUni factory and Uniswap V3 factory are consistent + require( + GUniFactory(gUniFactory).factory() == uniswapV3Factory, + "GUniFactory.factory() does not match given Uniswap V3 factory address" + ); // Get the salt bytes32 salt_ = _getSalt( - "UniswapV3DirectToLiquidity", + deploymentKey, type(UniswapV3DirectToLiquidity).creationCode, abi.encode(batchAuctionHouse, uniswapV3Factory, gUniFactory) ); @@ -410,22 +436,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { batchAuctionHouse, uniswapV3Factory, gUniFactory ); console2.log(""); - console2.log( - " UniswapV3DirectToLiquidity (Batch) deployed at:", address(cbBatchUniswapV3Dtl) - ); + console2.log(" deployed at:", address(cbBatchUniswapV3Dtl)); - return (address(cbBatchUniswapV3Dtl), _PREFIX_CALLBACKS); + return (address(cbBatchUniswapV3Dtl), _PREFIX_CALLBACKS, deploymentKey); } - function deployAtomicCappedMerkleAllowlist(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployAtomicCappedMerkleAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying CappedMerkleAllowlist (Atomic)"); + // Get configuration variables address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -439,7 +464,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "CappedMerkleAllowlist", + deploymentKey, type(CappedMerkleAllowlist).creationCode, abi.encode(atomicAuctionHouse, permissions) ); @@ -454,23 +479,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { CappedMerkleAllowlist cbAtomicCappedMerkleAllowlist = new CappedMerkleAllowlist{salt: salt_}(atomicAuctionHouse, permissions); console2.log(""); - console2.log( - " CappedMerkleAllowlist (Atomic) deployed at:", - address(cbAtomicCappedMerkleAllowlist) - ); + console2.log(" deployed at:", address(cbAtomicCappedMerkleAllowlist)); - return (address(cbAtomicCappedMerkleAllowlist), _PREFIX_CALLBACKS); + return (address(cbAtomicCappedMerkleAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchCappedMerkleAllowlist(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployBatchCappedMerkleAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying CappedMerkleAllowlist (Batch)"); + // Get configuration variables address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -484,7 +507,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "CappedMerkleAllowlist", + deploymentKey, type(CappedMerkleAllowlist).creationCode, abi.encode(batchAuctionHouse, permissions) ); @@ -499,19 +522,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { CappedMerkleAllowlist cbBatchCappedMerkleAllowlist = new CappedMerkleAllowlist{salt: salt_}(batchAuctionHouse, permissions); console2.log(""); - console2.log( - " CappedMerkleAllowlist (Batch) deployed at:", address(cbBatchCappedMerkleAllowlist) - ); + console2.log(" deployed at:", address(cbBatchCappedMerkleAllowlist)); - return (address(cbBatchCappedMerkleAllowlist), _PREFIX_CALLBACKS); + return (address(cbBatchCappedMerkleAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployAtomicMerkleAllowlist(bytes memory) public returns (address, string memory) { - // No args used + function deployAtomicMerkleAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying MerkleAllowlist (Atomic)"); + // Get configuration variables address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -525,7 +550,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "MerkleAllowlist", + deploymentKey, type(MerkleAllowlist).creationCode, abi.encode(atomicAuctionHouse, permissions) ); @@ -540,17 +565,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { MerkleAllowlist cbAtomicMerkleAllowlist = new MerkleAllowlist{salt: salt_}(atomicAuctionHouse, permissions); console2.log(""); - console2.log(" MerkleAllowlist (Atomic) deployed at:", address(cbAtomicMerkleAllowlist)); + console2.log(" deployed at:", address(cbAtomicMerkleAllowlist)); - return (address(cbAtomicMerkleAllowlist), _PREFIX_CALLBACKS); + return (address(cbAtomicMerkleAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchMerkleAllowlist(bytes memory) public returns (address, string memory) { - // No args used + function deployBatchMerkleAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying MerkleAllowlist (Batch)"); + // Get configuration variables address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -564,7 +593,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "MerkleAllowlist", + deploymentKey, type(MerkleAllowlist).creationCode, abi.encode(batchAuctionHouse, permissions) ); @@ -579,17 +608,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { MerkleAllowlist cbBatchMerkleAllowlist = new MerkleAllowlist{salt: salt_}(batchAuctionHouse, permissions); console2.log(""); - console2.log(" MerkleAllowlist (Batch) deployed at:", address(cbBatchMerkleAllowlist)); + console2.log(" deployed at:", address(cbBatchMerkleAllowlist)); - return (address(cbBatchMerkleAllowlist), _PREFIX_CALLBACKS); + return (address(cbBatchMerkleAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployAtomicTokenAllowlist(bytes memory) public returns (address, string memory) { - // No args used + function deployAtomicTokenAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying TokenAllowlist (Atomic)"); + // Get configuration variables address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -603,7 +636,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "TokenAllowlist", + deploymentKey, type(TokenAllowlist).creationCode, abi.encode(atomicAuctionHouse, permissions) ); @@ -618,17 +651,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { TokenAllowlist cbAtomicTokenAllowlist = new TokenAllowlist{salt: salt_}(atomicAuctionHouse, permissions); console2.log(""); - console2.log(" TokenAllowlist (Atomic) deployed at:", address(cbAtomicTokenAllowlist)); + console2.log(" deployed at:", address(cbAtomicTokenAllowlist)); - return (address(cbAtomicTokenAllowlist), _PREFIX_CALLBACKS); + return (address(cbAtomicTokenAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchTokenAllowlist(bytes memory) public returns (address, string memory) { - // No args used + function deployBatchTokenAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying TokenAllowlist (Batch)"); + // Get configuration variables address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -642,7 +679,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "TokenAllowlist", + deploymentKey, type(TokenAllowlist).creationCode, abi.encode(batchAuctionHouse, permissions) ); @@ -657,20 +694,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { TokenAllowlist cbBatchTokenAllowlist = new TokenAllowlist{salt: salt_}(batchAuctionHouse, permissions); console2.log(""); - console2.log(" TokenAllowlist (Batch) deployed at:", address(cbBatchTokenAllowlist)); + console2.log(" deployed at:", address(cbBatchTokenAllowlist)); - return (address(cbBatchTokenAllowlist), _PREFIX_CALLBACKS); + return (address(cbBatchTokenAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployAtomicAllocatedMerkleAllowlist(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployAtomicAllocatedMerkleAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying AllocatedMerkleAllowlist (Atomic)"); + // Get configuration variables address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -684,7 +722,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "AllocatedMerkleAllowlist", + deploymentKey, type(AllocatedMerkleAllowlist).creationCode, abi.encode(atomicAuctionHouse, permissions) ); @@ -699,23 +737,21 @@ contract Deploy is Script, WithEnvironment, WithSalts { AllocatedMerkleAllowlist cbAtomicAllocatedMerkleAllowlist = new AllocatedMerkleAllowlist{salt: salt_}(atomicAuctionHouse, permissions); console2.log(""); - console2.log( - " AllocatedMerkleAllowlist (Atomic) deployed at:", - address(cbAtomicAllocatedMerkleAllowlist) - ); + console2.log(" deployed at:", address(cbAtomicAllocatedMerkleAllowlist)); - return (address(cbAtomicAllocatedMerkleAllowlist), _PREFIX_CALLBACKS); + return (address(cbAtomicAllocatedMerkleAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchAllocatedMerkleAllowlist(bytes memory) - public - returns (address, string memory) - { - // No args used + function deployBatchAllocatedMerkleAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying AllocatedMerkleAllowlist (Batch)"); + // Get configuration variables address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -729,7 +765,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { // Get the salt bytes32 salt_ = _getSalt( - "AllocatedMerkleAllowlist", + deploymentKey, type(AllocatedMerkleAllowlist).creationCode, abi.encode(batchAuctionHouse, permissions) ); @@ -744,40 +780,99 @@ contract Deploy is Script, WithEnvironment, WithSalts { AllocatedMerkleAllowlist cbBatchAllocatedMerkleAllowlist = new AllocatedMerkleAllowlist{salt: salt_}(batchAuctionHouse, permissions); console2.log(""); - console2.log( - " AllocatedMerkleAllowlist (Batch) deployed at:", - address(cbBatchAllocatedMerkleAllowlist) - ); + console2.log(" deployed at:", address(cbBatchAllocatedMerkleAllowlist)); - return (address(cbBatchAllocatedMerkleAllowlist), _PREFIX_CALLBACKS); + return (address(cbBatchAllocatedMerkleAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchBaselineAllocatedAllowlist(bytes memory args_) - public - returns (address, string memory) - { - // Decode arguments - (address baselineKernel, address baselineOwner, address reserveToken) = - abi.decode(args_, (address, address, address)); + function deployBatchBaselineAxisLaunch( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { + console2.log(""); + console2.log("Deploying BaselineAxisLaunch (Batch)"); + + // Get configuration variables + address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + address baselineKernel = _getSequenceAddress(sequenceName_, "args.baselineKernel"); + console2.log(" baselineKernel:", baselineKernel); + address baselineOwner = _getSequenceAddress(sequenceName_, "args.baselineOwner"); + console2.log(" baselineOwner:", baselineOwner); + address reserveToken = _getSequenceAddress(sequenceName_, "args.reserveToken"); + console2.log(" reserveToken:", reserveToken); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); // Validate arguments require(baselineKernel != address(0), "baselineKernel not set"); require(baselineOwner != address(0), "baselineOwner not set"); require(reserveToken != address(0), "reserveToken not set"); + // Get the salt + // This supports an arbitrary salt key, which can be set in the deployment sequence + // This is required as each callback is single-use + bytes32 salt_ = _getSalt( + deploymentKey, + type(BaselineAxisLaunch).creationCode, + abi.encode(batchAuctionHouse, baselineKernel, reserveToken, baselineOwner) + ); + + // Revert if the salt is not set + require(salt_ != bytes32(0), "Salt not set"); + + // Deploy the module + console2.log(" salt:", vm.toString(salt_)); + + vm.broadcast(); + BaselineAxisLaunch batchCallback = new BaselineAxisLaunch{salt: salt_}( + batchAuctionHouse, baselineKernel, reserveToken, baselineOwner + ); + console2.log(""); + console2.log(" deployed at:", address(batchCallback)); + + // If the deployer is the executor, + // install the module as a policy in the Baseline kernel + BaselineKernel kernel = BaselineKernel(baselineKernel); + if (kernel.executor() == msg.sender) { + vm.broadcast(); + BaselineKernel(baselineKernel).executeAction( + BaselineKernelActions.ActivatePolicy, address(batchCallback) + ); + + console2.log(" Policy activated in Baseline Kernel"); + } else { + console2.log(" Policy activation skipped"); + } + + return (address(batchCallback), _PREFIX_CALLBACKS, deploymentKey); + } + + function deployBatchBaselineAllocatedAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { console2.log(""); console2.log("Deploying BaselineAllocatedAllowlist (Batch)"); - console2.log(" Kernel", baselineKernel); - console2.log(" Owner", baselineOwner); - console2.log(" ReserveToken", reserveToken); + // Get configuration variables address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + address baselineKernel = _getSequenceAddress(sequenceName_, "args.baselineKernel"); + console2.log(" baselineKernel:", baselineKernel); + address baselineOwner = _getSequenceAddress(sequenceName_, "args.baselineOwner"); + console2.log(" baselineOwner:", baselineOwner); + address reserveToken = _getSequenceAddress(sequenceName_, "args.reserveToken"); + console2.log(" reserveToken:", reserveToken); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); + + // Validate arguments + require(baselineKernel != address(0), "baselineKernel not set"); + require(baselineOwner != address(0), "baselineOwner not set"); + require(reserveToken != address(0), "reserveToken not set"); // Get the salt // This supports an arbitrary salt key, which can be set in the deployment sequence // This is required as each callback is single-use bytes32 salt_ = _getSalt( - "BaselineAllocatedAllowlist", + deploymentKey, type(BALwithAllocatedAllowlist).creationCode, abi.encode(batchAuctionHouse, baselineKernel, reserveToken, baselineOwner) ); @@ -793,45 +888,52 @@ contract Deploy is Script, WithEnvironment, WithSalts { batchAuctionHouse, baselineKernel, reserveToken, baselineOwner ); console2.log(""); - console2.log(" BaselineAllocatedAllowlist (Batch) deployed at:", address(batchAllowlist)); - - // Install the module as a policy in the Baseline kernel - vm.broadcast(); - BaselineKernel(baselineKernel).executeAction( - BaselineKernelActions.ActivatePolicy, address(batchAllowlist) - ); + console2.log(" deployed at:", address(batchAllowlist)); + + // If the deployer is the executor, + // install the module as a policy in the Baseline kernel + BaselineKernel kernel = BaselineKernel(baselineKernel); + if (kernel.executor() == msg.sender) { + vm.broadcast(); + BaselineKernel(baselineKernel).executeAction( + BaselineKernelActions.ActivatePolicy, address(batchAllowlist) + ); - console2.log(" Policy activated in Baseline Kernel"); + console2.log(" Policy activated in Baseline Kernel"); + } else { + console2.log(" Policy activation skipped"); + } - return (address(batchAllowlist), _PREFIX_CALLBACKS); + return (address(batchAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchBaselineAllowlist(bytes memory args_) - public - returns (address, string memory) - { - // Decode arguments - (address baselineKernel, address baselineOwner, address reserveToken) = - abi.decode(args_, (address, address, address)); + function deployBatchBaselineAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { + console2.log(""); + console2.log("Deploying BaselineAllowlist (Batch)"); + + // Get configuration variables + address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + address baselineKernel = _getSequenceAddress(sequenceName_, "args.baselineKernel"); + console2.log(" baselineKernel:", baselineKernel); + address baselineOwner = _getSequenceAddress(sequenceName_, "args.baselineOwner"); + console2.log(" baselineOwner:", baselineOwner); + address reserveToken = _getSequenceAddress(sequenceName_, "args.reserveToken"); + console2.log(" reserveToken:", reserveToken); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); // Validate arguments require(baselineKernel != address(0), "baselineKernel not set"); require(baselineOwner != address(0), "baselineOwner not set"); require(reserveToken != address(0), "reserveToken not set"); - console2.log(""); - console2.log("Deploying BaselineAllowlist (Batch)"); - console2.log(" Kernel", baselineKernel); - console2.log(" Owner", baselineOwner); - console2.log(" ReserveToken", reserveToken); - - address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); - // Get the salt // This supports an arbitrary salt key, which can be set in the deployment sequence // This is required as each callback is single-use bytes32 salt_ = _getSalt( - "BaselineAllowlist", + deploymentKey, type(BALwithAllowlist).creationCode, abi.encode(batchAuctionHouse, baselineKernel, reserveToken, baselineOwner) ); @@ -847,45 +949,52 @@ contract Deploy is Script, WithEnvironment, WithSalts { batchAuctionHouse, baselineKernel, reserveToken, baselineOwner ); console2.log(""); - console2.log(" BaselineAllowlist (Batch) deployed at:", address(batchAllowlist)); - - // Install the module as a policy in the Baseline kernel - vm.broadcast(); - BaselineKernel(baselineKernel).executeAction( - BaselineKernelActions.ActivatePolicy, address(batchAllowlist) - ); + console2.log(" deployed at:", address(batchAllowlist)); + + // If the deployer is the executor, + // install the module as a policy in the Baseline kernel + BaselineKernel kernel = BaselineKernel(baselineKernel); + if (kernel.executor() == msg.sender) { + vm.broadcast(); + BaselineKernel(baselineKernel).executeAction( + BaselineKernelActions.ActivatePolicy, address(batchAllowlist) + ); - console2.log(" Policy activated in Baseline Kernel"); + console2.log(" Policy activated in Baseline Kernel"); + } else { + console2.log(" Policy activation skipped"); + } - return (address(batchAllowlist), _PREFIX_CALLBACKS); + return (address(batchAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchBaselineCappedAllowlist(bytes memory args_) - public - returns (address, string memory) - { - // Decode arguments - (address baselineKernel, address baselineOwner, address reserveToken) = - abi.decode(args_, (address, address, address)); + function deployBatchBaselineCappedAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { + console2.log(""); + console2.log("Deploying BaselineCappedAllowlist (Batch)"); + + // Get configuration variables + address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + address baselineKernel = _getSequenceAddress(sequenceName_, "args.baselineKernel"); + console2.log(" baselineKernel:", baselineKernel); + address baselineOwner = _getSequenceAddress(sequenceName_, "args.baselineOwner"); + console2.log(" baselineOwner:", baselineOwner); + address reserveToken = _getSequenceAddress(sequenceName_, "args.reserveToken"); + console2.log(" reserveToken:", reserveToken); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); // Validate arguments require(baselineKernel != address(0), "baselineKernel not set"); require(baselineOwner != address(0), "baselineOwner not set"); require(reserveToken != address(0), "reserveToken not set"); - console2.log(""); - console2.log("Deploying BaselineCappedAllowlist (Batch)"); - console2.log(" Kernel", baselineKernel); - console2.log(" Owner", baselineOwner); - console2.log(" ReserveToken", reserveToken); - - address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); - // Get the salt // This supports an arbitrary salt key, which can be set in the deployment sequence // This is required as each callback is single-use bytes32 salt_ = _getSalt( - "BaselineCappedAllowlist", + deploymentKey, type(BALwithCappedAllowlist).creationCode, abi.encode(batchAuctionHouse, baselineKernel, reserveToken, baselineOwner) ); @@ -901,45 +1010,52 @@ contract Deploy is Script, WithEnvironment, WithSalts { batchAuctionHouse, baselineKernel, reserveToken, baselineOwner ); console2.log(""); - console2.log(" BaselineCappedAllowlist (Batch) deployed at:", address(batchAllowlist)); - - // Install the module as a policy in the Baseline kernel - vm.broadcast(); - BaselineKernel(baselineKernel).executeAction( - BaselineKernelActions.ActivatePolicy, address(batchAllowlist) - ); + console2.log(" deployed at:", address(batchAllowlist)); + + // If the deployer is the executor, + // install the module as a policy in the Baseline kernel + BaselineKernel kernel = BaselineKernel(baselineKernel); + if (kernel.executor() == msg.sender) { + vm.broadcast(); + BaselineKernel(baselineKernel).executeAction( + BaselineKernelActions.ActivatePolicy, address(batchAllowlist) + ); - console2.log(" Policy activated in Baseline Kernel"); + console2.log(" Policy activated in Baseline Kernel"); + } else { + console2.log(" Policy activation skipped"); + } - return (address(batchAllowlist), _PREFIX_CALLBACKS); + return (address(batchAllowlist), _PREFIX_CALLBACKS, deploymentKey); } - function deployBatchBaselineTokenAllowlist(bytes memory args_) - public - returns (address, string memory) - { - // Decode arguments - (address baselineKernel, address baselineOwner, address reserveToken) = - abi.decode(args_, (address, address, address)); + function deployBatchBaselineTokenAllowlist( + string memory sequenceName_ + ) public returns (address, string memory, string memory) { + console2.log(""); + console2.log("Deploying BaselineTokenAllowlist (Batch)"); + + // Get configuration variables + address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + address baselineKernel = _getSequenceAddress(sequenceName_, "args.baselineKernel"); + console2.log(" baselineKernel:", baselineKernel); + address baselineOwner = _getSequenceAddress(sequenceName_, "args.baselineOwner"); + console2.log(" baselineOwner:", baselineOwner); + address reserveToken = _getSequenceAddress(sequenceName_, "args.reserveToken"); + console2.log(" reserveToken:", reserveToken); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); // Validate arguments require(baselineKernel != address(0), "baselineKernel not set"); require(baselineOwner != address(0), "baselineOwner not set"); require(reserveToken != address(0), "reserveToken not set"); - console2.log(""); - console2.log("Deploying BaselineTokenAllowlist (Batch)"); - console2.log(" Kernel", baselineKernel); - console2.log(" Owner", baselineOwner); - console2.log(" ReserveToken", reserveToken); - - address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); - // Get the salt // This supports an arbitrary salt key, which can be set in the deployment sequence // This is required as each callback is single-use bytes32 salt_ = _getSalt( - "BaselineTokenAllowlist", + deploymentKey, type(BALwithTokenAllowlist).creationCode, abi.encode(batchAuctionHouse, baselineKernel, reserveToken, baselineOwner) ); @@ -955,37 +1071,51 @@ contract Deploy is Script, WithEnvironment, WithSalts { batchAuctionHouse, baselineKernel, reserveToken, baselineOwner ); console2.log(""); - console2.log(" BaselineTokenAllowlist (Batch) deployed at:", address(batchAllowlist)); - - // Install the module as a policy in the Baseline kernel - vm.broadcast(); - BaselineKernel(baselineKernel).executeAction( - BaselineKernelActions.ActivatePolicy, address(batchAllowlist) - ); + console2.log(" deployed at:", address(batchAllowlist)); + + // If the deployer is the executor, + // install the module as a policy in the Baseline kernel + BaselineKernel kernel = BaselineKernel(baselineKernel); + if (kernel.executor() == msg.sender) { + vm.broadcast(); + BaselineKernel(baselineKernel).executeAction( + BaselineKernelActions.ActivatePolicy, address(batchAllowlist) + ); - console2.log(" Policy activated in Baseline Kernel"); + console2.log(" Policy activated in Baseline Kernel"); + } else { + console2.log(" Policy activation skipped"); + } - return (address(batchAllowlist), _PREFIX_CALLBACKS); + return (address(batchAllowlist), _PREFIX_CALLBACKS, deploymentKey); } // ========== HELPER FUNCTIONS ========== // function _configureDeployment(string memory data_, string memory name_) internal { + console2.log(""); console2.log(" Configuring", name_); - // Parse and store args - // Note: constructor args need to be provided in alphabetical order - // due to changes with forge-std or a struct needs to be used - argsMap[name_] = _readDataValue(data_, name_, "args"); - // Check if it should be installed in the AtomicAuctionHouse - if (_readDataBoolean(data_, name_, "installAtomicAuctionHouse")) { + if ( + _sequenceKeyExists(name_, "installAtomicAuctionHouse") + && _getSequenceBool(name_, "installAtomicAuctionHouse") + ) { installAtomicAuctionHouseMap[name_] = true; + console2.log(" Queueing for installation in AtomicAuctionHouse"); + } else { + console2.log(" Skipping installation in AtomicAuctionHouse"); } // Check if it should be installed in the BatchAuctionHouse - if (_readDataBoolean(data_, name_, "installBatchAuctionHouse")) { + if ( + _sequenceKeyExists(name_, "installBatchAuctionHouse") + && _getSequenceBool(name_, "installBatchAuctionHouse") + ) { installBatchAuctionHouseMap[name_] = true; + console2.log(" Queueing for installation in BatchAuctionHouse"); + } else { + console2.log(" Skipping installation in BatchAuctionHouse"); } // Check if max fees need to be initialized @@ -1025,38 +1155,13 @@ contract Deploy is Script, WithEnvironment, WithSalts { return _envAddressNotZero(key_); } + /// @notice Reads a raw bytes value from the deployment sequence function _readDataValue( string memory data_, string memory name_, string memory key_ ) internal pure returns (bytes memory) { // This will return "0x" if the key doesn't exist - return data_.parseRaw(string.concat(".sequence[?(@.name == '", name_, "')].", key_)); - } - - function _readStringValue( - string memory data_, - string memory name_, - string memory key_ - ) internal pure returns (string memory) { - bytes memory dataValue = _readDataValue(data_, name_, key_); - - // If the key is not set, return an empty string - if (dataValue.length == 0) { - return ""; - } - - return abi.decode(dataValue, (string)); - } - - function _readDataBoolean( - string memory data_, - string memory name_, - string memory key_ - ) internal pure returns (bool) { - bytes memory dataValue = _readDataValue(data_, name_, key_); - - // Comparing `bytes memory` directly doesn't work, so we need to convert to `bytes32` - return bytes32(dataValue) == bytes32(abi.encodePacked(uint256(1))); + return data_.parseRaw(_getSequenceKey(name_, key_)); } } diff --git a/script/deploy/README.md b/script/deploy/README.md index 4b7f0633..4ba0c895 100644 --- a/script/deploy/README.md +++ b/script/deploy/README.md @@ -34,7 +34,7 @@ Notes: First, support for the contract needs to be added in the deployment script, `./script/deploy/Deploy.s.sol`. -This involves creating a function in the format of `function deploy(bytes memory args_) public virtual returns (address, string memory)`. +This involves creating a function in the format of `function deploy(string memory sequenceName_) public virtual returns (address, string memory, string memory)`. For example, a deployment with `name` set to "AtomicLinearVesting" would require a function to be present in `Deploy.s.sol` with the name `deployAtomicLinearVesting`. @@ -55,6 +55,8 @@ Notes: } ``` +- If a contract deployment requires different variants with different configuration values, the standard deployment function (e.g. `deployBatchUniswapV2DirectToLiquidity()`) can be re-used. See the [uniswap-dtl-blast-thruster.json](/script/deploy/sequences/uniswap-dtl-blast-thruster.json) file for an example that uses the Thruster contracts on Blast (instead of the canonical Uniswap contracts) and saves the resulting contract address to a custom key in [env.json](/script/env.json). + #### Running the Deployment To perform a deployment, run the following script: @@ -108,8 +110,11 @@ Apart from first-party deployments, the `script/env.json` file contains the addr - [Uniswap V2](https://github.com/Uniswap/docs/blob/65d3f21e6cb2879b0672ad791563de0e54fcc089/docs/contracts/v2/reference/smart-contracts/08-deployment-addresses.md) - Exceptions - Arbitrum Sepolia, Base Sepolia, Blast Sepolia and Mode Sepolia are custom deployments, due to the unavailability of the Uniswap V2 contracts. + - The Blast deployment of UniswapV2DirectToLiquidity uses the Uniswap V2 Factory and Router deployed by Thruster. - [Uniswap V3](https://github.com/Uniswap/docs/tree/65d3f21e6cb2879b0672ad791563de0e54fcc089/docs/contracts/v3/reference/deployments) - Exceptions - Arbitrum Sepolia and Blast Sepolia are custom deployments by Axis Finance alongside the G-UNI deployment. + - The Blast deployment of UniswapV3DirectToLiquidity uses the Uniswap V3 Factory deployed by Thruster. - G-UNI - All of the addresses mentioned are custom deployments by Axis Finance. This is because the addresses from the deployments recorded in the [g-uni-v1-core repository](https://github.com/gelatodigital/g-uni-v1-core/tree/bea63422e2155242b051896b635508b7a99d2a1a/deployments) point to proxies, which have since been upgraded to point to Arrakis contracts that have different interfaces. + - Axis Finance maintains a [fork](https://github.com/Axis-Fi/g-uni-v1-core) of the repository. diff --git a/script/deploy/WithDeploySequence.s.sol b/script/deploy/WithDeploySequence.s.sol new file mode 100644 index 00000000..38e99213 --- /dev/null +++ b/script/deploy/WithDeploySequence.s.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Scripting libraries +import {Script, console2} from "@forge-std-1.9.1/Script.sol"; +import {stdJson} from "@forge-std-1.9.1/StdJson.sol"; + +import {WithEnvironment} from "./WithEnvironment.s.sol"; + +/// @notice A script that loads a deployment sequence from a JSON file +/// @dev This script loads a deployment sequence from a JSON file, and provides helper functions to access values from it +abstract contract WithDeploySequence is Script, WithEnvironment { + using stdJson for string; + + string internal _sequenceJson; + + function _loadSequence( + string calldata chain_, + string calldata sequenceFilePath_ + ) internal virtual { + _loadEnv(chain_); + + // Load deployment data + _sequenceJson = vm.readFile(sequenceFilePath_); + } + + // === Higher-level script functions === // + + function _getDeploymentKey(string memory sequenceName_) internal view returns (string memory) { + return string.concat( + sequenceName_, _getSequenceStringOrFallback(sequenceName_, "deploymentKeySuffix", "") + ); + } + + /// @notice Obtains an address value from the deployment sequence (if it exists), or the env.json as a fallback + function _getEnvAddressOrOverride( + string memory envKey_, + string memory sequenceName_, + string memory key_ + ) internal view returns (address) { + // Check if the key is set in the deployment sequence + if (_sequenceKeyExists(sequenceName_, key_)) { + address sequenceAddress = _getSequenceAddress(sequenceName_, key_); + console2.log(" %s: %s (from deployment sequence)", envKey_, sequenceAddress); + return sequenceAddress; + } + + // Otherwsie return from the environment variables + return _envAddressNotZero(envKey_); + } + + function _getSequenceNames() internal view returns (string[] memory) { + // Parse deployment sequence and names + bytes memory sequence = abi.decode(_sequenceJson.parseRaw(".sequence"), (bytes)); + uint256 len = sequence.length; + + if (len == 0) { + return new string[](0); + } else if (len == 1) { + // Only one deployment + string memory name = abi.decode(_sequenceJson.parseRaw(".sequence..name"), (string)); + + string[] memory names = new string[](1); + names[0] = name; + + return names; + } else { + // More than one deployment + string[] memory names = + abi.decode(_sequenceJson.parseRaw(".sequence..name"), (string[])); + + return names; + } + } + + // === Low-level JSON functions === // + + /// @notice Construct a key to access a value in the deployment sequence + function _getSequenceKey( + string memory name_, + string memory key_ + ) internal pure returns (string memory) { + return string.concat(".sequence[?(@.name == '", name_, "')].", key_); + } + + /// @notice Determines if a key exists in the deployment sequence + function _sequenceKeyExists( + string memory name_, + string memory key_ + ) internal view returns (bool) { + return vm.keyExists(_sequenceJson, _getSequenceKey(name_, key_)); + } + + /// @notice Obtains a string value from the given key in the deployment sequence + /// @dev This will revert if the key does not exist + function _getSequenceString( + string memory name_, + string memory key_ + ) internal view returns (string memory) { + return vm.parseJsonString(_sequenceJson, _getSequenceKey(name_, key_)); + } + + /// @notice Obtains a string value from the deployment sequence (if it exists), or a fallback value + function _getSequenceStringOrFallback( + string memory name_, + string memory key_, + string memory fallbackValue_ + ) internal view returns (string memory) { + // Check if the key is set in the deployment sequence + if (_sequenceKeyExists(name_, key_)) { + return _getSequenceString(name_, key_); + } + + // Otherwise, return the fallback value + return fallbackValue_; + } + + /// @notice Obtains an address value from the given key in the deployment sequence + /// @dev This will revert if the key does not exist + function _getSequenceAddress( + string memory name_, + string memory key_ + ) internal view returns (address) { + return vm.parseJsonAddress(_sequenceJson, _getSequenceKey(name_, key_)); + } + + /// @notice Obtains an bool value from the given key in the deployment sequence + /// @dev This will revert if the key does not exist + function _getSequenceBool( + string memory name_, + string memory key_ + ) internal view returns (bool) { + return vm.parseJsonBool(_sequenceJson, _getSequenceKey(name_, key_)); + } +} diff --git a/script/deploy/WithEnvironment.s.sol b/script/deploy/WithEnvironment.s.sol index e5bb2043..854628e3 100644 --- a/script/deploy/WithEnvironment.s.sol +++ b/script/deploy/WithEnvironment.s.sol @@ -18,7 +18,7 @@ abstract contract WithEnvironment is Script { // Load environment file env = vm.readFile("./script/env.json"); - envAxisCore = vm.readFile("dependencies/axis-core-1.0.0/script/env.json"); + envAxisCore = vm.readFile("dependencies/axis-core-1.0.1/script/env.json"); } /// @notice Get address from environment file @@ -27,7 +27,6 @@ abstract contract WithEnvironment is Script { /// @param key_ The key to look up in the environment file /// @return address The address from the environment file, or the zero address function _envAddress(string memory key_) internal view returns (address) { - console2.log(" Checking in env.json"); string memory fullKey = string.concat(".current.", chain, ".", key_); address addr; bool keyExists = vm.keyExists(env, fullKey); diff --git a/script/deploy/deploy.sh b/script/deploy/deploy.sh index fd0fc5b0..08a95f0a 100755 --- a/script/deploy/deploy.sh +++ b/script/deploy/deploy.sh @@ -67,14 +67,6 @@ fi DEPLOY_SCRIPT=${DEPLOY_SCRIPT:-"./script/deploy/Deploy.s.sol"} DEPLOY_CONTRACT=${DEPLOY_CONTRACT:-"Deploy"} -# If the chain contains "blast", use the Blast-specific contracts to deploy -if [[ $CHAIN == *"blast"* ]] -then - echo "Using Blast-specific contracts" - DEPLOY_SCRIPT="./script/deploy/DeployBlast.s.sol" - DEPLOY_CONTRACT="DeployBlast" -fi - echo "Using deploy script and contract: $DEPLOY_SCRIPT:$DEPLOY_CONTRACT" echo "Using deployment configuration: $DEPLOY_FILE" echo "Using chain: $CHAIN" @@ -146,4 +138,4 @@ $VERIFY_FLAG \ $RESUME_FLAG # Insert for Mantle deployments -# -g 4000000 --with-gas-price 20000000 --priority-gas-price 10000000 \ \ No newline at end of file +# --legacy -g 1000000 --with-gas-price 20000000 \ \ No newline at end of file diff --git a/script/deploy/sequence_schema.json b/script/deploy/sequence_schema.json index d2f07b5b..3707ccbf 100644 --- a/script/deploy/sequence_schema.json +++ b/script/deploy/sequence_schema.json @@ -16,6 +16,11 @@ "description": "The name of the module to deploy", "exclusiveMinimum": 0 }, + "deploymentKeySuffix": { + "type": "string", + "description": "The deployment key suffix to use when storing the deployment address", + "exclusiveMinimum": 0 + }, "installAtomicAuctionHouse": { "type": "boolean", "description": "Whether to install the module into the Atomic Auction House", @@ -41,4 +46,4 @@ }, "required": ["sequence"] } -} \ No newline at end of file +} diff --git a/script/deploy/sequences/allowlists.json b/script/deploy/sequences/allowlists.json index ffde5399..a766097b 100644 --- a/script/deploy/sequences/allowlists.json +++ b/script/deploy/sequences/allowlists.json @@ -1,28 +1,28 @@ { "sequence": [ - { - "name": "AtomicCappedMerkleAllowlist" - }, - { - "name": "BatchCappedMerkleAllowlist" - }, - { - "name": "AtomicMerkleAllowlist" - }, - { - "name": "BatchMerkleAllowlist" - }, - { - "name": "AtomicTokenAllowlist" - }, - { - "name": "BatchTokenAllowlist" - }, - { - "name": "AtomicAllocatedMerkleAllowlist" - }, - { - "name": "BatchAllocatedMerkleAllowlist" - } + { + "name": "AtomicCappedMerkleAllowlist" + }, + { + "name": "BatchCappedMerkleAllowlist" + }, + { + "name": "AtomicMerkleAllowlist" + }, + { + "name": "BatchMerkleAllowlist" + }, + { + "name": "AtomicTokenAllowlist" + }, + { + "name": "BatchTokenAllowlist" + }, + { + "name": "AtomicAllocatedMerkleAllowlist" + }, + { + "name": "BatchAllocatedMerkleAllowlist" + } ] -} \ No newline at end of file +} diff --git a/script/deploy/sequences/baselineAllocatedAllowlist-sample.json b/script/deploy/sequences/baselineAllocatedAllowlist-sample.json index 64a5b17d..49f34d3f 100644 --- a/script/deploy/sequences/baselineAllocatedAllowlist-sample.json +++ b/script/deploy/sequences/baselineAllocatedAllowlist-sample.json @@ -2,6 +2,7 @@ "sequence": [ { "name": "BatchBaselineAllocatedAllowlist", + "deploymentKeySuffix": "Sample", "args": { "baselineKernel": "0x0000000000000000000000000000000000000001", "baselineOwner": "0xB47C8e4bEb28af80eDe5E5bF474927b110Ef2c0e", @@ -9,4 +10,4 @@ } } ] -} \ No newline at end of file +} diff --git a/script/deploy/sequences/batch-allowlists.json b/script/deploy/sequences/batch-allowlists.json index 8ca7db8e..5e7c846e 100644 --- a/script/deploy/sequences/batch-allowlists.json +++ b/script/deploy/sequences/batch-allowlists.json @@ -1,16 +1,16 @@ { "sequence": [ - { - "name": "BatchCappedMerkleAllowlist" - }, - { - "name": "BatchMerkleAllowlist" - }, - { - "name": "BatchTokenAllowlist" - }, - { - "name": "BatchAllocatedMerkleAllowlist" - } + { + "name": "BatchCappedMerkleAllowlist" + }, + { + "name": "BatchMerkleAllowlist" + }, + { + "name": "BatchTokenAllowlist" + }, + { + "name": "BatchAllocatedMerkleAllowlist" + } ] -} \ No newline at end of file +} diff --git a/script/deploy/sequences/batch-callbacks.json b/script/deploy/sequences/batch-callbacks.json index 6d24632d..de0d3046 100644 --- a/script/deploy/sequences/batch-callbacks.json +++ b/script/deploy/sequences/batch-callbacks.json @@ -1,22 +1,22 @@ { "sequence": [ - { - "name": "BatchCappedMerkleAllowlist" - }, - { - "name": "BatchMerkleAllowlist" - }, - { - "name": "BatchTokenAllowlist" - }, - { - "name": "BatchAllocatedMerkleAllowlist" - }, - { - "name": "BatchUniswapV2DirectToLiquidity" - }, - { - "name": "BatchUniswapV3DirectToLiquidity" - } + { + "name": "BatchCappedMerkleAllowlist" + }, + { + "name": "BatchMerkleAllowlist" + }, + { + "name": "BatchTokenAllowlist" + }, + { + "name": "BatchAllocatedMerkleAllowlist" + }, + { + "name": "BatchUniswapV2DirectToLiquidity" + }, + { + "name": "BatchUniswapV3DirectToLiquidity" + } ] -} \ No newline at end of file +} diff --git a/script/deploy/sequences/uniswap-dtl-blast-thruster.json b/script/deploy/sequences/uniswap-dtl-blast-thruster.json new file mode 100644 index 00000000..252becdb --- /dev/null +++ b/script/deploy/sequences/uniswap-dtl-blast-thruster.json @@ -0,0 +1,19 @@ +{ + "sequence": [ + { + "name": "BatchUniswapV2DirectToLiquidity", + "deploymentKeySuffix": "Thruster", + "args": { + "factory": "0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13", + "router": "0x98994a9A7a2570367554589189dC9772241650f6" + } + }, + { + "name": "BatchUniswapV3DirectToLiquidity", + "deploymentKeySuffix": "Thruster", + "args": { + "factory": "0x71b08f13B3c3aF35aAdEb3949AFEb1ded1016127" + } + } + ] +} diff --git a/script/deploy/sequences/uniswap-dtl.json b/script/deploy/sequences/uniswap-dtl.json index b0465b98..2cc608c5 100644 --- a/script/deploy/sequences/uniswap-dtl.json +++ b/script/deploy/sequences/uniswap-dtl.json @@ -1,10 +1,10 @@ { - "sequence": [ - { - "name": "BatchUniswapV2DirectToLiquidity" - }, - { - "name": "BatchUniswapV3DirectToLiquidity" - } - ] -} \ No newline at end of file + "sequence": [ + { + "name": "BatchUniswapV2DirectToLiquidity" + }, + { + "name": "BatchUniswapV3DirectToLiquidity" + } + ] +} diff --git a/script/env.json b/script/env.json index dea53a88..e5c2c97b 100644 --- a/script/env.json +++ b/script/env.json @@ -1,9 +1,9 @@ { "current": { - "arbitrum": { + "arbitrum-one": { "constants": { "gUni": { - "factory": "0x0000000000000000000000000000000000000000" + "factory": "0x873966578C8ECcD61fc68F1Be3681146f86587e4" }, "uniswapV2": { "factory": "0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9", @@ -12,6 +12,16 @@ "uniswapV3": { "factory": "0x1F98431c8aD98523631AE4a59f267346ea31F984" } + }, + "deployments": { + "callbacks": { + "BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69", + "BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9", + "BatchUniswapV2DirectToLiquidity": "0xE67cB70883fBBf4BDAe501e73E9dC5E881E53452", + "BatchUniswapV3DirectToLiquidity": "0xE6C7C76D480075658789f6a0ed87771a7179E0b3" + } } }, "arbitrum-sepolia": { @@ -29,19 +39,19 @@ }, "deployments": { "callbacks": { - "BatchAllocatedMerkleAllowlist": "0x98765dB6F0c4334075FB06d8D2998749b8CF604e", - "BatchCappedMerkleAllowlist": "0x9868c93CDec64f471b1DFf60f5e8249249dBa126", - "BatchMerkleAllowlist": "0x989F7525b5e0c804e5A2707F14d34C523D56B43b", - "BatchTokenAllowlist": "0x982598d92BBB2a2E54EdCd262EeF114a517fEeCE", - "BatchUniswapV2DirectToLiquidity": "0xE6c6B510E4b0e442BF01fabDc93d4bf032683C3C", - "BatchUniswapV3DirectToLiquidity": "0xE6f7888cfc8219C93E70Ca528195b4afD80D7894" + "BatchAllocatedMerkleAllowlist": "0x98C8ffFf24bcfC3A5B0b463c43F10932Cedb7B8F", + "BatchCappedMerkleAllowlist": "0x9859AcCA8a9afEbb9b3986036d4E0efc0246cEeA", + "BatchMerkleAllowlist": "0x98d64E00D9d6550913E73C940Ff476Cf1723d834", + "BatchTokenAllowlist": "0x9801e45362a2bb7C9F22486CC3F5cA9224e9CC55", + "BatchUniswapV2DirectToLiquidity": "0xE63Ea32d7D1BF1cfe10801E6B7Aa7f3E6d21f2Cd", + "BatchUniswapV3DirectToLiquidity": "0xE603A98566BA6ca4C898a759Ef13c7E6A7A26f4A" } } }, "base": { "constants": { "gUni": { - "factory": "0x0000000000000000000000000000000000000000" + "factory": "0x5D7ddDFee9fB5709Ccdea49Acd51db3d73BC75Fa" }, "uniswapV2": { "factory": "0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6", @@ -50,6 +60,16 @@ "uniswapV3": { "factory": "0x33128a8fC17869897dcE68Ed026d694621f6FDfD" } + }, + "deployments": { + "callbacks": { + "BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69", + "BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9", + "BatchUniswapV2DirectToLiquidity": "0xE6F93df14cB554737A26acd2aB5fEf649921D7F2", + "BatchUniswapV3DirectToLiquidity": "0xE64d6e058dD5F76CCc8566c07b994090a24CCB75" + } } }, "base-sepolia": { @@ -67,26 +87,36 @@ }, "deployments": { "callbacks": { - "BatchAllocatedMerkleAllowlist": "0x987E2DB8E83c57Ad9aDf808e5394d77f72b49ab4", - "BatchCappedMerkleAllowlist": "0x98a27160E2879334AaE4415E24C1feaa3D111392", - "BatchMerkleAllowlist": "0x987e7515985887092582Cc4ea94be837a99C0b02", - "BatchTokenAllowlist": "0x989a21D82D86e4934D3B8E94043F13Fb1C312F8a", - "BatchUniswapV2DirectToLiquidity": "0xE6F478d800e9807efAb601a1CD60BBd7CBA1c0b8", - "BatchUniswapV3DirectToLiquidity": "0xE6BC347aA7Ce46bDb45F938605C5d4b88881bB1c" + "BatchAllocatedMerkleAllowlist": "0x98C8ffFf24bcfC3A5B0b463c43F10932Cedb7B8F", + "BatchCappedMerkleAllowlist": "0x9859AcCA8a9afEbb9b3986036d4E0efc0246cEeA", + "BatchMerkleAllowlist": "0x98d64E00D9d6550913E73C940Ff476Cf1723d834", + "BatchTokenAllowlist": "0x9801e45362a2bb7C9F22486CC3F5cA9224e9CC55", + "BatchUniswapV2DirectToLiquidity": "0xE6546c03B1b9DFC4238f0A2923FdefD5E4af7659", + "BatchUniswapV3DirectToLiquidity": "0xE68b21C071534781BC4c40E6BF1bCFC23638fF4B" } } }, "blast": { "constants": { "gUni": { - "factory": "0x0000000000000000000000000000000000000000" + "factory": "0xC11a71a304aB4c147e926342BA4CcCFd62d7368e" }, "uniswapV2": { - "factory": "0x5C346464d33F90bABaf70dB6388507CC889C1070", - "router": "0xBB66Eb1c5e875933D44DAe661dbD80e5D9B03035" + "factory": "0xb4A7D971D0ADea1c73198C97d7ab3f9CE4aaFA13", + "router": "0x98994a9A7a2570367554589189dC9772241650f6" }, "uniswapV3": { - "factory": "0x792edAdE80af5fC680d96a2eD80A44247D2Cf6Fd" + "factory": "0x71b08f13B3c3aF35aAdEb3949AFEb1ded1016127" + } + }, + "deployments": { + "callbacks": { + "BatchAllocatedMerkleAllowlist": "0x986827E5FC7ECE5704449aAA3B4A4b16b417b8e7", + "BatchCappedMerkleAllowlist": "0x980997561d92D30Cae5bae01CCdfc6923E0c54a6", + "BatchMerkleAllowlist": "0x9817F12e9461e9D68b0ecB7ff2e8E9da5cf0959C", + "BatchTokenAllowlist": "0x989Dc1DAa66139ed26361cc3A24CdA372370a7dA", + "BatchUniswapV2DirectToLiquidity": "0xE6512c5F23403585916C936a194D63880DAd7EDF", + "BatchUniswapV3DirectToLiquidity": "0xE6B27B702DB07a3a7b6d5572f9fA5dc7F201a439" } } }, @@ -105,12 +135,12 @@ }, "deployments": { "callbacks": { - "BatchAllocatedMerkleAllowlist": "0x9895D556E8d1be6C909EC88D43Dd0F6Ae6f7f4D7", - "BatchCappedMerkleAllowlist": "0x98e72F243db7CDfDB015865415A0AE1bFb071507", - "BatchMerkleAllowlist": "0x9824De4B9A917D30a5427813E43e5DF968e19066", - "BatchTokenAllowlist": "0x986e7E24A33Ef6fbDc6C3a94657fBb59d85AbE1C", - "BatchUniswapV2DirectToLiquidity": "0xE6F7049c79AA674D1E7b709C3Dc42C614359956A", - "BatchUniswapV3DirectToLiquidity": "0xE6EDA80884CF4D44841fF33a3162A68EebBa2b7b" + "BatchAllocatedMerkleAllowlist": "0x98e37312E0Bdb9012Eb1F6b70fe5b1cB82AC07dc", + "BatchCappedMerkleAllowlist": "0x98Fa90B451171389a2132191a95a0d4109C16B36", + "BatchMerkleAllowlist": "0x98bd3C7ddF2510553A858e3Fb636299BDD61992b", + "BatchTokenAllowlist": "0x983df377a8c64F26f947312374e6859ebc00dA81", + "BatchUniswapV2DirectToLiquidity": "0xE6BFCC5C3f3e71b5A7a9F4c0C8952f084d649850", + "BatchUniswapV3DirectToLiquidity": "0xE6531D363c29A453c2589D9CDEFcC02773f8ee95" } } }, @@ -128,6 +158,28 @@ } } }, + "mantle": { + "constants": { + "gUni": { + "factory": "0x0000000000000000000000000000000000000000" + }, + "uniswapV2": { + "factory": "0x0000000000000000000000000000000000000000", + "router": "0x0000000000000000000000000000000000000000" + }, + "uniswapV3": { + "factory": "0x0000000000000000000000000000000000000000" + } + }, + "deployments": { + "callbacks": { + "BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69", + "BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9" + } + } + }, "mantle-sepolia": { "constants": { "gUni": { @@ -143,12 +195,22 @@ }, "deployments": { "callbacks": { - "BatchAllocatedMerkleAllowlist": "0x987E2DB8E83c57Ad9aDf808e5394d77f72b49ab4", - "BatchCappedMerkleAllowlist": "0x98a27160E2879334AaE4415E24C1feaa3D111392", - "BatchMerkleAllowlist": "0x987e7515985887092582Cc4ea94be837a99C0b02", - "BatchTokenAllowlist": "0x989a21D82D86e4934D3B8E94043F13Fb1C312F8a", - "BatchUniswapV2DirectToLiquidity": "0xE6feE7a689Ff7493032Ba48B15fE841c6cC30DB9", - "BatchUniswapV3DirectToLiquidity": "0xE6c1ab82a2b4a194C87e643668bb715619766F0B" + "BatchAllocatedMerkleAllowlist": "0x98b7402E399ff864a3277bd4c3e01Df8ef0234e8", + "BatchCappedMerkleAllowlist": "0x98a0826b19B412a159cedb38Bd38899930382972", + "BatchMerkleAllowlist": "0x98E56d6466fC7B2c88acb39e9e4C6E7671e28CBd", + "BatchTokenAllowlist": "0x98a16dF00DB1ea2bC4Cb05E8e2c06d43F56f8e62", + "BatchUniswapV2DirectToLiquidity": "0xE60a178a2f5e86BF77fB1D6814Ed47790B0993f0", + "BatchUniswapV3DirectToLiquidity": "0xE6bF04764268B46ddE10e9e8a13c3E7fF0a181d6" + } + } + }, + "mode": { + "deployments": { + "callbacks": { + "BatchAllocatedMerkleAllowlist": "0x98F28A689275dFC6376dFe54280dfac85fB7bA69", + "BatchCappedMerkleAllowlist": "0x986ADD36BEF1B7a5aF9C659776a601fdECF27ccC", + "BatchMerkleAllowlist": "0x98c5c24eB3FFEFeCd1a666423978f7A030319A78", + "BatchTokenAllowlist": "0x98a5d4827A57056d30df93C7Bd4Bc294cC6dC0b9" } } }, @@ -167,12 +229,12 @@ }, "deployments": { "callbacks": { - "BatchAllocatedMerkleAllowlist": "0x987E2DB8E83c57Ad9aDf808e5394d77f72b49ab4", - "BatchCappedMerkleAllowlist": "0x98a27160E2879334AaE4415E24C1feaa3D111392", - "BatchMerkleAllowlist": "0x987e7515985887092582Cc4ea94be837a99C0b02", - "BatchTokenAllowlist": "0x989a21D82D86e4934D3B8E94043F13Fb1C312F8a", - "BatchUniswapV2DirectToLiquidity": "0xE6F478d800e9807efAb601a1CD60BBd7CBA1c0b8", - "BatchUniswapV3DirectToLiquidity": "0xE6BB80a6B8628F150E340BdE2e0C49fA17E1e566" + "BatchAllocatedMerkleAllowlist": "0x98b7402E399ff864a3277bd4c3e01Df8ef0234e8", + "BatchCappedMerkleAllowlist": "0x98a0826b19B412a159cedb38Bd38899930382972", + "BatchMerkleAllowlist": "0x98E56d6466fC7B2c88acb39e9e4C6E7671e28CBd", + "BatchTokenAllowlist": "0x98a16dF00DB1ea2bC4Cb05E8e2c06d43F56f8e62", + "BatchUniswapV2DirectToLiquidity": "0xE60c0BFa8DE1250eFDF46d80F98AE5eAe947e5E2", + "BatchUniswapV3DirectToLiquidity": "0xE68d298ef59B3ddFc82d12aFC9B8Db67ac068266" } } }, diff --git a/script/install.sh b/script/install.sh index cc320155..5ede6f67 100755 --- a/script/install.sh +++ b/script/install.sh @@ -1,10 +1,30 @@ #!/bin/bash +echo "" +echo "*** Removing submodules" +rm -rf lib/ + +echo "" +echo "*** Setting up submodules" +git submodule init +git submodule update + echo "" echo "*** Installing forge dependencies" forge install echo " Done" +echo "" +echo "*** Restoring submodule commits" + +echo "" +echo "baseline" +cd lib/baseline-v2/ && git checkout 8950018baec27d6497fba409cb361a596535447d && cd ../.. + +echo "" +echo "*** Applying patch to Baseline submodule" +patch -d lib/baseline-v2/ -p1 < script/patch/baseline.patch + echo "" echo "*** Installing soldeer dependencies" rm -rf dependencies/* && forge soldeer update diff --git a/script/patch/baseline.patch b/script/patch/baseline.patch new file mode 100644 index 00000000..ca0fda81 --- /dev/null +++ b/script/patch/baseline.patch @@ -0,0 +1,139 @@ +diff --git a/src/modules/BPOOL.v1.sol b/src/modules/BPOOL.v1.sol +index 82aa5d7..d889bae 100644 +--- a/src/modules/BPOOL.v1.sol ++++ b/src/modules/BPOOL.v1.sol +@@ -1,17 +1,17 @@ + // SPDX-License-Identifier: AGPL-3.0-only + pragma solidity ^0.8.0; + +-import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; +- +-import "src/Kernel.sol"; +-import {ERC20} from "solmate/tokens/ERC20.sol"; +-import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +-import {TickMath} from "v3-core/libraries/TickMath.sol"; +-import {FixedPoint96} from "v3-core/libraries/FixedPoint96.sol"; +-import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; +-import {IUniswapV3Factory} from "v3-core/interfaces/IUniswapV3Factory.sol"; +-import {LiquidityAmounts} from "v3-periphery/libraries/LiquidityAmounts.sol"; +-import {BlastClaimer} from "src/utils/BlastClaimer.sol"; ++import {FixedPointMathLib} from "@solady-0.0.124/utils/FixedPointMathLib.sol"; ++ ++import {Kernel, Module, Keycode, toKeycode} from "../Kernel.sol"; ++import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; ++import {SafeTransferLib} from "@solmate-6.7.0/utils/SafeTransferLib.sol"; ++import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; ++import {FixedPoint96} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/FixedPoint96.sol"; ++import {IUniswapV3Pool} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; ++import {IUniswapV3Factory} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Factory.sol"; ++import {LiquidityAmounts} from "@uniswap-v3-periphery-1.4.2-solc-0.8/libraries/LiquidityAmounts.sol"; ++import {BlastClaimer} from "../utils/BlastClaimer.sol"; + + + // Liquidity range +diff --git a/src/modules/CREDT.v1.sol b/src/modules/CREDT.v1.sol +index 2afa1c3..e920452 100644 +--- a/src/modules/CREDT.v1.sol ++++ b/src/modules/CREDT.v1.sol +@@ -1,11 +1,11 @@ + // SPDX-License-Identifier: MIT +-pragma solidity ^0.8.23; ++pragma solidity ^0.8.19; + +-import {ERC20} from "solmate/tokens/ERC20.sol"; ++import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; + +-import "src/Kernel.sol"; +-import {TimeslotLib} from "src/utils/TimeslotLib.sol"; +-import {BlastClaimer} from "src/utils/BlastClaimer.sol"; ++import {Kernel, Module, Keycode, toKeycode} from "../Kernel.sol"; ++import {TimeslotLib} from "../utils/TimeslotLib.sol"; ++import {BlastClaimer} from "../utils/BlastClaimer.sol"; + + /// @notice Individual credit account information per user + struct CreditAccount { +diff --git a/src/modules/LOOPS.v1.sol b/src/modules/LOOPS.v1.sol +index 0ee6fe1..966c622 100644 +--- a/src/modules/LOOPS.v1.sol ++++ b/src/modules/LOOPS.v1.sol +@@ -1,22 +1,20 @@ + // SPDX-License-Identifier: MIT +-pragma solidity ^0.8.23; ++pragma solidity ^0.8.19; + +-import {console2} from "forge-std/console2.sol"; ++import {console2} from "@forge-std-1.9.1/console2.sol"; + +-import {Owned} from "solmate/auth/Owned.sol"; +-import {ERC20} from "solmate/tokens/ERC20.sol"; +-import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +-import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; ++import {Owned} from "@solmate-6.7.0/auth/Owned.sol"; ++import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; ++import {SafeTransferLib} from "@solmate-6.7.0/utils/SafeTransferLib.sol"; ++import {FixedPointMathLib} from "@solady-0.0.124/utils/FixedPointMathLib.sol"; + +-import {FixedPoint96} from "@uniswap/v3-core/contracts/libraries/FixedPoint96.sol"; ++import {FixedPoint96} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/FixedPoint96.sol"; + + // Kernel dependencies +-import "src/Kernel.sol"; +-import {BPOOLv1, Range, Position, IUniswapV3Pool} from "src/modules/BPOOL.v1.sol"; ++import "../Kernel.sol"; ++import {BPOOLv1, Range, Position, IUniswapV3Pool} from "../modules/BPOOL.v1.sol"; + +-import {console2} from "forge-std/console2.sol"; +- +-import {BlastClaimer} from "src/utils/BlastClaimer.sol"; ++import {BlastClaimer} from "../utils/BlastClaimer.sol"; + + /// @title LOOPSv1 + +@@ -142,7 +140,7 @@ contract LOOPSv1 is Module { + + bAsset.transfer(msg.sender, collateralRedeemed_); + } +- ++ + function chargeFunding() external permissioned { + _chargeFunding(); + } +diff --git a/src/policies/MarketMaking.sol b/src/policies/MarketMaking.sol +index 4b9eb25..de74542 100644 +--- a/src/policies/MarketMaking.sol ++++ b/src/policies/MarketMaking.sol +@@ -1,20 +1,20 @@ + // SPDX-Identifier: AGPL-3.0-only + pragma solidity ^0.8.0; + +-import {Owned} from "solmate/auth/Owned.sol"; +-import {ERC20} from "solmate/tokens/ERC20.sol"; +-import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; +-import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +-import {LiquidityAmounts} from "v3-periphery/libraries/LiquidityAmounts.sol"; +- +-import {TickMath} from "v3-core/libraries/TickMath.sol"; +-import {FixedPoint96} from "v3-core/libraries/FixedPoint96.sol"; +- +-import "src/Kernel.sol"; +-import {BPOOLv1, Range, Ticks, Position, IUniswapV3Pool} from "src/modules/BPOOL.v1.sol"; +-import {CREDTv1} from "src/modules/CREDT.v1.sol"; +-import {LOOPSv1} from "src/modules/LOOPS.v1.sol"; +-import {BlastClaimer} from "src/utils/BlastClaimer.sol"; ++import {Owned} from "@solmate-6.7.0/auth/Owned.sol"; ++import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; ++import {FixedPointMathLib} from "@solady-0.0.124/utils/FixedPointMathLib.sol"; ++import {SafeTransferLib} from "@solmate-6.7.0/utils/SafeTransferLib.sol"; ++import {LiquidityAmounts} from "@uniswap-v3-periphery-1.4.2-solc-0.8/libraries/LiquidityAmounts.sol"; ++ ++import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; ++import {FixedPoint96} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/FixedPoint96.sol"; ++ ++import "../Kernel.sol"; ++import {BPOOLv1, Range, Ticks, Position, IUniswapV3Pool} from "../modules/BPOOL.v1.sol"; ++import {CREDTv1} from "../modules/CREDT.v1.sol"; ++import {LOOPSv1} from "../modules/LOOPS.v1.sol"; ++import {BlastClaimer} from "../utils/BlastClaimer.sol"; + + import {console2} from "forge-std/console2.sol"; + diff --git a/script/patch/baseline.sh b/script/patch/baseline.sh new file mode 100755 index 00000000..83f63b2d --- /dev/null +++ b/script/patch/baseline.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Move into the right directory +cd lib/baseline-v2 + +# Generate the diff +git diff . > ../../script/patch/baseline.patch + +echo "Done!" diff --git a/script/salts/README.md b/script/salts/README.md index d330bff7..a40f6b69 100644 --- a/script/salts/README.md +++ b/script/salts/README.md @@ -37,20 +37,20 @@ To generate a salt for a specific callback deployment, the following addresses a - Owner of the Baseline deployment (which will have the ability to withdraw reserves) - Reserve (quote) token -These addresses should match the arguments in the deployment sequence file. +These addresses are pulled from the deployment sequence file. The following script can then be run: ```bash -./script/salts/dtl-baseline/baseline_allocated_allowlist_salts.sh --kernel --owner --reserveToken +./script/salts/dtl-baseline/baseline_salts.sh --deployFile ``` ### Generating Salts for Uniswap Direct to Liquidity -Assuming that the developer wants to deploy a Uniswap V3 direct to liquidity callback for atomic auctions, the following command would be run: +The following command will generate salts for any Uniswap DTL callbacks in the specified deployment sequence file: ```bash -./script/salts/dtl-uniswap/uniswap_dtl_salts.sh --type atomic --version 3 +./script/salts/dtl-uniswap/uniswap_dtl_salts.sh --deployFile ``` ### Generating Salts for Any Contract diff --git a/script/salts/allowlist/AllowlistSalts.s.sol b/script/salts/allowlist/AllowlistSalts.s.sol index ff48e55a..2bf41a45 100644 --- a/script/salts/allowlist/AllowlistSalts.s.sol +++ b/script/salts/allowlist/AllowlistSalts.s.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.19; // Scripting libraries import {Script, console2} from "@forge-std-1.9.1/Script.sol"; -import {WithEnvironment} from "../../deploy/WithEnvironment.s.sol"; import {WithSalts} from "../WithSalts.s.sol"; +import {WithDeploySequence} from "../../deploy/WithDeploySequence.s.sol"; // Libraries -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; // Callbacks import {MerkleAllowlist} from "../../../src/callbacks/allowlists/MerkleAllowlist.sol"; @@ -16,30 +16,18 @@ import {TokenAllowlist} from "../../../src/callbacks/allowlists/TokenAllowlist.s import {AllocatedMerkleAllowlist} from "../../../src/callbacks/allowlists/AllocatedMerkleAllowlist.sol"; -contract AllowlistSalts is Script, WithEnvironment, WithSalts { +contract AllowlistSalts is Script, WithDeploySequence, WithSalts { + // All of these allowlists have the same permissions and constructor args string internal constant _ADDRESS_PREFIX = "98"; - function _setUp(string calldata chain_) internal { - _loadEnv(chain_); + function _setUp(string calldata chain_, string calldata sequenceFilePath_) internal { + _loadSequence(chain_, sequenceFilePath_); _createBytecodeDirectory(); } - function generate(string calldata chain_, bool atomic_) public { - _setUp(chain_); - - address auctionHouse; - if (atomic_) { - auctionHouse = _envAddress("deployments.AtomicAuctionHouse"); - console2.log("AtomicAuctionHouse:", auctionHouse); - } else { - auctionHouse = _envAddress("deployments.BatchAuctionHouse"); - console2.log("BatchAuctionHouse:", auctionHouse); - } - - // All of these allowlists have the same permissions and constructor args - string memory prefix = "98"; - bytes memory args = abi.encode( - auctionHouse, + function _getContractArgs(address auctionHouse_) internal pure returns (bytes memory) { + return abi.encode( + auctionHouse_, Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -51,33 +39,139 @@ contract AllowlistSalts is Script, WithEnvironment, WithSalts { sendBaseTokens: false }) ); + } + + function _getAuctionHouse(bool atomic_) internal view returns (address) { + return atomic_ + ? _envAddressNotZero("deployments.AtomicAuctionHouse") + : _envAddressNotZero("deployments.BatchAuctionHouse"); + } + + function generate(string calldata chain_, string calldata deployFilePath_) public { + _setUp(chain_, deployFilePath_); - // Merkle Allowlist - // 10011000 = 0x98 + // Iterate over the deployment sequence + string[] memory sequenceNames = _getSequenceNames(); + for (uint256 i; i < sequenceNames.length; i++) { + string memory sequenceName = sequenceNames[i]; + console2.log(""); + console2.log("Generating salt for :", sequenceName); + + string memory deploymentKey = _getDeploymentKey(sequenceName); + console2.log(" deploymentKey: %s", deploymentKey); + + // Atomic MerkleAllowlist + if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicMerkleAllowlist")) + ) { + _generateMerkleAllowlist(sequenceName, deploymentKey, true); + } + // Batch MerkleAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchMerkleAllowlist")) + ) { + _generateMerkleAllowlist(sequenceName, deploymentKey, false); + } + // Atomic CappedMerkleAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicCappedMerkleAllowlist")) + ) { + _generateCappedMerkleAllowlist(sequenceName, deploymentKey, true); + } + // Batch CappedMerkleAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchCappedMerkleAllowlist")) + ) { + _generateCappedMerkleAllowlist(sequenceName, deploymentKey, false); + } + // Atomic AllocatedMerkleAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicAllocatedMerkleAllowlist")) + ) { + _generateAllocatedMerkleAllowlist(sequenceName, deploymentKey, true); + } + // Batch AllocatedMerkleAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchAllocatedMerkleAllowlist")) + ) { + _generateAllocatedMerkleAllowlist(sequenceName, deploymentKey, false); + } + // Atomic TokenAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicTokenAllowlist")) + ) { + _generateTokenAllowlist(sequenceName, deploymentKey, true); + } + // Batch TokenAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchTokenAllowlist")) + ) { + _generateTokenAllowlist(sequenceName, deploymentKey, false); + } + // Something else + else { + console2.log(" Skipping unknown sequence: %s", sequenceName); + } + } + } + + function _generateMerkleAllowlist( + string memory, + string memory deploymentKey_, + bool atomic_ + ) internal { bytes memory contractCode = type(MerkleAllowlist).creationCode; - string memory saltKey = "MerkleAllowlist"; + bytes memory contractArgs = _getContractArgs(_getAuctionHouse(atomic_)); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateCappedMerkleAllowlist( + string memory, + string memory deploymentKey_, + bool atomic_ + ) internal { + bytes memory contractCode = type(CappedMerkleAllowlist).creationCode; + bytes memory contractArgs = _getContractArgs(_getAuctionHouse(atomic_)); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateAllocatedMerkleAllowlist( + string memory, + string memory deploymentKey_, + bool atomic_ + ) internal { + bytes memory contractCode = type(AllocatedMerkleAllowlist).creationCode; + bytes memory contractArgs = _getContractArgs(_getAuctionHouse(atomic_)); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateTokenAllowlist( + string memory, + string memory deploymentKey_, + bool atomic_ + ) internal { + bytes memory contractCode = type(TokenAllowlist).creationCode; + bytes memory contractArgs = _getContractArgs(_getAuctionHouse(atomic_)); + (string memory bytecodePath, bytes32 bytecodeHash) = - _writeBytecode(saltKey, contractCode, args); - _setSalt(bytecodePath, prefix, saltKey, bytecodeHash); - - // Capped Merkle Allowlist - // 10011000 = 0x98 - contractCode = type(CappedMerkleAllowlist).creationCode; - saltKey = "CappedMerkleAllowlist"; - (bytecodePath, bytecodeHash) = _writeBytecode(saltKey, contractCode, args); - _setSalt(bytecodePath, prefix, saltKey, bytecodeHash); - - // Token Allowlist - // 10011000 = 0x98 - contractCode = type(TokenAllowlist).creationCode; - saltKey = "TokenAllowlist"; - (bytecodePath, bytecodeHash) = _writeBytecode(saltKey, contractCode, args); - _setSalt(bytecodePath, prefix, saltKey, bytecodeHash); - - // Allocated Allowlist - contractCode = type(AllocatedMerkleAllowlist).creationCode; - saltKey = "AllocatedMerkleAllowlist"; - (bytecodePath, bytecodeHash) = _writeBytecode(saltKey, contractCode, args); - _setSalt(bytecodePath, prefix, saltKey, bytecodeHash); + _writeBytecode(deploymentKey_, contractCode, contractArgs); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); } } diff --git a/script/salts/allowlist/allowlist_salts.sh b/script/salts/allowlist/allowlist_salts.sh index d2dfbe0c..1f97efb2 100755 --- a/script/salts/allowlist/allowlist_salts.sh +++ b/script/salts/allowlist/allowlist_salts.sh @@ -1,7 +1,7 @@ #!/bin/bash # Usage: -# ./allowlist_salts.sh --type --envFile <.env> +# ./allowlist_salts.sh --deployFile --envFile <.env> # # Expects the following environment variables: # CHAIN: The chain to deploy to, based on values from the ./script/env.json file. @@ -17,6 +17,8 @@ while [ $# -gt 0 ]; do shift done +DEPLOY_FILE=$deployFile + # Get the name of the .env file or use the default ENV_FILE=${envFile:-".env"} echo "Sourcing environment variables from $ENV_FILE" @@ -33,18 +35,15 @@ then exit 1 fi -# Check that the mode is "atomic" or "batch" -if [ "$type" != "atomic" ] && [ "$type" != "batch" ] +# Check if DEPLOY_FILE is set +if [ -z "$DEPLOY_FILE" ] then - echo "Invalid type specified. Provide 'atomic' or 'batch' after the --type flag." + echo "No deploy file specified. Provide the relative path after the --deployFile flag." exit 1 fi -# Set flag for atomic or batch auction -ATOMIC=$( if [ "$type" == "atomic" ]; then echo "true"; else echo "false"; fi ) - -echo "Using RPC at URL: $RPC_URL" echo "Using chain: $CHAIN" -echo "Using type: $type" +echo "Using RPC at URL: $RPC_URL" +echo "Using deploy file: $DEPLOY_FILE" -forge script ./script/salts/allowlist/AllowListSalts.s.sol:AllowlistSalts --sig "generate(string,bool)()" $CHAIN $ATOMIC +forge script ./script/salts/allowlist/AllowListSalts.s.sol:AllowlistSalts --sig "generate(string,string)()" $CHAIN $DEPLOY_FILE diff --git a/script/salts/dtl-baseline/BaselineAllocatedAllowlistSalts.s.sol b/script/salts/dtl-baseline/BaselineAllocatedAllowlistSalts.s.sol deleted file mode 100644 index 1c9d5a9a..00000000 --- a/script/salts/dtl-baseline/BaselineAllocatedAllowlistSalts.s.sol +++ /dev/null @@ -1,51 +0,0 @@ -/// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -// Scripting libraries -import {Script, console2} from "@forge-std-1.9.1/Script.sol"; -import {WithEnvironment} from "../../deploy/WithEnvironment.s.sol"; -import {WithSalts} from "../WithSalts.s.sol"; - -import {BALwithAllocatedAllowlist} from - "../../../src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol"; - -contract BaselineAllocatedAllowlistSalts is Script, WithEnvironment, WithSalts { - string internal constant _ADDRESS_PREFIX = "EF"; - - address internal _envBatchAuctionHouse; - - function _setUp(string calldata chain_) internal { - _loadEnv(chain_); - _createBytecodeDirectory(); - - // Cache auction houses - _envBatchAuctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); - console2.log("BatchAuctionHouse:", _envBatchAuctionHouse); - } - - function generate( - string calldata chain_, - string calldata baselineKernel_, - string calldata baselineOwner_, - string calldata reserveToken_ - ) public { - _setUp(chain_); - - _generateSalt( - abi.encode( - _envBatchAuctionHouse, - vm.parseAddress(baselineKernel_), - vm.parseAddress(reserveToken_), - vm.parseAddress(baselineOwner_) - ) - ); - } - - function _generateSalt(bytes memory contractArgs_) internal { - bytes memory contractCode = type(BALwithAllocatedAllowlist).creationCode; - - (string memory bytecodePath, bytes32 bytecodeHash) = - _writeBytecode("BaselineAllocatedAllowlist", contractCode, contractArgs_); - _setSalt(bytecodePath, _ADDRESS_PREFIX, "BaselineAllocatedAllowlist", bytecodeHash); - } -} diff --git a/script/salts/dtl-baseline/BaselineSalts.s.sol b/script/salts/dtl-baseline/BaselineSalts.s.sol new file mode 100644 index 00000000..ba1d0f74 --- /dev/null +++ b/script/salts/dtl-baseline/BaselineSalts.s.sol @@ -0,0 +1,171 @@ +/// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Scripting libraries +import {Script, console2} from "@forge-std-1.9.1/Script.sol"; +import {WithSalts} from "../WithSalts.s.sol"; +import {WithDeploySequence} from "../../deploy/WithDeploySequence.s.sol"; + +import {BaselineAxisLaunch} from + "../../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; +import {BALwithAllocatedAllowlist} from + "../../../src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol"; +import {BALwithAllowlist} from "../../../src/callbacks/liquidity/BaselineV2/BALwithAllowlist.sol"; +import {BALwithCappedAllowlist} from + "../../../src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol"; +import {BALwithTokenAllowlist} from + "../../../src/callbacks/liquidity/BaselineV2/BALwithTokenAllowlist.sol"; + +contract BaselineSalts is Script, WithDeploySequence, WithSalts { + string internal constant _ADDRESS_PREFIX = "EF"; + + address internal _envBatchAuctionHouse; + + function _setUp(string calldata chain_, string calldata sequenceFilePath_) internal { + _loadSequence(chain_, sequenceFilePath_); + _createBytecodeDirectory(); + + // Cache auction houses + _envBatchAuctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); + } + + function generate(string calldata chain_, string calldata deployFilePath_) public { + _setUp(chain_, deployFilePath_); + + // Iterate over the deployment sequence + string[] memory sequenceNames = _getSequenceNames(); + for (uint256 i; i < sequenceNames.length; i++) { + string memory sequenceName = sequenceNames[i]; + console2.log(""); + console2.log("Generating salt for :", sequenceName); + + string memory deploymentKey = _getDeploymentKey(sequenceName); + console2.log(" deploymentKey: %s", deploymentKey); + + // BaselineAxisLaunch + if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchBaselineAxisLaunch")) + ) { + _generateBaselineAxisLaunch(sequenceName, deploymentKey); + } + // BaselineAllocatedAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchBaselineAllocatedAllowlist")) + ) { + _generateBaselineAllocatedAllowlist(sequenceName, deploymentKey); + } + // BaselineAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchBaselineAllowlist")) + ) { + _generateBaselineAllowlist(sequenceName, deploymentKey); + } + // BaselineCappedAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchBaselineCappedAllowlist")) + ) { + _generateBaselineCappedAllowlist(sequenceName, deploymentKey); + } + // BaselineTokenAllowlist + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchBaselineTokenAllowlist")) + ) { + _generateBaselineTokenAllowlist(sequenceName, deploymentKey); + } + // Something else + else { + console2.log(" Skipping unknown sequence: %s", sequenceName); + } + } + } + + function _generateBaselineAxisLaunch( + string memory sequenceName_, + string memory deploymentKey_ + ) internal { + bytes memory contractCode = type(BaselineAxisLaunch).creationCode; + bytes memory contractArgs_ = abi.encode( + _envBatchAuctionHouse, + _getSequenceAddress(sequenceName_, "args.baselineKernel"), + _getSequenceAddress(sequenceName_, "args.reserveToken"), + _getSequenceAddress(sequenceName_, "args.baselineOwner") + ); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs_); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateBaselineAllocatedAllowlist( + string memory sequenceName_, + string memory deploymentKey_ + ) internal { + bytes memory contractCode = type(BALwithAllocatedAllowlist).creationCode; + bytes memory contractArgs_ = abi.encode( + _envBatchAuctionHouse, + _getSequenceAddress(sequenceName_, "args.baselineKernel"), + _getSequenceAddress(sequenceName_, "args.reserveToken"), + _getSequenceAddress(sequenceName_, "args.baselineOwner") + ); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs_); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateBaselineAllowlist( + string memory sequenceName_, + string memory deploymentKey_ + ) internal { + bytes memory contractCode = type(BALwithAllowlist).creationCode; + bytes memory contractArgs_ = abi.encode( + _envBatchAuctionHouse, + _getSequenceAddress(sequenceName_, "args.baselineKernel"), + _getSequenceAddress(sequenceName_, "args.reserveToken"), + _getSequenceAddress(sequenceName_, "args.baselineOwner") + ); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs_); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateBaselineCappedAllowlist( + string memory sequenceName_, + string memory deploymentKey_ + ) internal { + bytes memory contractCode = type(BALwithCappedAllowlist).creationCode; + bytes memory contractArgs_ = abi.encode( + _envBatchAuctionHouse, + _getSequenceAddress(sequenceName_, "args.baselineKernel"), + _getSequenceAddress(sequenceName_, "args.reserveToken"), + _getSequenceAddress(sequenceName_, "args.baselineOwner") + ); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs_); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateBaselineTokenAllowlist( + string memory sequenceName_, + string memory deploymentKey_ + ) internal { + bytes memory contractCode = type(BALwithTokenAllowlist).creationCode; + bytes memory contractArgs_ = abi.encode( + _envBatchAuctionHouse, + _getSequenceAddress(sequenceName_, "args.baselineKernel"), + _getSequenceAddress(sequenceName_, "args.reserveToken"), + _getSequenceAddress(sequenceName_, "args.baselineOwner") + ); + + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode(deploymentKey_, contractCode, contractArgs_); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } +} diff --git a/script/salts/dtl-baseline/baseline_allocated_allowlist_salts.sh b/script/salts/dtl-baseline/baseline_allocated_allowlist_salts.sh deleted file mode 100755 index babf4aee..00000000 --- a/script/salts/dtl-baseline/baseline_allocated_allowlist_salts.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -# Usage: -# ./baseline_allocated_allowlist_salts.sh --kernel --owner --reserveToken --envFile <.env> -# -# Expects the following environment variables: -# CHAIN: The chain to deploy to, based on values from the ./script/env.json file. - -# Iterate through named arguments -# Source: https://unix.stackexchange.com/a/388038 -while [ $# -gt 0 ]; do - if [[ $1 == *"--"* ]]; then - v="${1/--/}" - declare $v="$2" - fi - - shift -done - -# Get the name of the .env file or use the default -ENV_FILE=${envFile:-".env"} -echo "Sourcing environment variables from $ENV_FILE" - -# Load environment file -set -a # Automatically export all variables -source $ENV_FILE -set +a # Disable automatic export - -# Check that the CHAIN environment variable is set -if [ -z "$CHAIN" ] -then - echo "CHAIN environment variable is not set. Please set it in the .env file or provide it as an environment variable." - exit 1 -fi - -# Check that the kernel is a 40-byte address with a 0x prefix -if [[ ! "$kernel" =~ ^0x[0-9a-fA-F]{40}$ ]] -then - echo "Invalid kernel address specified. Provide a 40-byte address with a 0x prefix after the --kernel flag." - exit 1 -fi - -# Check that the owner is a 40-byte address with a 0x prefix -if [[ ! "$owner" =~ ^0x[0-9a-fA-F]{40}$ ]] -then - echo "Invalid owner address specified. Provide a 40-byte address with a 0x prefix after the --owner flag." - exit 1 -fi - -# Check that the reserve token is a 40-byte address with a 0x prefix -if [[ ! "$reserveToken" =~ ^0x[0-9a-fA-F]{40}$ ]] -then - echo "Invalid reserve token address specified. Provide a 40-byte address with a 0x prefix after the --reserveToken flag." - exit 1 -fi - -echo "Using chain: $CHAIN" -echo "Using RPC at URL: $RPC_URL" -echo "Using kernel: $kernel" -echo "Using owner: $owner" -echo "Using reserve token: $reserveToken" - -forge script ./script/salts/dtl-baseline/BaselineAllocatedAllowlistSalts.s.sol:BaselineAllocatedAllowlistSalts --sig "generate(string,string,string,string)()" $CHAIN $kernel $owner $reserveToken diff --git a/script/salts/dtl-baseline/baseline_salts.sh b/script/salts/dtl-baseline/baseline_salts.sh new file mode 100755 index 00000000..94378961 --- /dev/null +++ b/script/salts/dtl-baseline/baseline_salts.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Usage: +# ./baseline_salts.sh --deployFile --envFile <.env> +# +# Expects the following environment variables: +# CHAIN: The chain to deploy to, based on values from the ./script/env.json file. + +# Iterate through named arguments +# Source: https://unix.stackexchange.com/a/388038 +while [ $# -gt 0 ]; do + if [[ $1 == *"--"* ]]; then + v="${1/--/}" + declare $v="$2" + fi + + shift +done + +DEPLOY_FILE=$deployFile + +# Get the name of the .env file or use the default +ENV_FILE=${envFile:-".env"} +echo "Sourcing environment variables from $ENV_FILE" + +# Load environment file +set -a # Automatically export all variables +source $ENV_FILE +set +a # Disable automatic export + +# Check that the CHAIN environment variable is set +if [ -z "$CHAIN" ] +then + echo "CHAIN environment variable is not set. Please set it in the .env file or provide it as an environment variable." + exit 1 +fi + +# Check if DEPLOY_FILE is set +if [ -z "$DEPLOY_FILE" ] +then + echo "No deploy file specified. Provide the relative path after the --deployFile flag." + exit 1 +fi + +echo "Using chain: $CHAIN" +echo "Using RPC at URL: $RPC_URL" +echo "Using deploy file: $DEPLOY_FILE" + +forge script ./script/salts/dtl-baseline/BaselineSalts.s.sol:BaselineSalts --sig "generate(string,string)()" $CHAIN $DEPLOY_FILE diff --git a/script/salts/dtl-uniswap/UniswapDTLSalts.s.sol b/script/salts/dtl-uniswap/UniswapDTLSalts.s.sol index 0964fd97..a2452eb9 100644 --- a/script/salts/dtl-uniswap/UniswapDTLSalts.s.sol +++ b/script/salts/dtl-uniswap/UniswapDTLSalts.s.sol @@ -3,106 +3,117 @@ pragma solidity 0.8.19; // Scripting libraries import {Script, console2} from "@forge-std-1.9.1/Script.sol"; -import {WithEnvironment} from "../../deploy/WithEnvironment.s.sol"; import {WithSalts} from "../WithSalts.s.sol"; +import {WithDeploySequence} from "../../deploy/WithDeploySequence.s.sol"; // Uniswap import {UniswapV2DirectToLiquidity} from "../../../src/callbacks/liquidity/UniswapV2DTL.sol"; import {UniswapV3DirectToLiquidity} from "../../../src/callbacks/liquidity/UniswapV3DTL.sol"; -contract UniswapDTLSalts is Script, WithEnvironment, WithSalts { +contract UniswapDTLSalts is Script, WithDeploySequence, WithSalts { string internal constant _ADDRESS_PREFIX = "E6"; - address internal _envUniswapV2Factory; - address internal _envUniswapV2Router; - address internal _envUniswapV3Factory; - address internal _envGUniFactory; - - function _setUp(string calldata chain_) internal { - _loadEnv(chain_); + function _setUp(string calldata chain_, string calldata sequenceFilePath_) internal { + _loadSequence(chain_, sequenceFilePath_); _createBytecodeDirectory(); - - // Cache Uniswap factories - _envUniswapV2Factory = _envAddressNotZero("constants.uniswapV2.factory"); - console2.log("UniswapV2Factory:", _envUniswapV2Factory); - _envUniswapV2Router = _envAddressNotZero("constants.uniswapV2.router"); - console2.log("UniswapV2Router:", _envUniswapV2Router); - _envUniswapV3Factory = _envAddressNotZero("constants.uniswapV3.factory"); - console2.log("UniswapV3Factory:", _envUniswapV3Factory); - _envGUniFactory = _envAddressNotZero("constants.gUni.factory"); - console2.log("GUniFactory:", _envGUniFactory); } - function generate( - string calldata chain_, - string calldata uniswapVersion_, - bool atomic_ - ) public { - _setUp(chain_); - - if (keccak256(abi.encodePacked(uniswapVersion_)) == keccak256(abi.encodePacked("2"))) { - _generateV2(atomic_); - } else if (keccak256(abi.encodePacked(uniswapVersion_)) == keccak256(abi.encodePacked("3"))) - { - _generateV3(atomic_); - } else { - revert("Invalid Uniswap version: 2 or 3"); + function generate(string calldata chain_, string calldata deployFilePath_) public { + _setUp(chain_, deployFilePath_); + + // Iterate over the deployment sequence + string[] memory sequenceNames = _getSequenceNames(); + for (uint256 i; i < sequenceNames.length; i++) { + string memory sequenceName = sequenceNames[i]; + console2.log(""); + console2.log("Generating salt for :", sequenceName); + + string memory deploymentKey = _getDeploymentKey(sequenceName); + console2.log(" deploymentKey: %s", deploymentKey); + + // Atomic Uniswap V2 + if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicUniswapV2DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.AtomicAuctionHouse"); + + _generateV2(sequenceName, auctionHouse, deploymentKey); + } + // Batch Uniswap V2 + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchUniswapV2DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); + + _generateV2(sequenceName, auctionHouse, deploymentKey); + } + // Atomic Uniswap V3 + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicUniswapV3DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.AtomicAuctionHouse"); + + _generateV3(sequenceName, auctionHouse, deploymentKey); + } + // Batch Uniswap V3 + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchUniswapV3DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); + + _generateV3(sequenceName, auctionHouse, deploymentKey); + } + // Something else + else { + console2.log(" Skipping unknown sequence: %s", sequenceName); + } } } - function _generateV2(bool atomic_) internal { - if (atomic_) { - address _envAtomicAuctionHouse = _envAddressNotZero("deployments.AtomicAuctionHouse"); - console2.log("AtomicAuctionHouse:", _envAtomicAuctionHouse); - - // Calculate salt for the UniswapV2DirectToLiquidity - bytes memory contractCode = type(UniswapV2DirectToLiquidity).creationCode; - (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( - "UniswapV2DirectToLiquidity", - contractCode, - abi.encode(_envAtomicAuctionHouse, _envUniswapV2Factory, _envUniswapV2Router) - ); - _setSalt(bytecodePath, _ADDRESS_PREFIX, "UniswapV2DirectToLiquidity", bytecodeHash); - } else { - address _envBatchAuctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); - console2.log("BatchAuctionHouse:", _envBatchAuctionHouse); - - // Calculate salt for the UniswapV2DirectToLiquidity - bytes memory contractCode = type(UniswapV2DirectToLiquidity).creationCode; - (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( - "UniswapV2DirectToLiquidity", - contractCode, - abi.encode(_envBatchAuctionHouse, _envUniswapV2Factory, _envUniswapV2Router) - ); - _setSalt(bytecodePath, _ADDRESS_PREFIX, "UniswapV2DirectToLiquidity", bytecodeHash); - } + function _generateV2( + string memory sequenceName_, + address auctionHouse_, + string memory deploymentKey_ + ) internal { + // Get input variables or overrides + address envUniswapV2Factory = + _getEnvAddressOrOverride("constants.uniswapV2.factory", sequenceName_, "args.factory"); + address envUniswapV2Router = + _getEnvAddressOrOverride("constants.uniswapV2.router", sequenceName_, "args.router"); + + // Calculate salt for the UniswapV2DirectToLiquidity + bytes memory contractCode = type(UniswapV2DirectToLiquidity).creationCode; + (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( + deploymentKey_, + contractCode, + abi.encode(auctionHouse_, envUniswapV2Factory, envUniswapV2Router) + ); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); } - function _generateV3(bool atomic_) internal { - if (atomic_) { - address _envAtomicAuctionHouse = _envAddressNotZero("deployments.AtomicAuctionHouse"); - console2.log("AtomicAuctionHouse:", _envAtomicAuctionHouse); - - // Calculate salt for the UniswapV3DirectToLiquidity - bytes memory contractCode = type(UniswapV3DirectToLiquidity).creationCode; - (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( - "UniswapV3DirectToLiquidity", - contractCode, - abi.encode(_envAtomicAuctionHouse, _envUniswapV3Factory, _envGUniFactory) - ); - _setSalt(bytecodePath, _ADDRESS_PREFIX, "UniswapV3DirectToLiquidity", bytecodeHash); - } else { - address _envBatchAuctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); - console2.log("BatchAuctionHouse:", _envBatchAuctionHouse); - - // Calculate salt for the UniswapV3DirectToLiquidity - bytes memory contractCode = type(UniswapV3DirectToLiquidity).creationCode; - (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( - "UniswapV3DirectToLiquidity", - contractCode, - abi.encode(_envBatchAuctionHouse, _envUniswapV3Factory, _envGUniFactory) - ); - _setSalt(bytecodePath, _ADDRESS_PREFIX, "UniswapV3DirectToLiquidity", bytecodeHash); - } + function _generateV3( + string memory sequenceName_, + address auctionHouse_, + string memory deploymentKey_ + ) internal { + // Get input variables or overrides + address envUniswapV3Factory = _getEnvAddressOrOverride( + "constants.uniswapV3.factory", sequenceName_, "args.uniswapV3Factory" + ); + address envGUniFactory = + _getEnvAddressOrOverride("constants.gUni.factory", sequenceName_, "args.gUniFactory"); + + // Calculate salt for the UniswapV2DirectToLiquidity + bytes memory contractCode = type(UniswapV3DirectToLiquidity).creationCode; + (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( + deploymentKey_, + contractCode, + abi.encode(auctionHouse_, envUniswapV3Factory, envGUniFactory) + ); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); } } diff --git a/script/salts/dtl-uniswap/uniswap_dtl_salts.sh b/script/salts/dtl-uniswap/uniswap_dtl_salts.sh index 68640f24..07f6b1ee 100755 --- a/script/salts/dtl-uniswap/uniswap_dtl_salts.sh +++ b/script/salts/dtl-uniswap/uniswap_dtl_salts.sh @@ -1,7 +1,7 @@ #!/bin/bash # Usage: -# ./uniswap_dtl_salts.sh --version <2 | 3> --type --envFile <.env> +# ./uniswap_dtl_salts.sh --deployFile --envFile <.env> # # Expects the following environment variables: # CHAIN: The chain to deploy to, based on values from the ./script/env.json file. @@ -17,6 +17,8 @@ while [ $# -gt 0 ]; do shift done +DEPLOY_FILE=$deployFile + # Get the name of the .env file or use the default ENV_FILE=${envFile:-".env"} echo "Sourcing environment variables from $ENV_FILE" @@ -33,26 +35,15 @@ then exit 1 fi -# Check that the version is 2 or 3 -if [ "$version" != "2" ] && [ "$version" != "3" ] -then - echo "Invalid version specified. Provide '2' or '3' after the --version flag." - exit 1 -fi - -# Check that the mode is "atomic" or "batch" -if [ "$type" != "atomic" ] && [ "$type" != "batch" ] +# Check if DEPLOY_FILE is set +if [ -z "$DEPLOY_FILE" ] then - echo "Invalid mode specified. Provide 'atomic' or 'batch' after the --type flag." + echo "No deploy file specified. Provide the relative path after the --deployFile flag." exit 1 fi -# Set flag for atomic or batch auction -ATOMIC=$( if [ "$type" == "atomic" ]; then echo "true"; else echo "false"; fi ) - echo "Using chain: $CHAIN" echo "Using RPC at URL: $RPC_URL" -echo "Using Uniswap version: $version" -echo "Using auction type: $type" +echo "Using deploy file: $DEPLOY_FILE" -forge script ./script/salts/dtl-uniswap/UniswapDTLSalts.s.sol:UniswapDTLSalts --sig "generate(string,string,bool)()" $CHAIN $version $ATOMIC +forge script ./script/salts/dtl-uniswap/UniswapDTLSalts.s.sol:UniswapDTLSalts --sig "generate(string,string)()" $CHAIN $DEPLOY_FILE diff --git a/script/salts/salts.json b/script/salts/salts.json index e8756b2d..9f105055 100644 --- a/script/salts/salts.json +++ b/script/salts/salts.json @@ -4,12 +4,18 @@ "0x1330bbebf9210aa32cfa7221ca21157fee8f977c21e2e9bb9d396e02000726b2": "0x8cdbe3133136ece80f286b64a3dabf2a9d1bd5868e8a851a7747048f17e44717", "0x23a85d2e69471278dd573d17331b360da25418c731483eca071acc91667846fa": "0x775d17e9071a23da2e1bcf5addb052676b2e180f3e69fb810613fa19f203a48f", "0x30b51492b0fefba6189938e77c04874f66af1ae415576b9a0870e3d831af3aeb": "0xb21446946154104802ca94a722f3d9c017dedcf52633e778343e049f558fc5e5", + "0x4d30b549c7212559fb363873d0a919519a777aa48b43283e6706c559feedbe3d": "0xeba93e294d98498cbcf13353d41fcd20e6614db69524ca151e940625ddeef2a0", "0x4db54f4a6df5f03f26f181efffd95909d94800d3b955d3606e12682b9bcbd89a": "0x5438ed572ff2d286c6686cc1ff5ab265f61aa3dc48953f7aa2b2483b84d4cd4e", "0x6257c89e534c1414806da36437177a5ad7e372d98a2c231396c638f0e3e01986": "0x4d82fff8053192aea9b71c7cbc74250b30c2b870b08016dac5cd40eaf6f0ea68", + "0x675e9cfd316f1b82acb4cd74b88730542e3d78d1e7dbe2d08e5898f91a6ae566": "0xe136e5a60fd476c2c8b8b12d85694e064a763fa8f800beb1f66e6d377ce7c738", + "0x6a8e22a0cb459c939dc0e824e541595eec23331a54407277eb159c352d6818ae": "0xd77b6006c974ac081a05f819c213f47f35b3d57706a8fcb69bf3582eaf009edf", "0x829a6a78304229812355cc3fd4ffbb383e35120e39942f1547bce97c9156f204": "0x12550234064c7913456e44b288c40022cf5b78c608f5e7f5ef0489157f49478f", + "0x8ad2d0fd5d14e63b5ca8eca64b6c401a81561e4e396f71d705aea3804e83fe72": "0xd0d3a6d37454e9ac9294a4c3ae19da00d4ef6cc410649a8710fa6fe1674c0a7c", "0x940f58c92177218354ef28efcd7056b131053fc33f27336c68af3eda9b7a537f": "0x9471763f5a9abcd58fee06a10aede685e570750ddc1de9f9e742130bb8f6369c", "0xaa5c1bd02f7b04980f0e4ef0778e9165f8404f41c1c2bc372f2902f80dc646b0": "0x3c781e03bd8272c21052708923b6b00b60d976b84bc792a25a8eeb527d166944", - "0xc329d36cea27b1f8e044c99a5eda27503fd6087f50b6d27b7bb12ac4493507e1": "0xb724ad108002e85a8144c2b305013b4d302fb1ef4a39d477f6d18e96bc219a3d" + "0xb26a516f1306dfc5ce38c64779dc8e1df2939c53690f6581c4d4045ddaa1725b": "0x5404e4d1cd90b76de4433faeb8d6df8be341d96c36c835e21bd9701bffe6dcf4", + "0xc329d36cea27b1f8e044c99a5eda27503fd6087f50b6d27b7bb12ac4493507e1": "0xb724ad108002e85a8144c2b305013b4d302fb1ef4a39d477f6d18e96bc219a3d", + "0xf4d7d42633353828c29211c77c068516380a9c75c942fba88b1c01215c1001a6": "0xb07de304d67101f3cef70ee4e023a361d50b19eb13b59729724ad64e1f6aa88b" }, "CappedMerkleAllowlist": { "0x0249dded8310d17581f166a526ea9ded65b81112ccac0ee8a144e9770d2b8432": "0xf5746fa34aeff9866dc5ec58712f7f47045aea15db39a0aabf4dadc9d35c8854", @@ -20,9 +26,15 @@ "0x3b13d752dd521a5917210eff552f4d0ec3ff4ddb2d194f5d02b7e2247489ebdc": "0xf1ff9fa131a9733c7bdb0d3a3bd2f0ff01a4460e6886df4c7099947ebcfb1ee9", "0x3fc1cd506bf64c403917a5799b49e036dd3aae5428060a18d3c19d6e6065498e": "0xd24c83ddb154d7c984384db1f8da3ab3307028e5f1cc43031c4243a81e25edb6", "0x41e4959210f69f1bc43a7e525cd5c4d9552ded294727f411ff30fe70c69a4b76": "0x20e7f379ad74ce616829a0e336c3b9c48e0d2a74f219219e32e02fe2a982da7d", + "0x523a12ad295c7c977774285d48837fa623be38bef2945d5c66b099bd68594493": "0x373dbd52eceb8a0a5cb6ac33704c08e69e489feb8eda83b814f687ed3d2173bd", + "0x562eee79fc29bd2a5fc214ed38a42fcbe56b2b9e76670511815c8efe315ba1f0": "0x1fff162f437cf948a2465879268134cd406a6b8248c414082b74c8183ce71a98", + "0x56385d3b5a2d25f060adfb109c32f9176695c349b2443e4133bb316f648d2193": "0xf4dc1410206ca7e9004a11a43a442978badd0180e71cc7dc0539de1a16f6f178", "0x7629e3374867d748eaab783ad2eedb4e39634e55428c39713fd82415d41043a3": "0x31246341c7c82c31414f184b807da273fdd6cdf591cb6f997fec41babc279fd9", "0x78ee3a1a70f7e9a784590ca00ef3290f3478105f6338266b9e5f0d10951b4aa9": "0xdb5af3b6d32454d69075509db94196460a19acb9849c5dc0d5ccef5a1b4ab032", + "0x80508160c6c61727258a21aace5a739f56cd765182ce378f16b9c67c48e13e3b": "0x87edaedcc135ccbb0b593338d1ba220a3a945a57ccaa312006638e4ba4570d66", + "0x856ab34b7fc9fb4d1c6ea6e991bb8c54610a00292a7690fd42fa17a0ba98f334": "0xbb33ffa4c725b59cf9ee69478c1d7961f1cea7ac3909d033fa18f9cac0521b26", "0xa59fba1bee7eda5c33e9d52daa5505c2cf60b89d41566b07c2e601a34990a631": "0x5613adffef93f5433b34f4c2f38569b6694af67aa4cbacf7faab427824aa8b94", + "0xaeb77d3e4122b31a81198a272b5b8791e362466d3c99cbc47248e09e428c08f9": "0xf525528a63311abbb496e0ec55cb55c800bdc17645aba920d17e9ee12a3deb9f", "0xba7c6b1a20d51f9d58af92b2ad7ef3e41e1468b723bedeaa967ad18324a01396": "0x008360de5819a25fa0b4f08212b0b7745df530809622970e4dfefc77cb8c5066", "0xbece95cede2c4b6f2712067328ed372606a7ec24649023996465365239ed5c23": "0x43af01d498eaa96528f948867f7b771b700b090e1b1077c95ecc1394265f1af9", "0xec2a597ad93b51f9a58ce3a90dc777d51183931ff91f23ba7d685f36d08a5fb5": "0x09ef29485dadd335f15b530a213bf16fe826b712a6d9704d86d2980a775c6309" @@ -31,118 +43,128 @@ "0x00e31f39c32ce8511a5db04dec4a0a92b29eb2cb02a965f107dbeb9549517e6a": "0xcfcc4407dd569ab1e457b9d475420c8d10e7db6751646b5aa84cfacf500947ba", "0x13f8dde16815b53fad1d222ffd783f6ed99b01992a48aac29791352ac7578576": "0x605db39e79afd6a7e67bd3055afc358a69143d97544404d75b8091c2d2c82fa1", "0x2cc37ca69a4e1ef00579453aad3e7dd939350a9b53f3dfa1f20896f0cd1dbddb": "0x87a479d8c7b3a31f53b0ae53979510bcb06d09a2e202432ac40581c6bc37ff08", + "0x34c99e87b5c6e6aef4da408c1176f7219591222bd8de719e604ea5abb6e2406c": "0xc138b9a5ac8fa91c369fb3cf5288293e7461d8d55136d5d6b59730e4f7f79a7f", + "0x445be4d72498dcd078ba9cb34919b6fdeb29fbe9655a1acc10f8fd845412877a": "0x471764505a245cb0e5c03f0643a3c61f4fb328fb8e78e144bd8809d70bd7159f", "0x48ea0384a799d4ff2b7287d3cf0736171352670aeb2b33bc18c70cbc5abd0fce": "0x179e7c820be6b4a6cfb0ce475443dd900c553322dbd3d38eda34dcfab7b20b81", + "0x54a93e1adcdc386c2f00f655905adc086dc031d337a3bad4e5c3fd5eced91a5a": "0x5eb6ce5d2582c46e7002c5f21e847692f7ef102e2e6c42becd0fdb65b7064961", + "0x66a93d33b3385f49f1d7b15290ebee270691da8a7d3b810c569f070b00b09c92": "0x958c9b00cbcc7fb850da4dcad487280d86667b9c70d49e886af320376dcfd189", "0x772e272fd6e30185ccd0d9d485a87d71b47d93ad31dcdb274bbde5002f12b12d": "0x5b5c6bacfcaa186167a4345af4ba4facedb0bb7693daa623b9db75f06b89c13f", + "0x890c6aa705b33a22cca29b6ccc2d5c62f08fd53c37d979d5cb5d3b34d49560bb": "0x9bc9b96b729219d1fe499d065d642e99847b7ba841ddd71e483550549f8050cb", "0x8dabf7c769c996b5b33b4da1013d93b9daeeb7cf468d179804c9130fd7eaa891": "0x71072df633972dfa166196430a104dbc30b8dc5997a8b965253ca299fd96feaf", "0xb4128aa3cf1f256d76081a556cbe66c83429e49ce28bca3b2e3b07b926f0bda8": "0xcbac00a3b5465e91d8c65d179de9fa8f52be1c99b8ecfd9ce35b51faa0046232", "0xc56445d8a92973e5e7a453750ca38da11f04e2c965b3ae42a0f77ccdf2cd5b33": "0xc38225a585e134360208d9f73f4bdfaca9b43cd130f1a6cf0fd631e9877e32a6", "0xc8e647c61be9e05b08050eb7d8cfc1102b5328d9540b81891da5783ba25d2978": "0x368c44b5208a15f6292a0ac1246d7d6973d5c919d32cebd2f30da0844356e63a", "0xcd18d417a886a13a14969e2690fc3af13f228e73a02e48d9d4e8a6b3fa61504a": "0x7aa449a66650f30013cfd8021a16640d3489de2246ec5ad62f3ccbe5e84f819a", + "0xcedd373f78cb0d3f0efca85cf6c2566e0a877105b03c388c6f51d9a323ac7640": "0xbcc955cab573d3a54bfcd24a3252693606891f85e51e2f043e9e3a38f95797b5", "0xd3da57bd92a493efbf623f32c63315442acb0a1519cdeb57c846803150efd238": "0x1ce173dc622d4d68f9b5fbe79de27e376bf66f560df27fb52c56d89fc0c4d66f", "0xed4ba02077e70f78db70ce191509333d2dcccfff136d30dcb011f567305e88ad": "0x2b8bc588174d409692f1bd993942e3806c9a11344dbe674f46aed32a030aacf1", "0xf758779927a94ce49a1ea7e126142cca8472adf8cc3133234411737bb193ce41": "0xe826d67a09b7f9d1da2beacf436b833ae46aae40524c788fd02a10b5c3258c9a" }, "Test_AllocatedMerkleAllowlist": { - "0x39680c039db01fecaf275d243c503478018b01952084b422ac3cc81a4d45a603": "0x623a518ede2299ed9a0c85b4e9533b1e588d46668304ea9c7f1402c025ab3e2e", - "0x3e786da8306fcbed7cac84942338a1270f92283628b6c00f68be021274a9398c": "0x0f25886775a191f8793d3da1493fe07c98943f0d34227e3ceebae3dfb555b56a" + "0xb50d93e7156ecaf7b8e01d292b66cea14cc1a7b6830f183fd6e72f561523e23b": "0x2a5f4703b3701216527004c2f617b17acb95440ca086eef1f6ee54bf6f09a984", + "0xfafe60b7eb7a21b3b9a1b72d01dddba667a59544624fcd58b2b6e02fea347d7f": "0x78e03f1906753cf0f03e02e6af8940b82fe6dd0d45b6b2861c8f9af6bb968696" }, "Test_BaselineAllocatedAllowlist": { - "0x5d746474bd6f58dea871b468d53e3ab576b19043d224904750600a2d128829a1": "0x8452e66de7fac583f0fc44105edcef467d77d8edcf4c938a71aa76596ecfb67c" + "0x71af7b8cb507e6f210338d9ecd860830fdae66b8bfcfcd4b09992f478c1db762": "0x75847aa632b809de1dc6e0b65ddcb67cc927630dc73589d0fdc425b8f8fd39a8" }, "Test_BaselineAllowlist": { - "0x5994dd0cafe772dd48a75ac3eba409d0654bf2cac96c9f15c8096296038e2a00": "0xf6689b20de39181dd95dcd925b9bc3b10e545bf381bedaaa54dbac2d54a8334b" + "0x96af9f52be0a74d16b32511e95a0ab41543135e7365337f5aa5c909f858d1bce": "0x33f9a171ed5fd424f995c2deac8fcff836f2575fb43df0d957456629e54020ab" }, "Test_BaselineAxisLaunch": { - "0x52496df1c08f8ccb2ff3d5890297b3619ae3d670581df798367699ac94530d12": "0x9a04d55297d9a3348f8fe51141f2c75825152eb31e306f1411ddbd741d4d14dc" + "0x31b0450a728dd3bed00d83e9c5e9b3b4d30a1b83e1527bd587519aab02500667": "0x42c9ff158bfe2f663311f9f75cff7335a86d597946fc892b01b4cd0f20e93dd4" }, "Test_BaselineCappedAllowlist": { - "0x1ed7b5269875d6fd24f19a8b6a1906cca2ca94bba1d23cf1fb3e95877b8b0338": "0x6f2e4acbd33ef40152b0e5679bd694d0f60f4a9af611cded049d04f3598d8f10" + "0x70c1941dfe3f605e8136f574eaa6c2c394fb304755c5d79fc7fd73d6dd5c0361": "0x975b4921658727ab3a8413f512d77e71b735e48f0e3c0c9e56d5a61001c97796" }, "Test_BaselineTokenAllowlist": { - "0x3f8ca4e10bd4e9daa2aee9c323586dc24d4a3358d7599a3668eed1dd1860a829": "0xee3f8d4ffa9ffed5fa846f99d80c6080722f07cca9e388c72bfd5c49b007d8b4" + "0x21993fcd4b05b8ad948628ddcaf0e8066926a7f2f504009c6b562e5aa9026160": "0xb50386120448533c5d0eaf4d45809bcb3d2631c7109617a60283970a31b09584" }, "Test_CappedMerkleAllowlist": { - "0x1dc038ba91f15889eedf059525478d760c3b01dbf5288c0c995ef5c8f1395e8b": "0x6bfac83b21063468377650c02a19f8b39750ffa72f1763596c48d62833e54e12", - "0xb092f03e11d329d47afaec052626436946facd0fa3cb8821145d3dcfc13f6dff": "0x89152c018a4041b7ae10dacb9da894f32cdb91bd07774c0b61ac105db77f12ba" + "0x29dc229c79db0e97b991dc5816522298310d7167312ac507208acbfdf1dbf5b4": "0xb587e8409114393923bd3bc128bdbcfb7f09ccbd55f1213e94471ae03434c0a6", + "0x76de86550db7e3bd70f21a58c886217a8d9704918c88d409e8104a25013bd701": "0x70a0959d63ce03f2f1240d0a3f52c94748971250407c10095655b50773aa570a" }, "Test_GUniFactory": { - "0x049627578a379e876af59b3ba6d1971a2095f048ea7dafb5449c84380e8bfd15": "0x32842bb4a2d9dcf8ae6970d43167191b5de3e3faca6c89d40cc1376d75ab61f0" - }, - "Test_MockCallback": { - "0x18c0c4a203022c869e6a4d2f0f72202083b78d034dbf569595ffef54446168d6": "0xba554b3b9f57a3b9512139e822aaab6780f17721d5be4345dc0e233f992f7ea3", - "0x246a9ef32a9aedc544233acd1bec40f7544370124dfd3b129549cb9b901b3973": "0x8d8205623640dcc8ea7f4e91fcb011c9ea70c699690f81b643b9f81c37b644d7", - "0x2a22f20c5aec1586d91e6f79d0ee68833629f3f8ad388dd9c5b08490021839a1": "0xad416f316f140eb3340b107900440aed4ce9da6e7eb5f50417d6fe5d553c5360", - "0x37b74dddea29aff1e3f3446362a3a9366ea9615e4acdcbb3677f21a08143c302": "0xd97bde3b537a31706755b53fe0c38b8ad8bdd244f51b938fa8205b2eccc235bf", - "0x6577347f20583a23bea37598160ec033a848507cc9cb059180522940d6f73c18": "0x40fdbaaf81a5542e0781e67b8086f61f89475f8f7cc0c69bbcd78db62cf2aef4", - "0x71f820d78f195a1cc435b52c14b3f3a593a911553aa658f6f3ced2b67e1340a8": "0x141cbc3d3939a43c42211838f20da07996ac0725ff1888f2da63dba3838b8997", - "0x7bcca3812161fe98c45f837b4f6eeac5ddac5a9e21e47f8656321f2a62738f01": "0x6c1e5bf8548492121a145ceb6a131a9c4e60493df8a5e11da943c50dd4b95425", - "0x830bd63c7559459f634c9e096fd315310a9f47c15a326b250dedf040d49af4f8": "0x016c27ea396b1c7482e0d374af074944bbe7e4fb478a5b6978fb96ebdf921701", - "0x8f97e9fe0f0eec83241f1a3c7d6fcf22ea03132fb4e58e53ce45270f6ed3be99": "0xf85b03ca9debb2f322f45c11170ec0b25d3b9f5f61d05ccc55f444b8bd1b40b3", - "0x9eab9fee995d7bddb995270d4e9ffd35dbdd33a1473f856b0b000e7afae61ddd": "0x9519245db0457a681685b7693b0903d0b3bfd98457ef5ebbe4bac152b0892e2b", - "0x9ebf15f3780982aafb88736b14363e9e32a105dd1a460d30c5418c4d04a68c75": "0x0bfb88a78bc743a9efc2b325b3bf6213ec5883a0c151d8fbbf478ea0cbc525ab", - "0xac901f906d629548f2b375a31c26eaa05c91203446f72b9dffadd724d49cf8e3": "0x192442057f6c1c805dfc0779e2d48a3780e99e817d3f1d5b83f8a57044f1d7d0", - "0xbcd0165ac6f52e7b92933461a9a9708fef2a9596a515240289f47957504c018f": "0x039346c31cf88ee5d6edc003d9e603db91e104cdaa435785fdf85736b70c3a07", - "0xc0e9ec7f07b9bf013d6c0a523707cbe7fcfd523cf5b711122440c719a81bc8c9": "0x2d6b29dfa18f091c73ec49fcda550c3946afc3963b9b18f683fb3f3236b8d371", - "0xc5e30600fb5186d731bd91de002598c87160f505908fda4300928722b6e139df": "0xe159c55ffef3606f20531c8cdddcf12d8906e147d660c3bff824bee1a9472d32", - "0xea0d1dc1e06da05175c34dc290a99395555a424aa7e00e2213ce81b8e158166f": "0x65bb66595830b551c2d7a3f4067cbad23fb26af7d9561c4b68015812605cf72c", - "0xef2a152970f9a8b603ce87130191da4224c3c77530a7ebe61fa186fd25fbf640": "0xcdc1e38d3bad5521d29af68b135a5b66bfa4ee50ef7a038dcc47211996958fbc", - "0xf9ac5aacece0893f1ab61c05cbc7e271afba59a1d9b45c2919540bbacbf512a3": "0x80c3af756e69cafc46005babc743e5d39c696a61733e7d5babe2a6698bb835c5" + "0x03be53080cca58d45da8875100a5b3067b67b0c2b3f8f7e3e5894546a47568d5": "0xff5f02edac96ba795efc3e991746f87c36a9484a58f542c8c6f10d71613b5c08" }, "Test_QuoteToken": { - "0x73857a6649000d8d1c6d627e9eda2a6079605eda4cd860cffe3bb939dfe3e3ef": "0xe9bd28f6c7d9ac5ecc0f59b51855093220efab06f25f16c4d0e82f04eabaf13c" + "0x444caed4e1704bd9e214cb6c8a562f99e8806c74185f0debc4dca8dda3002ebb": "0xc53b4a2c323d99618c23aad7238495234e5eaddba872f3d079aa082872e098c5", + "0x58fea475f289c0e3fcddb49b7900555f1a11d4b36caf2f735992776e1d007bba": "0x312f8d3dce539794a883e359491df7844e21233914dd4343fb483c44eeea3709", + "0xb24a3be6a8bdede031d12faa20163604d4e3ac83d74bd62343527c19d02d6bd7": "0x2cad71e04bcb4b291ee2c07deb5949371de1e15c0f0fe33b0d33ed63e8b10e44" }, "Test_TokenAllowlist": { - "0x3cf6404d9502da98e1ed6560fce585a0a87f56f598397915ef5a6207eb9b439f": "0xd8172a08ee7909a096d99b3ef34fe0d4212c85e77206518e9b5e1640926dde85", - "0xdb5689fd93b97ad35ee469bbcb38d4472c005f1ae951b154f2437bd1207a03f3": "0xf35c7565f04f89db7d1783343067728c4db35f70d7aea4d7dbd732fa462bb956" + "0x238371f92ead2927fde1bf05e297357a8f5eb2725e1dc509c577eee1772aa0e9": "0x7d39a797e209a7859f9563ae65448fd68edd78e6c864f36e771780cd9dde5229", + "0xa5d022492135bdf93503117957c22ccc97483fedf5e60761b30ddd4adced90ea": "0x9c316bbf1a8b1e5ef1b344f8c8b9f696f53fca1c18f88a7f2fc99aa58687241f" }, "Test_UniswapV2DirectToLiquidity": { - "0x44d9f5974232d8dc047dadc9a93a66dde3b1938d906428d1c52dc9e356988d87": "0xd3cddc8f40b0d22205b1eaf044af0e3131bbd63922c5c14a38e2440552c8fd5f" + "0x73e92c9fa7edbd64b00a40b781f6c757025945f0a761bc0b75764a6e401c91db": "0x1faa834688af2ee88010e925fac2876c9331a48dae57d50028260fcb42cca505" }, "Test_UniswapV2Router": { - "0x3aeb5c743a058e3c9d871533d2537013b819c4e401acaca3619f046cd9422258": "0x4c096423447dffd4ffce23f8e15e2d1b08619f72abc81536b5492d95b7c8f03f" + "0x874532e739feebacbbe36f8c61fa97ab6ab1e6f7316e48763abf915e4af90c02": "0x5e9f7ca3ee281335c8a4fe4a4f33b950a5359d6a95fdc81892d9103ffd689659" }, "Test_UniswapV3DirectToLiquidity": { - "0x5bd1c45b9f8ee81f6de61284469b1e580289694e339da3bf7f89422b2f6acee2": "0xf4188c3dde8973a334f65d1f532e5b4e022e75a69140173cd2ee38fa05f1d789" + "0x40c266b9dec7915f86953624b76dde1d0dbd23b2d4331ae2ef505750cb949c43": "0xa998f887fd77653aaeea91ab3507da8eb3d236b8513ed69b7de5173f33b94c55" }, "Test_UniswapV3Factory": { - "0x33479a3955b5801da918af0aa61f4501368d483ac02e8ae036d4e6f9c56d2038": "0x715638753ed78c13d0d385c1758204daad3b308083604fb0d555eb285199ed30" + "0x862fde1cd9de35dc112d759742b17a659781dfe0dddb8136e0b63fa9c2e00dab": "0x5fbe68e6c093ea70e56909c84fe99890a237a4af7f7599ace8471a48c78a8aae" }, "TokenAllowlist": { "0x09db47d395a68db033a3b222b9d1a212cec8422b03aeafc313aa4d2813c6dd60": "0xe07a005213f35e1f050a4da040d6bc3cae2ad333647b51a6ffa2e7941980043a", "0x30d8cf89cc8c815884740dc27e72a1f8b5dacbbe39f3d1e35deb436176390b20": "0xaa12d3f9826fbc3d54f789bfc36030a3dfe0ea0a538d4968716e10eecc2b91b2", + "0x451d4719c6bd1d7d4b691f366424d7cfc58d1bf8c601a976606fd4fb15d6281d": "0x6807f3494bc9b3f764e28e644605b979cd8a236fc4fcaaf04c75983bd16baba8", "0x48612e5a97a3e434e0158a8ece7bf1c0876f0a82987580936e63084228144cd6": "0xd51382b4a80b7fa68a37f611bf095a15818465788fead6a0a9a0515251cb66e5", "0x57df0bf7cfe081111b36726104f5bf23ec172de0ba96f2fb36c2e41108989b71": "0x7e184f1697deb99d9bc7ba50324c40e98b987f83a14245174e6eacc6ab240330", "0x90da485cf65fddc4badddf32cea25b4bd9bb73886c8c43efd9bd24023ae5ae3e": "0xde52518303adaeab837dbb64e6ef884ef3e1851f659e52795c375a465f400799", "0x9c02e32281bdc5e8d243f1888eca0f993061ed7f21f28cd90582d59b1c3b0b67": "0xa139dbe82632af667302739d6a4ff8f87b7c235a0cfc2a677865264cc7154507", "0xa4b72411c68147f4439bf4211d12dfbada62d17aba88a86648226a2cb61feff6": "0x18133c11f2fba3e102d2900a469e89202dd55927014a0a4062f21f2444bfcc9e", "0xcc0cb46859311aa362f48add9ffc284efd8aad3e4ec5a29b68eee7ab1b4cd8c4": "0x99628f6902caaf046d171a8754943a29a3906a9b32f3ee471a12b470363999c7", + "0xcc20b1dc13a7fc04d8bac79c68a17ff3c45371eeb2411d79bf2aa78766e00074": "0xecdb5d1163d7a2cab69d77deaf386c05a3d0d94206ad736846350781d0e07398", "0xcc3bdd1460c690d76ecd2a6068b712cac53eb622458a6246e13a9f22114c33ea": "0x69c7006142e7b85d6c199c18ab3274df41e776aede6923972c1434b4dcc0931c", + "0xde2cf9aec490bf9eb0443f4406c66781cb6a88365809154cf60e87027785fb6f": "0xd59936b5c545c7ea0cb6354768e98e23f7c50d6ae1ca64dde9b54ab8c01c196e", "0xdf5415e95420bc8deaf38312453772d847f73c7131a11dcd4bb9b5abdd289d13": "0x0ceea63bb6cc59164b6cabb6605bef340f456cc0e5ce5eca62ec5d981c97afd6", - "0xf30f7c9cc7b51f335e23d73e1aca7e8b8a9bbc357c580d7cf4deb34209d2fd2e": "0xeb55ef52a3b1f1dd0a1afeac73957dffa2d091fe2356f9a53393cf4df3c7d103" + "0xe9d1c1a89c4331dd63a839aa8f7b5976e6827b71ffa942f02ee177bea0441a9d": "0xf2922e3604177fdc52b46b39b5bddf07975bf6e453f1214e80c1269d4654bf81", + "0xf30f7c9cc7b51f335e23d73e1aca7e8b8a9bbc357c580d7cf4deb34209d2fd2e": "0xeb55ef52a3b1f1dd0a1afeac73957dffa2d091fe2356f9a53393cf4df3c7d103", + "0xf75e1fd045a4dc5df074a3615e9dd0f44bd432ac4ca6c70dc1922ea22bdb9ce6": "0x0c06b232f71590efd64483c93a7a59b81b5d6176c659d2243fe07a8a76cdd8b2", + "0xff938171f63f003b27bb1c770f79a707e1029bec50d8af4dea667bb56c28474b": "0x0a56b4b733a65630492009fc2de22b52640aabbe61411c620b1354680a455e41" }, "UniswapV2DirectToLiquidity": { + "0x1b6e370e83885fa1f044651f42775df11f4f3c4d7a3765012373945960955dad": "0x365cb2f75f5c1003583415efb77f27bf7645fe5b373df472cc71fbc7a4992703", + "0x218e5a13a1bf85eb3090445cf402a8e388b8ebea8739e89946bd8d1548bc58eb": "0x42c7984aed67e802fa5ba94f8cde85e003d13852bcbe013e6e61cf54a679cacc", + "0x28363dc5946ae75adcaa7e00eff38a30e46c8b84aaffd3bacef615ba4ec17598": "0xeb7cc1ea93353665ab03f895ef2a45960dd5bb9307e213b3ac43f8099d546efa", "0x2952f587868594ee4aed1e483cdbb653d4ef61c9c662a737fc6ae8f271225fe1": "0x000174bce07575898f52c13859450718eea202638a7089ff3d358a023b48bbfb", "0x32dcc0983af67a522f275494ce3a9a2aa88e6402989414825c965c9ca5fba907": "0xf588d14ef594a9c7c4fa378cfdebcbec0356153259bfe8a58d168f4866f7f2fb", "0x3da878eaa4aa411d061618a12b80bd1a2896d0c89cdfc57bf95f335ec4c95544": "0xc281eab2cbc2fa51cc5bf3dde7cf9bef394d9db473c797d48a7da85e2c4bfb95", + "0x468235e17e0be4665abdfcfa2a7b00f6b24c14a3d3dc04b303080ab99f049369": "0x77ea9042639cf1415c5ecf21e7022040cc6180ac77b4ed3b6c39dc13aebb8a23", "0x49a44765f82e21a2455ecafadfd36f1dd7066ae8733dab3bf13fa33f271faabe": "0x036007589a41359e5858eddc394a9e4c2fe917360175a38d6139d77c8d8b0a89", "0x5e493cf7f7157b59101a569467e19f4a2618cb03f6414d4a8b4f1d2b4ac2912e": "0x43996039523bd2f955e18733dafabd2ab45c0e40865e93cca76dd2773c453d97", "0x60bc649627ebd535e11e848bd93dbbb4dc819e0279d20e1d3088983f65562dbf": "0xc4534d72b992c13813b52daa775c9dbe230eaf0b139a76fdc78baf72487148d0", + "0x6ef511e4ec9af7176bc678bff9183f3c984cd8ade89554596981d7de76b2e03b": "0xcc808d6797d3d3ad6b5bd6c3104cf23551821581574e2ada042d1bf239c4308f", "0x72c2f4c8abb52265f37b52eb8732d93bdf72ff14512a77bb62d85f44283f81c1": "0x193e02d9a140f48949d8e6d06639c2b6f07aa2ac3aede83f1c2806fb603fde06", "0x77834a13e33676dc8468301cbfe5d88575a3619535b8a8ad47860465e78c5c8b": "0x68b95f9ecf712e05599e2f2a12c0fad379c27bc930d96697fc5e32047ba91b2f", + "0x80a36863f32903d5a08414641ff0ffd14d46d31418684dd07320f47fc8223dac": "0x8ca2345654ab0e0f20ec6d62412eb56305ad376a0f4bed61a9e9f8c902d20f6c", "0x8b4463835dc772630989c2d09011d747347fc03f6cdf5b4e820ed1ef05f26942": "0xc3a15bdea9242368d2db15a46055a9bf88b551562eda6217f0a02f1d6a93f73b", "0xbc71cbf901d67b58727ed8599032f3d8b926976ee3cc5881125e2b088a7bde0b": "0x7ebdc84ca6ef21adca9994a32af3690e0ae6a81d2a05caeb96d184fd2e68fa9a", + "0xc9407852f1a499d5896cc2559b0df8e93f25dbf959ab1a9449a27721ce374861": "0x2d38093c5d0945987a269f42b2a13c704aab793d137c19bb40f354df7c7abdfe", "0xc985d500ac6c9d631be52d45c756fe09f80e5b40d556953272bbd89b357085cf": "0x64dbbf7b7777277d1a8527e8395c92e4681ec29b5716bb5f9d0a5fd8920bab58", + "0xcf33b3d9b56e27ccea13a88dbf94689e6b6ce444418ca43cf384dfd9dd888eaa": "0x708f84630b91165ccfbde48f3c5941c27d0595a8fd69eff86c1b162455ffe5bd", "0xd1807b8e8fef23d2bd5acb7fe74e7dab661f1ef2f7aaaa66e286398a6d9e8011": "0xec56a79198c3d690cad3e6e39521bd7dd90dc8692cf50a7ab027408d139bd189", "0xd2933152e78d2d8b4bc2fd47ec67df6c92de359d55ada6804e2071bbe3597d29": "0x8a132dcd9dd6726916af25a65e707d2d6e654b73df598b3b3edd92360b16d4e7", "0xdc59367cddedf0ee421d7ee6f138d758617043d0f33169b7cb21627362c8715e": "0x2c543613342ae228284625a7e8e9c696d1d816c4fdbf2c49148e91c7eb8a4644", "0xe8242d2e1eae0d89d0ca278cea284cd4056496fb12fd0405237827e252efdfea": "0x65a71869dd1c30cc12ac786991b9492a5286c8cd942161d738ca34b3e0e29053", + "0xecd0e9cab2b52b27a7468beb7114e3203eca40dd7da95580640967e0db8a237a": "0x78a4b4435fdf551bc11ceefad26017d58d9341b0b465def06c0a00131b89d011", "0xf40b0b3a6c36f510d58633db604bcd978fe1e804891ce9792ac9bbb064cbe97a": "0x4fdd82a331ef80cde3af5905bd2b39c5b1d2f1da38e059fc7e077a355f4caa92", - "0xfbaca62f01c99e602fa7ba74d5be6deb601550f6e77659246f80fec8fa9c8170": "0x729c4bdbdea56155220a562d0e398c08e3273ac5bafe974f18e9fa7148f23f46" + "0xfbaca62f01c99e602fa7ba74d5be6deb601550f6e77659246f80fec8fa9c8170": "0x729c4bdbdea56155220a562d0e398c08e3273ac5bafe974f18e9fa7148f23f46", + "0xfcbb28adbe4b209c102b7ea57e2f5841bfa98483b60443d25a669765e2eb185e": "0xed0a69134f6bf94864de504c616eca73a7a7df685f745bcc72d8c37982844dfa" }, "UniswapV3DirectToLiquidity": { "0x076b93782e37198c2a62c801267d4971c0da2276424f4232d380714790b596fa": "0xb61cd26ee29ace89139c3ac9f8bc6a81a77449e35a3e1929e1bcf58963784719", + "0x15279d485c75a0a589f0c2f10ca1ddc99afb2eb2f5ea7ad5923bf1aad9e14871": "0x94387250ed9d21be857e43c2b56894dac1354e1c876e26124191ec9355698b32", "0x234ab2726d35ce5664ce25cfc80a9e32fc21f0155ca0773e69f737daaab440df": "0x5f83e1a288cbafb92518c563b1f4655de2f3802e9e648897610501cba50cd92d", + "0x273e1c3096234982f91734019666f6698c3503591e65fb920369ff2f6d34fe49": "0x98425d4b15bbf907cc136ad8c044faabb9bfd7f6ee87be36d7c09ee0db37678d", + "0x3379bed0e5371eef70bb85903ae496166cdcc680c85d4d13ad1829ed993065cb": "0xa13166b228a63b7710d0ed802e005f9a64aeb72861d22ec74f08299d3a00a8bf", + "0x6cf76c1ea5207d1a31ae3fc0ba2df68de799c27ba43312e318a7617cb2b3aad2": "0x15cefb2fc529c5866030d94fea2aa7b1d426f1007bc900c499e9dc7830b77eff", + "0x7121bbc37753b93730e4987a453e199a85e4ff5cf47d81167a57431e539838b1": "0x26770dd4662976439d8b2baaabfdb371d32a8dbf87539ed7ce6f021ee9425602", "0x7994e3c9afaffb722300f0aaa71ebeeceab756245e82e981180edb84fc0896c6": "0xb8ca51788fb6f83a631aa3f288f8976542c83998f4249a948cc9d6cb9eee227f", + "0x86f3f07fc62e0a7e01c8a662550770880f033ba15660867201f9da065c2fac3a": "0xd883f1afd5d621059556746b60ee99c4493d1f0be9702665e0f671a4efcce7fc", "0x8a637231772ce045881e7e2f78f90b0627e46e92edd646e5ec2a953546603e7d": "0xa02f5d32bca01b1a3c58fa4ae4ba0c5684e02b571271414ab2950a7a80746ba7", "0x8d22341b4d62cede70950a3665bf0dbf6d26f2f06155bc07e1520b6f8a479b57": "0x4a13f3bb700e88e4d4d98f1d78fc5c8b0f2e112ed4ec1fe9efbaa2e72dcb03e9", "0x9e46c4894fb34752211ed50d65bd9d28c0bc64b05efbfa8bc2016985dcf8c854": "0xe14c04ae17c798f930e1d15f488a9d97d6aab2ad2f031779d332c6e533cd6531", @@ -151,9 +173,14 @@ "0xbec20283ce7bc93c64136a26a297a60583dfc16d6f3f0e82e187c0e22cf5f65c": "0xa9fcd092c072800515406d4d6f36c7338211dfdafce54f7178480f1d89d47efa", "0xc0e4ff339d51c71014ab77dd1ea8d79eb1aeb129ef9f2bbdd3b725e95a128a0e": "0x1ce3937a3784e7993ad893daf7368bd17b49721935b92d78caacae70ae31c336", "0xc20973bca1d02a267a6621b19e57e978ed7a4d36d7ffe8a46d4db09026a21a59": "0x0d05817446e0a1831722ea74a58f3b675bfb5abf8b575a2c23a0abaed90f294c", + "0xd0ead848eb5c9eedf8ce9e8998d4e6f03a3e9531efbe2067e52cc7a3162f0172": "0x4cefbd460987c484a9df3561e70be58e7c2a9ed94fafbaf066f83047454f8a4c", + "0xd82699572a29e373d5bc9f44a3089321c24629ef2caf53b99d7d46c4cb21fd15": "0x1828074d2b11139f32bc9c76d547713a239c13d46fd84f59f05708a627a12a0f", "0xdb1aca9d9d747dbf091b839f28419e861b79502fa4598efd84b9a7857e9c4ad9": "0xf63a1c74edaa9327592fd21199595de6052e34fca6e81a1236ae14e018eaa1a5", + "0xe95584d277ab5c2151cce2be768dde2b558e211ed969dee467d912dd1cabb5cf": "0xe3b911492b6919bb37af2dd825dbdcc0e193b9b5c03cfe20b9b9a08a1af54cdf", + "0xeab3ac7d794bfb974e623bc8899d63385e53ad9179d70f1370a4a0f14b3f6ad2": "0x04bdea85004184c9d0e31068eeb702e192419f545fb33c6d6f4782551232797f", "0xec0f4ce3ca7fd526e8ee9ec1a9d36ecfca69b1725bfcbe2609ade044c4b858bb": "0xb1edac890bf849adfd17886a15f2dd2e41cae91820536c09fa22233333159564", "0xeff9c7afc4489c7e0a42e1d83cbd3388e811ab517b6a9a5887c815c6a2f7069c": "0x934b1c21f3402d7497d77c62e824ca83ce44113ffbb8bdb8150085586e9b3c81", - "0xf0a5dea336a37fa63979efe11d875ee7fb6453e5d78244d30113a087b7455f61": "0x71f0e171054a9dd4fd79719da9d8d19603ce79b95d8f8f4a29d8a59da612771b" + "0xf0a5dea336a37fa63979efe11d875ee7fb6453e5d78244d30113a087b7455f61": "0x71f0e171054a9dd4fd79719da9d8d19603ce79b95d8f8f4a29d8a59da612771b", + "0xfa7d52f12dff302ad42c0bec3f5d56d471c734fe19a1d7175a78762f4829c833": "0xcdd97460660dd480fb27764d768bbd02f8ad80ac4e02bcf1b9fde36e7b780639" } } diff --git a/script/salts/test/TestSalts.s.sol b/script/salts/test/TestSalts.s.sol index b451a678..9afae80d 100644 --- a/script/salts/test/TestSalts.s.sol +++ b/script/salts/test/TestSalts.s.sol @@ -9,8 +9,8 @@ import {WithSalts} from "../WithSalts.s.sol"; import {TestConstants} from "../../../test/Constants.sol"; // Libraries -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; // Uniswap @@ -267,7 +267,7 @@ contract TestSalts is Script, WithEnvironment, Permit2User, WithSalts, TestConst function generateBaselineAxisLaunch() public { // Get the salt bytes memory callbackArgs = - abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); (string memory callbackBytecodePath, bytes32 callbackBytecodeHash) = _writeBytecode( "BaselineAxisLaunch", type(BaselineAxisLaunch).creationCode, callbackArgs ); @@ -277,7 +277,7 @@ contract TestSalts is Script, WithEnvironment, Permit2User, WithSalts, TestConst function generateBaselineAllocatedAllowlist() public { // Get the salt bytes memory callbackArgs = - abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); (string memory callbackBytecodePath, bytes32 callbackBytecodeHash) = _writeBytecode( "BaselineAllocatedAllowlist", type(BALwithAllocatedAllowlist).creationCode, callbackArgs ); @@ -287,7 +287,7 @@ contract TestSalts is Script, WithEnvironment, Permit2User, WithSalts, TestConst function generateBaselineAllowlist() public { // Get the salt bytes memory callbackArgs = - abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); (string memory callbackBytecodePath, bytes32 callbackBytecodeHash) = _writeBytecode("BaselineAllowlist", type(BALwithAllowlist).creationCode, callbackArgs); _setTestSalt(callbackBytecodePath, "EF", "BaselineAllowlist", callbackBytecodeHash); @@ -296,7 +296,7 @@ contract TestSalts is Script, WithEnvironment, Permit2User, WithSalts, TestConst function generateBaselineCappedAllowlist() public { // Get the salt bytes memory callbackArgs = - abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); (string memory callbackBytecodePath, bytes32 callbackBytecodeHash) = _writeBytecode( "BaselineCappedAllowlist", type(BALwithCappedAllowlist).creationCode, callbackArgs ); @@ -306,7 +306,7 @@ contract TestSalts is Script, WithEnvironment, Permit2User, WithSalts, TestConst function generateBaselineTokenAllowlist() public { // Get the salt bytes memory callbackArgs = - abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(_AUCTION_HOUSE, _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); (string memory callbackBytecodePath, bytes32 callbackBytecodeHash) = _writeBytecode( "BaselineTokenAllowlist", type(BALwithTokenAllowlist).creationCode, callbackArgs ); diff --git a/soldeer.lock b/soldeer.lock index 9d7a637f..8c2a927c 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -7,9 +7,9 @@ checksum = "110b35ad3604d91a919c521c71206c18cd07b29c750bd90b5cbbaf37288c9636" [[dependencies]] name = "axis-core" -version = "1.0.0" -source = "https://soldeer-revisions.s3.amazonaws.com/axis-core/1_0_0_18-07-2024_01:46:16_axis-core.zip" -checksum = "9b5716ade218b4064bba7cca7d31065e6aa66574ad92386aaf2cc71b5c21bbb4" +version = "1.0.1" +source = "https://soldeer-revisions.s3.amazonaws.com/axis-core/1_0_1_22-08-2024_01:53:20_axis-core.zip" +checksum = "90ee8eca451f4454ad911c52d014bebbbacc3e0ba2260ad19e56e32598ea9d21" [[dependencies]] name = "@openzeppelin-contracts" @@ -47,6 +47,12 @@ version = "1.0.1" source = "git@github.com:Axis-Fi/uniswap-v2-periphery.git" checksum = "19be650786731dfe43cac3aac7a2d1f0731d18e2" +[[dependencies]] +name = "@uniswap-v3-periphery" +version = "1.4.2-solc-0.8" +source = "git@github.com:Uniswap/v3-periphery.git" +checksum = "b325bb0905d922ae61fcc7df85ee802e8df5e96c" + [[dependencies]] name = "solmate" version = "6.7.0" diff --git a/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol b/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol index 684f76db..71cc45bc 100644 --- a/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol +++ b/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.19; import {MerkleProof} from "@openzeppelin-contracts-4.9.2/utils/cryptography/MerkleProof.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; import {MerkleAllowlist} from "./MerkleAllowlist.sol"; diff --git a/src/callbacks/allowlists/CappedMerkleAllowlist.sol b/src/callbacks/allowlists/CappedMerkleAllowlist.sol index b3eff77c..df5f6ec3 100644 --- a/src/callbacks/allowlists/CappedMerkleAllowlist.sol +++ b/src/callbacks/allowlists/CappedMerkleAllowlist.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; import {MerkleAllowlist} from "./MerkleAllowlist.sol"; diff --git a/src/callbacks/allowlists/MerkleAllowlist.sol b/src/callbacks/allowlists/MerkleAllowlist.sol index 0aaa2afd..aa648511 100644 --- a/src/callbacks/allowlists/MerkleAllowlist.sol +++ b/src/callbacks/allowlists/MerkleAllowlist.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.19; import {MerkleProof} from "@openzeppelin-contracts-4.9.2/utils/cryptography/MerkleProof.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; -import {IAuctionHouse} from "@axis-core-1.0.0/interfaces/IAuctionHouse.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; /// @title MerkleAllowlist /// @notice This contract implements a merkle tree-based allowlist for buyers to participate in an auction. diff --git a/src/callbacks/allowlists/TokenAllowlist.sol b/src/callbacks/allowlists/TokenAllowlist.sol index 405ae645..21a35ad3 100644 --- a/src/callbacks/allowlists/TokenAllowlist.sol +++ b/src/callbacks/allowlists/TokenAllowlist.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; /// @notice Generic interface for tokens that implement a balanceOf function (includes ERC-20 and ERC-721) interface ITokenBalance { diff --git a/src/callbacks/liquidity/BaseDTL.sol b/src/callbacks/liquidity/BaseDTL.sol index 379a9b7f..5cb638b2 100644 --- a/src/callbacks/liquidity/BaseDTL.sol +++ b/src/callbacks/liquidity/BaseDTL.sol @@ -6,15 +6,21 @@ import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; import {SafeTransferLib} from "@solmate-6.7.0/utils/SafeTransferLib.sol"; // Callbacks -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; // AuctionHouse -import {ILinearVesting} from "@axis-core-1.0.0/interfaces/modules/derivatives/ILinearVesting.sol"; -import {LinearVesting} from "@axis-core-1.0.0/modules/derivatives/LinearVesting.sol"; -import {AuctionHouse} from "@axis-core-1.0.0/bases/AuctionHouse.sol"; -import {Keycode, wrapVeecode} from "@axis-core-1.0.0/modules/Modules.sol"; - +import {ILinearVesting} from "@axis-core-1.0.1/interfaces/modules/derivatives/ILinearVesting.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; +import {AuctionHouse} from "@axis-core-1.0.1/bases/AuctionHouse.sol"; +import {Keycode, wrapVeecode} from "@axis-core-1.0.1/modules/Modules.sol"; + +/// @notice Base contract for DirectToLiquidity callbacks +/// @dev This contract is intended to be inherited by a callback contract that supports a particular liquidity platform, such as Uniswap V2 or V3. +/// +/// It provides integration points that enable the implementing contract to support different liquidity platforms. +/// +/// NOTE: The parameters to the functions in this contract refer to linear vesting, which is currently only supported for ERC20 pool tokens. A future version could improve upon this by shifting the (ERC20) linear vesting functionality into a variant that inherits from this contract. abstract contract BaseDirectToLiquidity is BaseCallback { using SafeTransferLib for ERC20; @@ -34,24 +40,27 @@ abstract contract BaseDirectToLiquidity is BaseCallback { error Callback_LinearVestingModuleNotFound(); + /// @notice The auction lot has already been completed + error Callback_AlreadyComplete(); + // ========== STRUCTS ========== // /// @notice Configuration for the DTL callback /// - /// @param recipient The recipient of the LP tokens - /// @param lotCapacity The capacity of the lot - /// @param lotCuratorPayout The maximum curator payout of the lot - /// @param proceedsUtilisationPercent The percentage of the proceeds to deposit into the pool, in basis points (1% = 100) - /// @param vestingStart The start of the vesting period for the LP tokens (0 if disabled) - /// @param vestingExpiry The end of the vesting period for the LP tokens (0 if disabled) - /// @param linearVestingModule The LinearVesting module for the LP tokens (only set if linear vesting is enabled) - /// @param active Whether the lot is active - /// @param implParams The implementation-specific parameters + /// @param recipient Recipient of the LP tokens + /// @param lotCapacity Capacity of the lot + /// @param lotCuratorPayout Maximum curator payout of the lot + /// @param poolPercent Percentage of the proceeds to allocate to the pool, in basis points (1% = 100). The remainder will be sent to the `recipient`. + /// @param vestingStart Start of the vesting period for the LP tokens (0 if disabled) + /// @param vestingExpiry End of the vesting period for the LP tokens (0 if disabled) + /// @param linearVestingModule LinearVesting module for the LP tokens (only set if linear vesting is enabled) + /// @param active Whether the lot is active + /// @param implParams Implementation-specific parameters struct DTLConfiguration { address recipient; uint256 lotCapacity; uint256 lotCuratorPayout; - uint24 proceedsUtilisationPercent; + uint24 poolPercent; uint48 vestingStart; uint48 vestingExpiry; LinearVesting linearVestingModule; @@ -61,13 +70,13 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// @notice Parameters used in the onCreate callback /// - /// @param proceedsUtilisationPercent The percentage of the proceeds to use in the pool - /// @param vestingStart The start of the vesting period for the LP tokens (0 if disabled) - /// @param vestingExpiry The end of the vesting period for the LP tokens (0 if disabled) - /// @param recipient The recipient of the LP tokens - /// @param implParams The implementation-specific parameters + /// @param poolPercent Percentage of the proceeds to allocate to the pool, in basis points (1% = 100). The remainder will be sent to the `recipient`. + /// @param vestingStart Start of the vesting period for the LP tokens (0 if disabled) + /// @param vestingExpiry End of the vesting period for the LP tokens (0 if disabled) + /// @param recipient Recipient of the LP tokens + /// @param implParams Implementation-specific parameters struct OnCreateParams { - uint24 proceedsUtilisationPercent; + uint24 poolPercent; uint48 vestingStart; uint48 vestingExpiry; address recipient; @@ -84,7 +93,9 @@ abstract contract BaseDirectToLiquidity is BaseCallback { // ========== CONSTRUCTOR ========== // - constructor(address auctionHouse_) + constructor( + address auctionHouse_ + ) BaseCallback( auctionHouse_, Callbacks.Permissions({ @@ -110,7 +121,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// - Stores the configuration for the lot /// /// This function reverts if: - /// - OnCreateParams.proceedsUtilisationPercent is out of bounds + /// - OnCreateParams.poolPercent is out of bounds /// - OnCreateParams.vestingStart or OnCreateParams.vestingExpiry do not pass validation /// - Vesting is enabled and the linear vesting module is not found /// - The OnCreateParams.recipient address is the zero address @@ -134,13 +145,8 @@ abstract contract BaseDirectToLiquidity is BaseCallback { // Validate the parameters // Proceeds utilisation - if ( - params.proceedsUtilisationPercent == 0 - || params.proceedsUtilisationPercent > ONE_HUNDRED_PERCENT - ) { - revert Callback_Params_PercentOutOfBounds( - params.proceedsUtilisationPercent, 1, ONE_HUNDRED_PERCENT - ); + if (params.poolPercent < 10e2 || params.poolPercent > ONE_HUNDRED_PERCENT) { + revert Callback_Params_PercentOutOfBounds(params.poolPercent, 10e2, ONE_HUNDRED_PERCENT); } // Vesting @@ -151,7 +157,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { // Get the linear vesting module (or revert) linearVestingModule = LinearVesting(_getLatestLinearVestingModule()); - // Validate + // Use the native LinearVesting validation if ( // We will actually use the LP tokens, but this is a placeholder as we really want to validate the vesting parameters !linearVestingModule.validate( @@ -161,6 +167,13 @@ abstract contract BaseDirectToLiquidity is BaseCallback { ) { revert Callback_Params_InvalidVestingParams(); } + + // Validate that the vesting will not start before auction conclusion + (, uint48 conclusion,,,,,,) = + AuctionHouse(AUCTION_HOUSE).getAuctionModuleForId(lotId_).lotData(lotId_); + if (params.vestingStart < conclusion) { + revert Callback_Params_InvalidVestingParams(); + } } // If the recipient is the zero address @@ -173,7 +186,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { recipient: params.recipient, lotCapacity: capacity_, lotCuratorPayout: 0, - proceedsUtilisationPercent: params.proceedsUtilisationPercent, + poolPercent: params.poolPercent, vestingStart: params.vestingStart, vestingExpiry: params.vestingExpiry, linearVestingModule: linearVestingModule, @@ -216,9 +229,15 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// /// This function reverts if: /// - The lot is not registered + /// - The lot has already been completed /// /// @param lotId_ The lot ID function _onCancel(uint96 lotId_, uint256, bool, bytes calldata) internal override { + // Check that the lot is active + if (!lotConfiguration[lotId_].active) { + revert Callback_AlreadyComplete(); + } + // Mark the lot as inactive to prevent further actions DTLConfiguration storage config = lotConfiguration[lotId_]; config.active = false; @@ -230,6 +249,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// /// This function reverts if: /// - The lot is not registered + /// - The lot has already been completed /// /// @param lotId_ The lot ID /// @param curatorPayout_ The maximum curator payout @@ -239,6 +259,11 @@ abstract contract BaseDirectToLiquidity is BaseCallback { bool, bytes calldata ) internal override { + // Check that the lot is active + if (!lotConfiguration[lotId_].active) { + revert Callback_AlreadyComplete(); + } + // Update the funding DTLConfiguration storage config = lotConfiguration[lotId_]; config.lotCuratorPayout = curatorPayout_; @@ -279,6 +304,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// /// This function reverts if: /// - The lot is not registered + /// - The lot is already complete /// /// @param lotId_ The lot ID /// @param proceeds_ The proceeds from the auction @@ -290,7 +316,16 @@ abstract contract BaseDirectToLiquidity is BaseCallback { uint256 refund_, bytes calldata callbackData_ ) internal virtual override { - DTLConfiguration memory config = lotConfiguration[lotId_]; + DTLConfiguration storage config = lotConfiguration[lotId_]; + + // Check that the lot is active + if (!config.active) { + revert Callback_AlreadyComplete(); + } + + // Mark the lot as inactive + lotConfiguration[lotId_].active = false; + address seller; address baseToken; address quoteToken; @@ -317,10 +352,8 @@ abstract contract BaseDirectToLiquidity is BaseCallback { } // Calculate the base tokens required to create the pool - baseTokensRequired = - _tokensRequiredForPool(capacityUtilised, config.proceedsUtilisationPercent); - quoteTokensRequired = - _tokensRequiredForPool(proceeds_, config.proceedsUtilisationPercent); + baseTokensRequired = _tokensRequiredForPool(capacityUtilised, config.poolPercent); + quoteTokensRequired = _tokensRequiredForPool(proceeds_, config.poolPercent); } // Ensure the required tokens are present before minting @@ -345,7 +378,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { // If vesting is enabled, create the vesting tokens if (address(config.linearVestingModule) != address(0)) { // Approve spending of the tokens - poolToken.approve(address(config.linearVestingModule), poolTokenQuantity); + poolToken.safeApprove(address(config.linearVestingModule), poolTokenQuantity); // Mint the vesting tokens (it will deploy if necessary) config.linearVestingModule.mint( @@ -356,24 +389,24 @@ abstract contract BaseDirectToLiquidity is BaseCallback { true // Wrap vesting LP tokens so they are easily visible ); } - // Send the LP tokens to the seller + // Send the LP tokens to the specified recipient else { poolToken.safeTransfer(config.recipient, poolTokenQuantity); } - // Send any remaining quote tokens to the seller + // Send any remaining quote tokens to the specified recipient { uint256 quoteTokenBalance = ERC20(quoteToken).balanceOf(address(this)); if (quoteTokenBalance > 0) { - ERC20(quoteToken).safeTransfer(seller, quoteTokenBalance); + ERC20(quoteToken).safeTransfer(config.recipient, quoteTokenBalance); } } - // Send any remaining base tokens to the seller + // Send any remaining base tokens to the specified recipient { uint256 baseTokenBalance = ERC20(baseToken).balanceOf(address(this)); if (baseTokenBalance > 0) { - ERC20(baseToken).safeTransfer(seller, baseTokenBalance); + ERC20(baseToken).safeTransfer(config.recipient, baseTokenBalance); } } } @@ -418,9 +451,9 @@ abstract contract BaseDirectToLiquidity is BaseCallback { function _tokensRequiredForPool( uint256 amount_, - uint24 proceedsUtilisationPercent_ + uint24 poolPercent_ ) internal pure returns (uint256) { - return (amount_ * proceedsUtilisationPercent_) / ONE_HUNDRED_PERCENT; + return (amount_ * poolPercent_) / ONE_HUNDRED_PERCENT; } function _getLatestLinearVestingModule() internal view returns (address) { diff --git a/src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol b/src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol index 063fe48f..833aff30 100644 --- a/src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol +++ b/src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol @@ -58,7 +58,7 @@ contract BALwithAllocatedAllowlist is BaselineAxisLaunch { /// @dev This function reverts if: /// - `allowlistData_` is not of the correct length /// - /// @param allowlistData_ abi-encoded data: (bytes32) representing the merkle root + /// @param allowlistData_ abi-encoded data: (bytes32) representing the merkle root function __onCreate( uint96, address, diff --git a/src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol b/src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol index 1ccd0ff1..fecf405a 100644 --- a/src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol +++ b/src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol @@ -46,7 +46,7 @@ contract BALwithCappedAllowlist is BALwithAllowlist { /// @dev This function reverts if: /// - `allowlistData_` is not of the correct length /// - /// @param allowlistData_ abi-encoded data: (bytes32) representing the merkle root + /// @param allowlistData_ abi-encoded data: (bytes32, uint256) representing the merkle root and buyer limit function __onCreate( uint96, address, diff --git a/src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol b/src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol index a439ad8f..207fd608 100644 --- a/src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol +++ b/src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol @@ -3,16 +3,17 @@ pragma solidity 0.8.19; // Axis dependencies import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {IAuctionHouse} from "@axis-core-1.0.0/interfaces/IAuctionHouse.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; import { Keycode as AxisKeycode, keycodeFromVeecode, fromKeycode as fromAxisKeycode -} from "@axis-core-1.0.0/modules/Keycode.sol"; -import {Module as AxisModule} from "@axis-core-1.0.0/modules/Modules.sol"; -import {IFixedPriceBatch} from "@axis-core-1.0.0/interfaces/modules/auctions/IFixedPriceBatch.sol"; +} from "@axis-core-1.0.1/modules/Keycode.sol"; +import {Module as AxisModule} from "@axis-core-1.0.1/modules/Modules.sol"; +import {IFixedPriceBatch} from "@axis-core-1.0.1/interfaces/modules/auctions/IFixedPriceBatch.sol"; +import {Transfer} from "@axis-core-1.0.1/lib/Transfer.sol"; // Baseline dependencies import { @@ -22,14 +23,15 @@ import { toKeycode as toBaselineKeycode, Permissions as BaselinePermissions } from "./lib/Kernel.sol"; -import {Range, IBPOOLv1} from "./lib/IBPOOL.sol"; -import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; +import {Position, Range, IBPOOLv1, IUniswapV3Pool} from "./lib/IBPOOL.sol"; +import {ICREDTv1} from "./lib/ICREDT.sol"; +import {ILOOPSv1} from "./lib/ILOOPS.sol"; // Other libraries -import {Owned} from "@solmate-6.7.0/auth/Owned.sol"; import {FixedPointMathLib} from "@solady-0.0.124/utils/FixedPointMathLib.sol"; -import {Transfer} from "@axis-core-1.0.0/lib/Transfer.sol"; +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; import {SqrtPriceMath} from "../../../lib/uniswap-v3/SqrtPriceMath.sol"; +import {Owned} from "@solmate-6.7.0/auth/Owned.sol"; /// @notice Axis auction callback to initialize a Baseline token using proceeds from a batch auction. /// @dev This contract combines Baseline's InitializeProtocol Policy and Axis' Callback functionality to build an Axis auction callback specific to Baseline V2 token launches @@ -45,66 +47,112 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { /// @notice The address of the quote token (passed in the `onCreate` callback) does not match the address of the reserve that the callback was initialized with error Callback_Params_ReserveTokenMismatch(address quoteToken_, address reserve_); - /// @notice The auction price and the pool active tick do not match - error Callback_Params_PoolTickMismatch(int24 auctionTick_, int24 poolTick_); - /// @notice The auction format is not supported error Callback_Params_UnsupportedAuctionFormat(); + /// @notice The pool fee tier is not supported + error Callback_Params_UnsupportedPoolFeeTier(); + /// @notice The anchor tick width is invalid error Callback_Params_InvalidAnchorTickWidth(); /// @notice The discovery tick width is invalid error Callback_Params_InvalidDiscoveryTickWidth(); + /// @notice The floor range gap is invalid + error Callback_Params_InvalidFloorRangeGap(); + + /// @notice The anchor tick upper is invalid + error Callback_Params_InvalidAnchorTickUpper(); + + /// @notice One of the ranges is out of bounds + error Callback_Params_RangeOutOfBounds(); + /// @notice The floor reserves percent is invalid error Callback_Params_InvalidFloorReservesPercent(); + /// @notice The pool percent is invalid + error Callback_Params_InvalidPoolPercent(); + + /// @notice The recipient address is invalid + error Callback_Params_InvalidRecipient(); + /// @notice The auction tied to this callbacks contract has already been completed error Callback_AlreadyComplete(); /// @notice The required funds were not sent to this callbacks contract error Callback_MissingFunds(); + /// @notice The initialization is invalid + error Callback_InvalidInitialization(); + + /// @notice The capacity ratio is invalid + /// + /// @param capacityRatio The ratio of the pool capacity to the circulating supply + error Callback_InvalidCapacityRatio(uint256 capacityRatio); + + /// @notice The pool price is lower than the auction price + /// + /// @param currentTick The current tick of the pool + /// @param auctionTick The tick corresponding to the auction price + error Callback_PoolLessThanAuctionPrice(int24 currentTick, int24 auctionTick); + /// @notice The BPOOL reserve token does not match the configured `RESERVE` address - error InvalidModule(); + error Callback_BPOOLReserveMismatch(); - /// @notice Deploying reserves and liquidity would result in the Baseline pool being insolvent - error Insolvent(); + /// @notice The address of the BPOOL is higher than the RESERVE token address, when it must be lower + error Callback_BPOOLInvalidAddress(); + + /// @notice The caller to the Uniswap V3 swap callback is invalid + error Callback_Swap_InvalidCaller(); + + /// @notice The case for the Uniswap V3 swap callback is invalid + error Callback_Swap_InvalidCase(); // ========== EVENTS ========== // - event LiquidityDeployed(int24 tickLower, int24 tickUpper, uint128 liquidity); + event LiquidityDeployed( + int24 floorTickLower, int24 anchorTickUpper, uint128 floorLiquidity, uint128 anchorLiquidity + ); // ========== DATA STRUCTURES ========== // /// @notice Data struct for the onCreate callback /// - /// @param floorReservesPercent The percentage of the proceeds to allocate to the floor range, in basis points (1% = 100). The remainder will be allocated to the anchor range. + /// @param recipient The address to receive proceeds that do not go to the pool + /// @param poolPercent The percentage of the proceeds to allocate to the pool, in basis points (1% = 100). The remainder will be sent to the `recipient`. + /// @param floorReservesPercent The percentage of the pool proceeds to allocate to the floor range, in basis points (1% = 100). The remainder will be allocated to the anchor range. + /// @param floorRangeGap The gap between the floor and anchor ranges, as a multiple of the pool tick spacing. + /// @param anchorTickU The upper tick of the anchor range. Validated against the calculated upper bound of the anchor range. This is provided off-chain to prevent front-running. /// @param anchorTickWidth The width of the anchor tick range, as a multiple of the pool tick spacing. - /// @param discoveryTickWidth The width of the discovery tick range, as a multiple of the pool tick spacing. /// @param allowlistParams Additional parameters for an allowlist, passed to `__onCreate()` for further processing struct CreateData { + address recipient; + uint24 poolPercent; uint24 floorReservesPercent; + int24 floorRangeGap; + int24 anchorTickU; int24 anchorTickWidth; - int24 discoveryTickWidth; bytes allowlistParams; } // ========== STATE VARIABLES ========== // + // TickMath constants + int24 internal constant _MAX_TICK = 887_272; + int24 internal constant _MIN_TICK = -887_272; + // Baseline Modules - // solhint-disable-next-line var-name-mixedcase + // solhint-disable var-name-mixedcase IBPOOLv1 public BPOOL; + ICREDTv1 public CREDT; + ILOOPSv1 public LOOPS; // Pool variables ERC20 public immutable RESERVE; + // solhint-enable var-name-mixedcase ERC20 public bAsset; - // Accounting - uint256 public initialCirculatingSupply; - uint256 public reserveBalance; - // Axis Auction Variables /// @notice Lot ID of the auction for the baseline market. This callback only supports one lot. @@ -115,13 +163,24 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { /// @dev This is used to prevent the callback from being called multiple times. It is set in the `onSettle()` callback. bool public auctionComplete; + /// @notice The percentage of the proceeds to allocate to the pool + /// @dev This value is set in the `onCreate()` callback. + uint24 public poolPercent; + /// @notice The percentage of the proceeds to allocate to the floor range /// @dev This value is set in the `onCreate()` callback. uint24 public floorReservesPercent; + /// @notice The address to receive proceeds that do not go to the pool + /// @dev This value is set in the `onCreate()` callback. + address public recipient; + // solhint-disable-next-line private-vars-leading-underscore uint48 internal constant ONE_HUNDRED_PERCENT = 100e2; + /// @notice The tick spacing width of the discovery range + int24 internal constant _DISCOVERY_TICK_SPACING_WIDTH = 350; + // ========== CONSTRUCTOR ========== // /// @notice Constructor for BaselineAxisLaunch @@ -129,7 +188,7 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { /// @param auctionHouse_ The AuctionHouse the callback is paired with /// @param baselineKernel_ Address of the Baseline kernel /// @param reserve_ Address of the reserve token. This should match the quote token for the auction lot. - /// @param owner_ Address of the owner of this policy. Will be permitted to perform admin functions. This is explicitly required, as `msg.sender` cannot be used due to the use of CREATE2 for deployment. + /// @param owner_ Address of the owner of the contract. Must be the same as the eventual seller of the auction lot. constructor( address auctionHouse_, address baselineKernel_, @@ -165,20 +224,27 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { function configureDependencies() external override + onlyKernel returns (BaselineKeycode[] memory dependencies) { BaselineKeycode bpool = toBaselineKeycode("BPOOL"); + BaselineKeycode credt = toBaselineKeycode("CREDT"); + BaselineKeycode loops = toBaselineKeycode("LOOPS"); // Populate the dependencies array - dependencies = new BaselineKeycode[](1); + dependencies = new BaselineKeycode[](3); dependencies[0] = bpool; + dependencies[1] = credt; + dependencies[2] = loops; // Set local values BPOOL = IBPOOLv1(getModuleAddress(bpool)); bAsset = ERC20(address(BPOOL)); + CREDT = ICREDTv1(getModuleAddress(credt)); + LOOPS = ILOOPSv1(getModuleAddress(loops)); // Require that the BPOOL's reserve token be the same as the callback's reserve token - if (address(BPOOL.reserve()) != address(RESERVE)) revert InvalidModule(); + if (address(BPOOL.reserve()) != address(RESERVE)) revert Callback_BPOOLReserveMismatch(); } /// @inheritdoc Policy @@ -186,16 +252,32 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { external view override + onlyKernel returns (BaselinePermissions[] memory requests) { BaselineKeycode bpool = toBaselineKeycode("BPOOL"); - requests = new BaselinePermissions[](5); + requests = new BaselinePermissions[](6); requests[0] = BaselinePermissions(bpool, BPOOL.addReservesTo.selector); requests[1] = BaselinePermissions(bpool, BPOOL.addLiquidityTo.selector); requests[2] = BaselinePermissions(bpool, BPOOL.burnAllBAssetsInContract.selector); requests[3] = BaselinePermissions(bpool, BPOOL.mint.selector); requests[4] = BaselinePermissions(bpool, BPOOL.setTicks.selector); + requests[5] = BaselinePermissions(bpool, BPOOL.setTransferLock.selector); + } + + // ========== MODIFIERS ========== // + + /// @notice Validates that the lot id matches the stored lot id + modifier onlyValidLot(uint96 lotId_) { + if (lotId_ != lotId) revert Callback_InvalidParams(); + _; + } + + /// @notice Validates that the auction is not already settled or cancelled + modifier onlyActiveLot() { + if (auctionComplete) revert Callback_AlreadyComplete(); + _; } // ========== CALLBACK FUNCTIONS ========== // @@ -216,18 +298,30 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { /// - Performs validation /// - Sets the `lotId`, `percentReservesFloor`, `anchorTickWidth`, and `discoveryTickWidth` variables /// - Calls the allowlist callback + /// - Performs a solvency check to ensure the pool can support the intended supply /// - Mints the required bAsset tokens to the AuctionHouse /// + /// This function has the following assumptions: + /// - Any Baseline credit allocations have been minted and allocated prior to auction creation (and this callback) + /// /// This function reverts if: + /// - `seller_` is not the owner /// - `baseToken_` is not the same as `bAsset` /// - `quoteToken_` is not the same as `RESERVE` + /// - `baseToken_` is not lower than `quoteToken_` + /// - `recipient` is the zero address /// - `lotId` is already set - /// - `CreateData.floorReservesPercent` is less than 0% or greater than 100% - /// - `CreateData.anchorTickWidth` is 0 - /// - `CreateData.discoveryTickWidth` is 0 + /// - The pool fee tier is not supported + /// - `CreateData.floorReservesPercent` is less than 10% or greater than 90% + /// - `CreateData.poolPercent` is less than 10% or greater than 100% + /// - `CreateData.floorRangeGap` is < 0 + /// - `CreateData.anchorTickWidth` is < 10 or > 50 /// - The auction format is not supported /// - The auction is not prefunded - /// - The active tick of the Baseline pool (from `baseToken_`) is not the same as the tick corresponding to the auction price + /// - Any of the tick ranges would exceed the tick bounds + /// - The provided anchor range upper tick is not the same as the calculated value + /// - The pool tick is less than the auction price (in terms of ticks) + /// - The pool capacity is not sufficient to support the intended supply function _onCreate( uint96 lotId_, address seller_, @@ -237,6 +331,12 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { bool prefund_, bytes calldata callbackData_ ) internal override { + // Validate that the seller is the owner + // Otherwise this single-use callback could be used by anyone + if (seller_ != owner) { + revert Callback_NotAuthorized(); + } + // Validate the base token is the baseline token // and the quote token is the reserve if (baseToken_ != address(bAsset)) { @@ -245,6 +345,10 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { if (quoteToken_ != address(RESERVE)) { revert Callback_Params_ReserveTokenMismatch(quoteToken_, address(RESERVE)); } + // Ensure the base token is lower than the quote token + if (address(bAsset) > address(RESERVE)) { + revert Callback_BPOOLInvalidAddress(); + } // Validate that the lot ID is not already set if (lotId != type(uint96).max) revert Callback_InvalidParams(); @@ -252,19 +356,34 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { // Decode the provided callback data (must be correctly formatted even if not using parts of it) CreateData memory cbData = abi.decode(callbackData_, (CreateData)); - // Validate that the anchor tick width is at least 1 tick spacing - if (cbData.anchorTickWidth <= 0) { + // Validate that the recipient is not the zero address + if (cbData.recipient == address(0)) revert Callback_Params_InvalidRecipient(); + + // Validate that the pool fee tier is supported + // This callback only supports the 1% fee tier (tick spacing = 200) + // as other fee tiers are not supported by the Baseline pool + if (BPOOL.TICK_SPACING() != 200) revert Callback_Params_UnsupportedPoolFeeTier(); + + // Validate that the floor range gap is at least 0 + if (cbData.floorRangeGap < 0) revert Callback_Params_InvalidFloorRangeGap(); + + // Validate that the anchor tick width is at least 10 tick spacing and at most 50 + // Baseline supports only within this range + if (cbData.anchorTickWidth < 10 || cbData.anchorTickWidth > 50) { revert Callback_Params_InvalidAnchorTickWidth(); } - // Validate that the discovery tick width is at least 1 tick spacing - if (cbData.discoveryTickWidth <= 0) { - revert Callback_Params_InvalidDiscoveryTickWidth(); + // Validate that the floor reserves percent is between 10% and 90% + // If the floor reserves are too low, it can render `MarketMaking.slide()` inoperable + // If the floor reserves are too high, it can render `MarketMaking.sweep()` inoperable + // as the anchor and discovery liquidity will be too thin + if (cbData.floorReservesPercent < 10e2 || cbData.floorReservesPercent > 90e2) { + revert Callback_Params_InvalidFloorReservesPercent(); } - // Validate that the floor reserves percent is between 0% and 100% - if (cbData.floorReservesPercent > 99e2) { - revert Callback_Params_InvalidFloorReservesPercent(); + // Validate that the pool percent is at least 10% and at most 100% + if (cbData.poolPercent < 10e2 || cbData.poolPercent > 100e2) { + revert Callback_Params_InvalidPoolPercent(); } // Auction must be prefunded for batch auctions (which is the only type supported with this callback), @@ -274,17 +393,26 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { // Set the lot ID lotId = lotId_; + // Set the recipient + recipient = cbData.recipient; + + // Set the pool percent + poolPercent = cbData.poolPercent; + // Set the floor reserves percent floorReservesPercent = cbData.floorReservesPercent; - // Get the auction format - AxisKeycode auctionFormat = keycodeFromVeecode( - AxisModule(address(IAuctionHouse(AUCTION_HOUSE).getAuctionModuleForId(lotId))).VEECODE() - ); - // Only supports Fixed Price Batch Auctions initially - if (fromAxisKeycode(auctionFormat) != bytes5("FPBA")) { - revert Callback_Params_UnsupportedAuctionFormat(); + { + // Get the auction format + AxisKeycode auctionFormat = keycodeFromVeecode( + AxisModule(address(IAuctionHouse(AUCTION_HOUSE).getAuctionModuleForId(lotId_))) + .VEECODE() + ); + + if (fromAxisKeycode(auctionFormat) != bytes5("FPBA")) { + revert Callback_Params_UnsupportedAuctionFormat(); + } } // This contract can be extended with an allowlist for the auction @@ -294,36 +422,6 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { lotId_, seller_, baseToken_, quoteToken_, capacity_, prefund_, cbData.allowlistParams ); - // Calculate the initial active tick from the auction price without rounding - int24 activeTick; - { - IFixedPriceBatch auctionModule = IFixedPriceBatch( - address(IAuctionHouse(AUCTION_HOUSE).getAuctionModuleForId(lotId_)) - ); - - // Get the fixed price from the auction module - // This value is in the number of reserve tokens per baseline token - uint256 auctionPrice = auctionModule.getAuctionData(lotId_).price; - (,,, uint8 baseTokenDecimals,,,,) = auctionModule.lotData(lotId_); - - // Calculate the active tick from the auction price - // `getSqrtPriceX96` handles token ordering - // The resulting tick will incorporate any differences in decimals between the tokens - uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96( - address(RESERVE), address(bAsset), auctionPrice, 10 ** baseTokenDecimals - ); - activeTick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); - - // Check that the pool is initialized at the active tick - // This is to ensure that the pool is initialized at the tick closest to the auction price - (, int24 poolCurrentTick,,,,,) = BPOOL.pool().slot0(); - if (poolCurrentTick != activeTick) { - revert Callback_Params_PoolTickMismatch(activeTick, poolCurrentTick); - } - } - - int24 tickSpacing = BPOOL.TICK_SPACING(); - // Set the ticks for the Baseline pool initially with the following assumptions: // - The floor range is 1 tick spacing wide // - The anchor range is `anchorTickWidth` tick spacings wide, above the floor range @@ -332,8 +430,18 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { // - The anchor range upper tick is the active tick rounded up to the nearest tick spacing // - The other range boundaries are calculated accordingly { - // Initially, the anchor range upper is the active tick rounded up - int24 anchorRangeUpper = _roundUpTickToSpacing(activeTick, tickSpacing); + // Check that the anchor tick range upper bound is the same + // as the closest tick spacing boundary above the active tick on the BPOOL + // We check this value against a parameter instead of reading + // directly to avoid a situation where someone front-runs the + // auction creation transaction and moves the active tick + if (cbData.anchorTickU != BPOOL.getActiveTS()) { + revert Callback_Params_InvalidAnchorTickUpper(); + } + int24 anchorRangeUpper = cbData.anchorTickU; + + // Get the tick spacing from the pool + int24 tickSpacing = BPOOL.TICK_SPACING(); // Anchor range lower is the anchor tick width below the anchor range upper int24 anchorRangeLower = anchorRangeUpper - cbData.anchorTickWidth * tickSpacing; @@ -342,20 +450,133 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { BPOOL.setTicks(Range.ANCHOR, anchorRangeLower, anchorRangeUpper); // Set the floor range - // Floor range lower is the anchor range lower minus one tick spacing - BPOOL.setTicks(Range.FLOOR, anchorRangeLower - tickSpacing, anchorRangeLower); + // The creator can provide the `floorRangeGap` to space the floor range from the anchor range + // If `floorRangeGap` is 0, the floor range will be directly below the anchor range + // The floor range is one tick spacing wide + int24 floorRangeUpper = anchorRangeLower - cbData.floorRangeGap * tickSpacing; + int24 floorRangeLower = floorRangeUpper - tickSpacing; + + BPOOL.setTicks(Range.FLOOR, floorRangeLower, floorRangeUpper); // Set the discovery range - BPOOL.setTicks( - Range.DISCOVERY, - anchorRangeUpper, - anchorRangeUpper + tickSpacing * cbData.discoveryTickWidth - ); + int24 discoveryRangeUpper = + anchorRangeUpper + tickSpacing * _DISCOVERY_TICK_SPACING_WIDTH; + BPOOL.setTicks(Range.DISCOVERY, anchorRangeUpper, discoveryRangeUpper); + + // If the floor range lower tick (or any other above it) is below the min tick, it will cause problems + // If the discovery range upper tick (or any other below it) is above the max tick, it will cause problems + if (floorRangeLower < _MIN_TICK || discoveryRangeUpper > _MAX_TICK) { + revert Callback_Params_RangeOutOfBounds(); + } + } + + // Perform a pre-check to make sure the setup can be valid + // This avoids certain bad configurations that would lead to failed initializations + // Specifically, we check that the pool can support the intended supply + // factoring in the capacity, curator fee, and any additional spot or collateralized supply + // that already exists. + // We assume that the auction capacity will be completely filled. This can be guaranteed by + // setting the minFillPercent to 100e2 on the auction. + { + // Calculate the initial circulating supply (including collateralized supply) + uint256 initialCircSupply; + { + // Get the current supply values + uint256 totalSupply = bAsset.totalSupply(); // can use totalSupply here since no bAssets are in the pool yet + + // Calculate the maximum curator fee that can be paid + (,, uint48 curatorFeePerc,,) = IAuctionHouse(AUCTION_HOUSE).lotFees(lotId_); + uint256 curatorFee = (capacity_ * curatorFeePerc) / ONE_HUNDRED_PERCENT; + + // Capacity and curator fee have not yet been minted, so we add those + // Collateralized supply is already minted and included in total supply, so we do not need to add it + initialCircSupply = totalSupply + capacity_ + curatorFee; + } + + // Calculate the initial capacity of the pool based on the ticks set and the expected proceeds to deposit in the pool + uint256 initialCapacity; + { + // Get the fixed price from the auction module + // This value is in the number of reserve tokens per baseline token + uint256 auctionPrice; + { + auctionPrice = IFixedPriceBatch( + address(IAuctionHouse(AUCTION_HOUSE).getAuctionModuleForId(lotId_)) + ).getAuctionData(lotId_).price; + } + + // Get the active tick from the pool and confirm it is >= the auction price corresponds to + { + // We do this to avoid a situation where buyers are disincentivized to bid on the auction + // Pool price is number of token1 (reserve) per token0 (bAsset), which is what we want, but it needs to be squared + (, int24 activeTick,,,,,) = BPOOL.pool().slot0(); + + // Calculate the tick for the auction price + // `getSqrtPriceX96` handles token ordering + // The resulting tick will incorporate any differences in decimals between the tokens + uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96( + address(RESERVE), address(bAsset), auctionPrice, 10 ** bAsset.decimals() + ); + int24 auctionTick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); + + // Verify that the active tick is at least the auction tick + if (activeTick < auctionTick) { + revert Callback_PoolLessThanAuctionPrice(activeTick, auctionTick); + } + } + + uint256 poolProceeds = ( + _getExpectedProceeds(lotId_, capacity_, auctionPrice) * poolPercent + ) / ONE_HUNDRED_PERCENT; + + // Calculate the expected reserves for the floor and anchor ranges + uint256 floorReserves = (poolProceeds * floorReservesPercent) / ONE_HUNDRED_PERCENT; + uint256 anchorReserves = poolProceeds - floorReserves; + + // Calculate the expected capacity of the pool + // Skip discovery range since no reserves will be deposited in it + Position memory floor = BPOOL.getPosition(Range.FLOOR); + Position memory anchor = BPOOL.getPosition(Range.ANCHOR); + + uint256 floorCapacity = + BPOOL.getCapacityForReserves(floor.sqrtPriceL, floor.sqrtPriceU, floorReserves); + uint256 anchorCapacity = BPOOL.getCapacityForReserves( + anchor.sqrtPriceL, anchor.sqrtPriceU, anchorReserves + ); + + // Calculate the debt capacity at the floor range + uint256 debtCapacity = BPOOL.getCapacityForReserves( + floor.sqrtPriceL, + floor.sqrtPriceU, + CREDT.totalCreditIssued() + LOOPS.totalDebt() + ); + + // Calculate the total initial capacity of the pool + initialCapacity = debtCapacity + floorCapacity + anchorCapacity; + } + + // Verify the liquidity can support the intended supply + // and that there is no significant initial surplus + // + // If the solvency check is failing, it can be resolved by adjusting the following: + // - auction price (via the auction fixed price) + // - system liquidity (via the pool allocation and floor reserves allocation) + uint256 capacityRatio = initialCapacity.divWad(initialCircSupply); + if (capacityRatio < 100e16 || capacityRatio > 102e16) { + revert Callback_InvalidCapacityRatio(capacityRatio); + } } + // Unlock BPOOL transfers + BPOOL.setTransferLock(false); + // Mint the capacity of baseline tokens to the auction house to prefund the auction BPOOL.mint(msg.sender, capacity_); - initialCirculatingSupply += capacity_; + + // Lock BPOOL transfers + // This is to prevent any transfers of bAssets until the auction is settled + // The BPOOL will need to be manually unlocked prior to the auction being cancelled or settled + BPOOL.setTransferLock(true); } /// @notice Override this function to implement allowlist functionality @@ -377,18 +598,18 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { /// This function has the following assumptions: /// - BaseCallback has already validated the lot ID /// - The AuctionHouse has already sent the correct amount of bAsset tokens + /// - The auction seller has manually unlocked BPOOL transfers, in order to allow the transfer of BPOOL tokens from the AuctionHouse to the callback /// /// This function reverts if: /// - `lotId_` is not the same as the stored `lotId` /// - The auction is already complete /// - Sufficient quantity of `bAsset` have not been sent to the callback - function _onCancel(uint96 lotId_, uint256 refund_, bool, bytes calldata) internal override { - // Validate the lot ID - if (lotId_ != lotId) revert Callback_InvalidParams(); - - // Validate that the lot is not already settled or cancelled - if (auctionComplete) revert Callback_AlreadyComplete(); - + function _onCancel( + uint96 lotId_, + uint256 refund_, + bool, + bytes calldata + ) internal override onlyValidLot(lotId_) onlyActiveLot { // Burn any refunded tokens (all auctions are prefunded) // Verify that the callback received the correct amount of bAsset tokens if (bAsset.balanceOf(address(this)) < refund_) revert Callback_MissingFunds(); @@ -396,10 +617,15 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { // Set the auction lot to be cancelled auctionComplete = true; + // Unlock BPOOL transfers + BPOOL.setTransferLock(false); + // Send tokens to BPOOL and then burn - initialCirculatingSupply -= refund_; Transfer.transfer(bAsset, address(BPOOL), refund_, false); BPOOL.burnAllBAssetsInContract(); + + // Lock BPOOL transfers + BPOOL.setTransferLock(true); } /// @inheritdoc BaseCallback @@ -417,13 +643,17 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { uint256 curatorFee_, bool, bytes calldata - ) internal view override { - // Validate the lot ID - if (lotId_ != lotId) revert Callback_InvalidParams(); + ) internal override onlyValidLot(lotId_) onlyActiveLot { + // Mint tokens for curator fee if it's not zero + if (curatorFee_ > 0) { + // Unlock BPOOL transfers + BPOOL.setTransferLock(false); + + BPOOL.mint(msg.sender, curatorFee_); - // Require that the curator fee in the Auction House is zero - // We do this to not dilute the buyer's backing (and therefore the price that the Baseline pool is initialized at) - if (curatorFee_ > 0) revert Callback_InvalidParams(); + // Lock BPOOL transfers + BPOOL.setTransferLock(true); + } } /// @inheritdoc BaseCallback @@ -454,11 +684,10 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { /// - Performs validation /// - Sets the auction as complete /// - Burns any refunded bAsset tokens - /// - Calculates the deployment parameters for the Baseline pool - /// - EMP auction format: calculates the ticks based on the clearing price - /// - Deploys reserves into the Baseline pool - /// - /// Note that there may be reserve assets left over after liquidity deployment, which must be manually withdrawn by the owner using `withdrawReserves()`. + /// - Ensures that the pool is at the correct price + /// - Deploys reserves and liquidity into the Baseline pool + /// - Performs a solvency check to ensure the pool can support the supply + /// - Transfers any remaining proceeds (reserves) to the recipient /// /// Next steps: /// - Activate the market making and credit facility policies in the Baseline stack, which cannot be enabled before the auction is settled and the pool is initialized @@ -466,25 +695,26 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { /// This function has the following assumptions: /// - BaseCallback has already validated the lot ID /// - The AuctionHouse has already sent the correct amount of quote tokens (proceeds) + /// - The AuctionHouse has already sent the correct amount of refunded base tokens /// - The AuctionHouse is pre-funded, so does not require additional base tokens (bAssets) to be supplied + /// - No new Baseline credit allocations have been made since the auction was created (and `onCreate` was called) + /// - The auction seller has manually unlocked BPOOL transfers, in order to allow the transfer of refunded BPOOL tokens from the AuctionHouse to the callback /// /// This function reverts if: /// - `lotId_` is not the same as the stored `lotId` /// - The auction is already complete /// - The reported proceeds received are less than the reserve balance /// - The reported refund received is less than the bAsset balance + /// - The pool price is not at the target price + /// - The pool capacity is not sufficient to support the intended supply + /// + /// Note that while the solvency check in both `onCreate` and `onSettle` are consistent, if the auction is not fully subscribed (and hence there is a refund), it can cause the solvency check to fail in `onSettle`. function _onSettle( uint96 lotId_, uint256 proceeds_, uint256 refund_, bytes calldata - ) internal virtual override { - // Validate the lot ID - if (lotId_ != lotId) revert Callback_InvalidParams(); - - // Validate that the auction is not already complete - if (auctionComplete) revert Callback_AlreadyComplete(); - + ) internal virtual override onlyValidLot(lotId_) onlyActiveLot { // Validate that the callback received the correct amount of proceeds // As this is a single-use contract, reserve balance is likely 0 prior, but extra funds will not affect it if (proceeds_ > RESERVE.balanceOf(address(this))) revert Callback_MissingFunds(); @@ -497,26 +727,104 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { auctionComplete = true; //// Step 1: Burn any refunded bAsset tokens //// - - // Subtract refund from initial supply - initialCirculatingSupply -= refund_; + // Unlock BPOOL transfers + BPOOL.setTransferLock(false); // Burn any refunded bAsset tokens that were sent from the auction house Transfer.transfer(bAsset, address(BPOOL), refund_, false); BPOOL.burnAllBAssetsInContract(); - //// Step 2: Deploy liquidity to the Baseline pool //// + //// Step 2: Ensure the pool is at the correct price //// + // Since there is no bAsset liquidity deployed yet, + // External LPs can move the current price of the pool. + // We move it back to the target active tick to ensure + // the pool is at the correct price. + { + IUniswapV3Pool pool = BPOOL.pool(); + + // Current price of the pool + (uint160 currentSqrtPrice,,,,,,) = pool.slot0(); + + // Get the target sqrt price from the anchor position + uint160 targetSqrtPrice = BPOOL.getPosition(Range.ANCHOR).sqrtPriceU; + + // We assume there are no circulating bAssets that could be provided as liquidity yet. + // Therefore, there are three cases: + // 1. The current price is above the target price + if (currentSqrtPrice > targetSqrtPrice) { + // In this case, an external LP has provided reserve liquidity above our range. + // We can sell bAssets into this liquidity and the external LP will effectively be + // bAssets at a premium to the initial price of the pool. + // This does not affect the solvency of the system since the reserves received + // are greater than the tokens minted, but it may cause the system to be + // initialized with a surplus, which would allow for an immediate bump or snipe. + // + // We want to swap out all of the reserves currently in the pool above the target price for bAssets. + // We just use the total balance in the pool because the price limit will prevent buying lower. + int256 amount1Out = -int256(RESERVE.balanceOf(address(pool))); + + // If there is no liquidity in the pool, this ensures that the swap will suceed + if (amount1Out == 0) amount1Out = -1; + + pool.swap( + address(this), // recipient + true, // zeroToOne, swapping token0 (bAsset) for token1 (reserve) so this is true + amount1Out, // amountSpecified, positive is exactIn, negative is exactOut + targetSqrtPrice, // sqrtPriceLimitX96 + abi.encode(1) // data, case 1 + ); + } + // 2. The current price is below the target price + else if (currentSqrtPrice < targetSqrtPrice) { + // Swap 1 wei of token1 (reserve) for token0 (bAsset) with a limit at the targetSqrtPrice + // There are no bAssets in the pool, so we receive none. Because of this, + // we don't end up paying any reserves either, but the price of the pool is shifted. + pool.swap( + address(this), // recipient + false, // zeroToOne, swapping token1 (reserve) for token0 (bAsset) so this is false + int256(1), // amountSpecified, positive is exactIn, negative is exactOut + targetSqrtPrice, // sqrtPriceLimitX96 + abi.encode(2) // data, case 2 + ); + } + // 3. The current price is at the target price. + // If so, we don't need to do anything. + + // We don't need to track any of these amounts because the liquidity deployment and + // will handle any extra reserves and the solvency check ensures that the system + // can support the supply. + + // Check that the price is now at the target price + (currentSqrtPrice,,,,,,) = pool.slot0(); + if (currentSqrtPrice != targetSqrtPrice) { + revert Callback_InvalidInitialization(); + } + } + + //// Step 3: Deploy liquidity to the Baseline pool //// + + // Calculate reserves to add to the pool + // Because we potentially extracted reserves from the pool in the previous step, + // we use the current balance minus the seller proceeds from the auction as + // the pool proceeds amount so that the surplus is provided to the pool. + // If no reserves were extracted, this will be the same amount as expected. + uint256 sellerProceeds = + proceeds_ * (ONE_HUNDRED_PERCENT - poolPercent) / ONE_HUNDRED_PERCENT; + + uint256 poolProceeds = RESERVE.balanceOf(address(this)) - sellerProceeds; // Approve spending of the reserve token - // There should not be any dangling approvals left - Transfer.approve(RESERVE, address(BPOOL), proceeds_); + Transfer.approve(RESERVE, address(BPOOL), poolProceeds); // Add the configured percentage of the proceeds to the Floor range - uint256 floorReserves = proceeds_ * floorReservesPercent / ONE_HUNDRED_PERCENT; + uint256 floorReserves = poolProceeds * floorReservesPercent / ONE_HUNDRED_PERCENT; BPOOL.addReservesTo(Range.FLOOR, floorReserves); // Add the remainder of the proceeds to the Anchor range - BPOOL.addReservesTo(Range.ANCHOR, proceeds_ - floorReserves); + BPOOL.addReservesTo(Range.ANCHOR, poolProceeds - floorReserves); + + // Ensure that there are no dangling approvals + Transfer.approve(RESERVE, address(BPOOL), 0); // Add proportional liquidity to the Discovery range. // Only the anchor range is used, otherwise the liquidity would be too thick. @@ -524,49 +832,100 @@ contract BaselineAxisLaunch is BaseCallback, Policy, Owned { // and to have reserves of at least 1% of the proceeds. BPOOL.addLiquidityTo(Range.DISCOVERY, BPOOL.getLiquidity(Range.ANCHOR) * 11 / 10); - //// Step 3: Verify Solvency //// - uint256 totalCapacity = BPOOL.getPosition(Range.FLOOR).capacity - + BPOOL.getPosition(Range.ANCHOR).capacity + BPOOL.getPosition(Range.DISCOVERY).capacity; + //// Step 4: Send remaining proceeds (and any excess reserves) to the recipient //// + Transfer.transfer(RESERVE, recipient, RESERVE.balanceOf(address(this)), false); + + //// Step 5: Verify Solvency //// + { + Position memory floor = BPOOL.getPosition(Range.FLOOR); + Position memory anchor = BPOOL.getPosition(Range.ANCHOR); + Position memory discovery = BPOOL.getPosition(Range.DISCOVERY); + + // Calculate the debt capacity at the floor range + uint256 debtCapacity = BPOOL.getCapacityForReserves( + floor.sqrtPriceL, floor.sqrtPriceU, CREDT.totalCreditIssued() + LOOPS.totalDebt() + ); + + uint256 totalCapacity = + debtCapacity + floor.capacity + anchor.capacity + discovery.capacity; + // Includes collateralised supply + uint256 totalSpotSupply = + bAsset.totalSupply() - floor.bAssets - anchor.bAssets - discovery.bAssets; + + // verify the liquidity can support the intended supply + // we do not check for a surplus at this point to avoid a DoS attack vector + // during the onCreate callback, we check for a surplus and there shouldn't + // be one from this initialization at this point. + // any surplus reserves added to the pool by a 3rd party before + // the system is initialized will be snipable and effectively donated to the snipers + uint256 capacityRatio = totalCapacity.divWad(totalSpotSupply); + if (capacityRatio < 100e16) { + revert Callback_InvalidCapacityRatio(capacityRatio); + } + } - // Note: if this reverts, then the auction will not be able to be settled - // and users will be able to claim refunds from the auction house - if (totalCapacity < initialCirculatingSupply) revert Insolvent(); + // Lock BPOOL transfers to prevent any token interactions until the system is ready + BPOOL.setTransferLock(true); // Emit an event { - (int24 floorTickLower, int24 floorTickUpper) = BPOOL.getTicks(Range.FLOOR); - emit LiquidityDeployed(floorTickLower, floorTickUpper, BPOOL.getLiquidity(Range.FLOOR)); + (int24 floorTickLower,) = BPOOL.getTicks(Range.FLOOR); + (, int24 anchorTickUpper) = BPOOL.getTicks(Range.ANCHOR); + emit LiquidityDeployed( + floorTickLower, + anchorTickUpper, + BPOOL.getLiquidity(Range.FLOOR), + BPOOL.getLiquidity(Range.ANCHOR) + ); } } - // ========== HELPER FUNCTIONS ========== // + // ========== INTERNAL FUNCTIONS ========== // - /// @notice Rounds up the provided tick to the nearest tick spacing - /// @dev This function behaves differently to BPOOL.getActiveTS() in handling edge cases. In particular, if the tick is equal to the rounded tick, it will not be adjusted. + /// @notice Calculate the expected proceeds from the auction and how much will be deposited in the pool + /// @dev The proceeds sent to the onSettle callback function will exclude any protocol and referrer fees, so this calculation mimics the behaviour /// - /// @param tick_ The tick to round - /// @param tickSpacing_ The tick spacing to round to - function _roundUpTickToSpacing(int24 tick_, int24 tickSpacing_) internal pure returns (int24) { - int24 roundedTick = (tick_ / tickSpacing_) * tickSpacing_; - - if (tick_ > roundedTick) { - roundedTick += tickSpacing_; - } - - return roundedTick; + /// @param lotId_ Lot ID of the auction + /// @param auctionPrice_ Price of the auction + /// @param capacity_ Capacity of the auction + /// @return proceeds Expected proceeds from the auction + function _getExpectedProceeds( + uint96 lotId_, + uint256 auctionPrice_, + uint256 capacity_ + ) internal view returns (uint256) { + // Get the fees from the auction house + (,,, uint48 protocolFee, uint48 referrerFee) = IAuctionHouse(AUCTION_HOUSE).lotFees(lotId_); + + // Calculate the expected proceeds after fees + uint256 proceedsBeforeFees = (auctionPrice_ * capacity_) / (10 ** bAsset.decimals()); + return proceedsBeforeFees - (proceedsBeforeFees * protocolFee) / ONE_HUNDRED_PERCENT + - (proceedsBeforeFees * referrerFee) / ONE_HUNDRED_PERCENT; } - // ========== OWNER FUNCTIONS ========== // + // ========== UNIV3 FUNCTIONS ========== // - /// @notice Withdraws any remaining reserve tokens from the contract - /// @dev This is access-controlled to the owner - /// - /// @return withdrawnAmount The amount of reserve tokens withdrawn - function withdrawReserves() external onlyOwner returns (uint256 withdrawnAmount) { - withdrawnAmount = RESERVE.balanceOf(address(this)); + // Provide tokens when adjusting the pool price via a swap before deploying liquidity + function uniswapV3SwapCallback(int256 bAssetDelta_, int256, bytes calldata data_) external { + // Only the pool can call + address pool = address(BPOOL.pool()); + if (msg.sender != pool) revert Callback_Swap_InvalidCaller(); - Transfer.transfer(RESERVE, owner, withdrawnAmount, false); + // Decode the data + (uint8 case_) = abi.decode(data_, (uint8)); - return withdrawnAmount; + // Handle the swap case + if (case_ == 1) { + // Mint the bAsset delta to the pool (if greater than 0) + if (bAssetDelta_ > 0) { + BPOOL.mint(pool, uint256(bAssetDelta_)); + } + } else if (case_ == 2) { + // Case 2: Swapped in 1 wei of reserve tokens + // We don't need to do anything here + } + else { + revert Callback_Swap_InvalidCase(); + } } } diff --git a/src/callbacks/liquidity/BaselineV2/README.md b/src/callbacks/liquidity/BaselineV2/README.md index 30259578..4e7f2a7c 100644 --- a/src/callbacks/liquidity/BaselineV2/README.md +++ b/src/callbacks/liquidity/BaselineV2/README.md @@ -10,35 +10,37 @@ This callbacks contract currently only supported the Fixed Price Batch auction f ## Lifecycle 1. Deploy Baseline stack - - The BPOOL module will create a Uniswap V3 pool in the constructor. - As a result, it requires the initial tick of the pool to be - specified as a constructor argument. - The price can be calculated using the following: - - ```solidity - uint256 auctionPrice = 2e18; // Example price - uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96( - address(quoteToken), address(BPOOL), auctionPrice, 10 ** baseTokenDecimals - ); - int24 activeTick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); - ``` - - - Deploy the Baseline `Kernel`, `BPOOL` module and other policies (except for `BaselineInit`) + + - The BPOOL module will create a Uniswap V3 pool in the constructor. + As a result, it requires the initial tick of the pool to be + specified as a constructor argument. + The price can be calculated using the following: + + ```solidity + uint256 auctionPrice = 2e18; // Example price + uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96( + address(quoteToken), address(BPOOL), auctionPrice, 10 ** baseTokenDecimals + ); + int24 activeTick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); + ``` + + - Deploy the Baseline `Kernel`, `BPOOL` module and other policies (except for `BaselineInit`) + 2. Deploy the Baseline-Axis callback - - The `BatchAuctionHouse`, Baseline `Kernel`, quote token (aka reserve) and owner need to be specified as constructor arguments. See the `baselineAllocatedAllowlist-sample.json` file for an example of how to configure this. - - The salt for the callback will need to be generated. See the [salts README](/script/salts/README.md) for instructions. - - Run the deployment script. See the [deployment README](/script/deploy/README.md#running-the-deployment) for instructions. - - Each callbacks contract is single-use, specific to the auction and Baseline stack. + - The `BatchAuctionHouse`, Baseline `Kernel`, quote token (aka reserve) and owner need to be specified as constructor arguments. See the `baselineAllocatedAllowlist-sample.json` file for an example of how to configure this. + - The salt for the callback will need to be generated. See the [salts README](/script/salts/README.md) for instructions. + - Run the deployment script. See the [deployment README](/script/deploy/README.md#running-the-deployment) for instructions. + - Each callbacks contract is single-use, specific to the auction and Baseline stack. 3. Deploy the Axis auction, specifying the callbacks contract and parameters. - - See [TestData.s.sol:createAuction](/script/test/FixedPriceBatch-Baseline/TestData.s.sol) for an example of this. - - Note that curator fees are not supported when using the Baseline-Axis callback. - - `onCreate()` will be called on the callbacks contract. This results in: - - The tick ranges on the Baseline `BPOOL` being configured - - The auction capacity (in terms of `BPOOL` tokens) being minted and transferred to the `BatchAuctionHouse`. + - See [TestData.s.sol:createAuction](/script/test/FixedPriceBatch-Baseline/TestData.s.sol) for an example of this. + - Note that curator fees are not supported when using the Baseline-Axis callback. + - `onCreate()` will be called on the callbacks contract. This results in: + - The tick ranges on the Baseline `BPOOL` being configured + - The auction capacity (in terms of `BPOOL` tokens) being minted and transferred to the `BatchAuctionHouse`. 4. When a bid is submitted, if the configured callbacks contract has allowlist functionality, it will determine if the bidder is allowed to bid. 5. On settlement of the auction, the following will happen: - - Any refunded base tokens (`BPOOL` tokens) are burnt - - The configured percentage of proceeds (quote/reserve tokens) are deposited into the floor range of the Baseline pool. - - The remaining proceeds are deposited into the anchor range of the Baseline pool. - - Proportional liquidity (currently 11/10 of the anchor range liquidity) is deployed as `BPOOL` tokens in the discovery range. - - The solvency of the Baseline pool is verified. + - Any refunded base tokens (`BPOOL` tokens) are burnt + - The configured percentage of proceeds (quote/reserve tokens) are deposited into the floor range of the Baseline pool. + - The remaining proceeds are deposited into the anchor range of the Baseline pool. + - Proportional liquidity (currently 11/10 of the anchor range liquidity) is deployed as `BPOOL` tokens in the discovery range. + - The solvency of the Baseline pool is verified. diff --git a/src/callbacks/liquidity/BaselineV2/lib/IBPOOL.sol b/src/callbacks/liquidity/BaselineV2/lib/IBPOOL.sol index c8746b68..bb5be27e 100644 --- a/src/callbacks/liquidity/BaselineV2/lib/IBPOOL.sol +++ b/src/callbacks/liquidity/BaselineV2/lib/IBPOOL.sol @@ -55,7 +55,9 @@ interface IBPOOLv1 { uint128 _liquidity ) external returns (uint256 bAssetsAdded_, uint256 reservesAdded_, uint128 liquidityFinal_); - function removeAllFrom(Range _range) + function removeAllFrom( + Range _range + ) external returns ( uint256 bAssetsRemoved_, @@ -73,8 +75,12 @@ interface IBPOOLv1 { /// @dev No need to discount collateralizedBAssets because it's in a separate contract now. function burnAllBAssetsInContract() external; + function setTransferLock(bool _locked) external; + // ========= PUBLIC READ FUNCTIONS ========= // + function locked() external view returns (bool); + /// @notice Returns the price at the lower tick of the floor position function getBaselineValue() external view returns (uint256); diff --git a/src/callbacks/liquidity/BaselineV2/lib/ICREDT.sol b/src/callbacks/liquidity/BaselineV2/lib/ICREDT.sol index d5d7e2c3..d34aa7af 100644 --- a/src/callbacks/liquidity/BaselineV2/lib/ICREDT.sol +++ b/src/callbacks/liquidity/BaselineV2/lib/ICREDT.sol @@ -19,10 +19,9 @@ interface ICREDTv1 { function bAsset() external view returns (ERC20); /// @notice Individual credit account state - function creditAccounts(address) - external - view - returns (uint256 credit, uint256 collateral, uint256 expiry); + function creditAccounts( + address + ) external view returns (uint256 credit, uint256 collateral, uint256 expiry); /// @notice Container for aggregate credit and collateral to be defaulted at a timeslot struct Defaultable { @@ -49,16 +48,14 @@ interface ICREDTv1 { /// @notice Gets current credit account for user. /// @dev Returns zeroed account after full repayment or default. - function getCreditAccount(address _user) - external - view - returns (CreditAccount memory account_); + function getCreditAccount( + address _user + ) external view returns (CreditAccount memory account_); function updateCreditAccount( address _user, uint256 _newCollateral, uint256 _newCredit, - uint256 _newInterest, uint256 _newExpiry ) external; } diff --git a/src/callbacks/liquidity/BaselineV2/lib/ILOOPS.sol b/src/callbacks/liquidity/BaselineV2/lib/ILOOPS.sol new file mode 100644 index 00000000..c68f855c --- /dev/null +++ b/src/callbacks/liquidity/BaselineV2/lib/ILOOPS.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/// @notice Baseline LOOPS Module +/// @dev Imported at commit 8950018baec27d6497fba409cb361a596535447d +interface ILOOPSv1 { + function totalDebt() external view returns (uint256); +} diff --git a/src/callbacks/liquidity/UniswapV2DTL.sol b/src/callbacks/liquidity/UniswapV2DTL.sol index 3dfb43b5..0fde44cc 100644 --- a/src/callbacks/liquidity/UniswapV2DTL.sol +++ b/src/callbacks/liquidity/UniswapV2DTL.sol @@ -3,9 +3,12 @@ pragma solidity ^0.8.19; // Libraries import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; +import {FullMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/FullMath.sol"; +import {SafeTransferLib} from "@solmate-6.7.0/utils/SafeTransferLib.sol"; // Uniswap import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Factory.sol"; +import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Pair.sol"; import {IUniswapV2Router02} from "@uniswap-v2-periphery-1.0.1/interfaces/IUniswapV2Router02.sol"; // Callbacks @@ -24,13 +27,15 @@ import {BaseDirectToLiquidity} from "./BaseDTL.sol"; /// @dev As a general rule, this callback contract does not retain balances of tokens between calls. /// Transfers are performed within the same function that requires the balance. contract UniswapV2DirectToLiquidity is BaseDirectToLiquidity { + using SafeTransferLib for ERC20; + // ========== STRUCTS ========== // - /// @notice Parameters for the onClaimProceeds callback + /// @notice Parameters for the onCreate callback /// @dev This will be encoded in the `callbackData_` parameter /// - /// @param maxSlippage The maximum slippage allowed when adding liquidity (in terms of `ONE_HUNDRED_PERCENT`) - struct OnSettleParams { + /// @param maxSlippage The maximum slippage allowed when adding liquidity (in terms of basis points, where 1% = 1e2) + struct UniswapV2OnCreateParams { uint24 maxSlippage; } @@ -69,36 +74,44 @@ contract UniswapV2DirectToLiquidity is BaseDirectToLiquidity { /// - Validates the parameters /// /// This function reverts if: - /// - The pool for the token combination already exists + /// - The callback data is of the incorrect length + /// - `UniswapV2OnCreateParams.maxSlippage` is out of bounds + /// + /// Note that this function does not check if the pool already exists. The reason for this is that it could be used as a DoS vector. function __onCreate( - uint96, + uint96 lotId_, + address, + address, address, - address baseToken_, - address quoteToken_, uint256, bool, bytes calldata ) internal virtual override { - // Check that the pool does not exist - if (uniV2Factory.getPair(baseToken_, quoteToken_) != address(0)) { - revert Callback_Params_PoolExists(); + UniswapV2OnCreateParams memory params = _decodeOnCreateParameters(lotId_); + + // Check that the slippage amount is within bounds + // The maxSlippage is stored during onCreate, as the callback data is passed in by the auction seller. + // As AuctionHouse.settle() can be called by anyone, a value for maxSlippage could be passed that would result in a loss for the auction seller. + if (params.maxSlippage > ONE_HUNDRED_PERCENT) { + revert Callback_Params_PercentOutOfBounds(params.maxSlippage, 0, ONE_HUNDRED_PERCENT); } } /// @inheritdoc BaseDirectToLiquidity /// @dev This function implements the following: /// - Creates the pool if necessary + /// - Detects and handles (if necessary) manipulation of pool reserves /// - Deposits the tokens into the pool function _mintAndDeposit( - uint96, + uint96 lotId_, address quoteToken_, uint256 quoteTokenAmount_, address baseToken_, uint256 baseTokenAmount_, - bytes memory callbackData_ + bytes memory ) internal virtual override returns (ERC20 poolToken) { // Decode the callback data - OnSettleParams memory params = abi.decode(callbackData_, (OnSettleParams)); + UniswapV2OnCreateParams memory params = _decodeOnCreateParameters(lotId_); // Create and initialize the pool if necessary // Token orientation is irrelevant @@ -107,31 +120,158 @@ contract UniswapV2DirectToLiquidity is BaseDirectToLiquidity { pairAddress = uniV2Factory.createPair(baseToken_, quoteToken_); } + // Handle a potential DoS attack caused by donate and sync + uint256 quoteTokensToAdd = quoteTokenAmount_; + uint256 baseTokensToAdd = baseTokenAmount_; + { + uint256 baseTokenScale = 10 ** ERC20(baseToken_).decimals(); + uint256 auctionPrice = + FullMath.mulDiv(quoteTokenAmount_, baseTokenScale, baseTokenAmount_); + + (, uint256 baseTokensUsed) = + _mitigateDonation(pairAddress, auctionPrice, quoteToken_, baseToken_); + + if (baseTokensUsed > 0) { + baseTokensToAdd -= baseTokensUsed; + + // Re-calculate quoteTokensToAdd to be aligned with baseTokensToAdd + quoteTokensToAdd = FullMath.mulDiv(baseTokensToAdd, auctionPrice, baseTokenScale); + } + } + // Calculate the minimum amount out for each token - uint256 quoteTokenAmountMin = _getAmountWithSlippage(quoteTokenAmount_, params.maxSlippage); - uint256 baseTokenAmountMin = _getAmountWithSlippage(baseTokenAmount_, params.maxSlippage); + uint256 quoteTokenAmountMin = _getAmountWithSlippage(quoteTokensToAdd, params.maxSlippage); + uint256 baseTokenAmountMin = _getAmountWithSlippage(baseTokensToAdd, params.maxSlippage); // Approve the router to spend the tokens - ERC20(quoteToken_).approve(address(uniV2Router), quoteTokenAmount_); - ERC20(baseToken_).approve(address(uniV2Router), baseTokenAmount_); + ERC20(quoteToken_).safeApprove(address(uniV2Router), quoteTokensToAdd); + ERC20(baseToken_).safeApprove(address(uniV2Router), baseTokensToAdd); // Deposit into the pool uniV2Router.addLiquidity( quoteToken_, baseToken_, - quoteTokenAmount_, - baseTokenAmount_, + quoteTokensToAdd, + baseTokensToAdd, quoteTokenAmountMin, baseTokenAmountMin, - address(this), + address(this), // LP tokens are sent to this contract and transferred later block.timestamp ); // Remove any dangling approvals // This is necessary, since the router may not spend all available tokens - ERC20(quoteToken_).approve(address(uniV2Router), 0); - ERC20(baseToken_).approve(address(uniV2Router), 0); + ERC20(quoteToken_).safeApprove(address(uniV2Router), 0); + ERC20(baseToken_).safeApprove(address(uniV2Router), 0); return ERC20(pairAddress); } + + /// @notice Decodes the configuration parameters from the DTLConfiguration + /// @dev The configuration parameters are stored in `DTLConfiguration.implParams` + function _decodeOnCreateParameters( + uint96 lotId_ + ) internal view returns (UniswapV2OnCreateParams memory) { + DTLConfiguration memory lotConfig = lotConfiguration[lotId_]; + // Validate that the callback data is of the correct length + if (lotConfig.implParams.length != 32) { + revert Callback_InvalidParams(); + } + + return abi.decode(lotConfig.implParams, (UniswapV2OnCreateParams)); + } + + /// @notice This function mitigates the risk of a third-party having donated quote tokens to the pool causing the auction settlement to fail. + /// @dev It performs the following: + /// - Checks if the pool has had quote tokens donated, or exits + /// - Swaps the quote tokens for base tokens to adjust the reserves to the correct price + /// + /// @param pairAddress_ The address of the Uniswap V2 pair + /// @param auctionPrice_ The price of the auction + /// @param quoteToken_ The quote token of the pair + /// @param baseToken_ The base token of the pair + /// @return quoteTokensUsed The amount of quote tokens used in the swap + /// @return baseTokensUsed The amount of base tokens used in the swap + function _mitigateDonation( + address pairAddress_, + uint256 auctionPrice_, + address quoteToken_, + address baseToken_ + ) internal returns (uint256 quoteTokensUsed, uint256 baseTokensUsed) { + IUniswapV2Pair pair = IUniswapV2Pair(pairAddress_); + uint256 quoteTokenBalance = ERC20(quoteToken_).balanceOf(pairAddress_); + { + // Check if the pool has had quote tokens donated (whether synced or not) + // Base tokens are not liquid, so we don't need to check for them + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + uint112 quoteTokenReserve = pair.token0() == quoteToken_ ? reserve0 : reserve1; + + if (quoteTokenReserve == 0 && quoteTokenBalance == 0) { + return (0, 0); + } + } + + // If there has been a donation into the pool, we need to adjust the reserves so that the price is correct + // This can be performed by swapping the quote tokens for base tokens + + // To perform the swap, both reserves need to be non-zero, so we need to transfer in some base tokens and update the reserves using `sync()`. + { + ERC20(baseToken_).safeTransfer(pairAddress_, 1); + pair.sync(); + baseTokensUsed += 1; + } + + // We want the pool to end up at the auction price + // The simplest way to do this is to have the auctionPrice_ of quote tokens + // and 1 of base tokens in the pool (accounting for decimals) + uint256 desiredQuoteTokenReserves = auctionPrice_; + uint256 desiredBaseTokenReserves = 10 ** ERC20(baseToken_).decimals(); + + // Handle quote token transfers + uint256 quoteTokensOut; + { + // If the balance is less than required, transfer in + if (quoteTokenBalance < desiredQuoteTokenReserves) { + uint256 quoteTokensToTransfer = desiredQuoteTokenReserves - quoteTokenBalance; + ERC20(quoteToken_).safeTransfer(pairAddress_, quoteTokensToTransfer); + + quoteTokensUsed += quoteTokensToTransfer; + + // Update the balance + quoteTokenBalance = desiredQuoteTokenReserves; + } + + // Determine the amount of quote tokens to swap out + quoteTokensOut = quoteTokenBalance - desiredQuoteTokenReserves; + } + + // Handle base token transfers + { + ERC20 baseToken = ERC20(baseToken_); + uint256 baseTokensToTransfer = + desiredBaseTokenReserves - baseToken.balanceOf(pairAddress_); + + if (baseTokensToTransfer > 0) { + baseToken.safeTransfer(pairAddress_, baseTokensToTransfer); + baseTokensUsed += baseTokensToTransfer; + } + } + + // Perform the swap + uint256 amount0Out = pair.token0() == quoteToken_ ? quoteTokensOut : 0; + uint256 amount1Out = pair.token0() == quoteToken_ ? 0 : quoteTokensOut; + + if (amount0Out > 0 || amount1Out > 0) { + pair.swap(amount0Out, amount1Out, address(this), ""); + } else { + // If no swap is needed, sync the pair to update the reserves + pair.sync(); + } + + // The pool reserves should now indicate the correct price. + // This contract may now hold additional quote tokens that were transferred from the pool. + // These tokens will be transferred to the seller during cleanup. + + return (quoteTokensUsed, baseTokensUsed); + } } diff --git a/src/callbacks/liquidity/UniswapV3DTL.sol b/src/callbacks/liquidity/UniswapV3DTL.sol index 4efdfeec..8fb6d86e 100644 --- a/src/callbacks/liquidity/UniswapV3DTL.sol +++ b/src/callbacks/liquidity/UniswapV3DTL.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; // Libraries import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; +import {SafeTransferLib} from "@solmate-6.7.0/utils/SafeTransferLib.sol"; // Uniswap import {IUniswapV3Pool} from @@ -34,19 +35,25 @@ import {BaseDirectToLiquidity} from "./BaseDTL.sol"; /// @dev As a general rule, this callback contract does not retain balances of tokens between calls. /// Transfers are performed within the same function that requires the balance. contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { + using SafeTransferLib for ERC20; + // ========== ERRORS ========== // error Callback_Params_PoolFeeNotEnabled(); - error Callback_Slippage(address token_, uint256 amountActual_, uint256 amountMin_); + error Callback_Swap_InvalidData(); + error Callback_Swap_InvalidCaller(); + error Callback_Swap_InvalidCase(); // ========== STRUCTS ========== // - /// @notice Parameters for the onSettle callback + /// @notice Parameters for the onCreate callback /// @dev This will be encoded in the `callbackData_` parameter /// + /// @param poolFee The fee of the Uniswap V3 pool /// @param maxSlippage The maximum slippage allowed when adding liquidity (in terms of `ONE_HUNDRED_PERCENT`) - struct OnSettleParams { + struct UniswapV3OnCreateParams { + uint24 poolFee; uint24 maxSlippage; } @@ -85,30 +92,33 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { /// - Validates the input data /// /// This function reverts if: - /// - OnCreateParams.implParams.poolFee is not enabled - /// - The pool for the token and fee combination already exists + /// - `UniswapV3OnCreateParams.poolFee` is not enabled + /// - `UniswapV3OnCreateParams.maxSlippage` is out of bounds + /// + /// Note that this function does not check if the pool already exists. The reason for this is that it could be used as a DoS vector. function __onCreate( - uint96, + uint96 lotId_, + address, + address, address, - address baseToken_, - address quoteToken_, uint256, bool, - bytes calldata callbackData_ + bytes calldata ) internal virtual override { - OnCreateParams memory params = abi.decode(callbackData_, (OnCreateParams)); - (uint24 poolFee) = abi.decode(params.implParams, (uint24)); + UniswapV3OnCreateParams memory params = _decodeOnCreateParameters(lotId_); // Validate the parameters // Pool fee // Fee not enabled - if (uniV3Factory.feeAmountTickSpacing(poolFee) == 0) { + if (uniV3Factory.feeAmountTickSpacing(params.poolFee) == 0) { revert Callback_Params_PoolFeeNotEnabled(); } - // Check that the pool does not exist - if (uniV3Factory.getPool(baseToken_, quoteToken_, poolFee) != address(0)) { - revert Callback_Params_PoolExists(); + // Check that the maxSlippage is in bounds + // The maxSlippage is stored during onCreate, as the callback data is passed in by the auction seller. + // As AuctionHouse.settle() can be called by anyone, a value for maxSlippage could be passed that would result in a loss for the auction seller. + if (params.maxSlippage > ONE_HUNDRED_PERCENT) { + revert Callback_Params_PercentOutOfBounds(params.maxSlippage, 0, ONE_HUNDRED_PERCENT); } } @@ -128,18 +138,18 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { uint256 quoteTokenAmount_, address baseToken_, uint256 baseTokenAmount_, - bytes memory callbackData_ + bytes memory ) internal virtual override returns (ERC20 poolToken) { // Decode the callback data - OnSettleParams memory params = abi.decode(callbackData_, (OnSettleParams)); - - // Extract the pool fee from the implParams - (uint24 poolFee) = abi.decode(lotConfiguration[lotId_].implParams, (uint24)); + UniswapV3OnCreateParams memory params = _decodeOnCreateParameters(lotId_); // Determine the ordering of tokens bool quoteTokenIsToken0 = quoteToken_ < baseToken_; // Create and initialize the pool if necessary + // This may involve swapping tokens to adjust the pool price + // if it already exists and has single-sided quote token liquidity + // provided. { // Determine sqrtPriceX96 uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96( @@ -148,19 +158,25 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { // If the pool already exists and is initialized, it will have no effect // Please see the risks section in the contract documentation for more information + (, uint256 quoteTokensReceived, uint256 baseTokensUsed) = _createAndInitializePoolIfNecessary( - quoteTokenIsToken0 ? quoteToken_ : baseToken_, - quoteTokenIsToken0 ? baseToken_ : quoteToken_, - poolFee, + quoteToken_, + baseToken_, + quoteTokenIsToken0, + baseTokenAmount_ / 2, + params.poolFee, sqrtPriceX96 ); + + quoteTokenAmount_ += quoteTokensReceived; + baseTokenAmount_ -= baseTokensUsed; } // Deploy the pool token address poolTokenAddress; { // Adjust the full-range ticks according to the tick spacing for the current fee - int24 tickSpacing = uniV3Factory.feeAmountTickSpacing(poolFee); + int24 tickSpacing = uniV3Factory.feeAmountTickSpacing(params.poolFee); // Create an unmanaged pool // The range of the position will not be changed after deployment @@ -168,7 +184,7 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { poolTokenAddress = gUniFactory.createPool( quoteTokenIsToken0 ? quoteToken_ : baseToken_, quoteTokenIsToken0 ? baseToken_ : quoteToken_, - poolFee, + params.poolFee, TickMath.MIN_TICK / tickSpacing * tickSpacing, TickMath.MAX_TICK / tickSpacing * tickSpacing ); @@ -188,27 +204,23 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { // Revert if the slippage is too high { uint256 quoteTokenRequired = quoteTokenIsToken0 ? amount0Actual : amount1Actual; - - // Ensures that `quoteTokenRequired` (as specified by GUniPool) is within the slippage range from the actual quote token amount - uint256 lower = _getAmountWithSlippage(quoteTokenAmount_, params.maxSlippage); - if (quoteTokenRequired < lower) { - revert Callback_Slippage(quoteToken_, quoteTokenRequired, lower); - } - - // Approve the vault to spend the tokens - ERC20(quoteToken_).approve(address(poolTokenAddress), quoteTokenRequired); + _approveMintAmount( + quoteToken_, + poolTokenAddress, + quoteTokenAmount_, + quoteTokenRequired, + params.maxSlippage + ); } { uint256 baseTokenRequired = quoteTokenIsToken0 ? amount1Actual : amount0Actual; - - // Ensures that `baseTokenRequired` (as specified by GUniPool) is within the slippage range from the actual base token amount - uint256 lower = _getAmountWithSlippage(baseTokenAmount_, params.maxSlippage); - if (baseTokenRequired < lower) { - revert Callback_Slippage(baseToken_, baseTokenRequired, lower); - } - - // Approve the vault to spend the tokens - ERC20(baseToken_).approve(address(poolTokenAddress), baseTokenRequired); + _approveMintAmount( + baseToken_, + poolTokenAddress, + baseTokenAmount_, + baseTokenRequired, + params.maxSlippage + ); } // Mint the LP tokens @@ -221,14 +233,17 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { // ========== INTERNAL FUNCTIONS ========== // - /// @dev Copied from UniswapV3's PoolInitializer (which is GPL >= 2) + /// @dev Modified from UniswapV3's PoolInitializer (which is GPL >= 2) function _createAndInitializePoolIfNecessary( - address token0, - address token1, + address quoteToken, + address baseToken, + bool quoteTokenIsToken0, + uint256 maxBaseTokens, uint24 fee, uint160 sqrtPriceX96 - ) internal returns (address pool) { - require(token0 < token1); + ) internal returns (address pool, uint256 quoteTokensReceived, uint256 baseTokensUsed) { + address token0 = quoteTokenIsToken0 ? quoteToken : baseToken; + address token1 = quoteTokenIsToken0 ? baseToken : quoteToken; pool = uniV3Factory.getPool(token0, token1, fee); if (pool == address(0)) { @@ -238,7 +253,135 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { (uint160 sqrtPriceX96Existing,,,,,,) = IUniswapV3Pool(pool).slot0(); if (sqrtPriceX96Existing == 0) { IUniswapV3Pool(pool).initialize(sqrtPriceX96); + } else { + // if the pool already exists and is initialized, we need to make sure the price + // is consistent with the price we would have initialized it with + // the price comparison depends on which token is token0 + // because price is always expressed as the price of token0 + // in terms of token1 + // + // there are 3 cases, represented here assuming price is in terms of quote tokens per base token + // the opposite case is also handle in the same block + // 1. actual price < target price + if ( + !quoteTokenIsToken0 && sqrtPriceX96Existing < sqrtPriceX96 + || quoteTokenIsToken0 && sqrtPriceX96Existing > sqrtPriceX96 + ) { + // price in terms of quote tokens is lower than expected. + // we can swap net 0 quote tokens for base tokens + // and move the price to the target. + bytes memory data = abi.encode(quoteToken, baseToken, fee, 1); + IUniswapV3Pool(pool).swap( + address(this), // recipient -> this contract + quoteTokenIsToken0, // zeroForOne -> we are swapping quoteToken for baseToken + int256(1), // amountSpecified -> swap 1 wei of quote token (positive value means amountIn), this won't actually be used since there are no base tokens in the pool + sqrtPriceX96, // sqrtPriceLimitX96 -> the max price we will pay, this will move the pool price to the value provided + data // arbitrary data -> case 1 + ); + } + // 2. actual price > target price + else if ( + !quoteTokenIsToken0 && sqrtPriceX96Existing > sqrtPriceX96 + || quoteTokenIsToken0 && sqrtPriceX96Existing < sqrtPriceX96 + ) { + // price is terms of quote tokens is higher than expected + // we need to sell up to half of the base tokens into the liquidity + // pool to move the price as close as possible to the target + // The amountDeltas represent the +/- change from the pool's perspective + // Negative values mean this contract received tokens and positive values mean it sent tokens + bytes memory data = abi.encode(quoteToken, baseToken, fee, 2); + (int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(pool).swap( + address(this), // recipient -> this contract + !quoteTokenIsToken0, // zeroForOne -> we are swapping baseToken for quoteToken + int256(maxBaseTokens), // amountSpecified -> sell up to the max base tokens (positive value means amountIn) + sqrtPriceX96, // sqrtPriceLimitX96 -> the min price will we accept for the base tokens, depending on the amount of tokens to sell this will move at most down to the price provided + data // arbitrary data -> case 2 + ); + + quoteTokensReceived = + uint256(quoteTokenIsToken0 ? -amount0Delta : -amount1Delta); + baseTokensUsed = uint256(quoteTokenIsToken0 ? amount1Delta : amount0Delta); + } + // 3. actual price == target price (where we don't need to do anything) } } } + + /// @notice Decodes the configuration parameters from the DTLConfiguration + /// @dev The configuration parameters are stored in `DTLConfiguration.implParams` + function _decodeOnCreateParameters( + uint96 lotId_ + ) internal view returns (UniswapV3OnCreateParams memory) { + DTLConfiguration memory lotConfig = lotConfiguration[lotId_]; + // Validate that the callback data is of the correct length + if (lotConfig.implParams.length != 64) { + revert Callback_InvalidParams(); + } + + return abi.decode(lotConfig.implParams, (UniswapV3OnCreateParams)); + } + + /// @notice Approves the spender to spend the token amount with a maximum slippage + /// @dev This function reverts if the slippage is too high from the original amount + /// + /// @param token_ The token to approve + /// @param spender_ The spender + /// @param amount_ The amount available + /// @param amountActual_ The actual amount required + /// @param maxSlippage_ The maximum slippage allowed + function _approveMintAmount( + address token_, + address spender_, + uint256 amount_, + uint256 amountActual_, + uint24 maxSlippage_ + ) internal { + // Revert if the slippage is too high + uint256 lower = _getAmountWithSlippage(amount_, maxSlippage_); + if (amountActual_ < lower) { + revert Callback_Slippage(token_, amountActual_, lower); + } + + // Approve the vault to spend the tokens + ERC20(token_).safeApprove(spender_, amountActual_); + } + + // ========== UNIV3 FUNCTIONS ========== // + + // Provide tokens when adjusting the pool price via a swap before deploying liquidity + function uniswapV3SwapCallback( + int256 amount0Delta_, + int256 amount1Delta_, + bytes calldata data_ + ) external { + // Data should be 4 words long + if (data_.length != 128) revert Callback_Swap_InvalidData(); + + // Decode the data + (address quoteToken, address baseToken, uint24 fee, uint8 case_) = + abi.decode(data_, (address, address, uint24, uint8)); + + // Only the pool can call + address token0 = quoteToken < baseToken ? quoteToken : baseToken; + address token1 = quoteToken < baseToken ? baseToken : quoteToken; + address pool = uniV3Factory.getPool(token0, token1, fee); + if (msg.sender != pool) revert Callback_Swap_InvalidCaller(); + + // Handle the swap case + if (case_ == 1) { + // Case 1: Swapped in 1 wei of quote tokens + // We don't need to do anything here + } + else if (case_ == 2) { + // Case 2: We sold up to half of the base tokens into the pool to move the price down + // Transfer the requested token1 amount to the pool + if (token0 == baseToken) { + if (amount0Delta_ > 0) ERC20(baseToken).safeTransfer(pool, uint256(amount0Delta_)); + } else { + if (amount1Delta_ > 0) ERC20(baseToken).safeTransfer(pool, uint256(amount1Delta_)); + } + } else { + revert Callback_Swap_InvalidCase(); + } + } } diff --git a/test/Constants.sol b/test/Constants.sol index 1f37435d..9e2d8f32 100644 --- a/test/Constants.sol +++ b/test/Constants.sol @@ -1,19 +1,21 @@ /// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; -import {TestConstants as TestConstantsCore} from "@axis-core-1.0.0-test/Constants.sol"; +import {TestConstants as TestConstantsCore} from "@axis-core-1.0.1-test/Constants.sol"; abstract contract TestConstants is TestConstantsCore { address internal constant _UNISWAP_V2_FACTORY = address(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); address internal constant _UNISWAP_V2_ROUTER = - address(0xAAe4679eF7310cf51b402Fb4F94F44ead5ECc4dE); + address(0xAA90Afc992900e395D77cBb02D22FF5ef04bC9b9); address internal constant _UNISWAP_V3_FACTORY = - address(0xAA488cE61A9bE80659e2C6Fd5A9E7BeFD58378E8); - address internal constant _GUNI_FACTORY = address(0xAA874586eAaF809890C6d2F00862225b6Bb3577f); + address(0xAA6e0bD8aA20a2Fb885f378d8d98088aFEf56faD); + address internal constant _GUNI_FACTORY = address(0xAAF4DB8Fc32Cb0Fee88cAA609466608C10e01940); address internal constant _BASELINE_KERNEL = address(0xBB); address internal constant _BASELINE_QUOTE_TOKEN = - address(0xAA22883d39ea4e42f7033e3e931aA476DEe30b73); + address(0xAA5962E03F408601D4044cb90592f9075772641F); address internal constant _CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + + address internal constant _SELLER = address(0x2); } diff --git a/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol b/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol index 3d34493f..5c5957cd 100644 --- a/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol +++ b/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol @@ -2,24 +2,23 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {IAuctionHouse} from "@axis-core-1.0.0/interfaces/IAuctionHouse.sol"; -import {AtomicAuctionHouse} from "@axis-core-1.0.0/AtomicAuctionHouse.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {AtomicAuctionHouse} from "@axis-core-1.0.1/AtomicAuctionHouse.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {AllocatedMerkleAllowlist} from "../../src/callbacks/allowlists/AllocatedMerkleAllowlist.sol"; -import {toVeecode} from "@axis-core-1.0.0/modules/Keycode.sol"; +import {toVeecode} from "@axis-core-1.0.1/modules/Keycode.sol"; import {WithSalts} from "../lib/WithSalts.sol"; +import {TestConstants} from "../Constants.sol"; -contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts { +contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for AllocatedMerkleAllowlist; - address internal constant _OWNER = address(0x1); - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _BUYER_TWO = address(0x5); diff --git a/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol b/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol index 16b4d9d5..ccd8621e 100644 --- a/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol +++ b/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol @@ -2,24 +2,23 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {IAuctionHouse} from "@axis-core-1.0.0/interfaces/IAuctionHouse.sol"; -import {BatchAuctionHouse} from "@axis-core-1.0.0/BatchAuctionHouse.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {AllocatedMerkleAllowlist} from "../../src/callbacks/allowlists/AllocatedMerkleAllowlist.sol"; -import {toVeecode} from "@axis-core-1.0.0/modules/Keycode.sol"; +import {toVeecode} from "@axis-core-1.0.1/modules/Keycode.sol"; import {WithSalts} from "../lib/WithSalts.sol"; +import {TestConstants} from "../Constants.sol"; -contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts { +contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for AllocatedMerkleAllowlist; - address internal constant _OWNER = address(0x1); - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _BUYER_TWO = address(0x5); diff --git a/test/callbacks/CappedMerkleAllowlistAtomic.t.sol b/test/callbacks/CappedMerkleAllowlistAtomic.t.sol index 0af14a20..ed6e3e8d 100644 --- a/test/callbacks/CappedMerkleAllowlistAtomic.t.sol +++ b/test/callbacks/CappedMerkleAllowlistAtomic.t.sol @@ -2,22 +2,21 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {AtomicAuctionHouse} from "@axis-core-1.0.0/AtomicAuctionHouse.sol"; +import {AtomicAuctionHouse} from "@axis-core-1.0.1/AtomicAuctionHouse.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {CappedMerkleAllowlist} from "../../src/callbacks/allowlists/CappedMerkleAllowlist.sol"; import {WithSalts} from "../lib/WithSalts.sol"; +import {TestConstants} from "../Constants.sol"; -contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts { +contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for CappedMerkleAllowlist; - address internal constant _OWNER = address(0x1); - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _BUYER_TWO = address(0x5); diff --git a/test/callbacks/CappedMerkleAllowlistBatch.t.sol b/test/callbacks/CappedMerkleAllowlistBatch.t.sol index a5ef6c57..a95623c1 100644 --- a/test/callbacks/CappedMerkleAllowlistBatch.t.sol +++ b/test/callbacks/CappedMerkleAllowlistBatch.t.sol @@ -2,22 +2,21 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {BatchAuctionHouse} from "@axis-core-1.0.0/BatchAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {CappedMerkleAllowlist} from "../../src/callbacks/allowlists/CappedMerkleAllowlist.sol"; import {WithSalts} from "../lib/WithSalts.sol"; +import {TestConstants} from "../Constants.sol"; -contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts { +contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for CappedMerkleAllowlist; - address internal constant _OWNER = address(0x1); - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _BUYER_TWO = address(0x5); diff --git a/test/callbacks/TokenAllowlistAtomic.t.sol b/test/callbacks/TokenAllowlistAtomic.t.sol index 88dac79f..d02e2a7c 100644 --- a/test/callbacks/TokenAllowlistAtomic.t.sol +++ b/test/callbacks/TokenAllowlistAtomic.t.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {AtomicAuctionHouse} from "@axis-core-1.0.0/AtomicAuctionHouse.sol"; +import {AtomicAuctionHouse} from "@axis-core-1.0.1/AtomicAuctionHouse.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {TokenAllowlist, ITokenBalance} from "../../src/callbacks/allowlists/TokenAllowlist.sol"; @@ -18,7 +18,6 @@ import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; contract TokenAllowlistAtomicTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for TokenAllowlist; - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _BUYER_TWO = address(0x5); diff --git a/test/callbacks/TokenAllowlistBatch.t.sol b/test/callbacks/TokenAllowlistBatch.t.sol index 7b08cd4b..c7ddefd6 100644 --- a/test/callbacks/TokenAllowlistBatch.t.sol +++ b/test/callbacks/TokenAllowlistBatch.t.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {BatchAuctionHouse} from "@axis-core-1.0.0/BatchAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {TokenAllowlist, ITokenBalance} from "../../src/callbacks/allowlists/TokenAllowlist.sol"; @@ -18,7 +18,6 @@ import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; contract TokenAllowlistBatchTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for TokenAllowlist; - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _BUYER_TWO = address(0x5); diff --git a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/BaselineAllocatedAllowlistTest.sol b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/BaselineAllocatedAllowlistTest.sol index 441e8ab0..2c160001 100644 --- a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/BaselineAllocatedAllowlistTest.sol +++ b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/BaselineAllocatedAllowlistTest.sol @@ -8,13 +8,16 @@ import {BaselineAxisLaunchTest} from "../BaselineAxisLaunchTest.sol"; import {BALwithAllocatedAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol"; +// Baseline +import {Actions as BaselineKernelActions} from "@baseline/Kernel.sol"; + contract BaselineAllocatedAllowlistTest is BaselineAxisLaunchTest { // ========== MODIFIERS ========== // modifier givenCallbackIsCreated() override { // Get the salt bytes memory args = - abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); bytes32 salt = _getTestSalt( "BaselineAllocatedAllowlist", type(BALwithAllocatedAllowlist).creationCode, args ); @@ -23,15 +26,15 @@ contract BaselineAllocatedAllowlistTest is BaselineAxisLaunchTest { // Source: https://github.com/foundry-rs/foundry/issues/6402 vm.startBroadcast(); _dtl = new BALwithAllocatedAllowlist{salt: salt}( - address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER + address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER ); vm.stopBroadcast(); _dtlAddress = address(_dtl); - // Call configureDependencies to set everything that's needed - _mockBaselineGetModuleForKeycode(); - _dtl.configureDependencies(); + // Install as a policy + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.ActivatePolicy, _dtlAddress); _; } diff --git a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onBid.t.sol b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onBid.t.sol index e4761352..6a74b1a5 100644 --- a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onBid.t.sol +++ b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onBid.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineAllocatedAllowlistTest} from "./BaselineAllocatedAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BALwithAllocatedAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol"; diff --git a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onCreate.t.sol b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onCreate.t.sol index c8722add..4b627622 100644 --- a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onCreate.t.sol +++ b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/onCreate.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineAllocatedAllowlistTest} from "./BaselineAllocatedAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BALwithAllocatedAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithAllocatedAllowlist.sol"; @@ -16,6 +16,8 @@ contract BaselineAllocatedAllowlistOnCreateTest is BaselineAllocatedAllowlistTes // [X] when the allowlist parameters are in an incorrect format // [X] it reverts + // [X] when the seller is not the owner + // [X] it reverts // [X] it decodes and stores the merkle root function test_allowlistParamsIncorrect_reverts() @@ -35,6 +37,21 @@ contract BaselineAllocatedAllowlistOnCreateTest is BaselineAllocatedAllowlistTes _onCreate(); } + function test_sellerNotOwner_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAllowlistParams(_MERKLE_ROOT) + { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the callback + _onCreate(_OWNER); + } + function test_success() public givenBPoolIsCreated diff --git a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/setMerkleRoot.t.sol b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/setMerkleRoot.t.sol index 61132180..c318193e 100644 --- a/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/setMerkleRoot.t.sol +++ b/test/callbacks/liquidity/BaselineV2/AllocatedAllowlist/setMerkleRoot.t.sol @@ -14,7 +14,7 @@ contract BaselineAllocatedAllowlistSetMerkleRootTest is BaselineAllocatedAllowli 0x1234567890123456789012345678901234567890123456789012345678901234; function _setMerkleRoot() internal { - vm.prank(_OWNER); + vm.prank(_SELLER); BALwithAllocatedAllowlist(address(_dtl)).setMerkleRoot(_NEW_MERKLE_ROOT); } @@ -67,9 +67,11 @@ contract BaselineAllocatedAllowlistSetMerkleRootTest is BaselineAllocatedAllowli givenAllowlistParams(_MERKLE_ROOT) givenOnCreate givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) - givenAddressHasBaseTokenBalance(_dtlAddress, _REFUND_AMOUNT) - givenOnSettle + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) { + // Perform onSettle callback + _onSettle(); + // Expect revert bytes memory err = abi.encodeWithSelector(BALwithAllocatedAllowlist.Callback_InvalidState.selector); diff --git a/test/callbacks/liquidity/BaselineV2/Allowlist/BaselineAllowlistTest.sol b/test/callbacks/liquidity/BaselineV2/Allowlist/BaselineAllowlistTest.sol index 065a7b5a..134b71a4 100644 --- a/test/callbacks/liquidity/BaselineV2/Allowlist/BaselineAllowlistTest.sol +++ b/test/callbacks/liquidity/BaselineV2/Allowlist/BaselineAllowlistTest.sol @@ -8,6 +8,9 @@ import {BaselineAxisLaunchTest} from "../BaselineAxisLaunchTest.sol"; import {BALwithAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithAllowlist.sol"; +// Baseline +import {Actions as BaselineKernelActions} from "@baseline/Kernel.sol"; + contract BaselineAllowlistTest is BaselineAxisLaunchTest { uint256 internal constant _BUYER_LIMIT = 5e18; @@ -16,22 +19,22 @@ contract BaselineAllowlistTest is BaselineAxisLaunchTest { modifier givenCallbackIsCreated() override { // Get the salt bytes memory args = - abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); bytes32 salt = _getTestSalt("BaselineAllowlist", type(BALwithAllowlist).creationCode, args); // Required for CREATE2 address to work correctly. doesn't do anything in a test // Source: https://github.com/foundry-rs/foundry/issues/6402 vm.startBroadcast(); _dtl = new BALwithAllowlist{salt: salt}( - address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER + address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER ); vm.stopBroadcast(); _dtlAddress = address(_dtl); - // Call configureDependencies to set everything that's needed - _mockBaselineGetModuleForKeycode(); - _dtl.configureDependencies(); + // Install as a policy + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.ActivatePolicy, _dtlAddress); _; } diff --git a/test/callbacks/liquidity/BaselineV2/Allowlist/onBid.t.sol b/test/callbacks/liquidity/BaselineV2/Allowlist/onBid.t.sol index c65e596e..55b91be4 100644 --- a/test/callbacks/liquidity/BaselineV2/Allowlist/onBid.t.sol +++ b/test/callbacks/liquidity/BaselineV2/Allowlist/onBid.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineAllowlistTest} from "./BaselineAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; contract BaselineAllowlistOnBidTest is BaselineAllowlistTest { // Use the @openzeppelin/merkle-tree package or the scripts in axis-utils to generate the merkle tree @@ -99,7 +99,9 @@ contract BaselineAllowlistOnBidTest is BaselineAllowlistTest { _dtl.onBid(_lotId, _BID_ID, address(0x55), 5e18, abi.encode(_proof)); } - function test_success(uint256 bidAmount_) + function test_success( + uint256 bidAmount_ + ) public givenBPoolIsCreated givenCallbackIsCreated diff --git a/test/callbacks/liquidity/BaselineV2/Allowlist/onCreate.t.sol b/test/callbacks/liquidity/BaselineV2/Allowlist/onCreate.t.sol index 17a1c60e..827347e3 100644 --- a/test/callbacks/liquidity/BaselineV2/Allowlist/onCreate.t.sol +++ b/test/callbacks/liquidity/BaselineV2/Allowlist/onCreate.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineAllowlistTest} from "./BaselineAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BALwithAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithAllowlist.sol"; @@ -16,6 +16,8 @@ contract BaselineAllowlistOnCreateTest is BaselineAllowlistTest { // [X] when the allowlist parameters are in an incorrect format // [X] it reverts + // [X] when the seller is not the owner + // [X] it reverts // [X] it decodes and stores the merkle root function test_allowlistParamsIncorrect_reverts() @@ -35,6 +37,21 @@ contract BaselineAllowlistOnCreateTest is BaselineAllowlistTest { _onCreate(); } + function test_sellerNotOwner_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAllowlistParams(_MERKLE_ROOT) + { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the callback + _onCreate(_OWNER); + } + function test_success() public givenBPoolIsCreated diff --git a/test/callbacks/liquidity/BaselineV2/Allowlist/setMerkleRoot.t.sol b/test/callbacks/liquidity/BaselineV2/Allowlist/setMerkleRoot.t.sol index 7b0cb299..0486005f 100644 --- a/test/callbacks/liquidity/BaselineV2/Allowlist/setMerkleRoot.t.sol +++ b/test/callbacks/liquidity/BaselineV2/Allowlist/setMerkleRoot.t.sol @@ -14,7 +14,7 @@ contract BaselineAllowlistSetMerkleRootTest is BaselineAllowlistTest { 0x1234567890123456789012345678901234567890123456789012345678901234; function _setMerkleRoot() internal { - vm.prank(_OWNER); + vm.prank(_SELLER); BALwithAllowlist(address(_dtl)).setMerkleRoot(_NEW_MERKLE_ROOT); } @@ -66,9 +66,11 @@ contract BaselineAllowlistSetMerkleRootTest is BaselineAllowlistTest { givenAllowlistParams(_MERKLE_ROOT) givenOnCreate givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) - givenAddressHasBaseTokenBalance(_dtlAddress, _REFUND_AMOUNT) - givenOnSettle + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) { + // Perform onSettle callback + _onSettle(); + // Expect revert bytes memory err = abi.encodeWithSelector(BALwithAllowlist.Callback_InvalidState.selector); vm.expectRevert(err); diff --git a/test/callbacks/liquidity/BaselineV2/BPOOLMinter.sol b/test/callbacks/liquidity/BaselineV2/BPOOLMinter.sol new file mode 100644 index 00000000..39d6d292 --- /dev/null +++ b/test/callbacks/liquidity/BaselineV2/BPOOLMinter.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Owned} from "@solmate-6.7.0/auth/Owned.sol"; + +import {Kernel, Keycode, toKeycode, Policy, Permissions} from "@baseline/Kernel.sol"; +import {BPOOLv1} from "@baseline/modules/BPOOL.v1.sol"; +import {CREDTv1} from "@baseline/modules/CREDT.v1.sol"; + +contract BPOOLMinter is Policy, Owned { + // solhint-disable var-name-mixedcase + BPOOLv1 public BPOOL; + CREDTv1 public CREDT; + // solhint-enable var-name-mixedcase + + constructor(Kernel kernel_) Policy(kernel_) Owned(kernel_.executor()) {} + + function configureDependencies() external override returns (Keycode[] memory dependencies) { + dependencies = new Keycode[](2); + dependencies[0] = toKeycode("BPOOL"); + dependencies[1] = toKeycode("CREDT"); + + BPOOL = BPOOLv1(getModuleAddress(toKeycode("BPOOL"))); + CREDT = CREDTv1(getModuleAddress(toKeycode("CREDT"))); + } + + function requestPermissions() external view override returns (Permissions[] memory requests) { + requests = new Permissions[](3); + requests[0] = Permissions(toKeycode("BPOOL"), BPOOL.mint.selector); + requests[1] = Permissions(toKeycode("BPOOL"), BPOOL.setTransferLock.selector); + requests[2] = Permissions(toKeycode("CREDT"), CREDT.updateCreditAccount.selector); + } + + function mint(address to_, uint256 amount_) external onlyOwner { + BPOOL.mint(to_, amount_); + } + + function setTransferLock(bool lock_) external onlyOwner { + BPOOL.setTransferLock(lock_); + } + + /// @notice Mimics allocating credit (call options) to a user + function allocateCreditAccount(address user_, uint256 amount_, uint256 days_) external { + // Transfer collateral + BPOOL.transferFrom(user_, address(this), amount_); + + // Calculate the amount of debt to record against the collateral + uint256 debt = amount_ * BPOOL.getBaselineValue() / 1e18; + + // Approve spending + BPOOL.approve(address(CREDT), amount_); + + // Update credit account + CREDT.updateCreditAccount(user_, amount_, debt, block.timestamp + days_ * 1 days); + } +} diff --git a/test/callbacks/liquidity/BaselineV2/BaselineAxisLaunchTest.sol b/test/callbacks/liquidity/BaselineV2/BaselineAxisLaunchTest.sol index 77849e27..3033cae9 100644 --- a/test/callbacks/liquidity/BaselineV2/BaselineAxisLaunchTest.sol +++ b/test/callbacks/liquidity/BaselineV2/BaselineAxisLaunchTest.sol @@ -4,10 +4,9 @@ pragma solidity 0.8.19; // Test scaffolding import {Test} from "@forge-std-1.9.1/Test.sol"; import {console2} from "@forge-std-1.9.1/console2.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; import {WithSalts} from "../../../lib/WithSalts.sol"; import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; -import {MockBPOOL} from "../../../callbacks/liquidity/BaselineV2/mocks/MockBPOOL.sol"; import {IUniswapV3Factory} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Factory.sol"; import {UniswapV3Factory} from "../../../lib/uniswap-v3/UniswapV3Factory.sol"; @@ -18,48 +17,69 @@ import {SqrtPriceMath} from "../../../../src/lib/uniswap-v3/SqrtPriceMath.sol"; import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; // Axis core -import {IAuction} from "@axis-core-1.0.0/interfaces/modules/IAuction.sol"; -import {IAuctionHouse} from "@axis-core-1.0.0/interfaces/IAuctionHouse.sol"; -import {BatchAuctionHouse} from "@axis-core-1.0.0/BatchAuctionHouse.sol"; -import {EncryptedMarginalPrice} from "@axis-core-1.0.0/modules/auctions/batch/EMP.sol"; -import {IFixedPriceBatch} from "@axis-core-1.0.0/interfaces/modules/auctions/IFixedPriceBatch.sol"; -import {FixedPriceBatch} from "@axis-core-1.0.0/modules/auctions/batch/FPB.sol"; +import {IAuction} from "@axis-core-1.0.1/interfaces/modules/IAuction.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; +import {EncryptedMarginalPrice} from "@axis-core-1.0.1/modules/auctions/batch/EMP.sol"; +import {IFixedPriceBatch} from "@axis-core-1.0.1/interfaces/modules/auctions/IFixedPriceBatch.sol"; +import {FixedPriceBatch} from "@axis-core-1.0.1/modules/auctions/batch/FPB.sol"; // Callbacks -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; import {BaselineAxisLaunch} from "../../../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; // Baseline -import {toKeycode as toBaselineKeycode} from - "../../../../src/callbacks/liquidity/BaselineV2/lib/Kernel.sol"; +import {Kernel as BaselineKernel, Actions as BaselineKernelActions} from "@baseline/Kernel.sol"; +import {BPOOLv1, Range, Position} from "@baseline/modules/BPOOL.v1.sol"; +import {CREDTv1} from "@baseline/modules/CREDT.v1.sol"; +import {LOOPSv1} from "@baseline/modules/LOOPS.v1.sol"; +import {MarketMaking} from "@baseline/policies/MarketMaking.sol"; +import {BPOOLMinter} from "./BPOOLMinter.sol"; +import {MockBlast} from "./MockBlast.sol"; + +// solhint-disable max-states-count abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for BaselineAxisLaunch; - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); + address internal constant _BORROWER = address(0x10); address internal constant _NOT_SELLER = address(0x20); uint24 internal constant _ONE_HUNDRED_PERCENT = 100e2; - uint24 internal constant _NINETY_NINE_PERCENT = 99e2; uint96 internal constant _LOT_CAPACITY = 10e18; uint96 internal constant _REFUND_AMOUNT = 2e18; uint256 internal constant _PROCEEDS_AMOUNT = 24e18; - int24 internal constant _ANCHOR_TICK_WIDTH = 3; - int24 internal constant _DISCOVERY_TICK_WIDTH = 500; + int24 internal constant _ANCHOR_TICK_WIDTH = 10; + int24 internal constant _DISCOVERY_TICK_WIDTH = 350; uint24 internal constant _FLOOR_RESERVES_PERCENT = 50e2; // 50% + uint24 internal constant _POOL_PERCENT = 87e2; // 87% uint256 internal constant _FIXED_PRICE = 3e18; - uint24 internal constant _FEE_TIER = 3000; + int24 internal constant _FIXED_PRICE_TICK_UPPER = 11_000; // 10986 rounded up + uint256 internal constant _INITIAL_POOL_PRICE = 3e18; // 3 + uint24 internal constant _FEE_TIER = 10_000; uint256 internal constant _BASE_SCALE = 1e18; uint8 internal _quoteTokenDecimals = 18; uint8 internal _baseTokenDecimals = 18; - bool internal _isBaseTokenAddressLower = false; + bool internal _isBaseTokenAddressLower = true; + /// @dev Set in `givenBPoolFeeTier()` uint24 internal _feeTier = _FEE_TIER; - /// @dev Set in `_updatePoolInitialTick()` + /// @dev Set in `_setPoolInitialTickFromAuctionPrice()` int24 internal _poolInitialTick; + /// @dev Set in `_setFloorRangeGap()` + int24 internal _floorRangeGap; + uint48 internal _protocolFeePercent; + uint256 internal _protocolFee; + uint48 internal _referrerFeePercent; + uint256 internal _referrerFee; + uint48 internal _curatorFeePercent; + uint256 internal _curatorFee; + + uint256 internal _proceeds = _PROCEEDS_AMOUNT; + uint256 internal _refund = _REFUND_AMOUNT; uint48 internal constant _START = 1_000_000; @@ -77,7 +97,14 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo IAuction internal _auctionModule; MockERC20 internal _quoteToken; - MockBPOOL internal _baseToken; + BaselineKernel internal _baselineKernel; + BPOOLv1 internal _baseToken; + CREDTv1 internal _creditModule; + LOOPSv1 internal _loopsModule; + MarketMaking internal _marketMaking; + BPOOLMinter internal _bPoolMinter; + MockBlast internal _blast; + address internal _blastGovernor; // Inputs IFixedPriceBatch.AuctionDataParams internal _fpbParams = IFixedPriceBatch.AuctionDataParams({ @@ -86,9 +113,12 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo }); BaselineAxisLaunch.CreateData internal _createData = BaselineAxisLaunch.CreateData({ + recipient: _SELLER, + poolPercent: _POOL_PERCENT, floorReservesPercent: _FLOOR_RESERVES_PERCENT, + floorRangeGap: _floorRangeGap, + anchorTickU: _FIXED_PRICE_TICK_UPPER, anchorTickWidth: _ANCHOR_TICK_WIDTH, - discoveryTickWidth: _DISCOVERY_TICK_WIDTH, allowlistParams: abi.encode("") }); @@ -115,6 +145,13 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo revert("UniswapV3Factory address mismatch"); } + // Create the Baseline kernel at a deterministic address, since it is used as input to callbacks + vm.prank(_OWNER); + BaselineKernel baselineKernel = new BaselineKernel(); + _baselineKernel = BaselineKernel(_BASELINE_KERNEL); + vm.etch(address(_baselineKernel), address(baselineKernel).code); + vm.store(address(_baselineKernel), bytes32(uint256(0)), bytes32(abi.encode(_OWNER))); // Owner + // Create auction modules _empModule = new EncryptedMarginalPrice(address(_auctionHouse)); _fpbModule = new FixedPriceBatch(address(_auctionHouse)); @@ -136,62 +173,136 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo } _tickSpacing = _uniV3Factory.feeAmountTickSpacing(_feeTier); + console2.log("Tick spacing: ", _tickSpacing); + + _blast = new MockBlast(); + // Set up Baseline + _creditModule = new CREDTv1(_baselineKernel, address(_blast), _blastGovernor); + _loopsModule = new LOOPSv1(_baselineKernel, 1e18); + _marketMaking = new MarketMaking( + _baselineKernel, 25, 1000, 3e18, address(0), address(_blast), _blastGovernor + ); // Base token is created in the givenBPoolIsCreated modifier + _bPoolMinter = new BPOOLMinter(_baselineKernel); // Calculate the initial tick - _updatePoolInitialTick(); + _setPoolInitialTickFromPrice(_INITIAL_POOL_PRICE); } // ========== MODIFIERS ========== // - function _updatePoolInitialTick() internal { + function _setPoolInitialTickFromTick(int24 tick_) internal { + _poolInitialTick = tick_; + console2.log("Pool initial tick (using tick) set to: ", _poolInitialTick); + } + + function _setPoolInitialTickFromPrice(uint256 price_) internal { + _poolInitialTick = _getTickFromPrice(price_, _baseTokenDecimals, _isBaseTokenAddressLower); + console2.log("Pool initial tick (using specified price) set to: ", _poolInitialTick); + } + + function _setPoolInitialTickFromAuctionPrice() internal { + console2.log("Auction price: ", _fpbParams.price); _poolInitialTick = _getTickFromPrice(_fpbParams.price, _baseTokenDecimals, _isBaseTokenAddressLower); + console2.log("Pool initial tick (using auction price) set to: ", _poolInitialTick); } - modifier givenBPoolIsCreated() { + modifier givenPoolInitialTick(int24 poolInitialTick_) { + _setPoolInitialTickFromTick(poolInitialTick_); + _; + } + + function _setFloorRangeGap(int24 tickSpacingWidth_) internal { + _floorRangeGap = tickSpacingWidth_; + console2.log("Floor range gap set to: ", _floorRangeGap); + _createData.floorRangeGap = _floorRangeGap; + } + + modifier givenFloorRangeGap(int24 tickSpacingWidth_) { + _setFloorRangeGap(tickSpacingWidth_); + _; + } + + function _createBPOOL() internal { // Generate a salt so that the base token address is higher (or lower) than the quote token + console2.log("Generating salt for BPOOL"); bytes32 baseTokenSalt = ComputeAddress.generateSalt( _BASELINE_QUOTE_TOKEN, !_isBaseTokenAddressLower, - type(MockBPOOL).creationCode, + type(BPOOLv1).creationCode, abi.encode( + _baselineKernel, "Base Token", "BT", _baseTokenDecimals, address(_uniV3Factory), _BASELINE_QUOTE_TOKEN, _feeTier, - _poolInitialTick + _poolInitialTick, + address(_blast), + _blastGovernor ), address(this) ); - // Create a new mock BPOOL with the given fee tier - _baseToken = new MockBPOOL( + // Create a new BPOOL with the given fee tier + console2.log("Creating BPOOL"); + _baseToken = new BPOOLv1{salt: baseTokenSalt}( + _baselineKernel, "Base Token", "BT", _baseTokenDecimals, address(_uniV3Factory), _BASELINE_QUOTE_TOKEN, _feeTier, - _poolInitialTick + _poolInitialTick, + address(_blast), + _blastGovernor ); - // Update the mock - _mockBaselineGetModuleForKeycode(); + // Assert that the token ordering is correct + if (_isBaseTokenAddressLower) { + require(address(_baseToken) < _BASELINE_QUOTE_TOKEN, "Base token > quote token"); + } else { + require(address(_baseToken) > _BASELINE_QUOTE_TOKEN, "Base token < quote token"); + } + + // Install the BPOOL module + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.InstallModule, address(_baseToken)); + + // Install the CREDT module + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.InstallModule, address(_creditModule)); + + // Install the LOOPS module + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.InstallModule, address(_loopsModule)); + + // Activate MarketMaking + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.ActivatePolicy, address(_marketMaking)); + + // Activate the BPOOL minter + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.ActivatePolicy, address(_bPoolMinter)); + } + + modifier givenBPoolIsCreated() { + _createBPOOL(); _; } - modifier givenCallbackIsCreated() virtual { + function _createCallback() internal { if (address(_baseToken) == address(0)) { revert("Base token not created"); } // Get the salt bytes memory args = - abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); bytes32 salt = _getTestSalt("BaselineAxisLaunch", type(BaselineAxisLaunch).creationCode, args); @@ -199,14 +310,19 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo // Source: https://github.com/foundry-rs/foundry/issues/6402 vm.startBroadcast(); _dtl = new BaselineAxisLaunch{salt: salt}( - address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER + address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER ); vm.stopBroadcast(); _dtlAddress = address(_dtl); - // Call configureDependencies to set everything that's needed - _dtl.configureDependencies(); + // Install as a policy + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.ActivatePolicy, _dtlAddress); + } + + modifier givenCallbackIsCreated() virtual { + _createCallback(); _; } @@ -216,7 +332,10 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo _; } - modifier givenAuctionIsCreated() { + function _createAuction() internal { + console2.log(""); + console2.log("Creating auction in FPB module"); + // Create a dummy auction in the module IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ start: _START, @@ -228,14 +347,20 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo vm.prank(address(_auctionHouse)); _fpbModule.auction(_lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); + } + + modifier givenAuctionIsCreated() { + _createAuction(); _; } - function _onCreate() internal { + function _onCreate(address seller_) internal { + console2.log(""); + console2.log("Calling onCreate callback"); vm.prank(address(_auctionHouse)); _dtl.onCreate( _lotId, - _SELLER, + seller_, address(_baseToken), address(_quoteToken), _scaleBaseTokenAmount(_LOT_CAPACITY), @@ -244,12 +369,18 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo ); } + function _onCreate() internal { + _onCreate(_SELLER); + } + modifier givenOnCreate() { _onCreate(); _; } function _onCancel() internal { + console2.log(""); + console2.log("Calling onCancel callback"); vm.prank(address(_auctionHouse)); _dtl.onCancel(_lotId, _scaleBaseTokenAmount(_LOT_CAPACITY), true, abi.encode("")); } @@ -259,52 +390,89 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo _; } - function _onSettle() internal { + function _onSettle(bytes memory err_) internal { + console2.log(""); + console2.log("Calling onSettle callback"); + + uint256 capacityRefund = _scaleBaseTokenAmount(_refund); + console2.log("capacity refund", capacityRefund); + uint256 curatorFeeRefund = capacityRefund * _curatorFee / _LOT_CAPACITY; + console2.log("curator fee refund", curatorFeeRefund); + + if (err_.length > 0) { + vm.expectRevert(err_); + } + vm.prank(address(_auctionHouse)); _dtl.onSettle( - _lotId, _PROCEEDS_AMOUNT, _scaleBaseTokenAmount(_REFUND_AMOUNT), abi.encode("") + _lotId, + _proceeds - _protocolFee - _referrerFee, + capacityRefund + curatorFeeRefund, + abi.encode("") ); } + function _onSettle() internal { + _onSettle(""); + } + modifier givenOnSettle() { _onSettle(); _; } + function _onCurate(uint256 curatorFee_) internal { + console2.log(""); + console2.log("Calling onCurate callback"); + vm.prank(address(_auctionHouse)); + _dtl.onCurate(_lotId, curatorFee_, true, abi.encode("")); + } + + modifier givenOnCurate(uint256 curatorFee_) { + _onCurate(curatorFee_); + _; + } + modifier givenBPoolFeeTier(uint24 feeTier_) { _feeTier = feeTier_; _tickSpacing = _uniV3Factory.feeAmountTickSpacing(_feeTier); _; } - modifier givenBaseTokenAddressLower() { + modifier givenBaseTokenAddressHigher() { _isBaseTokenAddressLower = false; - _updatePoolInitialTick(); + _setPoolInitialTickFromAuctionPrice(); _; } modifier givenBaseTokenDecimals(uint8 decimals_) { _baseTokenDecimals = decimals_; - _updatePoolInitialTick(); + _setPoolInitialTickFromAuctionPrice(); _; } modifier givenFixedPrice(uint256 fixedPrice_) { _fpbParams.price = fixedPrice_; + console2.log("Fixed price set to: ", fixedPrice_); - _updatePoolInitialTick(); + _setPoolInitialTickFromAuctionPrice(); _; } - modifier givenAnchorTickWidth(int24 anchorTickWidth_) { - _createData.anchorTickWidth = anchorTickWidth_; + modifier givenAnchorUpperTick(int24 anchorTickU_) { + _createData.anchorTickU = anchorTickU_; + console2.log("Anchor tick U set to: ", anchorTickU_); _; } - modifier givenDiscoveryTickWidth(int24 discoveryTickWidth_) { - _createData.discoveryTickWidth = discoveryTickWidth_; + function _setAnchorTickWidth(int24 anchorTickWidth_) internal { + _createData.anchorTickWidth = anchorTickWidth_; + } + + modifier givenAnchorTickWidth(int24 anchorTickWidth_) { + _setAnchorTickWidth(anchorTickWidth_); _; } @@ -313,15 +481,51 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo } modifier givenAddressHasBaseTokenBalance(address account_, uint256 amount_) { - _baseToken.mint(account_, amount_); + _mintBaseTokens(account_, amount_); _; } modifier givenAddressHasQuoteTokenBalance(address account_, uint256 amount_) { + console2.log("Minting quote tokens to: ", account_); _quoteToken.mint(account_, amount_); _; } + function _disableTransferLock() internal { + vm.prank(_OWNER); + _bPoolMinter.setTransferLock(false); + } + + function _enableTransferLock() internal { + vm.prank(_OWNER); + _bPoolMinter.setTransferLock(true); + } + + function _transferBaseToken(address to_, uint256 amount_) internal { + // Unlock transfers + // This mimics the manual call that the seller needs to do before cancelling/settling + _disableTransferLock(); + + // Transfer refund from auction house to the callback + // We transfer instead of minting to not affect the supply + vm.prank(address(_auctionHouse)); + _baseToken.transfer(to_, amount_); + + // Lock transfers + _enableTransferLock(); + } + + function _transferBaseTokenRefund(uint256 amount_) internal { + console2.log("Transferring base token refund to DTL: ", amount_); + + _transferBaseToken(_dtlAddress, amount_); + } + + modifier givenBaseTokenRefundIsTransferred(uint256 amount_) { + _transferBaseTokenRefund(amount_); + _; + } + function _getTickFromPrice( uint256 price_, uint8 baseTokenDecimals_, @@ -341,23 +545,139 @@ abstract contract BaselineAxisLaunchTest is Test, Permit2User, WithSalts, TestCo return TickMath.getTickAtSqrtRatio(sqrtPriceX96); } - // ========== MOCKS ========== // + function _getRangeReserves(Range range_) internal view returns (uint256) { + Position memory position = _baseToken.getPosition(range_); - function _mockGetAuctionModuleForId() internal { + return position.reserves; + } + + function _getRangeBAssets(Range range_) internal view returns (uint256) { + Position memory position = _baseToken.getPosition(range_); + + return position.bAssets; + } + + function _mintBaseTokens(address account_, uint256 amount_) internal { + vm.prank(_OWNER); + _bPoolMinter.mint(account_, amount_); + } + + function _setPoolPercent(uint24 poolPercent_) internal { + _createData.poolPercent = poolPercent_; + } + + modifier givenPoolPercent(uint24 poolPercent_) { + _setPoolPercent(poolPercent_); + _; + } + + function _setFloorReservesPercent(uint24 floorReservesPercent_) internal { + _createData.floorReservesPercent = floorReservesPercent_; + } + + modifier givenFloorReservesPercent(uint24 floorReservesPercent_) { + _setFloorReservesPercent(floorReservesPercent_); + _; + } + + function _roundToTickSpacingUp(int24 activeTick_) internal view returns (int24) { + // Rounds down + int24 roundedTick = (activeTick_ / _tickSpacing) * _tickSpacing; + + // Add a tick spacing to round up + // This mimics BPOOL.getActiveTS() + if (activeTick_ >= 0 || activeTick_ % _tickSpacing == 0) { + roundedTick += _tickSpacing; + } + + return roundedTick; + } + + function _mockLotFees() internal { + // Mock on the AuctionHouse vm.mockCall( address(_auctionHouse), - abi.encodeWithSelector(IAuctionHouse.getAuctionModuleForId.selector, _lotId), - abi.encode(address(_auctionModule)) + abi.encodeWithSelector(IAuctionHouse.lotFees.selector, _lotId), + abi.encode( + address(0), true, _curatorFeePercent, _protocolFeePercent, _referrerFeePercent + ) ); } - function _mockBaselineGetModuleForKeycode() internal { + function _setCuratorFeePercent(uint48 curatorFeePercent_) internal { + _curatorFeePercent = curatorFeePercent_; + + // Update mock + _mockLotFees(); + + // Update the value + _curatorFee = _LOT_CAPACITY * _curatorFeePercent / 100e2; + } + + modifier givenCuratorFeePercent(uint48 curatorFeePercent_) { + _setCuratorFeePercent(curatorFeePercent_); + _; + } + + function _setProtocolFeePercent(uint48 protocolFeePercent_) internal { + _protocolFeePercent = protocolFeePercent_; + + // Update mock + _mockLotFees(); + + // Update the value + _protocolFee = _proceeds * _protocolFeePercent / 100e2; + } + + modifier givenProtocolFeePercent(uint48 protocolFeePercent_) { + _setProtocolFeePercent(protocolFeePercent_); + _; + } + + function _setReferrerFeePercent(uint48 referrerFeePercent_) internal { + _referrerFeePercent = referrerFeePercent_; + + // Update mock + _mockLotFees(); + + // Update the value + _referrerFee = _proceeds * _referrerFeePercent / 100e2; + } + + modifier givenReferrerFeePercent(uint48 referrerFeePercent_) { + _setReferrerFeePercent(referrerFeePercent_); + _; + } + + function _setTotalCollateralized(address user_, uint256 totalCollateralized_) internal { + // Mint enough base tokens to cover the total collateralized + _mintBaseTokens(user_, totalCollateralized_); + + // Approve the spending of the base tokens + vm.prank(user_); + _baseToken.approve(address(_bPoolMinter), totalCollateralized_); + + _disableTransferLock(); + + // Borrow + vm.prank(user_); + _bPoolMinter.allocateCreditAccount(user_, totalCollateralized_, 365); + + _enableTransferLock(); + } + + modifier givenCollateralized(address user_, uint256 totalCollateralized_) { + _setTotalCollateralized(user_, totalCollateralized_); + _; + } + + // ========== MOCKS ========== // + + function _mockGetAuctionModuleForId() internal { vm.mockCall( - _BASELINE_KERNEL, - abi.encodeWithSelector( - bytes4(keccak256("getModuleForKeycode(bytes5)")), toBaselineKeycode("BPOOL") - ), - abi.encode(address(_baseToken)) + address(_auctionHouse), + abi.encodeWithSelector(IAuctionHouse.getAuctionModuleForId.selector, _lotId), + abi.encode(address(_auctionModule)) ); } } diff --git a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/BaselineCappedAllowlistTest.sol b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/BaselineCappedAllowlistTest.sol index a68b6ad1..30674e5e 100644 --- a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/BaselineCappedAllowlistTest.sol +++ b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/BaselineCappedAllowlistTest.sol @@ -8,6 +8,9 @@ import {BaselineAxisLaunchTest} from "../BaselineAxisLaunchTest.sol"; import {BALwithCappedAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol"; +// Baseline +import {Actions as BaselineKernelActions} from "@baseline/Kernel.sol"; + contract BaselineCappedAllowlistTest is BaselineAxisLaunchTest { uint256 internal constant _BUYER_LIMIT = 5e18; @@ -16,7 +19,7 @@ contract BaselineCappedAllowlistTest is BaselineAxisLaunchTest { modifier givenCallbackIsCreated() override { // Get the salt bytes memory args = - abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); bytes32 salt = _getTestSalt("BaselineCappedAllowlist", type(BALwithCappedAllowlist).creationCode, args); @@ -24,15 +27,15 @@ contract BaselineCappedAllowlistTest is BaselineAxisLaunchTest { // Source: https://github.com/foundry-rs/foundry/issues/6402 vm.startBroadcast(); _dtl = new BALwithCappedAllowlist{salt: salt}( - address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER + address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER ); vm.stopBroadcast(); _dtlAddress = address(_dtl); - // Call configureDependencies to set everything that's needed - _mockBaselineGetModuleForKeycode(); - _dtl.configureDependencies(); + // Install as a policy + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.ActivatePolicy, _dtlAddress); _; } diff --git a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onBid.t.sol b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onBid.t.sol index f8600356..55b9fea5 100644 --- a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onBid.t.sol +++ b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onBid.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineCappedAllowlistTest} from "./BaselineCappedAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BALwithCappedAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol"; diff --git a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onCreate.t.sol b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onCreate.t.sol index b42219d8..2f54c6b5 100644 --- a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onCreate.t.sol +++ b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/onCreate.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineCappedAllowlistTest} from "./BaselineCappedAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BALwithCappedAllowlist} from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol"; @@ -18,6 +18,8 @@ contract BaselineCappedAllowlistOnCreateTest is BaselineCappedAllowlistTest { // [X] it reverts // [X] when the buyer limit is 0 // [X] it reverts + // [X] when the seller is not the owner + // [X] it reverts // [X] it decodes and stores the merkle root function test_allowlistParamsIncorrect_reverts() @@ -52,6 +54,21 @@ contract BaselineCappedAllowlistOnCreateTest is BaselineCappedAllowlistTest { _onCreate(); } + function test_sellerNotOwner_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAllowlistParams(_MERKLE_ROOT, _BUYER_LIMIT) + { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the callback + _onCreate(_OWNER); + } + function test_success() public givenBPoolIsCreated diff --git a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/setMerkleRoot.t.sol b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/setMerkleRoot.t.sol index b6f73551..e2e8df9a 100644 --- a/test/callbacks/liquidity/BaselineV2/CappedAllowlist/setMerkleRoot.t.sol +++ b/test/callbacks/liquidity/BaselineV2/CappedAllowlist/setMerkleRoot.t.sol @@ -16,7 +16,7 @@ contract BaselineCappedAllowlistSetMerkleRootTest is BaselineCappedAllowlistTest 0x1234567890123456789012345678901234567890123456789012345678901234; function _setMerkleRoot() internal { - vm.prank(_OWNER); + vm.prank(_SELLER); BALwithCappedAllowlist(address(_dtl)).setMerkleRoot(_NEW_MERKLE_ROOT); } @@ -68,9 +68,11 @@ contract BaselineCappedAllowlistSetMerkleRootTest is BaselineCappedAllowlistTest givenAllowlistParams(_MERKLE_ROOT, _BUYER_LIMIT) givenOnCreate givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) - givenAddressHasBaseTokenBalance(_dtlAddress, _REFUND_AMOUNT) - givenOnSettle + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) { + // Perform onSettle callback + _onSettle(); + // Expect revert bytes memory err = abi.encodeWithSelector(BALwithAllowlist.Callback_InvalidState.selector); vm.expectRevert(err); diff --git a/test/callbacks/liquidity/BaselineV2/MockBlast.sol b/test/callbacks/liquidity/BaselineV2/MockBlast.sol new file mode 100644 index 00000000..9f25d088 --- /dev/null +++ b/test/callbacks/liquidity/BaselineV2/MockBlast.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {YieldMode, GasMode} from "@baseline/utils/IBlast.sol"; + +/// @dev Implements the minimum interface required for BlastClaimer to interact with this contract. +contract MockBlast { + function configure(YieldMode _yield, GasMode gasMode, address governor) external { + // Do nothing + } +} diff --git a/test/callbacks/liquidity/BaselineV2/TokenAllowlist/BaselineTokenAllowlistTest.sol b/test/callbacks/liquidity/BaselineV2/TokenAllowlist/BaselineTokenAllowlistTest.sol index 8c57a166..1c22d339 100644 --- a/test/callbacks/liquidity/BaselineV2/TokenAllowlist/BaselineTokenAllowlistTest.sol +++ b/test/callbacks/liquidity/BaselineV2/TokenAllowlist/BaselineTokenAllowlistTest.sol @@ -11,6 +11,9 @@ import { ITokenBalance } from "../../../../../src/callbacks/liquidity/BaselineV2/BALwithTokenAllowlist.sol"; +// Baseline +import {Actions as BaselineKernelActions} from "@baseline/Kernel.sol"; + contract BaselineTokenAllowlistTest is BaselineAxisLaunchTest { uint96 internal constant _TOKEN_THRESHOLD = 5e18; MockERC20 internal _token; @@ -20,7 +23,7 @@ contract BaselineTokenAllowlistTest is BaselineAxisLaunchTest { modifier givenCallbackIsCreated() override { // Get the salt bytes memory args = - abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER); + abi.encode(address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER); bytes32 salt = _getTestSalt("BaselineTokenAllowlist", type(BALwithTokenAllowlist).creationCode, args); @@ -28,15 +31,15 @@ contract BaselineTokenAllowlistTest is BaselineAxisLaunchTest { // Source: https://github.com/foundry-rs/foundry/issues/6402 vm.startBroadcast(); _dtl = new BALwithTokenAllowlist{salt: salt}( - address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _OWNER + address(_auctionHouse), _BASELINE_KERNEL, _BASELINE_QUOTE_TOKEN, _SELLER ); vm.stopBroadcast(); _dtlAddress = address(_dtl); - // Call configureDependencies to set everything that's needed - _mockBaselineGetModuleForKeycode(); - _dtl.configureDependencies(); + // Install as a policy + vm.prank(_OWNER); + _baselineKernel.executeAction(BaselineKernelActions.ActivatePolicy, _dtlAddress); _; } diff --git a/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onBid.t.sol b/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onBid.t.sol index cfe06e77..dc74c24e 100644 --- a/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onBid.t.sol +++ b/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onBid.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineTokenAllowlistTest} from "./BaselineTokenAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; contract BaselineTokenAllowlistOnBidTest is BaselineTokenAllowlistTest { uint64 internal constant _BID_ID = 1; diff --git a/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onCreate.t.sol b/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onCreate.t.sol index eb72f9d9..208c1bdb 100644 --- a/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onCreate.t.sol +++ b/test/callbacks/liquidity/BaselineV2/TokenAllowlist/onCreate.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineTokenAllowlistTest} from "./BaselineTokenAllowlistTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import { BALwithTokenAllowlist, ITokenBalance @@ -14,6 +14,8 @@ contract BaselineTokenAllowlistOnCreateTest is BaselineTokenAllowlistTest { // [X] when the allowlist parameters are in an incorrect format // [X] it reverts + // [X] when the seller is not the owner + // [X] it reverts // [X] if the token is not a contract // [X] it reverts // [X] if the token balance is not retrievable @@ -38,6 +40,22 @@ contract BaselineTokenAllowlistOnCreateTest is BaselineTokenAllowlistTest { _onCreate(); } + function test_sellerNotOwner_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenTokenIsCreated + givenAuctionIsCreated + givenAllowlistParams(address(_token), _TOKEN_THRESHOLD) + { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the callback + _onCreate(_OWNER); + } + function test_tokenNotContract_reverts() public givenBPoolIsCreated diff --git a/test/callbacks/liquidity/BaselineV2/mocks/MockBPOOL.sol b/test/callbacks/liquidity/BaselineV2/mocks/MockBPOOL.sol deleted file mode 100644 index 631a451a..00000000 --- a/test/callbacks/liquidity/BaselineV2/mocks/MockBPOOL.sol +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -import { - IBPOOLv1, - Range, - Position, - Ticks -} from "../../../../../src/callbacks/liquidity/BaselineV2/lib/IBPOOL.sol"; -import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; -import {IUniswapV3Pool} from - "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; -import {IUniswapV3Factory} from - "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Factory.sol"; -import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; - -contract MockBPOOL is IBPOOLv1, ERC20 { - int24 public immutable TICK_SPACING; - uint24 public immutable FEE_TIER; - - ERC20 public immutable reserve; - - IUniswapV3Pool public pool; - IUniswapV3Factory public immutable factory; - - mapping(Range => Ticks) public getTicks; - - mapping(Range => uint256) public rangeReserves; - mapping(Range => uint128) public rangeLiquidity; - - int24 public activeTick; - - constructor( - string memory name_, - string memory symbol_, - uint8 decimals_, - address factory_, - address reserve_, - uint24 feeTier_, - int24 initialActiveTick_ - ) ERC20(name_, symbol_, decimals_) { - factory = IUniswapV3Factory(factory_); - reserve = ERC20(reserve_); - FEE_TIER = feeTier_; - TICK_SPACING = factory.feeAmountTickSpacing(feeTier_); - - // This mimics the behaviour of the real BPOOLv1 module - // Create the pool - pool = IUniswapV3Pool(factory.createPool(address(this), address(reserve), FEE_TIER)); - - // Set the initial active tick - pool.initialize(TickMath.getSqrtRatioAtTick(initialActiveTick_)); - activeTick = initialActiveTick_; - } - - function getLiquidity(Range range_) external view override returns (uint128) { - // If the reserves are 0, the liquidity is 0 - if (rangeReserves[range_] == 0) { - return 0; - } - - // If the reserves are not 0, the liquidity is a non-zero value - return 1; - } - - function addReservesTo( - Range _range, - uint256 _reserves - ) - external - override - returns (uint256 bAssetsAdded_, uint256 reservesAdded_, uint128 liquidityFinal_) - { - reserve.transferFrom(msg.sender, address(this), _reserves); - - rangeReserves[_range] += _reserves; - - // Mimic the Uniswap V3 callback transferring into the pool - reserve.transfer(address(pool), _reserves); - - return (0, _reserves, 0); - } - - function addLiquidityTo( - Range _range, - uint128 _liquidity - ) - external - override - returns (uint256 bAssetsAdded_, uint256 reservesAdded_, uint128 liquidityFinal_) - { - rangeLiquidity[_range] += _liquidity; - - return (0, 0, rangeLiquidity[_range]); - } - - function removeAllFrom(Range _range) - external - override - returns ( - uint256 bAssetsRemoved_, - uint256 bAssetFees_, - uint256 reservesRemoved_, - uint256 reserveFees_ - ) - {} - - function setTicks(Range _range, int24 _lower, int24 _upper) external override { - if (_lower > _upper) revert("Invalid tick range"); - - getTicks[_range] = Ticks({lower: _lower, upper: _upper}); - } - - function mint(address _to, uint256 _amount) external override { - _mint(_to, _amount); - } - - function burnAllBAssetsInContract() external override { - _burn(address(this), balanceOf[address(this)]); - } - - function getBaselineValue() external view override returns (uint256) {} - - function getActiveTS() public view returns (int24) { - (, int24 tick,,,,,) = pool.slot0(); - - // Round down to the nearest active tick spacing - tick = ((tick / TICK_SPACING) * TICK_SPACING); - - // Properly handle negative numbers and edge cases - if (tick >= 0 || tick % TICK_SPACING == 0) { - tick += TICK_SPACING; - } - - return tick; - } - - function getPosition(Range range_) external view override returns (Position memory position) { - return Position({ - liquidity: 0, - sqrtPriceL: 0, - sqrtPriceU: 0, - bAssets: 0, - reserves: rangeReserves[range_], - capacity: rangeReserves[range_] - }); - } - - function getBalancesForLiquidity( - uint160 _sqrtPriceL, - uint160 _sqrtPriceU, - uint128 _liquidity - ) external view override returns (uint256 bAssets_, uint256 reserves_) {} - - function getLiquidityForReserves( - uint160 _sqrtPriceL, - uint160 _sqrtPriceU, - uint256 _reserves, - uint160 _sqrtPriceA - ) external view override returns (uint128 liquidity_) {} - - function getCapacityForLiquidity( - uint160 _sqrtPriceL, - uint160 _sqrtPriceU, - uint128 _liquidity, - uint160 _sqrtPriceA - ) external view override returns (uint256 capacity_) {} - - function getCapacityForReserves( - uint160 _sqrtPriceL, - uint160 _sqrtPriceU, - uint256 _reserves - ) external view override returns (uint256 capacity_) {} -} diff --git a/test/callbacks/liquidity/BaselineV2/onCancel.t.sol b/test/callbacks/liquidity/BaselineV2/onCancel.t.sol index 4e1edd6d..2cd74d82 100644 --- a/test/callbacks/liquidity/BaselineV2/onCancel.t.sol +++ b/test/callbacks/liquidity/BaselineV2/onCancel.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {BaselineAxisLaunchTest} from "./BaselineAxisLaunchTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaselineAxisLaunch} from "../../../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; @@ -82,9 +82,11 @@ contract BaselineOnCancelTest is BaselineAxisLaunchTest { givenAuctionIsCreated givenOnCreate givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) - givenAddressHasBaseTokenBalance(_dtlAddress, _REFUND_AMOUNT) - givenOnSettle + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) { + // Perform onSettle callback + _onSettle(); + // Expect revert bytes memory err = abi.encodeWithSelector(BaselineAxisLaunch.Callback_AlreadyComplete.selector); @@ -115,13 +117,16 @@ contract BaselineOnCancelTest is BaselineAxisLaunchTest { givenCallbackIsCreated givenAuctionIsCreated givenOnCreate - givenAddressHasBaseTokenBalance(_dtlAddress, _LOT_CAPACITY) { + // Transfer capacity from auction house back to the callback + // We transfer instead of minting to not affect the supply + _transferBaseToken(_dtlAddress, _LOT_CAPACITY); + // Perform callback _onCancel(); // Check the circulating supply is updated - assertEq(_dtl.initialCirculatingSupply(), 0, "circulating supply"); + assertEq(_baseToken.totalSupply(), 0, "circulating supply"); // Check the auction is marked as completed assertEq(_dtl.auctionComplete(), true, "auction completed"); @@ -129,5 +134,8 @@ contract BaselineOnCancelTest is BaselineAxisLaunchTest { // Check the refunded base token quantity is burned assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token: callback balance"); assertEq(_baseToken.balanceOf(address(_baseToken)), 0, "base token: contract balance"); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); } } diff --git a/test/callbacks/liquidity/BaselineV2/onCreate.t.sol b/test/callbacks/liquidity/BaselineV2/onCreate.t.sol index 75d59e51..feff8b7a 100644 --- a/test/callbacks/liquidity/BaselineV2/onCreate.t.sol +++ b/test/callbacks/liquidity/BaselineV2/onCreate.t.sol @@ -5,8 +5,9 @@ import {BaselineAxisLaunchTest} from "./BaselineAxisLaunchTest.sol"; import {BaselineAxisLaunch} from "../../../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; -import {Range} from "../../../../src/callbacks/liquidity/BaselineV2/lib/IBPOOL.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {Range} from "@baseline/modules/BPOOL.v1.sol"; +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; import {console2} from "@forge-std-1.9.1/console2.sol"; @@ -29,7 +30,7 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { vm.expectRevert(err); } - function _assertBaseTokenBalances() internal { + function _assertBaseTokenBalances() internal view { assertEq(_baseToken.balanceOf(_SELLER), 0, "seller balance"); assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller balance"); assertEq(_baseToken.balanceOf(_dtlAddress), 0, "dtl balance"); @@ -118,48 +119,48 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { revert("Unsupported decimal permutation"); } - function _roundToTickSpacingUp(int24 tick_) internal view returns (int24) { - // Rounds down - int24 roundedTick = (tick_ / _tickSpacing) * _tickSpacing; - - // Add a tick spacing to round up - if (tick_ > roundedTick) { - roundedTick += _tickSpacing; - } - - return roundedTick; + function _getPoolActiveTick() internal view returns (int24) { + (, int24 activeTick,,,,,) = _baseToken.pool().slot0(); + return activeTick; } - function _assertTicks(int24 fixedPriceTick_) internal { - assertEq(_baseToken.activeTick(), fixedPriceTick_, "active tick"); - console2.log("Active tick: ", _baseToken.activeTick()); + function _assertTicks(int24 fixedPriceTick_) internal view { + // Get the tick from the pool + int24 activeTick = _getPoolActiveTick(); + + assertEq(activeTick, fixedPriceTick_, "active tick"); + console2.log("Active tick: ", activeTick); + console2.log("Tick spacing: ", _tickSpacing); // Calculate the active tick with rounding int24 anchorTickUpper = _roundToTickSpacingUp(fixedPriceTick_); int24 anchorTickLower = anchorTickUpper - _createData.anchorTickWidth * _tickSpacing; - console2.log("Anchor tick lower: ", anchorTickLower); - console2.log("Anchor tick upper: ", anchorTickUpper); - - // Active tick should be within the anchor range - assertGt(fixedPriceTick_, anchorTickLower, "active tick > anchor tick lower"); - assertLe(fixedPriceTick_, anchorTickUpper, "active tick <= anchor tick upper"); + console2.log("Calculated anchor tick lower: ", anchorTickLower); + console2.log("Calculated anchor tick upper: ", anchorTickUpper); // Anchor range should be the width of anchorTickWidth * tick spacing (int24 anchorTickLower_, int24 anchorTickUpper_) = _baseToken.getTicks(Range.ANCHOR); assertEq(anchorTickLower_, anchorTickLower, "anchor tick lower"); assertEq(anchorTickUpper_, anchorTickUpper, "anchor tick upper"); + // Active tick should be within the anchor range + assertGt(fixedPriceTick_, anchorTickLower_, "active tick > anchor tick lower"); + assertLe(fixedPriceTick_, anchorTickUpper_, "active tick <= anchor tick upper"); + // Floor range should be the width of the tick spacing and below the anchor range + int24 floorTickUpper_ = anchorTickLower_ - _floorRangeGap * _tickSpacing; + int24 floorTickLower_ = floorTickUpper_ - _tickSpacing; + (int24 floorTickLower, int24 floorTickUpper) = _baseToken.getTicks(Range.FLOOR); - assertEq(floorTickLower, anchorTickLower_ - _tickSpacing, "floor tick lower"); - assertEq(floorTickUpper, anchorTickLower_, "floor tick upper"); + assertEq(floorTickUpper, floorTickUpper_, "floor tick upper"); + assertEq(floorTickLower, floorTickLower_, "floor tick lower"); // Discovery range should be the width of discoveryTickWidth * tick spacing and above the active tick (int24 discoveryTickLower, int24 discoveryTickUpper) = _baseToken.getTicks(Range.DISCOVERY); assertEq(discoveryTickLower, anchorTickUpper_, "discovery tick lower"); assertEq( discoveryTickUpper, - anchorTickUpper_ + _createData.discoveryTickWidth * _tickSpacing, + anchorTickUpper_ + _DISCOVERY_TICK_WIDTH * _tickSpacing, "discovery tick upper" ); } @@ -168,6 +169,8 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { // [X] when the callback data is incorrect // [X] it reverts + // [X] when the seller is not the owner + // [X] it reverts // [X] when the callback is not called by the auction house // [X] it reverts // [X] when the lot has already been registered @@ -176,36 +179,76 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { // [X] it reverts // [X] when the quote token is not the reserve // [X] it reverts - // [X] when the floorReservesPercent is not between 0 and 99% + // [X] when the base token is higher than the reserve token + // [X] it reverts + // [X] when the recipient is the zero address + // [X] it reverts + // [X] when the poolPercent is < 1% // [X] it reverts - // [X] when the anchorTickWidth is <= 0 + // [X] when the poolPercent is > 100% // [X] it reverts - // [X] when the discoveryTickWidth is <= 0 + // [X] when the floorReservesPercent is not between 10% and 90% + // [X] it reverts + // [X] when the anchorTickWidth is < 10 // [X] it reverts // [X] when the auction format is not FPB // [X] it reverts // [X] when the auction is not prefunded // [X] it reverts - // [X] when the auction price does not match the pool active tick - // [X] it reverts - // [X] when the floorReservesPercent is 0-99% + // [X] when the floor reserves are too low + // [X] it reverts due to the solvency check + // [X] when the floor reserves are too high + // [X] it reverts due to the solvency check + // [X] when the pool percent is too low + // [X] it reverts due to the solvency check + // [X] when the pool percent is too high + // [X] it reverts due to the solvency check + // [X] when the floorReservesPercent is 10-90% // [X] it correctly records the allocation - // [X] when the tick spacing is narrow - // [X] the ticks do not overlap + // [X] when the fee tier is not 10000 (1%) + // [X] it reverts + // [X] when the anchorTickU parameter does not equal the calculated value + // [X] it reverts + // [X] when the pool price is < the auction price + // [X] it reverts + // [X] when the pool price is >= the auction price + // [X] it succeeds // [X] when the auction fixed price is very high - // [X] it correctly sets the active tick + // [X] it handles the active tick correctly // [X] when the auction fixed price is very low - // [X] it correctly sets the active tick - // [X] when the base token address is lower than the quote token address - // [X] it correctly sets the active tick + // [X] it handles the active tick correctly // [X] when the quote token decimals are higher than the base token decimals - // [X] it correctly sets the active tick + // [X] it handles it correctly // [X] when the quote token decimals are lower than the base token decimals - // [X] it correctly sets the active tick + // [X] it handles it correctly + // [X] when there is a gap specified between the floor range and anchor range + // [X] when the floor range gap is below 0 + // [X] it reverts + // [X] when the floor range is calculated to be below the minimum tick + // [X] it reverts + // [X] it sets the ranges correctly // [X] when the anchorTickWidth is small // [X] it correctly sets the anchor ticks to not overlap with the other ranges - // [X] when the discoveryTickWidth is small - // [X] it correctly sets the discovery ticks to not overlap with the other ranges + // [X] when the anchorTickWidth is less than 10 + // [X] it reverts + // [X] when the anchorTickWidth is greater than 50 + // [X] it reverts + // [X] when the activeTick and anchorTickWidth results in an overflow + // [X] it reverts + // [X] when the activeTick and anchorTickWidth results in an underflow + // [X] it reverts + // [X] when the activeTick and discoveryTickWidth results in an overflow + // [X] it reverts + // [X] when the activeTick and discoveryTickWidth results in an underflow + // [X] it reverts + // [X] given the protocol fee is set + // [X] given the protocol fee would result in a solvency check failure + // [X] it reverts + // [X] it correctly performs the solvency check + // [X] when the referrer fee is set + // [X] when the referrer fee would result in a solvency check failure + // [X] it reverts + // [X] it correctly performs the solvency check // [X] it transfers the base token to the auction house, updates circulating supply, sets the state variables, initializes the pool and sets the tick ranges function test_callbackDataIncorrect_reverts() @@ -230,6 +273,20 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { ); } + function test_sellerNotOwner_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the callback + _onCreate(_OWNER); + } + function test_notAuctionHouse_reverts() public givenBPoolIsCreated @@ -321,19 +378,51 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { ); } - function test_floorReservesPercentInvalid_reverts(uint24 floorReservesPercent_) + function test_floorReservesPercent_belowMin_reverts( + uint24 floorReservesPercent_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { + uint24 floorReservesPercent = uint24(bound(floorReservesPercent_, 0, 10e2 - 1)); + _createData.floorReservesPercent = floorReservesPercent; + + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_Params_InvalidFloorReservesPercent.selector + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_floorReservesPercent_aboveMax_reverts( + uint24 floorReservesPercent_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { + uint24 floorReservesPercent = + uint24(bound(floorReservesPercent_, 90e2 + 1, type(uint24).max)); + _createData.floorReservesPercent = floorReservesPercent; + + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_Params_InvalidFloorReservesPercent.selector + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_anchorTickU_below_reverts() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { - uint24 floorReservesPercent = - uint24(bound(floorReservesPercent_, _NINETY_NINE_PERCENT + 1, type(uint24).max)); - _createData.floorReservesPercent = floorReservesPercent; + // Set the anchor range upper to be below the expected value + _createData.anchorTickU = _FIXED_PRICE_TICK_UPPER - 1; // Expect revert bytes memory err = abi.encodeWithSelector( - BaselineAxisLaunch.Callback_Params_InvalidFloorReservesPercent.selector + BaselineAxisLaunch.Callback_Params_InvalidAnchorTickUpper.selector ); vm.expectRevert(err); @@ -341,18 +430,35 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { _onCreate(); } - function test_anchorTickWidthInvalid_reverts(int24 anchorTickWidth_) + function test_anchorTickU_above_reverts() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { - int24 anchorTickWidth = int24(bound(anchorTickWidth_, type(int24).min, 0)); - _createData.anchorTickWidth = anchorTickWidth; + // Set the anchor range upper to be above the expected value + _createData.anchorTickU = _FIXED_PRICE_TICK_UPPER + 1; // Expect revert bytes memory err = abi.encodeWithSelector( - BaselineAxisLaunch.Callback_Params_InvalidAnchorTickWidth.selector + BaselineAxisLaunch.Callback_Params_InvalidAnchorTickUpper.selector + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_poolPrice_belowAuctionPrice_reverts() + public + givenPoolInitialTick(10_985) // Below auction price tick of 10986 + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_PoolLessThanAuctionPrice.selector, 10_985, 10_986 ); vm.expectRevert(err); @@ -360,18 +466,120 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { _onCreate(); } - function test_discoveryTickWidthInvalid_reverts(int24 discoveryTickWidth_) + function test_poolPrice_aboveAuctionPrice() + public + givenPoolInitialTick(10_987) // Above auction price tick of 10986 + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + { + // Perform the call + _onCreate(); + + // Assert base token balances + _assertBaseTokenBalances(); + + // Lot ID is set + assertEq(_dtl.lotId(), _lotId, "lot ID"); + + // Check circulating supply + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); + + _assertTicks(10_987); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); + } + + function test_floorRangeGap_belowBounds_reverts( + int24 floorRangeGap_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { + int24 floorRangeGap = int24(bound(floorRangeGap_, type(int24).min, -1)); + _setFloorRangeGap(floorRangeGap); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_InvalidFloorRangeGap.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_floorRangeGap_underflow_reverts() + public + givenPoolInitialTick(TickMath.MIN_TICK + 200 * _ANCHOR_TICK_WIDTH) // This will result in the floor range to be below the MIN_TICK, which should cause a revert + givenAnchorUpperTick(-885_200) + givenFloorRangeGap(1) + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + { + // Expect a revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_RangeOutOfBounds.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_floorRangeGap_zero() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorRangeGap(0) + { + // Perform the call + _onCreate(); + + // Assert ticks + int24 fixedPriceTick = _getFixedPriceTick(); + _assertTicks(fixedPriceTick); + } + + function test_floorRangeGap_ten() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated + givenFloorRangeGap(10) + givenFloorReservesPercent(15e2) // For the solvency check { - int24 discoveryTickWidth = int24(bound(discoveryTickWidth_, type(int24).min, 0)); - _createData.discoveryTickWidth = discoveryTickWidth; + // Perform the call + _onCreate(); + + // Assert ticks + int24 fixedPriceTick = _getFixedPriceTick(); + _assertTicks(fixedPriceTick); + } + + function test_anchorTickWidth_belowBounds_reverts( + int24 anchorTickWidth_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { + int24 anchorTickWidth = int24(bound(anchorTickWidth_, type(int24).min, 9)); + _setAnchorTickWidth(anchorTickWidth); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_Params_InvalidAnchorTickWidth.selector + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_anchorTickWidth_aboveBounds_reverts( + int24 anchorTickWidth_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { + int24 anchorTickWidth = int24(bound(anchorTickWidth_, 51, type(int24).max)); + _setAnchorTickWidth(anchorTickWidth); // Expect revert bytes memory err = abi.encodeWithSelector( - BaselineAxisLaunch.Callback_Params_InvalidDiscoveryTickWidth.selector + BaselineAxisLaunch.Callback_Params_InvalidAnchorTickWidth.selector ); vm.expectRevert(err); @@ -379,6 +587,54 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { _onCreate(); } + function test_recipientZero_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + { + // Set the recipient to be the zero address + _createData.recipient = address(0); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_InvalidRecipient.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_poolPercent_belowBounds_reverts( + uint24 poolPercent_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 0, 10e2 - 1)); + _setPoolPercent(poolPercent); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_InvalidPoolPercent.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_poolPercent_aboveBounds_reverts( + uint24 poolPercent_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 100e2 + 1, type(uint24).max)); + _setPoolPercent(poolPercent); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_InvalidPoolPercent.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + function test_givenAuctionFormatNotFixedPriceBatch_reverts() public givenBPoolIsCreated @@ -421,23 +677,37 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { ); } - function test_auctionPriceDoesNotMatchPoolActiveTick_reverts() + function test_auctionPriceDoesNotMatchPoolActiveTick() public givenBPoolIsCreated // BPOOL will have an active tick of _FIXED_PRICE givenCallbackIsCreated - givenFixedPrice(2e18) + givenFixedPrice(25e17) givenAuctionIsCreated // Has to be after the fixed price is set + givenPoolPercent(99e2) // For the solvency check + givenFloorReservesPercent(90e2) // For the solvency check { - // Expect revert - bytes memory err = abi.encodeWithSelector( - BaselineAxisLaunch.Callback_Params_PoolTickMismatch.selector, - _getTickFromPrice(2e18, _baseTokenDecimals, _isBaseTokenAddressLower), - _baseToken.activeTick() - ); - vm.expectRevert(err); - // Perform the call _onCreate(); + + // Assert base token balances + _assertBaseTokenBalances(); + + // Lot ID is set + assertEq(_dtl.lotId(), _lotId, "lot ID"); + + // Check circulating supply + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); + + // The pool should be initialised with the tick equivalent to the auction's fixed price + // Fixed price = 2e18 + // SqrtPriceX96 = sqrt(2e18 * 2^192 / 1e18) + // = 1.12045542e29 + // Tick = log((1.12045542e29 / 2^96)^2) / log(1.0001) + // = 6,931.8183824009 (rounded down) + // Price = 1.0001^6931 / (10^(18-18)) = 1.9998363402 + int24 fixedPriceTick = 10_986; // Price: 3e18 + + _assertTicks(fixedPriceTick); } function test_success() @@ -449,9 +719,6 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { // Perform the call _onCreate(); - // Check that the callback owner is correct - assertEq(_dtl.owner(), _OWNER, "owner"); - // Assert base token balances _assertBaseTokenBalances(); @@ -459,49 +726,200 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { assertEq(_dtl.lotId(), _lotId, "lot ID"); // Check circulating supply - assertEq(_dtl.initialCirculatingSupply(), _LOT_CAPACITY, "circulating supply"); + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); // The pool should be initialised with the tick equivalent to the auction's fixed price int24 fixedPriceTick = _getFixedPriceTick(); _assertTicks(fixedPriceTick); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); } - function test_floorReservesPercent(uint24 floorReservesPercent_) + function test_floorReservesPercent_low() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated + givenFloorReservesPercent(10e2) + givenPoolPercent(91e2) // For the solvency check { - uint24 floorReservesPercent = uint24(bound(floorReservesPercent_, 0, _NINETY_NINE_PERCENT)); - _createData.floorReservesPercent = floorReservesPercent; + // Perform the call + _onCreate(); + + // Assert + assertEq(_dtl.floorReservesPercent(), 10e2, "floor reserves percent"); + } + + function test_floorReservesPercent_low_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorReservesPercent(10e2) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 971_983_049_689_268_138 + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_floorReservesPercent_middle() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorReservesPercent(50e2) + { + // Perform the call + _onCreate(); + + // Assert + assertEq(_dtl.floorReservesPercent(), 50e2, "floor reserves percent"); + } + + function test_floorReservesPercent_high() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorReservesPercent(90e2) + givenPoolPercent(83e2) // For the solvency check + { + // Perform the call + _onCreate(); + + // Assert + assertEq(_dtl.floorReservesPercent(), 90e2, "floor reserves percent"); + } + + function test_floorReservesPercent_high_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorReservesPercent(90e2) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 1_060_761_857_234_503_343 + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_poolPercent_lowPercent() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(10e2) + givenFloorRangeGap(110) // For the solvency check + givenFloorReservesPercent(90e2) // For the solvency check + { + // Perform the call + _onCreate(); + + // Assert + assertEq(_dtl.poolPercent(), 10e2, "pool percent"); + } + + function test_poolPercent_lowPercent_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(10e2) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 116_824_419_938_147_786 + ); + vm.expectRevert(err); // Perform the call _onCreate(); + } + + function test_poolPercent_highPercent() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(91e2) + givenFloorReservesPercent(10e2) // For the solvency check + { + // Perform the call + _onCreate(); // Assert - assertEq(_dtl.floorReservesPercent(), floorReservesPercent, "floor reserves percent"); + assertEq(_dtl.poolPercent(), 91e2, "pool percent"); + } + + function test_poolPercent_highPercent_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(91e2) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 1_063_102_221_437_144_855 + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); } - function test_tickSpacingNarrow() + function test_feeTier500_reverts() public givenBPoolFeeTier(500) givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated + givenPoolPercent(100e2) // For the solvency check { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_Params_UnsupportedPoolFeeTier.selector + ); + vm.expectRevert(err); + // Perform the call _onCreate(); + } - // The pool should be initialised with the tick equivalent to the auction's fixed price - int24 fixedPriceTick = _getFixedPriceTick(); + function test_feeTier3000_reverts() + public + givenBPoolFeeTier(3000) + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(100e2) // For the solvency check + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_Params_UnsupportedPoolFeeTier.selector + ); + vm.expectRevert(err); - _assertTicks(fixedPriceTick); + // Perform the call + _onCreate(); } function test_auctionHighPrice() public - givenFixedPrice(3e56) + givenFixedPrice(1e32) // Seems to cause a revert above this when calculating the tick + givenAnchorUpperTick(322_400) givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated @@ -516,27 +934,22 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { assertEq(_dtl.lotId(), _lotId, "lot ID"); // Check circulating supply - assertEq(_dtl.initialCirculatingSupply(), _LOT_CAPACITY, "circulating supply"); - - // Calculation for the maximum price - // By default, quote token is token0 - // Minimum sqrtPriceX96 = MIN_SQRT_RATIO = 4_295_128_739 - // 4_295_128_739^2 = 1e18 * 2^192 / amount0 - // amount0 = 1e18 * 2^192 / 4_295_128_739^2 = 3.402567867e56 ~= 3e56 + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); - // SqrtPriceX96 = sqrt(1e18 * 2^192 / 3e56) - // = 4,574,240,095.5009932534 - // Tick = log((4,574,240,095.5009932534 / 2^96)^2) / log(1.0001) - // = -886,012.7559071901 (rounded down) - // Price = 1.0001^-886013 / (10^(18-18)) = 0 - int24 fixedPriceTick = -886_013; + // SqrtPriceX96 = sqrt(1e32 * 2^192 / 1e18) + // = 7.9456983992e35 + // Tick = log((7.9456983992e35 / 2^96)^2) / log(1.0001) + // = 322,435.7131383481 (rounded down) + // Price = 1.0001^322435 / (10^(18-18)) = 100,571,288,720,819.0986858653 + int24 fixedPriceTick = 322_378; // Not the exact tick, but close enough _assertTicks(fixedPriceTick); } function test_auctionLowPrice() public - givenFixedPrice(1) + givenFixedPrice(1e6) + givenAnchorUpperTick(-276_200) givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated @@ -551,17 +964,17 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { assertEq(_dtl.lotId(), _lotId, "lot ID"); // Check circulating supply - assertEq(_dtl.initialCirculatingSupply(), _LOT_CAPACITY, "circulating supply"); + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); // The pool should be initialised with the tick equivalent to the auction's fixed price - // By default, quote token is token0 - // Fixed price = 1 - // SqrtPriceX96 = sqrt(1e18 * 2^192 / 1) - // = 7.9228162514e37 - // Tick = log((7.9228162514e37 / 2^96)^2) / log(1.0001) - // = 414,486.0396584532 (rounded down) - // Price = 1.0001^414486 / (10^(18-18)) = 9.9999603427e17 - int24 fixedPriceTick = 414_486; + // By default, quote token is token1 + // Fixed price = 1e6 + // SqrtPriceX96 = sqrt(1e6 * 2^192 / 1e18) + // = 7.9228162514e22 + // Tick = log((7.9228162514e22 / 2^96)^2) / log(1.0001) + // = -276,324.02643908 (rounded down) + // Price = 1.0001^-276,324.02643908 / (10^(18-18)) = 9.9999999999e-13 + int24 fixedPriceTick = -276_325; _assertTicks(fixedPriceTick); } @@ -571,7 +984,8 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated - givenAnchorTickWidth(1) + givenAnchorTickWidth(10) + givenFloorReservesPercent(50e2) // For the solvency check { // Perform the call _onCreate(); @@ -582,12 +996,14 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { _assertTicks(fixedPriceTick); } - function test_narrowDiscoveryTickWidth() + function test_wideAnchorTickWidth() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated - givenDiscoveryTickWidth(1) + givenAnchorTickWidth(50) + givenFloorReservesPercent(10e2) // For the solvency check + givenPoolPercent(58e2) // For the solvency check { // Perform the call _onCreate(); @@ -598,9 +1014,26 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { _assertTicks(fixedPriceTick); } - function test_baseTokenAddressLower() + function test_baseTokenAddressHigher_reverts() public - givenBaseTokenAddressLower + givenBaseTokenAddressHigher + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_BPOOLInvalidAddress.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_baseTokenDecimalsHigher() + public + givenBaseTokenDecimals(19) + givenAnchorUpperTick(-12_000) givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated @@ -615,7 +1048,9 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { assertEq(_dtl.lotId(), _lotId, "lot ID"); // Check circulating supply - assertEq(_dtl.initialCirculatingSupply(), _LOT_CAPACITY, "circulating supply"); + assertEq( + _baseToken.totalSupply(), _scaleBaseTokenAmount(_LOT_CAPACITY), "circulating supply" + ); // The pool should be initialised with the tick equivalent to the auction's fixed price int24 fixedPriceTick = _getFixedPriceTick(); @@ -623,9 +1058,10 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { _assertTicks(fixedPriceTick); } - function test_baseTokenDecimalsHigher() + function test_baseTokenDecimalsLower() public - givenBaseTokenDecimals(19) + givenBaseTokenDecimals(17) + givenAnchorUpperTick(34_200) givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated @@ -641,9 +1077,7 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { // Check circulating supply assertEq( - _dtl.initialCirculatingSupply(), - _scaleBaseTokenAmount(_LOT_CAPACITY), - "circulating supply" + _baseToken.totalSupply(), _scaleBaseTokenAmount(_LOT_CAPACITY), "circulating supply" ); // The pool should be initialised with the tick equivalent to the auction's fixed price @@ -652,9 +1086,11 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { _assertTicks(fixedPriceTick); } - function test_baseTokenDecimalsLower() + function test_activeTickRounded() public - givenBaseTokenDecimals(17) + givenBPoolFeeTier(10_000) + givenFixedPrice(1e18) + givenAnchorUpperTick(200) // Rounded up givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated @@ -669,31 +1105,189 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { assertEq(_dtl.lotId(), _lotId, "lot ID"); // Check circulating supply - assertEq( - _dtl.initialCirculatingSupply(), - _scaleBaseTokenAmount(_LOT_CAPACITY), - "circulating supply" + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); + + int24 fixedPriceTick = 0; + + _assertTicks(fixedPriceTick); + } + + function test_anchorRange_overflow_reverts() + public + givenPoolInitialTick(TickMath.MAX_TICK - 1) // This will result in the upper tick of the anchor range to be above the MAX_TICK, which should cause a revert + givenAnchorUpperTick(887_400) + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(10) + { + // Expect a revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_RangeOutOfBounds.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_anchorRange_underflow_reverts() + public + givenPoolInitialTick(TickMath.MIN_TICK + 1) // This will result in the lower tick of the anchor range to be below the MIN_TICK, which should cause a revert + givenAnchorUpperTick(-887_200) + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(10) + { + // Expect a revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_RangeOutOfBounds.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_discoveryRange_overflow_reverts() + public + givenPoolInitialTick(TickMath.MAX_TICK - _tickSpacing + 1) // This will result in the upper tick of the discovery range to be above the MAX_TICK, which should cause a revert + givenAnchorUpperTick(887_200) + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(10) + { + // Expect a revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_RangeOutOfBounds.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + // There are a few test scenarios that can't be tested: + // - The upper tick of the floor range is above the MAX_TICK: not possible, since that would require the pool to be initialised with a tick above the MAX_TICK + // - The lower tick of the discovery range is below the MIN_TICK: not possible, since that would require the pool to be initialised with a tick below the MIN_TICK + + function test_floorRange_underflow_reverts() + public + givenPoolInitialTick(TickMath.MIN_TICK) // This will result in the lower tick of the floor range to be below the MIN_TICK, which should cause a revert + givenAnchorUpperTick(-887_200) + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(10) + { + // Expect a revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_Params_RangeOutOfBounds.selector); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_givenProtocolFee() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenProtocolFeePercent(1e2) // 1% + { + // Perform the call + _onCreate(); + + // Assert base token balances + _assertBaseTokenBalances(); + + // Lot ID is set + assertEq(_dtl.lotId(), _lotId, "lot ID"); + + // Check circulating supply + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); + + // The pool should be initialised with the tick equivalent to the auction's fixed price + int24 fixedPriceTick = _getFixedPriceTick(); + + _assertTicks(fixedPriceTick); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); + } + + function test_givenProtocolFee_high_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenProtocolFeePercent(2e2) // 2% + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 996_045_004_392_648_025 ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); + } + + function test_givenReferrerFee() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenReferrerFeePercent(1e2) // 1% + { + // Perform the call + _onCreate(); + + // Assert base token balances + _assertBaseTokenBalances(); + + // Lot ID is set + assertEq(_dtl.lotId(), _lotId, "lot ID"); + + // Check circulating supply + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); // The pool should be initialised with the tick equivalent to the auction's fixed price int24 fixedPriceTick = _getFixedPriceTick(); _assertTicks(fixedPriceTick); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); } - function test_activeTickRounded() + function test_givenReferrerFee_high_reverts() public - givenBPoolFeeTier(10_000) - givenFixedPrice(1e18) givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated + givenReferrerFeePercent(2e2) // 2% { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 996_045_004_392_648_025 + ); + vm.expectRevert(err); + // Perform the call _onCreate(); + } - // Check that the callback owner is correct - assertEq(_dtl.owner(), _OWNER, "owner"); + function test_givenProtocolFee_givenReferrerFee() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenProtocolFeePercent(5e1) // 0.5% + givenReferrerFeePercent(5e1) // 0.5% + { + // Perform the call + _onCreate(); // Assert base token balances _assertBaseTokenBalances(); @@ -702,10 +1296,32 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { assertEq(_dtl.lotId(), _lotId, "lot ID"); // Check circulating supply - assertEq(_dtl.initialCirculatingSupply(), _LOT_CAPACITY, "circulating supply"); + assertEq(_baseToken.totalSupply(), _LOT_CAPACITY, "circulating supply"); - int24 fixedPriceTick = 0; + // The pool should be initialised with the tick equivalent to the auction's fixed price + int24 fixedPriceTick = _getFixedPriceTick(); _assertTicks(fixedPriceTick); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); + } + + function test_givenProtocolFee_givenReferrerFee_high_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenProtocolFeePercent(1e2) // 1% + givenReferrerFeePercent(1e2) // 1% + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 996_045_004_392_648_025 + ); + vm.expectRevert(err); + + // Perform the call + _onCreate(); } } diff --git a/test/callbacks/liquidity/BaselineV2/onCurate.t.sol b/test/callbacks/liquidity/BaselineV2/onCurate.t.sol index 721058b1..59ccf2b4 100644 --- a/test/callbacks/liquidity/BaselineV2/onCurate.t.sol +++ b/test/callbacks/liquidity/BaselineV2/onCurate.t.sol @@ -3,16 +3,11 @@ pragma solidity 0.8.19; import {BaselineAxisLaunchTest} from "./BaselineAxisLaunchTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; contract BaselineOnCurateTest is BaselineAxisLaunchTest { // ============ Modifiers ============ // - function _performCallback(uint256 curatorFee_) internal { - vm.prank(address(_auctionHouse)); - _dtl.onCurate(_lotId, curatorFee_, true, abi.encode("")); - } - // ============ Assertions ============ // // ============ Tests ============ // @@ -21,9 +16,9 @@ contract BaselineOnCurateTest is BaselineAxisLaunchTest { // [X] it reverts // [X] when the caller is not the auction house // [X] it reverts - // [X] when the curator fee is non-zero - // [X] it reverts - // [X] it does nothing + // [X] when the curator fee is zero + // [X] it does nothing + // [X] it mints the base token to the auction house function test_lotNotRegistered_reverts() public @@ -36,7 +31,7 @@ contract BaselineOnCurateTest is BaselineAxisLaunchTest { vm.expectRevert(err); // Call the callback - _performCallback(0); + _onCurate(0); } function test_notAuctionHouse_reverts() @@ -54,21 +49,24 @@ contract BaselineOnCurateTest is BaselineAxisLaunchTest { _dtl.onCurate(_lotId, 0, true, abi.encode("")); } - function test_curatorFeeNonZero_reverts(uint256 curatorFee_) - public - givenBPoolIsCreated - givenCallbackIsCreated - givenAuctionIsCreated - givenOnCreate - { - uint256 curatorFee = bound(curatorFee_, 1, type(uint256).max); - - // Expect revert - bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); - vm.expectRevert(err); + function test_curatorFeeNonZero( + uint256 curatorFee_ + ) public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated givenOnCreate { + uint256 curatorFee = bound(curatorFee_, 1, type(uint96).max); + uint256 balanceBefore = _baseToken.balanceOf(address(_auctionHouse)); // Perform callback - _performCallback(curatorFee); + _onCurate(curatorFee); + + // Assert that the base token was minted to the auction house + assertEq( + _baseToken.balanceOf(address(_auctionHouse)), + balanceBefore + curatorFee, + "base token: auction house" + ); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); } function test_curatorFeeZero() @@ -78,7 +76,17 @@ contract BaselineOnCurateTest is BaselineAxisLaunchTest { givenAuctionIsCreated givenOnCreate { + uint256 balanceBefore = _baseToken.balanceOf(address(_auctionHouse)); + // Perform callback - _performCallback(0); + _onCurate(0); + + // Assert that the base token was minted to the auction house + assertEq( + _baseToken.balanceOf(address(_auctionHouse)), balanceBefore, "base token: auction house" + ); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); } } diff --git a/test/callbacks/liquidity/BaselineV2/onSettle.t.sol b/test/callbacks/liquidity/BaselineV2/onSettle.t.sol index 50aa0d87..3c92f7bc 100644 --- a/test/callbacks/liquidity/BaselineV2/onSettle.t.sol +++ b/test/callbacks/liquidity/BaselineV2/onSettle.t.sol @@ -3,35 +3,149 @@ pragma solidity 0.8.19; import {BaselineAxisLaunchTest} from "./BaselineAxisLaunchTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaselineAxisLaunch} from "../../../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; -import {Range} from "../../../../src/callbacks/liquidity/BaselineV2/lib/IBPOOL.sol"; +import {Range} from "@baseline/modules/BPOOL.v1.sol"; import {FixedPointMathLib} from "@solmate-6.7.0/utils/FixedPointMathLib.sol"; +import {IUniswapV3Pool} from + "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; + +import {console2} from "@forge-std-1.9.1/console2.sol"; contract BaselineOnSettleTest is BaselineAxisLaunchTest { using FixedPointMathLib for uint256; + uint256 internal _additionalQuoteTokensMinted; + // ============ Modifiers ============ // + modifier givenFullCapacity() { + _proceeds = 30e18; + _refund = 0; + _; + } + // ============ Assertions ============ // + function _assertQuoteTokenBalances() internal view { + assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "quote token: callback"); + assertEq(_quoteToken.balanceOf(address(_quoteToken)), 0, "quote token: contract"); + + uint256 proceedsAfterFees = _proceeds - _protocolFee - _referrerFee; + uint256 poolProceeds = proceedsAfterFees * _createData.poolPercent / 100e2; + assertEq( + _quoteToken.balanceOf(address(_baseToken.pool())), + poolProceeds + _additionalQuoteTokensMinted, + "quote token: pool" + ); + assertEq( + _quoteToken.balanceOf(_SELLER), proceedsAfterFees - poolProceeds, "quote token: seller" + ); + } + + function _assertBaseTokenBalances(uint256 curatorFee_) internal view { + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token: callback"); + assertEq(_baseToken.balanceOf(address(_baseToken)), 0, "base token: contract"); + + uint256 totalSupply = _baseToken.totalSupply(); + console2.log("totalSupply", totalSupply); + + // No payout distributed to "bidders", so don't account for it here + uint256 spotSupply = _LOT_CAPACITY - _refund; + console2.log("spotSupply", spotSupply); + + uint256 poolSupply = + totalSupply - spotSupply - curatorFee_ - _creditModule.totalCollateralized(); + assertEq(_baseToken.balanceOf(address(_baseToken.pool())), poolSupply, "base token: pool"); + assertEq(_baseToken.balanceOf(_SELLER), 0, "base token: seller"); + } + + function _assertCirculatingSupply(uint256 curatorFee_) internal view { + uint256 totalSupply = _baseToken.totalSupply(); + + assertApproxEqAbs( + totalSupply - _baseToken.getPosition(Range.FLOOR).bAssets + - _baseToken.getPosition(Range.ANCHOR).bAssets + - _baseToken.getPosition(Range.DISCOVERY).bAssets, + _LOT_CAPACITY - _refund + curatorFee_ + _creditModule.totalCollateralized(), + 2, // There is a difference (rounding error?) of 2 + "circulating supply" + ); + } + + function _assertAuctionComplete() internal view { + assertEq(_dtl.auctionComplete(), true, "auction completed"); + } + + function _assertPoolReserves() internal view { + uint256 poolProceeds = + (_proceeds - _protocolFee - _referrerFee) * _createData.poolPercent / 100e2; + uint256 floorProceeds = poolProceeds * _createData.floorReservesPercent / 100e2; + assertApproxEqAbs( + _getRangeReserves(Range.FLOOR), + floorProceeds, + 1, // There is a difference (rounding error?) of 1 + "reserves: floor" + ); + assertApproxEqAbs( + _getRangeReserves(Range.ANCHOR), + poolProceeds - floorProceeds, + 1, // There is a difference (rounding error?) of 1 + "reserves: anchor" + ); + assertEq(_getRangeReserves(Range.DISCOVERY), 0, "reserves: discovery"); + + // BAssets deployed into the pool + assertEq(_getRangeBAssets(Range.FLOOR), 0, "bAssets: floor"); + assertEq(_getRangeBAssets(Range.ANCHOR), 0, "bAssets: anchor"); + assertGt(_getRangeBAssets(Range.DISCOVERY), 0, "bAssets: discovery"); + } + // ============ Tests ============ // // [X] when the lot has not been registered // [X] it reverts // [X] when the caller is not the auction house // [X] it reverts - // [X] when the lot has already been settled - // [X] it reverts + // [X] given the onSettle callback has already been called + // [X] when onSettle is called + // [X] it reverts + // [X] when onCancel is called + // [X] it reverts + // [X] when onCurate is called + // [X] it reverts + // [X] when onCreate is called + // [X] it reverts // [X] when the lot has already been cancelled // [X] it reverts // [X] when insufficient proceeds are sent to the callback // [X] it reverts // [X] when insufficient refund is sent to the callback // [X] it reverts + // [X] given there is liquidity in the pool at a higher tick + // [X] it adjusts the pool price + // [X] given there is liquidity in the pool at a lower tick + // [X] it adjusts the pool price // [X] when the percent in floor reserves changes // [X] it adds reserves to the floor and anchor ranges in the correct proportions + // [X] given a curator fee has been paid + // [X] the solvency check passes + // [X] given there are credit account allocations + // [X] it includes the allocations in the solvency check + // [ ] given there is loop vault debt + // [ ] it includes the debt in the solvency check + // [X] given the allocation of proceeds to the pool is not 100% + // [X] it allocates the proceeds correctly + // [X] given the anchor range width is fuzzed + // [X] it allocates the proceeds correctly + // [X] given the active tick is fuzzed + // [X] it allocates the proceeds correctly + // [X] given the protocol fee is set + // [X] it correctly performs the solvency check + // [X] given the referrer fee is set + // [X] it correctly performs the solvency check // [X] it burns refunded base tokens, updates the circulating supply, marks the auction as completed and deploys the reserves into the Baseline pool function test_lotNotRegistered_reverts() @@ -69,23 +183,87 @@ contract BaselineOnSettleTest is BaselineAxisLaunchTest { ); } - function test_lotAlreadySettled_reverts() + function test_auctionCompleted_onCreate_reverts() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated givenOnCreate givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) - givenAddressHasBaseTokenBalance(_dtlAddress, _REFUND_AMOUNT) - givenOnSettle + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); + + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + // Perform callback again + _onCreate(); + } + + function test_auctionCompleted_onCurate_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Perform callback again + _onCurate(0); + } + + function test_auctionCompleted_onCancel_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) { + // Perform callback + _onSettle(); + // Expect revert bytes memory err = abi.encodeWithSelector(BaselineAxisLaunch.Callback_AlreadyComplete.selector); vm.expectRevert(err); + // Perform callback again + _onCancel(); + } + + function test_auctionCompleted_onSettle_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { // Perform callback _onSettle(); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaselineAxisLaunch.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Perform callback again + _onSettle(); } function test_lotAlreadyCancelled_reverts() @@ -145,235 +323,688 @@ contract BaselineOnSettleTest is BaselineAxisLaunchTest { givenAuctionIsCreated givenOnCreate givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) - givenAddressHasBaseTokenBalance(_dtlAddress, _REFUND_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) { // Perform callback _onSettle(); - // Assert quote token balances - assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "quote token: callback"); - assertEq(_quoteToken.balanceOf(address(_quoteToken)), 0, "quote token: contract"); - assertEq( - _quoteToken.balanceOf(address(_baseToken.pool())), _PROCEEDS_AMOUNT, "quote token: pool" - ); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); - // Assert base token balances - assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token: callback"); - assertEq(_baseToken.balanceOf(address(_baseToken)), 0, "base token: contract"); - assertEq(_baseToken.balanceOf(address(_baseToken.pool())), 0, "base token: pool"); // No liquidity in the anchor range, so no base token in the discovery range + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); + } - // Circulating supply - assertEq( - _dtl.initialCirculatingSupply(), _LOT_CAPACITY - _REFUND_AMOUNT, "circulating supply" - ); + function test_curatorFee_low() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorReservesPercent(30e2) // For the solvency check + givenPoolPercent(90e2) // For the solvency check + { + uint48 curatorFeePercent = 1e2; + _setCuratorFeePercent(curatorFeePercent); - // Auction marked as complete - assertEq(_dtl.auctionComplete(), true, "auction completed"); + // Perform the onCreate callback + _onCreate(); - // Reserves deployed into the pool - assertEq( - _baseToken.rangeReserves(Range.FLOOR), - _PROCEEDS_AMOUNT.mulDivDown(_FLOOR_RESERVES_PERCENT, _ONE_HUNDRED_PERCENT), - "reserves: floor" - ); - assertEq( - _baseToken.rangeReserves(Range.ANCHOR), - _PROCEEDS_AMOUNT.mulDivDown( - _ONE_HUNDRED_PERCENT - _FLOOR_RESERVES_PERCENT, _ONE_HUNDRED_PERCENT - ), - "reserves: anchor" - ); - assertEq(_baseToken.rangeReserves(Range.DISCOVERY), 0, "reserves: discovery"); + // Perform the onCurate callback + _onCurate(_curatorFee); + + // Mint tokens + _quoteToken.mint(_dtlAddress, _PROCEEDS_AMOUNT); + _transferBaseTokenRefund(_REFUND_AMOUNT); + + uint256 refundedCuratorFee = _REFUND_AMOUNT * _curatorFee / _LOT_CAPACITY; + _transferBaseTokenRefund(refundedCuratorFee); - // Liquidity - assertEq(_baseToken.rangeLiquidity(Range.FLOOR), 0, "liquidity: floor"); - assertEq(_baseToken.rangeLiquidity(Range.ANCHOR), 0, "liquidity: anchor"); - assertGt(_baseToken.rangeLiquidity(Range.DISCOVERY), 0, "liquidity: discovery"); + // Perform the onSettle callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(_curatorFee - refundedCuratorFee); + _assertCirculatingSupply(_curatorFee - refundedCuratorFee); + _assertAuctionComplete(); + _assertPoolReserves(); } - function test_floorReservesPercent_zero() + function test_curatorFee_middle() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated + givenFloorReservesPercent(60e2) // For the solvency check + givenPoolPercent(90e2) // For the solvency check { - uint24 floorReservesPercent = 0; + uint48 curatorFeePercent = 5e2; + _setCuratorFeePercent(curatorFeePercent); + + // Perform the onCreate callback + _onCreate(); - // Update the callback parameters - _createData.floorReservesPercent = floorReservesPercent; + // Perform the onCurate callback + _onCurate(_curatorFee); + + // Mint tokens + _quoteToken.mint(_dtlAddress, _PROCEEDS_AMOUNT); + _transferBaseTokenRefund(_REFUND_AMOUNT); - // Call onCreate + uint256 refundedCuratorFee = _REFUND_AMOUNT * _curatorFee / _LOT_CAPACITY; + _transferBaseTokenRefund(refundedCuratorFee); + + // Perform the onSettle callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(_curatorFee - refundedCuratorFee); + _assertCirculatingSupply(_curatorFee - refundedCuratorFee); + _assertAuctionComplete(); + _assertPoolReserves(); + } + + function test_curatorFee_high() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorReservesPercent(20e2) // For the solvency check + givenPoolPercent(99e2) // For the solvency check + { + uint48 curatorFeePercent = 10e2; + _setCuratorFeePercent(curatorFeePercent); + + // Perform the onCreate callback _onCreate(); + // Perform the onCurate callback + _onCurate(_curatorFee); + // Mint tokens _quoteToken.mint(_dtlAddress, _PROCEEDS_AMOUNT); - _baseToken.mint(_dtlAddress, _REFUND_AMOUNT); + _transferBaseTokenRefund(_REFUND_AMOUNT); + + uint256 refundedCuratorFee = _REFUND_AMOUNT * _curatorFee / _LOT_CAPACITY; + _transferBaseTokenRefund(refundedCuratorFee); + // Perform the onSettle callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(_curatorFee - refundedCuratorFee); + _assertCirculatingSupply(_curatorFee - refundedCuratorFee); + _assertAuctionComplete(); + _assertPoolReserves(); + } + + function test_givenCreditAllocations_low() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(92e2) // For the solvency check + givenCollateralized(_BORROWER, 1e18) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenBaseTokenRefundIsTransferred(_refund) + { // Perform callback _onSettle(); - // Assert quote token balances - assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "quote token: callback"); - assertEq(_quoteToken.balanceOf(address(_quoteToken)), 0, "quote token: contract"); - assertEq( - _quoteToken.balanceOf(address(_baseToken.pool())), _PROCEEDS_AMOUNT, "quote token: pool" - ); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Assert base token balances - assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token: callback"); - assertEq(_baseToken.balanceOf(address(_baseToken)), 0, "base token: contract"); - assertEq(_baseToken.balanceOf(address(_baseToken.pool())), 0, "base token: pool"); // No liquidity in the anchor range, so no base token in the discovery range + function test_givenCreditAllocations_low_givenFullCapacity() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(92e2) // For the solvency check + givenCollateralized(_BORROWER, 1e18) + givenOnCreate + givenFullCapacity + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenBaseTokenRefundIsTransferred(_refund) + { + // Perform callback + _onSettle(); - // Circulating supply - assertEq( - _dtl.initialCirculatingSupply(), _LOT_CAPACITY - _REFUND_AMOUNT, "circulating supply" + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } + + function test_givenCreditAllocations_middle_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(20) // For the solvency check + givenFloorReservesPercent(25e2) // For the solvency check + givenPoolPercent(99e2) // For the solvency check + givenCollateralized(_BORROWER, 5e18) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenBaseTokenRefundIsTransferred(_refund) + { + // Expect revert + // The solvency check fails due to the refund + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 979_448_372_805_591_283 ); - // Auction marked as complete - assertEq(_dtl.auctionComplete(), true, "auction completed"); + // Perform callback + _onSettle(err); + } - // Reserves deployed into the pool - assertEq( - _baseToken.rangeReserves(Range.FLOOR), - _PROCEEDS_AMOUNT.mulDivDown(floorReservesPercent, _ONE_HUNDRED_PERCENT), - "reserves: floor" - ); - assertEq( - _baseToken.rangeReserves(Range.ANCHOR), - _PROCEEDS_AMOUNT.mulDivDown( - _ONE_HUNDRED_PERCENT - floorReservesPercent, _ONE_HUNDRED_PERCENT - ), - "reserves: anchor" + function test_givenCreditAllocations_middle_givenFullCapacity() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(20) // For the solvency check + givenFloorReservesPercent(25e2) // For the solvency check + givenPoolPercent(99e2) // For the solvency check + givenCollateralized(_BORROWER, 5e18) + givenOnCreate + givenFullCapacity + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenBaseTokenRefundIsTransferred(_refund) + { + // Perform callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } + + function test_givenCreditAllocations_high_reverts() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(31) // For the solvency check + givenCollateralized(_BORROWER, 10e18) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenBaseTokenRefundIsTransferred(_refund) + { + // Expect revert + // The solvency check fails due to the refund + bytes memory err = abi.encodeWithSelector( + BaselineAxisLaunch.Callback_InvalidCapacityRatio.selector, 972_981_853_960_360_268 ); - assertEq(_baseToken.rangeReserves(Range.DISCOVERY), 0, "reserves: discovery"); - // Liquidity - assertEq(_baseToken.rangeLiquidity(Range.FLOOR), 0, "liquidity: floor"); - assertEq(_baseToken.rangeLiquidity(Range.ANCHOR), 0, "liquidity: anchor"); - assertGt(_baseToken.rangeLiquidity(Range.DISCOVERY), 0, "liquidity: discovery"); + // Perform callback + _onSettle(err); } - function test_floorReservesPercent_ninetyNinePercent() + function test_givenCreditAllocations_high_givenFullCapacity() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated + givenAnchorTickWidth(31) // For the solvency check + givenCollateralized(_BORROWER, 10e18) + givenOnCreate + givenFullCapacity + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenBaseTokenRefundIsTransferred(_refund) { - uint24 floorReservesPercent = _NINETY_NINE_PERCENT; + // Perform callback + _onSettle(); - // Update the callback parameters - _createData.floorReservesPercent = floorReservesPercent; + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Call onCreate - _onCreate(); + function test_floorReservesPercent_lowPercent() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(91e2) // For the solvency check + givenFloorReservesPercent(10e2) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); - // Mint tokens - _quoteToken.mint(_dtlAddress, _PROCEEDS_AMOUNT); - _baseToken.mint(_dtlAddress, _REFUND_AMOUNT); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } + function test_floorReservesPercent_highPercent() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(83e2) // For the solvency check + givenFloorReservesPercent(90e2) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { // Perform callback _onSettle(); - // Assert quote token balances - assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "quote token: callback"); - assertEq(_quoteToken.balanceOf(address(_quoteToken)), 0, "quote token: contract"); - assertEq( - _quoteToken.balanceOf(address(_baseToken.pool())), _PROCEEDS_AMOUNT, "quote token: pool" - ); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Assert base token balances - assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token: callback"); - assertEq(_baseToken.balanceOf(address(_baseToken)), 0, "base token: contract"); - assertEq(_baseToken.balanceOf(address(_baseToken.pool())), 0, "base token: pool"); // No liquidity in the anchor range, so no base token in the discovery range + function test_floorReservesPercent_middlePercent() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenFloorReservesPercent(50e2) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); - // Circulating supply - assertEq( - _dtl.initialCirculatingSupply(), _LOT_CAPACITY - _REFUND_AMOUNT, "circulating supply" - ); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Auction marked as complete - assertEq(_dtl.auctionComplete(), true, "auction completed"); + function test_poolPercent_lowPercent() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(10e2) + givenFloorRangeGap(137) // For the solvency check + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); - // Reserves deployed into the pool - assertEq( - _baseToken.rangeReserves(Range.FLOOR), - _PROCEEDS_AMOUNT.mulDivDown(floorReservesPercent, _ONE_HUNDRED_PERCENT), - "reserves: floor" - ); - assertEq( - _baseToken.rangeReserves(Range.ANCHOR), - _PROCEEDS_AMOUNT.mulDivDown( - _ONE_HUNDRED_PERCENT - floorReservesPercent, _ONE_HUNDRED_PERCENT - ), - "reserves: anchor" - ); - assertEq(_baseToken.rangeReserves(Range.DISCOVERY), 0, "reserves: discovery"); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Liquidity - assertEq(_baseToken.rangeLiquidity(Range.FLOOR), 0, "liquidity: floor"); - assertEq(_baseToken.rangeLiquidity(Range.ANCHOR), 0, "liquidity: anchor"); - assertGt(_baseToken.rangeLiquidity(Range.DISCOVERY), 0, "liquidity: discovery"); + function test_poolPercent_middlePercent() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(50e2) + givenFloorRangeGap(44) // For the solvency check + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); } - function test_floorReservesPercent_fuzz(uint24 floorReservesPercent_) + function test_poolPercent_highPercent() public givenBPoolIsCreated givenCallbackIsCreated givenAuctionIsCreated + givenPoolPercent(90e2) + givenFloorReservesPercent(10e2) // For the solvency check + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) { - uint24 floorReservesPercent = uint24(bound(floorReservesPercent_, 0, _NINETY_NINE_PERCENT)); + // Perform callback + _onSettle(); - // Update the callback parameters - _createData.floorReservesPercent = floorReservesPercent; + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Call onCreate - _onCreate(); + function test_anchorTickWidth_low() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(10) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); - // Mint tokens - _quoteToken.mint(_dtlAddress, _PROCEEDS_AMOUNT); - _baseToken.mint(_dtlAddress, _REFUND_AMOUNT); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } + function test_anchorTickWidth_middle() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(30) + givenPoolPercent(63e2) // For the solvency check + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { // Perform callback _onSettle(); - // Assert quote token balances - assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "quote token: callback"); - assertEq(_quoteToken.balanceOf(address(_quoteToken)), 0, "quote token: contract"); - assertEq( - _quoteToken.balanceOf(address(_baseToken.pool())), _PROCEEDS_AMOUNT, "quote token: pool" - ); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Assert base token balances - assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token: callback"); - assertEq(_baseToken.balanceOf(address(_baseToken)), 0, "base token: contract"); - assertEq(_baseToken.balanceOf(address(_baseToken.pool())), 0, "base token: pool"); // No liquidity in the anchor range, so no base token in the discovery range + function test_anchorTickWidth_high() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenAnchorTickWidth(50) + givenFloorReservesPercent(10e2) // For the solvency check + givenPoolPercent(58e2) // For the solvency check + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); - // Circulating supply - assertEq( - _dtl.initialCirculatingSupply(), _LOT_CAPACITY - _REFUND_AMOUNT, "circulating supply" - ); + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } - // Auction marked as complete - assertEq(_dtl.auctionComplete(), true, "auction completed"); + function uniswapV3MintCallback(uint256, uint256 amount1Owed, bytes calldata) external { + console2.log("Minting additional quote tokens", amount1Owed); + _additionalQuoteTokensMinted += amount1Owed; - // Reserves deployed into the pool - assertEq( - _baseToken.rangeReserves(Range.FLOOR), - _PROCEEDS_AMOUNT.mulDivDown(floorReservesPercent, _ONE_HUNDRED_PERCENT), - "reserves: floor" - ); - assertEq( - _baseToken.rangeReserves(Range.ANCHOR), - _PROCEEDS_AMOUNT.mulDivDown( - _ONE_HUNDRED_PERCENT - floorReservesPercent, _ONE_HUNDRED_PERCENT - ), - "reserves: anchor" - ); - assertEq(_baseToken.rangeReserves(Range.DISCOVERY), 0, "reserves: discovery"); + // Transfer the quote tokens + _quoteToken.mint(msg.sender, amount1Owed); + } + + function _mintPosition(int24 tickLower_, int24 tickUpper_) internal { + // Using PoC: https://github.com/GuardianAudits/axis-1/pull/4/files + IUniswapV3Pool pool = _baseToken.pool(); + + pool.mint(address(this), tickLower_, tickUpper_, 1e18, ""); + } + + function uniswapV3SwapCallback(int256, int256, bytes memory) external pure { + return; + } + + function _swap(uint160 sqrtPrice_) internal { + IUniswapV3Pool pool = _baseToken.pool(); + + pool.swap(address(this), true, 1, sqrtPrice_, ""); + } + + function test_existingReservesAtHigherPoolTick() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 10_986, "pool tick after mint"); // Original active tick + + // Swap at a tick higher than the anchor range + IUniswapV3Pool pool = _baseToken.pool(); + pool.swap(address(this), false, 1, TickMath.getSqrtRatioAtTick(60_000), ""); + + // Assert that the pool tick has moved higher + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 60_000, "pool tick after swap"); + + // Provide reserve tokens to the pool at a tick higher than the anchor range and lower than the new active tick + _mintPosition(12_000, 12_000 + _tickSpacing); + + // Perform callback + _onSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 11_000, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + // _assertCirculatingSupply(0); // Difficult to calculate the additional base tokens minted + _assertAuctionComplete(); + // _assertPoolReserves(); // Difficult to calculate the additional quote and base tokens into ranges + } + + function test_existingReservesAtHigherPoolTick_noLiquidity() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 10_986, "pool tick after mint"); // Original active tick + + // Swap at a tick higher than the anchor range + IUniswapV3Pool pool = _baseToken.pool(); + pool.swap(address(this), false, 1, TickMath.getSqrtRatioAtTick(60_000), ""); + + // Assert that the pool tick has moved higher + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 60_000, "pool tick after swap"); + + // Do not mint any liquidity above the anchor range + + // Perform callback + _onSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 11_000, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } + + function test_existingReservesAtLowerPoolTick() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Provide reserve tokens to the pool at a lower tick + _mintPosition(-60_000 - _tickSpacing, -60_000); + + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 10_986, "pool tick after mint"); // Original active tick + + // Swap + _swap(TickMath.getSqrtRatioAtTick(-60_000)); + + // Assert that the pool price has moved lower + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, -60_001, "pool tick after swap"); + + // Perform callback + _onSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 11_000, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } + + function test_existingReservesAtLowerPoolTick_noLiquidity() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Don't mint any liquidity + + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 10_986, "pool tick after mint"); // Original active tick + + // Swap + _swap(TickMath.getSqrtRatioAtTick(-60_000)); + + // Assert that the pool price has moved lower + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, -60_000, "pool tick after swap"); + + // Perform callback + _onSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _baseToken.pool().slot0(); + assertEq(poolTick, 11_000, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + } + + function test_givenProtocolFee() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenProtocolFeePercent(1e2) // 1% + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds - _protocolFee) + givenBaseTokenRefundIsTransferred(_refund) + { + // Perform callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); + } + + function test_givenReferrerFee() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenReferrerFeePercent(1e2) // 1% + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds - _referrerFee) + givenBaseTokenRefundIsTransferred(_refund) + { + // Perform callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); + + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); + } + + function test_givenProtocolFee_givenReferrerFee() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenProtocolFeePercent(5e1) // 0.5% + givenReferrerFeePercent(5e1) // 0.5% + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds - _protocolFee - _referrerFee) + givenBaseTokenRefundIsTransferred(_refund) + { + // Perform callback + _onSettle(); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(0); + _assertCirculatingSupply(0); + _assertAuctionComplete(); + _assertPoolReserves(); - // Liquidity - assertEq(_baseToken.rangeLiquidity(Range.FLOOR), 0, "liquidity: floor"); - assertEq(_baseToken.rangeLiquidity(Range.ANCHOR), 0, "liquidity: anchor"); - assertGt(_baseToken.rangeLiquidity(Range.DISCOVERY), 0, "liquidity: discovery"); + // Transfer lock should be enabled + assertEq(_baseToken.locked(), true, "transfer lock"); } } diff --git a/test/callbacks/liquidity/BaselineV2/slide.t.sol b/test/callbacks/liquidity/BaselineV2/slide.t.sol new file mode 100644 index 00000000..a4209fd5 --- /dev/null +++ b/test/callbacks/liquidity/BaselineV2/slide.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaselineAxisLaunchTest} from "./BaselineAxisLaunchTest.sol"; + +import {Range} from "@baseline/modules/BPOOL.v1.sol"; +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; + +import {console2} from "@forge-std-1.9.1/console2.sol"; + +contract BaselineOnSettleSlideTest is BaselineAxisLaunchTest { + function uniswapV3SwapCallback( + int256 amount0Delta_, + int256 amount1Delta_, + bytes memory + ) external { + console2.log("amount0Delta", amount0Delta_); + console2.log("amount1Delta", amount1Delta_); + + if (amount0Delta_ > 0) { + _baseToken.transfer(msg.sender, uint256(amount0Delta_)); + } + + if (amount1Delta_ > 0) { + _quoteToken.transfer(msg.sender, uint256(amount1Delta_)); + } + + return; + } + + function test_floorReservesPercent_lowPercent_slide() + public + givenFixedPrice(1e18) + givenAnchorUpperTick(200) + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(92e2) // For the solvency check + givenFloorReservesPercent(10e2) + givenAnchorTickWidth(10) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, 8e18) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + _proceeds = 8e18; + _refund = _REFUND_AMOUNT; + + // Perform callback + _onSettle(); + + // Report the range ticks + (int24 floorL, int24 floorU) = _baseToken.getTicks(Range.FLOOR); + (int24 anchorL, int24 anchorU) = _baseToken.getTicks(Range.ANCHOR); + (int24 discoveryL, int24 discoveryU) = _baseToken.getTicks(Range.DISCOVERY); + console2.log("floor lower", floorL); + console2.log("floor upper", floorU); + console2.log("anchor lower", anchorL); + console2.log("anchor upper", anchorU); + console2.log("discovery lower", discoveryL); + console2.log("discovery upper", discoveryU); + + // Check the tick + (, int24 poolTick,,,,,) = _baseToken.pool().slot0(); + console2.log("pool tick after settlement", poolTick); + + // Transfer base tokens from AuctionHouse to here (so we don't mess with solvency) + _transferBaseToken(address(this), 8e18); + + // This only works if: + // - there are circulating base tokens + // - the transfer lock is disabled + + // Swap base tokens to reduce the pool price + _disableTransferLock(); + _baseToken.pool().swap(_SELLER, true, 8e18, TickMath.MIN_SQRT_RATIO + 1, ""); + _enableTransferLock(); + + // Check the tick + (, poolTick,,,,,) = _baseToken.pool().slot0(); + console2.log("pool tick after swap", poolTick); + + // Attempt to slide + _disableTransferLock(); + _marketMaking.slide(); + _enableTransferLock(); + + // Check the tick + (, poolTick,,,,,) = _baseToken.pool().slot0(); + console2.log("pool tick after slide", poolTick); + + // Report the range ticks + (floorL, floorU) = _baseToken.getTicks(Range.FLOOR); + (anchorL, anchorU) = _baseToken.getTicks(Range.ANCHOR); + (discoveryL, discoveryU) = _baseToken.getTicks(Range.DISCOVERY); + console2.log("floor lower", floorL); + console2.log("floor upper", floorU); + console2.log("anchor lower", anchorL); + console2.log("anchor upper", anchorU); + console2.log("discovery lower", discoveryL); + console2.log("discovery upper", discoveryU); + } +} diff --git a/test/callbacks/liquidity/BaselineV2/sweep.t.sol b/test/callbacks/liquidity/BaselineV2/sweep.t.sol new file mode 100644 index 00000000..227e1025 --- /dev/null +++ b/test/callbacks/liquidity/BaselineV2/sweep.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {BaselineAxisLaunchTest} from "./BaselineAxisLaunchTest.sol"; + +import {Range} from "@baseline/modules/BPOOL.v1.sol"; +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; + +import {console2} from "@forge-std-1.9.1/console2.sol"; + +contract BaselineOnSettleSlideTest is BaselineAxisLaunchTest { + function uniswapV3SwapCallback( + int256 amount0Delta_, + int256 amount1Delta_, + bytes memory + ) external { + console2.log("amount0Delta", amount0Delta_); + console2.log("amount1Delta", amount1Delta_); + + if (amount0Delta_ > 0) { + _baseToken.transfer(msg.sender, uint256(amount0Delta_)); + } + + if (amount1Delta_ > 0) { + _quoteToken.transfer(msg.sender, uint256(amount1Delta_)); + } + + return; + } + + function test_floorReservesPercent_highPercent_sweep() + public + givenBPoolIsCreated + givenCallbackIsCreated + givenAuctionIsCreated + givenPoolPercent(83e2) // For the solvency check + givenFloorReservesPercent(90e2) + givenAnchorTickWidth(10) + givenOnCreate + givenAddressHasQuoteTokenBalance(_dtlAddress, _PROCEEDS_AMOUNT) + givenBaseTokenRefundIsTransferred(_REFUND_AMOUNT) + { + // Perform callback + _onSettle(); + + // Report the range ticks + (int24 floorL, int24 floorU) = _baseToken.getTicks(Range.FLOOR); + (int24 anchorL, int24 anchorU) = _baseToken.getTicks(Range.ANCHOR); + (int24 discoveryL, int24 discoveryU) = _baseToken.getTicks(Range.DISCOVERY); + console2.log("floor lower", floorL); + console2.log("floor upper", floorU); + console2.log("anchor lower", anchorL); + console2.log("anchor upper", anchorU); + console2.log("discovery lower", discoveryL); + console2.log("discovery upper", discoveryU); + + // Check the tick + (, int24 poolTick,,,,,) = _baseToken.pool().slot0(); + console2.log("pool tick after settlement", poolTick); + + // Mint quote tokens for the swap + _quoteToken.mint(address(this), 150e18); + + // Swap quote tokens to reduce the pool price + _disableTransferLock(); + _baseToken.pool().swap(_SELLER, false, 150e18, TickMath.MAX_SQRT_RATIO - 1, ""); + _enableTransferLock(); + + // Check the tick + (, poolTick,,,,,) = _baseToken.pool().slot0(); + console2.log("pool tick after swap", poolTick); + + // Attempt to sweep + assertEq(_marketMaking.canSweep(), true, "canSweep"); + + // Do the sweep + _disableTransferLock(); + _marketMaking.sweep(); + _enableTransferLock(); + + // Report the range ticks + (floorL, floorU) = _baseToken.getTicks(Range.FLOOR); + (anchorL, anchorU) = _baseToken.getTicks(Range.ANCHOR); + (discoveryL, discoveryU) = _baseToken.getTicks(Range.DISCOVERY); + console2.log("floor lower", floorL); + console2.log("floor upper", floorU); + console2.log("anchor lower", anchorL); + console2.log("anchor upper", anchorU); + console2.log("discovery lower", discoveryL); + console2.log("discovery upper", discoveryU); + } +} diff --git a/test/callbacks/liquidity/BaselineV2/withdrawReserves.t.sol b/test/callbacks/liquidity/BaselineV2/withdrawReserves.t.sol deleted file mode 100644 index a840d53b..00000000 --- a/test/callbacks/liquidity/BaselineV2/withdrawReserves.t.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -import {BaselineAxisLaunchTest} from "./BaselineAxisLaunchTest.sol"; - -contract BaselineWithdrawReservesTest is BaselineAxisLaunchTest { - // ============ Tests ============ // - - // [X] when the caller is not the owner - // [X] it reverts - // [X] when there are no reserves - // [X] it returns 0 - // [X] it transfers the reserves to the owner - - function test_notOwner_reverts() - public - givenBPoolIsCreated - givenCallbackIsCreated - givenAddressHasBaseTokenBalance(_dtlAddress, 1e18) - { - // Expect revert - vm.expectRevert("UNAUTHORIZED"); - - // Perform callback - vm.prank(_BUYER); - _dtl.withdrawReserves(); - } - - function test_noReserves_returnsZero() public givenBPoolIsCreated givenCallbackIsCreated { - // Perform callback - vm.prank(_OWNER); - uint256 reserves = _dtl.withdrawReserves(); - - // Assert reserves - assertEq(reserves, 0, "reserves withdrawn"); - } - - function test_success() - public - givenBPoolIsCreated - givenCallbackIsCreated - givenAddressHasQuoteTokenBalance(_dtlAddress, 1e18) - { - // Perform callback - vm.prank(_OWNER); - uint256 reserves = _dtl.withdrawReserves(); - - // Assert reserves - assertEq(reserves, 1e18, "reserves withdrawn"); - - // Assert quote token balances - assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "quote token: callback"); - assertEq(_quoteToken.balanceOf(_OWNER), 1e18, "quote token: this"); - } -} diff --git a/test/callbacks/liquidity/UniswapV2DTL/UniswapV2DTLTest.sol b/test/callbacks/liquidity/UniswapV2DTL/UniswapV2DTLTest.sol index 68208851..d734f148 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/UniswapV2DTLTest.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/UniswapV2DTLTest.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {IAuction} from "@axis-core-1.0.0/interfaces/modules/IAuction.sol"; -import {IAuctionHouse} from "@axis-core-1.0.0/interfaces/IAuctionHouse.sol"; -import {BatchAuctionHouse} from "@axis-core-1.0.0/BatchAuctionHouse.sol"; +import {IAuction} from "@axis-core-1.0.1/interfaces/modules/IAuction.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Factory.sol"; import {UniswapV2FactoryClone} from "../../../lib/uniswap-v2/UniswapV2FactoryClone.sol"; @@ -17,11 +17,11 @@ import {UniswapV2Router02} from "@uniswap-v2-periphery-1.0.1/UniswapV2Router02.s import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; import {UniswapV2DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV2DTL.sol"; -import {LinearVesting} from "@axis-core-1.0.0/modules/derivatives/LinearVesting.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; import {MockBatchAuctionModule} from - "@axis-core-1.0.0-test/modules/Auction/MockBatchAuctionModule.sol"; + "@axis-core-1.0.1-test/modules/Auction/MockBatchAuctionModule.sol"; -import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.0/modules/Keycode.sol"; +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.1/modules/Keycode.sol"; import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; @@ -32,14 +32,17 @@ import {console2} from "@forge-std-1.9.1/console2.sol"; abstract contract UniswapV2DirectToLiquidityTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for UniswapV2DirectToLiquidity; - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _NOT_SELLER = address(0x20); uint96 internal constant _LOT_CAPACITY = 10e18; + uint24 internal constant _MAX_SLIPPAGE = 1; // 0.01% uint48 internal constant _START = 1_000_000; + uint48 internal constant _DURATION = 1 days; + uint48 internal constant _AUCTION_START = _START + 1; + uint48 internal constant _AUCTION_CONCLUSION = _AUCTION_START + _DURATION; uint96 internal _lotId = 1; @@ -54,14 +57,22 @@ abstract contract UniswapV2DirectToLiquidityTest is Test, Permit2User, WithSalts MockERC20 internal _quoteToken; MockERC20 internal _baseToken; + uint96 internal _lotCapacity = _LOT_CAPACITY; + uint96 internal _proceeds; + uint96 internal _refund; + + // TODO consider setting floor of max slippage to 0.01% + // Inputs + UniswapV2DirectToLiquidity.UniswapV2OnCreateParams internal _uniswapV2CreateParams = + UniswapV2DirectToLiquidity.UniswapV2OnCreateParams({maxSlippage: uint24(_MAX_SLIPPAGE)}); BaseDirectToLiquidity.OnCreateParams internal _dtlCreateParams = BaseDirectToLiquidity .OnCreateParams({ - proceedsUtilisationPercent: 100e2, + poolPercent: 100e2, vestingStart: 0, vestingExpiry: 0, recipient: _SELLER, - implParams: abi.encode("") + implParams: abi.encode(_uniswapV2CreateParams) }); function setUp() public { @@ -158,11 +169,34 @@ abstract contract UniswapV2DirectToLiquidityTest is Test, Permit2User, WithSalts _; } - function _createLot(address seller_) internal returns (uint96 lotId) { + function _setMaxSlippage(uint24 maxSlippage_) internal { + _uniswapV2CreateParams.maxSlippage = maxSlippage_; + _dtlCreateParams.implParams = abi.encode(_uniswapV2CreateParams); + } + + modifier givenMaxSlippage(uint24 maxSlippage_) { + _setMaxSlippage(maxSlippage_); + _; + } + + modifier givenQuoteTokenDecimals(uint8 decimals_) { + _quoteToken = new MockERC20("Quote Token", "QT", decimals_); + _; + } + + modifier givenBaseTokenDecimals(uint8 decimals_) { + _baseToken = new MockERC20("Base Token", "BT", decimals_); + + // Scale the capacity + _lotCapacity = uint96(_LOT_CAPACITY * 10 ** decimals_ / 10 ** 18); + _; + } + + function _createLot(address seller_, bytes memory err_) internal returns (uint96 lotId) { // Mint and approve the capacity to the owner - _baseToken.mint(seller_, _LOT_CAPACITY); + _baseToken.mint(seller_, _lotCapacity); vm.prank(seller_); - _baseToken.approve(address(_auctionHouse), _LOT_CAPACITY); + _baseToken.approve(address(_auctionHouse), _lotCapacity); // Prep the lot arguments IAuctionHouse.RoutingParams memory routingParams = IAuctionHouse.RoutingParams({ @@ -179,23 +213,48 @@ abstract contract UniswapV2DirectToLiquidityTest is Test, Permit2User, WithSalts }); IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ - start: uint48(block.timestamp) + 1, - duration: 1 days, + start: _AUCTION_START, + duration: _DURATION, capacityInQuote: false, - capacity: _LOT_CAPACITY, + capacity: _lotCapacity, implParams: abi.encode("") }); + if (err_.length > 0) { + vm.expectRevert(err_); + } + // Create a new lot vm.prank(seller_); return _auctionHouse.auction(routingParams, auctionParams, ""); } + function _createLot(address seller_) internal returns (uint96 lotId) { + return _createLot(seller_, ""); + } + modifier givenOnCreate() { _lotId = _createLot(_SELLER); _; } + function _performOnCreate(address seller_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCreate( + _lotId, + seller_, + address(_baseToken), + address(_quoteToken), + _lotCapacity, + false, + abi.encode(_dtlCreateParams) + ); + } + + function _performOnCreate() internal { + _performOnCreate(_SELLER); + } + function _performOnCurate(uint96 curatorPayout_) internal { vm.prank(address(_auctionHouse)); _dtl.onCurate(_lotId, curatorPayout_, false, abi.encode("")); @@ -206,8 +265,30 @@ abstract contract UniswapV2DirectToLiquidityTest is Test, Permit2User, WithSalts _; } - modifier givenProceedsUtilisationPercent(uint24 percent_) { - _dtlCreateParams.proceedsUtilisationPercent = percent_; + function _performOnCancel(uint96 lotId_, uint256 refundAmount_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCancel(lotId_, refundAmount_, false, abi.encode("")); + } + + function _performOnCancel() internal { + _performOnCancel(_lotId, 0); + } + + function _performOnSettle(uint96 lotId_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onSettle(lotId_, _proceeds, _refund, abi.encode("")); + } + + function _performOnSettle() internal { + _performOnSettle(_lotId); + } + + function _setPoolPercent(uint24 percent_) internal { + _dtlCreateParams.poolPercent = percent_; + } + + modifier givenPoolPercent(uint24 percent_) { + _setPoolPercent(percent_); _; } @@ -228,16 +309,14 @@ abstract contract UniswapV2DirectToLiquidityTest is Test, Permit2User, WithSalts // ========== FUNCTIONS ========== // - function _getDTLConfiguration(uint96 lotId_) - internal - view - returns (BaseDirectToLiquidity.DTLConfiguration memory) - { + function _getDTLConfiguration( + uint96 lotId_ + ) internal view returns (BaseDirectToLiquidity.DTLConfiguration memory) { ( address recipient_, uint256 lotCapacity_, uint256 lotCuratorPayout_, - uint24 proceedsUtilisationPercent_, + uint24 poolPercent_, uint48 vestingStart_, uint48 vestingExpiry_, LinearVesting linearVestingModule_, @@ -249,7 +328,7 @@ abstract contract UniswapV2DirectToLiquidityTest is Test, Permit2User, WithSalts recipient: recipient_, lotCapacity: lotCapacity_, lotCuratorPayout: lotCuratorPayout_, - proceedsUtilisationPercent: proceedsUtilisationPercent_, + poolPercent: poolPercent_, vestingStart: vestingStart_, vestingExpiry: vestingExpiry_, linearVestingModule: linearVestingModule_, diff --git a/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol index c2ccd09e..8660a47f 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {UniswapV2DirectToLiquidityTest} from "./UniswapV2DTLTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; contract UniswapV2DirectToLiquidityOnCancelTest is UniswapV2DirectToLiquidityTest { @@ -11,13 +11,17 @@ contract UniswapV2DirectToLiquidityOnCancelTest is UniswapV2DirectToLiquidityTes // ============ Modifiers ============ // - function _performCallback(uint96 lotId_) internal { - vm.prank(address(_auctionHouse)); - _dtl.onCancel(lotId_, _REFUND_AMOUNT, false, abi.encode("")); - } - // ============ Tests ============ // + // [X] given the onCancel callback has already been called + // [X] when onSettle is called + // [X] it reverts + // [X] when onCancel is called + // [X] it reverts + // [X] when onCurate is called + // [X] it reverts + // [X] when onCreate is called + // [X] it reverts // [X] when the lot has not been registered // [X] it reverts // [X] when multiple lots are created @@ -30,12 +34,12 @@ contract UniswapV2DirectToLiquidityOnCancelTest is UniswapV2DirectToLiquidityTes vm.expectRevert(err); // Call the function - _performCallback(_lotId); + _performOnCancel(); } function test_success() public givenCallbackIsCreated givenOnCreate { // Call the function - _performCallback(_lotId); + _performOnCancel(); // Check the values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); @@ -52,7 +56,7 @@ contract UniswapV2DirectToLiquidityOnCancelTest is UniswapV2DirectToLiquidityTes // Create a second lot and cancel it uint96 lotIdTwo = _createLot(_NOT_SELLER); - _performCallback(lotIdTwo); + _performOnCancel(lotIdTwo, _REFUND_AMOUNT); // Check the values BaseDirectToLiquidity.DTLConfiguration memory configurationOne = @@ -68,4 +72,55 @@ contract UniswapV2DirectToLiquidityOnCancelTest is UniswapV2DirectToLiquidityTes assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); } + + function test_auctionCancelled_onCreate_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseCallback determines if the lot has already been registered + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_auctionCancelled_onCurate_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + _performOnCurate(0); + } + + function test_auctionCancelled_onCancel_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + _performOnCancel(); + } + + function test_auctionCancelled_onSettle_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + _performOnSettle(); + } } diff --git a/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol index d65cae54..20c1d62d 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol @@ -3,29 +3,13 @@ pragma solidity 0.8.19; import {UniswapV2DirectToLiquidityTest} from "./UniswapV2DTLTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {UniswapV2DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV2DTL.sol"; contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTest { // ============ Modifiers ============ // - function _performCallback(address seller_) internal { - vm.prank(address(_auctionHouse)); - _dtl.onCreate( - _lotId, - seller_, - address(_baseToken), - address(_quoteToken), - _LOT_CAPACITY, - false, - abi.encode(_dtlCreateParams) - ); - } - - function _performCallback() internal { - _performCallback(_SELLER); - } - // ============ Assertions ============ // function _expectTransferFrom() internal { @@ -42,7 +26,7 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes vm.expectRevert(err); } - function _assertBaseTokenBalances() internal { + function _assertBaseTokenBalances() internal view { assertEq(_baseToken.balanceOf(_SELLER), 0, "seller balance"); assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller balance"); assertEq(_baseToken.balanceOf(_dtlAddress), 0, "dtl balance"); @@ -60,8 +44,14 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes // [X] it reverts // [X] when the proceeds utilisation is greater than 100% // [X] it reverts - // [X] given uniswap v2 pool already exists + // [X] when the implParams is not the correct length + // [X] it reverts + // [X] when the max slippage is between 0 and 100% + // [X] it succeeds + // [X] when the max slippage is greater than 100% // [X] it reverts + // [X] given uniswap v2 pool already exists + // [X] it succeeds // [X] when the start and expiry timestamps are the same // [X] it reverts // [X] when the start timestamp is after the expiry timestamp @@ -73,6 +63,8 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes // [X] when the start timestamp and expiry timestamp are specified // [X] given the linear vesting module is not installed // [X] it reverts + // [X] given the vesting start timestamp is before the auction conclusion + // [X] it reverts // [X] it records the address of the linear vesting module // [X] when the recipient is the zero address // [X] it reverts @@ -113,152 +105,244 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes } function test_whenLotHasAlreadyBeenRegistered_reverts() public givenCallbackIsCreated { - _performCallback(); + _performOnCreate(); _expectInvalidParams(); - _performCallback(); + _performOnCreate(); } - function test_whenProceedsUtilisationIs0_reverts() - public - givenCallbackIsCreated - givenProceedsUtilisationPercent(0) - { + function test_poolPercent_whenBelowBounds_reverts( + uint24 poolPercent_ + ) public givenCallbackIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 0, 10e2 - 1)); + + // Set pool percent + _setPoolPercent(poolPercent); + // Expect revert bytes memory err = abi.encodeWithSelector( - BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 0, 1, 100e2 + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, + poolPercent, + 10e2, + 100e2 ); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); } - function test_whenProceedsUtilisationIsGreaterThan100Percent_reverts() - public - givenCallbackIsCreated - givenProceedsUtilisationPercent(100e2 + 1) - { + function test_poolPercent_whenAboveBounds_reverts( + uint24 poolPercent_ + ) public givenCallbackIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 100e2 + 1, type(uint24).max)); + + // Set pool percent + _setPoolPercent(poolPercent); + // Expect revert bytes memory err = abi.encodeWithSelector( - BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 100e2 + 1, 1, 100e2 + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, + poolPercent, + 10e2, + 100e2 ); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); } - function test_givenUniswapV2PoolAlreadyExists_reverts() public givenCallbackIsCreated { - // Create the pool - _uniV2Factory.createPair(address(_baseToken), address(_quoteToken)); + function test_poolPercent_fuzz(uint24 poolPercent_) public givenCallbackIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 10e2, 100e2)); + + _setPoolPercent(poolPercent); + + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.poolPercent, poolPercent, "poolPercent"); + } + + function test_paramsIncorrectLength_reverts() public givenCallbackIsCreated { + // Set the implParams to an incorrect length + _dtlCreateParams.implParams = abi.encode(uint256(10), uint256(10)); // Expect revert - bytes memory err = - abi.encodeWithSelector(BaseDirectToLiquidity.Callback_Params_PoolExists.selector); + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); + } + + function test_maxSlippageGreaterThan100Percent_reverts( + uint24 maxSlippage_ + ) public givenCallbackIsCreated { + uint24 maxSlippage = uint24(bound(maxSlippage_, 100e2 + 1, type(uint24).max)); + _setMaxSlippage(maxSlippage); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, maxSlippage, 0, 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_maxSlippage_fuzz(uint24 maxSlippage_) public givenCallbackIsCreated { + uint24 maxSlippage = uint24(bound(maxSlippage_, 0, 100e2)); + _setMaxSlippage(maxSlippage); + + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.implParams, _dtlCreateParams.implParams, "implParams"); + + UniswapV2DirectToLiquidity.UniswapV2OnCreateParams memory uniswapV2CreateParams = abi.decode( + _dtlCreateParams.implParams, (UniswapV2DirectToLiquidity.UniswapV2OnCreateParams) + ); + assertEq(uniswapV2CreateParams.maxSlippage, maxSlippage, "maxSlippage"); + } + + function test_givenUniswapV2PoolAlreadyExists() public givenCallbackIsCreated { + // Create the pool + _uniV2Factory.createPair(address(_baseToken), address(_quoteToken)); + + // Perform the callback + _performOnCreate(); + + // Assert that the callback was successful + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.active, true, "active"); } function test_whenStartAndExpiryTimestampsAreTheSame_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 1) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 1) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); } function test_whenStartTimestampIsAfterExpiryTimestamp_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 2) - givenVestingExpiry(_START + 1) + givenVestingStart(_AUCTION_CONCLUSION + 2) + givenVestingExpiry(_AUCTION_CONCLUSION + 1) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); } - function test_whenStartTimestampIsBeforeCurrentTimestamp_succeeds() + function test_whenStartTimestampIsBeforeCurrentTimestamp_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled givenVestingStart(_START - 1) givenVestingExpiry(_START + 1) { - _performCallback(); - - // Assert values - BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); - assertEq(configuration.vestingStart, _START - 1, "vestingStart"); - assertEq(configuration.vestingExpiry, _START + 1, "vestingExpiry"); - assertEq( - address(configuration.linearVestingModule), - address(_linearVesting), - "linearVestingModule" + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - // Assert balances - _assertBaseTokenBalances(); + _createLot(address(_SELLER), err); } function test_whenExpiryTimestampIsBeforeCurrentTimestamp_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 1) - givenVestingExpiry(_START - 1) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION - 1) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); } function test_whenVestingSpecified_givenLinearVestingModuleNotInstalled_reverts() public givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_LinearVestingModuleNotFound.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); + } + + function test_whenVestingSpecified_whenStartTimestampIsBeforeAuctionConclusion_reverts() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_AUCTION_CONCLUSION - 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector + ); + + _createLot(address(_SELLER), err); + } + + function test_whenVestingSpecified_whenVestingStartTimestampIsOnAuctionConclusion() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_AUCTION_CONCLUSION) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) + { + _lotId = _createLot(address(_SELLER)); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.vestingStart, _AUCTION_CONCLUSION, "vestingStart"); + assertEq(configuration.vestingExpiry, _AUCTION_CONCLUSION + 2, "vestingExpiry"); + assertEq( + address(configuration.linearVestingModule), + address(_linearVesting), + "linearVestingModule" + ); + + // Assert balances + _assertBaseTokenBalances(); } function test_whenVestingSpecified() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) { - _performCallback(); + _lotId = _createLot(address(_SELLER)); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); - assertEq(configuration.vestingStart, _START + 1, "vestingStart"); - assertEq(configuration.vestingExpiry, _START + 2, "vestingExpiry"); + assertEq(configuration.vestingStart, _AUCTION_CONCLUSION + 1, "vestingStart"); + assertEq(configuration.vestingExpiry, _AUCTION_CONCLUSION + 2, "vestingExpiry"); assertEq( address(configuration.linearVestingModule), address(_linearVesting), @@ -277,7 +361,7 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes abi.encodeWithSelector(BaseDirectToLiquidity.Callback_Params_InvalidAddress.selector); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); } function test_whenRecipientIsNotSeller_succeeds() @@ -285,7 +369,7 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes givenCallbackIsCreated whenRecipientIsNotSeller { - _performCallback(); + _performOnCreate(); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); @@ -296,53 +380,59 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes } function test_succeeds() public givenCallbackIsCreated { - _performCallback(); + _performOnCreate(); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); assertEq(configuration.recipient, _SELLER, "recipient"); assertEq(configuration.lotCapacity, _LOT_CAPACITY, "lotCapacity"); assertEq(configuration.lotCuratorPayout, 0, "lotCuratorPayout"); - assertEq( - configuration.proceedsUtilisationPercent, - _dtlCreateParams.proceedsUtilisationPercent, - "proceedsUtilisationPercent" - ); + assertEq(configuration.poolPercent, _dtlCreateParams.poolPercent, "poolPercent"); assertEq(configuration.vestingStart, 0, "vestingStart"); assertEq(configuration.vestingExpiry, 0, "vestingExpiry"); assertEq(address(configuration.linearVestingModule), address(0), "linearVestingModule"); assertEq(configuration.active, true, "active"); assertEq(configuration.implParams, _dtlCreateParams.implParams, "implParams"); + UniswapV2DirectToLiquidity.UniswapV2OnCreateParams memory uniswapV2CreateParams = abi.decode( + _dtlCreateParams.implParams, (UniswapV2DirectToLiquidity.UniswapV2OnCreateParams) + ); + assertEq( + uniswapV2CreateParams.maxSlippage, _uniswapV2CreateParams.maxSlippage, "maxSlippage" + ); + // Assert balances _assertBaseTokenBalances(); } function test_succeeds_multiple() public givenCallbackIsCreated { // Lot one - _performCallback(); + _performOnCreate(); // Lot two _dtlCreateParams.recipient = _NOT_SELLER; _lotId = 2; - _performCallback(_NOT_SELLER); + _performOnCreate(_NOT_SELLER); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); assertEq(configuration.recipient, _NOT_SELLER, "recipient"); assertEq(configuration.lotCapacity, _LOT_CAPACITY, "lotCapacity"); assertEq(configuration.lotCuratorPayout, 0, "lotCuratorPayout"); - assertEq( - configuration.proceedsUtilisationPercent, - _dtlCreateParams.proceedsUtilisationPercent, - "proceedsUtilisationPercent" - ); + assertEq(configuration.poolPercent, _dtlCreateParams.poolPercent, "poolPercent"); assertEq(configuration.vestingStart, 0, "vestingStart"); assertEq(configuration.vestingExpiry, 0, "vestingExpiry"); assertEq(address(configuration.linearVestingModule), address(0), "linearVestingModule"); assertEq(configuration.active, true, "active"); assertEq(configuration.implParams, _dtlCreateParams.implParams, "implParams"); + UniswapV2DirectToLiquidity.UniswapV2OnCreateParams memory uniswapV2CreateParams = abi.decode( + _dtlCreateParams.implParams, (UniswapV2DirectToLiquidity.UniswapV2OnCreateParams) + ); + assertEq( + uniswapV2CreateParams.maxSlippage, _uniswapV2CreateParams.maxSlippage, "maxSlippage" + ); + // Assert balances _assertBaseTokenBalances(); } diff --git a/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol index 7078fc5e..afd4549c 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {UniswapV2DirectToLiquidityTest} from "./UniswapV2DTLTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; contract UniswapV2DirectToLiquidityOnCurateTest is UniswapV2DirectToLiquidityTest { diff --git a/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol index f073c4a0..4928005c 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol @@ -11,25 +11,27 @@ import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Pair.sol"; // AuctionHouse -import {ILinearVesting} from "@axis-core-1.0.0/interfaces/modules/derivatives/ILinearVesting.sol"; +import {ILinearVesting} from "@axis-core-1.0.1/interfaces/modules/derivatives/ILinearVesting.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; -import {UniswapV2DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV2DTL.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; + +import {console2} from "@forge-std-1.9.1/console2.sol"; contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTest { uint96 internal constant _PROCEEDS = 20e18; uint96 internal constant _REFUND = 0; + uint96 internal constant _PROCEEDS_PRICE_LESS_THAN_ONE = 5e18; + /// @dev The minimum amount of liquidity retained in the pool uint256 internal constant _MINIMUM_LIQUIDITY = 10 ** 3; - uint96 internal _proceeds; - uint96 internal _refund; uint96 internal _capacityUtilised; uint96 internal _quoteTokensToDeposit; uint96 internal _baseTokensToDeposit; uint96 internal _curatorPayout; - - uint24 internal _maxSlippage = 1; // 0.01% + uint256 internal _auctionPrice; + uint256 internal _quoteTokensDonated; // ========== Internal functions ========== // @@ -54,7 +56,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes // ========== Assertions ========== // - function _assertLpTokenBalance() internal { + function _assertLpTokenBalance() internal view { // Get the pools deployed by the DTL callback IUniswapV2Pair pool = _getUniswapV2Pool(); @@ -87,6 +89,50 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes ); } + function _assertLpUnderlyingBalances() internal view { + // Get the pools deployed by the DTL callback + IUniswapV2Pair pool = _getUniswapV2Pool(); + address poolAddress = address(pool); + + // Check the underlying balances + assertGe( + _quoteToken.balanceOf(poolAddress), _quoteTokensToDeposit, "pair: quote token balance" + ); + assertApproxEqRel( + _baseToken.balanceOf(poolAddress), + _baseTokensToDeposit, + 1e14, // 0.01% + "pair: base token balance" + ); + + // Check that the reserves match + (uint256 reserve0, uint256 reserve1,) = pool.getReserves(); + bool quoteTokenIsToken0 = pool.token0() == address(_quoteToken); + assertGe( + quoteTokenIsToken0 ? reserve0 : reserve1, + _quoteTokensToDeposit, + "pair: quote token reserve" + ); + assertApproxEqRel( + quoteTokenIsToken0 ? reserve1 : reserve0, + _baseTokensToDeposit, + 1e14, // 0.01% + "pair: base token reserve" + ); + + // Assert the price of the pool + assertApproxEqRel( + FixedPointMathLib.mulDivDown( + _quoteToken.balanceOf(poolAddress), + 10 ** _baseToken.decimals(), + _baseToken.balanceOf(poolAddress) + ), + _auctionPrice, + 1e14, // 0.01% + "pair: price" + ); + } + function _assertVestingTokenBalance() internal { // Exit if not vesting if (_dtlCreateParams.vestingStart == 0) { @@ -122,15 +168,35 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes ); } - function _assertQuoteTokenBalance() internal { + function _assertQuoteTokenBalance() internal view { assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: quote token balance"); + + uint256 nonPoolProceeds = _proceeds + _quoteTokensDonated - _quoteTokensToDeposit; + assertApproxEqAbs( + _quoteToken.balanceOf(_NOT_SELLER), + _dtlCreateParams.recipient == _NOT_SELLER ? nonPoolProceeds : 0, + _dtlCreateParams.recipient == _NOT_SELLER + ? _uniswapV2CreateParams.maxSlippage * _quoteTokensToDeposit / 100e2 + : 0, + "not seller: quote token balance" + ); + assertApproxEqAbs( + _quoteToken.balanceOf(_SELLER), + _dtlCreateParams.recipient == _SELLER ? nonPoolProceeds : 0, + _dtlCreateParams.recipient == _SELLER + ? _uniswapV2CreateParams.maxSlippage * _quoteTokensToDeposit / 100e2 + : 0, + "seller: quote token balance" + ); } - function _assertBaseTokenBalance() internal { + function _assertBaseTokenBalance() internal view { assertEq(_baseToken.balanceOf(_dtlAddress), 0, "DTL: base token balance"); + + // TODO check the base token balance for the seller } - function _assertApprovals() internal { + function _assertApprovals() internal view { // Ensure there are no dangling approvals assertEq( _quoteToken.allowance(_dtlAddress, address(_uniV2Router)), @@ -144,20 +210,6 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes // ========== Modifiers ========== // - function _performCallback(uint96 lotId_) internal { - vm.prank(address(_auctionHouse)); - _dtl.onSettle( - lotId_, - _proceeds, - _refund, - abi.encode(UniswapV2DirectToLiquidity.OnSettleParams({maxSlippage: _maxSlippage})) - ); - } - - function _performCallback() internal { - _performCallback(_lotId); - } - function _createPool() internal returns (address) { return _uniV2Factory.createPair(address(_quoteToken), address(_baseToken)); } @@ -167,37 +219,49 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes _; } + function _syncPool() internal { + _getUniswapV2Pool().sync(); + } + + modifier givenPoolSync() { + _syncPool(); + _; + } + modifier setCallbackParameters(uint96 proceeds_, uint96 refund_) { - _proceeds = proceeds_; - _refund = refund_; + // Adjust for the decimals + _proceeds = uint96(proceeds_ * 10 ** _quoteToken.decimals() / 1e18); + _refund = uint96(refund_ * 10 ** _baseToken.decimals() / 1e18); // Calculate the capacity utilised // Any unspent curator payout is included in the refund // However, curator payouts are linear to the capacity utilised // Calculate the percent utilisation uint96 capacityUtilisationPercent = 100e2 - - uint96(FixedPointMathLib.mulDivDown(_refund, 100e2, _LOT_CAPACITY + _curatorPayout)); - _capacityUtilised = _LOT_CAPACITY * capacityUtilisationPercent / 100e2; + - uint96(FixedPointMathLib.mulDivDown(_refund, 100e2, _lotCapacity + _curatorPayout)); + _capacityUtilised = _lotCapacity * capacityUtilisationPercent / 100e2; - // The proceeds utilisation percent scales the quote tokens and base tokens linearly - _quoteTokensToDeposit = _proceeds * _dtlCreateParams.proceedsUtilisationPercent / 100e2; - _baseTokensToDeposit = - _capacityUtilised * _dtlCreateParams.proceedsUtilisationPercent / 100e2; + // The pool percent scales the quote tokens and base tokens linearly + _quoteTokensToDeposit = _proceeds * _dtlCreateParams.poolPercent / 100e2; + _baseTokensToDeposit = _capacityUtilised * _dtlCreateParams.poolPercent / 100e2; + + _auctionPrice = _proceeds * 10 ** _baseToken.decimals() / (_lotCapacity - _refund); + console2.log("Derived auction price is: ", _auctionPrice); _; } - modifier givenUnboundedProceedsUtilisationPercent(uint24 percent_) { + modifier givenUnboundedPoolPercent(uint24 percent_) { // Bound the percent - uint24 percent = uint24(bound(percent_, 1, 100e2)); + uint24 percent = uint24(bound(percent_, 10e2, 100e2)); // Set the value on the DTL - _dtlCreateParams.proceedsUtilisationPercent = percent; + _dtlCreateParams.poolPercent = percent; _; } modifier givenUnboundedOnCurate(uint96 curationPayout_) { // Bound the value - _curatorPayout = uint96(bound(curationPayout_, 1e17, _LOT_CAPACITY)); + _curatorPayout = uint96(bound(curationPayout_, 1e17, _lotCapacity)); // Call the onCurate callback _performOnCurate(_curatorPayout); @@ -210,69 +274,28 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes _; } - modifier givenMaxSlippage(uint24 maxSlippage_) { - _maxSlippage = maxSlippage_; - _; - } - - modifier givenPoolHasDepositLowerPrice() { - uint256 quoteTokensToDeposit = _quoteTokensToDeposit * 95 / 100; - uint256 baseTokensToDeposit = _baseTokensToDeposit; - - // Mint additional tokens - _quoteToken.mint(address(this), quoteTokensToDeposit); - _baseToken.mint(address(this), baseTokensToDeposit); - - // Approve spending - _quoteToken.approve(address(_uniV2Router), quoteTokensToDeposit); - _baseToken.approve(address(_uniV2Router), baseTokensToDeposit); - - // Deposit tokens into the pool - _uniV2Router.addLiquidity( - address(_quoteToken), - address(_baseToken), - quoteTokensToDeposit, - baseTokensToDeposit, - quoteTokensToDeposit, - baseTokensToDeposit, - address(this), - block.timestamp - ); - _; - } - - modifier givenPoolHasDepositHigherPrice() { - uint256 quoteTokensToDeposit = _quoteTokensToDeposit; - uint256 baseTokensToDeposit = _baseTokensToDeposit * 95 / 100; - - // Mint additional tokens - _quoteToken.mint(address(this), quoteTokensToDeposit); - _baseToken.mint(address(this), baseTokensToDeposit); - - // Approve spending - _quoteToken.approve(address(_uniV2Router), quoteTokensToDeposit); - _baseToken.approve(address(_uniV2Router), baseTokensToDeposit); - - // Deposit tokens into the pool - _uniV2Router.addLiquidity( - address(_quoteToken), - address(_baseToken), - quoteTokensToDeposit, - baseTokensToDeposit, - quoteTokensToDeposit, - baseTokensToDeposit, - address(this), - block.timestamp - ); - _; - } - // ========== Tests ========== // + // [X] given the onSettle callback has already been called + // [X] when onSettle is called + // [X] it reverts + // [X] when onCancel is called + // [X] it reverts + // [X] when onCurate is called + // [X] it reverts + // [X] when onCreate is called + // [X] it reverts // [X] given the pool is created // [X] it initializes the pool // [X] given the pool is created and initialized // [X] it succeeds + // [X] given the pool has quote tokens donated + // [X] given the auction price is 1 + // [X] it corrects the pool price + // [X] given the auction price is < 1 + // [X] it corrects the pool price + // [X] given the auction price is > 1 + // [X] it corrects the pool price // [X] given the proceeds utilisation percent is set // [X] it calculates the deposit amount correctly // [X] given curation is enabled @@ -305,7 +328,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -314,154 +337,725 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes _assertApprovals(); } - function test_givenProceedsUtilisationPercent_fuzz(uint24 percent_) + function test_givenDonation_givenAuctionPriceGreaterThanOne_fuzz( + uint256 donatedQuoteTokens_ + ) public givenCallbackIsCreated - givenUnboundedProceedsUtilisationPercent(percent_) givenOnCreate - setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreated + setCallbackParameters(_PROCEEDS, _REFUND) // Price is 2 givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + // Callback + _performOnSettle(); + + // Assertions _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); _assertVestingTokenBalance(); _assertQuoteTokenBalance(); _assertBaseTokenBalance(); _assertApprovals(); } - function test_givenCurationPayout_fuzz(uint96 curationPayout_) + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_fuzz( + uint256 donatedQuoteTokens_ + ) public givenCallbackIsCreated givenOnCreate - givenUnboundedOnCurate(curationPayout_) - setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreated + setCallbackParameters(_PROCEEDS, _REFUND) // Price is 2 givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); _assertVestingTokenBalance(); _assertQuoteTokenBalance(); _assertBaseTokenBalance(); _assertApprovals(); } - function test_givenProceedsUtilisationPercent_givenCurationPayout_fuzz( - uint24 percent_, - uint96 curationPayout_ + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_givenDifferentQuoteTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ ) public givenCallbackIsCreated - givenUnboundedProceedsUtilisationPercent(percent_) + givenQuoteTokenDecimals(17) givenOnCreate - givenUnboundedOnCurate(curationPayout_) - setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreated + setCallbackParameters(_PROCEEDS, _REFUND) // Price is 2 givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) - givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) - givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e17); + _quoteTokensDonated += donatedQuoteTokens; + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); _assertVestingTokenBalance(); _assertQuoteTokenBalance(); _assertBaseTokenBalance(); _assertApprovals(); } - function test_whenRefund_fuzz(uint96 refund_) + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_givenLowQuoteTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) public givenCallbackIsCreated + givenQuoteTokenDecimals(6) givenOnCreate - whenRefundIsBounded(refund_) - setCallbackParameters(_PROCEEDS, _refund) + givenPoolIsCreated + setCallbackParameters(_PROCEEDS, _REFUND) // Price is 2 givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) - givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) - givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 1e24); + _quoteTokensDonated += donatedQuoteTokens; + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); _assertVestingTokenBalance(); _assertQuoteTokenBalance(); _assertBaseTokenBalance(); _assertApprovals(); } - function test_givenPoolHasDepositWithLowerPrice_reverts() + function test_givenDonation_givenAuctionPriceGreaterThanOne_givenDifferentBaseTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) public givenCallbackIsCreated + givenBaseTokenDecimals(17) givenOnCreate - setCallbackParameters(_PROCEEDS, _REFUND) givenPoolIsCreated - givenPoolHasDepositLowerPrice + setCallbackParameters(_PROCEEDS, _REFUND) // Price is 2 givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) - givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) - givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_givenDifferentBaseTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenBaseTokenDecimals(17) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_PROCEEDS, _REFUND) // Price is 2 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_givenDecimalPrice_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenOnCreate + givenPoolIsCreated + setCallbackParameters(15e18, _REFUND) // 1.5e18 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_givenDecimalPrice_givenDifferentQuoteTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenQuoteTokenDecimals(17) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(15e18, _REFUND) // 1.5e17 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e17); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_givenDecimalPrice_givenLowQuoteTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenQuoteTokenDecimals(6) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(15e18, _REFUND) // 1.5e6 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 1e24); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceGreaterThanOne_givenDecimalPrice_givenDifferentBaseTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenBaseTokenDecimals(17) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(15e18, _REFUND) // 1.5e18 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenAuctionPriceOne_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_LOT_CAPACITY, 0) // Price = 1 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceOne_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_LOT_CAPACITY, 0) // Price = 1 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceOne_givenDifferentQuoteTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenQuoteTokenDecimals(17) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_LOT_CAPACITY, 0) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e17); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceOne_givenLowQuoteTokenDecimals_reverts() + public + givenCallbackIsCreated + givenQuoteTokenDecimals(6) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_LOT_CAPACITY, 0) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { + // This is a demonstration that a ridiculous quantity of quote tokens will cause + // the donation mitigation functionality to revert + uint256 donatedQuoteTokens = 1e24; + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + // Expect revert - vm.expectRevert("UniswapV2Router: INSUFFICIENT_A_AMOUNT"); + vm.expectRevert("UniswapV2: K"); - _performCallback(); + // Callback + _performOnSettle(); } - function test_givenPoolHasDepositWithLowerPrice_whenMaxSlippageIsSet() + function test_givenDonation_givenSync_givenAuctionPriceOne_givenDifferentBaseTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) public givenCallbackIsCreated + givenBaseTokenDecimals(17) givenOnCreate - setCallbackParameters(_PROCEEDS, _REFUND) givenPoolIsCreated - givenPoolHasDepositLowerPrice + setCallbackParameters(_LOT_CAPACITY, 0) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) - givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) - givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) - givenMaxSlippage(500) // 5% + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e17); + _quoteTokensDonated += donatedQuoteTokens; + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); _assertVestingTokenBalance(); _assertQuoteTokenBalance(); _assertBaseTokenBalance(); _assertApprovals(); } - function test_givenPoolHasDepositWithHigherPrice_reverts() + function test_givenDonation_givenAuctionPriceLessThanOne_fuzz( + uint256 donatedQuoteTokens_ + ) public givenCallbackIsCreated givenOnCreate - setCallbackParameters(_PROCEEDS, _REFUND) givenPoolIsCreated - givenPoolHasDepositHigherPrice + setCallbackParameters(_PROCEEDS_PRICE_LESS_THAN_ONE, 0) // Price = 0.5 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceLessThanOne_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_PROCEEDS_PRICE_LESS_THAN_ONE, 0) // Price = 0.5 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceLessThanOne_givenDifferentQuoteTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenQuoteTokenDecimals(17) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_PROCEEDS_PRICE_LESS_THAN_ONE, 0) // Price = 0.5 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e17); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenDonation_givenSync_givenAuctionPriceLessThanOne_givenDifferentBaseTokenDecimals_fuzz( + uint256 donatedQuoteTokens_ + ) + public + givenCallbackIsCreated + givenBaseTokenDecimals(17) + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_PROCEEDS_PRICE_LESS_THAN_ONE, 0) // Price = 0.5 + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Donation amount could be more or less than the auction price + uint256 donatedQuoteTokens = bound(donatedQuoteTokens_, 1, 3e18); + _quoteTokensDonated += donatedQuoteTokens; + + // Donate to the pool + _quoteToken.mint(address(_getUniswapV2Pool()), donatedQuoteTokens); + + // Sync + _syncPool(); + + // Callback + _performOnSettle(); + + // Assertions + _assertLpTokenBalance(); + _assertLpUnderlyingBalances(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenPoolPercent_fuzz( + uint24 percent_ + ) + public + givenCallbackIsCreated + givenUnboundedPoolPercent(percent_) + whenRecipientIsNotSeller + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenCurationPayout_fuzz( + uint96 curationPayout_ + ) + public + givenCallbackIsCreated + givenOnCreate + givenUnboundedOnCurate(curationPayout_) + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenPoolPercent_givenCurationPayout_fuzz( + uint24 percent_, + uint96 curationPayout_ + ) + public + givenCallbackIsCreated + givenUnboundedPoolPercent(percent_) + givenOnCreate + givenUnboundedOnCurate(curationPayout_) + setCallbackParameters(_PROCEEDS, _REFUND) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - // Expect revert - vm.expectRevert("UniswapV2Router: INSUFFICIENT_B_AMOUNT"); + _performOnSettle(); - _performCallback(); + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); } - function test_givenPoolHasDepositWithHigherPrice_whenMaxSlippageIsSet() + function test_whenRefund_fuzz( + uint96 refund_ + ) public givenCallbackIsCreated givenOnCreate - setCallbackParameters(_PROCEEDS, _REFUND) - givenPoolIsCreated - givenPoolHasDepositHigherPrice + whenRefundIsBounded(refund_) + setCallbackParameters(_PROCEEDS, _refund) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) - givenMaxSlippage(500) // 5% { - _performCallback(); + _performOnSettle(); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -474,15 +1068,15 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes public givenLinearVestingModuleIsInstalled givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -495,8 +1089,8 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes public givenLinearVestingModuleIsInstalled givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) whenRecipientIsNotSeller givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) @@ -504,7 +1098,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -517,21 +1111,25 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes public givenLinearVestingModuleIsInstalled givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); // Warp to the end of the vesting period - vm.warp(_START + 3); + vm.warp(_AUCTION_CONCLUSION + 3); - // Redeem the vesting tokens + // Check that there is a vested token balance uint256 tokenId = _getVestingTokenId(); + uint256 redeemable = _linearVesting.redeemable(_SELLER, tokenId); + assertGt(redeemable, 0, "redeemable"); + + // Redeem the vesting tokens vm.prank(_SELLER); _linearVesting.redeemMax(tokenId); @@ -554,7 +1152,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); // Get the pools deployed by the DTL callback IUniswapV2Pair pool = _getUniswapV2Pool(); @@ -615,7 +1213,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes ); vm.expectRevert(err); - _performCallback(); + _performOnSettle(); } function test_givenInsufficientBaseTokenAllowance_reverts() @@ -630,7 +1228,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes // Expect revert vm.expectRevert("TRANSFER_FROM_FAILED"); - _performCallback(); + _performOnSettle(); } function test_success() @@ -642,7 +1240,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -663,7 +1261,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes // Create second lot uint96 lotIdTwo = _createLot(_NOT_SELLER); - _performCallback(lotIdTwo); + _performOnSettle(lotIdTwo); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -682,7 +1280,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -690,4 +1288,87 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes _assertBaseTokenBalance(); _assertApprovals(); } + + function test_auctionCompleted_onCreate_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseCallback determines if the lot has already been registered + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + // Try to call onCreate again + _performOnCreate(); + } + + function test_auctionCompleted_onCurate_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Try to call onCurate + _performOnCurate(_curatorPayout); + } + + function test_auctionCompleted_onCancel_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Try to call onCancel + _performOnCancel(); + } + + function test_auctionCompleted_onSettle_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Try to call onSettle + _performOnSettle(); + } } diff --git a/test/callbacks/liquidity/UniswapV3DTL/UniswapV3DTLTest.sol b/test/callbacks/liquidity/UniswapV3DTL/UniswapV3DTLTest.sol index 2c811c13..f5aaef0b 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/UniswapV3DTLTest.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/UniswapV3DTLTest.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.19; import {Test} from "@forge-std-1.9.1/Test.sol"; -import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; -import {Permit2User} from "@axis-core-1.0.0-test/lib/permit2/Permit2User.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; -import {IAuction} from "@axis-core-1.0.0/interfaces/modules/IAuction.sol"; -import {IAuctionHouse} from "@axis-core-1.0.0/interfaces/IAuctionHouse.sol"; -import {BatchAuctionHouse} from "@axis-core-1.0.0/BatchAuctionHouse.sol"; +import {IAuction} from "@axis-core-1.0.1/interfaces/modules/IAuction.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; import {GUniFactory} from "@g-uni-v1-core-0.9.9/GUniFactory.sol"; import {GUniPool} from "@g-uni-v1-core-0.9.9/GUniPool.sol"; @@ -18,11 +18,11 @@ import {UniswapV3Factory} from "../../../lib/uniswap-v3/UniswapV3Factory.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; import {UniswapV3DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV3DTL.sol"; -import {LinearVesting} from "@axis-core-1.0.0/modules/derivatives/LinearVesting.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; import {MockBatchAuctionModule} from - "@axis-core-1.0.0-test/modules/Auction/MockBatchAuctionModule.sol"; + "@axis-core-1.0.1-test/modules/Auction/MockBatchAuctionModule.sol"; -import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.0/modules/Keycode.sol"; +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.1/modules/Keycode.sol"; import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; @@ -30,10 +30,11 @@ import {WithSalts} from "../../../lib/WithSalts.sol"; import {console2} from "@forge-std-1.9.1/console2.sol"; import {TestConstants} from "../../../Constants.sol"; +// solhint-disable max-states-count + abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts, TestConstants { using Callbacks for UniswapV3DirectToLiquidity; - address internal constant _SELLER = address(0x2); address internal constant _PROTOCOL = address(0x3); address internal constant _BUYER = address(0x4); address internal constant _NOT_SELLER = address(0x20); @@ -41,6 +42,9 @@ abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts uint96 internal constant _LOT_CAPACITY = 10e18; uint48 internal constant _START = 1_000_000; + uint48 internal constant _DURATION = 1 days; + uint48 internal constant _AUCTION_START = _START + 1; + uint48 internal constant _AUCTION_CONCLUSION = _AUCTION_START + _DURATION; uint96 internal _lotId = 1; @@ -55,15 +59,24 @@ abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts MockERC20 internal _quoteToken; MockERC20 internal _baseToken; + uint96 internal _proceeds; + uint96 internal _refund; + uint24 internal _maxSlippage = 1; // 0.01% + // Inputs uint24 internal _poolFee = 500; + UniswapV3DirectToLiquidity.UniswapV3OnCreateParams internal _uniswapV3CreateParams = + UniswapV3DirectToLiquidity.UniswapV3OnCreateParams({ + poolFee: _poolFee, + maxSlippage: 1 // 0.01%, to handle rounding errors + }); BaseDirectToLiquidity.OnCreateParams internal _dtlCreateParams = BaseDirectToLiquidity .OnCreateParams({ - proceedsUtilisationPercent: 100e2, + poolPercent: 100e2, vestingStart: 0, vestingExpiry: 0, recipient: _SELLER, - implParams: abi.encode(_poolFee) + implParams: abi.encode(_uniswapV3CreateParams) }); function setUp() public { @@ -167,7 +180,7 @@ abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts _; } - function _createLot(address seller_) internal returns (uint96 lotId) { + function _createLot(address seller_, bytes memory err_) internal returns (uint96 lotId) { // Mint and approve the capacity to the owner _baseToken.mint(seller_, _LOT_CAPACITY); vm.prank(seller_); @@ -188,23 +201,48 @@ abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts }); IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ - start: uint48(block.timestamp) + 1, - duration: 1 days, + start: _AUCTION_START, + duration: _DURATION, capacityInQuote: false, capacity: _LOT_CAPACITY, implParams: abi.encode("") }); + if (err_.length > 0) { + vm.expectRevert(err_); + } + // Create a new lot vm.prank(seller_); return _auctionHouse.auction(routingParams, auctionParams, ""); } + function _createLot(address seller_) internal returns (uint96 lotId) { + return _createLot(seller_, ""); + } + modifier givenOnCreate() { _lotId = _createLot(_SELLER); _; } + function _performOnCreate(address seller_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCreate( + _lotId, + seller_, + address(_baseToken), + address(_quoteToken), + _LOT_CAPACITY, + false, + abi.encode(_dtlCreateParams) + ); + } + + function _performOnCreate() internal { + _performOnCreate(_SELLER); + } + function _performOnCurate(uint96 curatorPayout_) internal { vm.prank(address(_auctionHouse)); _dtl.onCurate(_lotId, curatorPayout_, false, abi.encode("")); @@ -215,14 +253,50 @@ abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts _; } - modifier givenProceedsUtilisationPercent(uint24 percent_) { - _dtlCreateParams.proceedsUtilisationPercent = percent_; + function _performOnCancel(uint96 lotId_, uint256 refundAmount_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCancel(lotId_, refundAmount_, false, abi.encode("")); + } + + function _performOnCancel() internal { + _performOnCancel(_lotId, 0); + } + + function _performOnSettle(uint96 lotId_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onSettle(lotId_, _proceeds, _refund, abi.encode("")); + } + + function _performOnSettle() internal { + _performOnSettle(_lotId); + } + + function _setPoolPercent(uint24 percent_) internal { + _dtlCreateParams.poolPercent = percent_; + } + + modifier givenPoolPercent(uint24 percent_) { + _setPoolPercent(percent_); _; } modifier givenPoolFee(uint24 fee_) { - _poolFee = fee_; - _dtlCreateParams.implParams = abi.encode(_poolFee); + _uniswapV3CreateParams.poolFee = fee_; + + // Update the callback data + _dtlCreateParams.implParams = abi.encode(_uniswapV3CreateParams); + _; + } + + function _setMaxSlippage(uint24 maxSlippage_) internal { + _uniswapV3CreateParams.maxSlippage = maxSlippage_; + + // Update the callback data + _dtlCreateParams.implParams = abi.encode(_uniswapV3CreateParams); + } + + modifier givenMaxSlippage(uint24 maxSlippage_) { + _setMaxSlippage(maxSlippage_); _; } @@ -243,16 +317,14 @@ abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts // ========== FUNCTIONS ========== // - function _getDTLConfiguration(uint96 lotId_) - internal - view - returns (BaseDirectToLiquidity.DTLConfiguration memory) - { + function _getDTLConfiguration( + uint96 lotId_ + ) internal view returns (BaseDirectToLiquidity.DTLConfiguration memory) { ( address recipient_, uint256 lotCapacity_, uint256 lotCuratorPayout_, - uint24 proceedsUtilisationPercent_, + uint24 poolPercent_, uint48 vestingStart_, uint48 vestingExpiry_, LinearVesting linearVestingModule_, @@ -264,7 +336,7 @@ abstract contract UniswapV3DirectToLiquidityTest is Test, Permit2User, WithSalts recipient: recipient_, lotCapacity: lotCapacity_, lotCuratorPayout: lotCuratorPayout_, - proceedsUtilisationPercent: proceedsUtilisationPercent_, + poolPercent: poolPercent_, vestingStart: vestingStart_, vestingExpiry: vestingExpiry_, linearVestingModule: linearVestingModule_, diff --git a/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol index 1383af27..ccaa7e35 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {UniswapV3DirectToLiquidityTest} from "./UniswapV3DTLTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; contract UniswapV3DirectToLiquidityOnCancelTest is UniswapV3DirectToLiquidityTest { @@ -11,13 +11,17 @@ contract UniswapV3DirectToLiquidityOnCancelTest is UniswapV3DirectToLiquidityTes // ============ Modifiers ============ // - function _performCallback(uint96 lotId_) internal { - vm.prank(address(_auctionHouse)); - _dtl.onCancel(lotId_, _REFUND_AMOUNT, false, abi.encode("")); - } - // ============ Tests ============ // + // [X] given the onCancel callback has already been called + // [X] when onSettle is called + // [X] it reverts + // [X] when onCancel is called + // [X] it reverts + // [X] when onCurate is called + // [X] it reverts + // [X] when onCreate is called + // [X] it reverts // [X] when the lot has not been registered // [X] it reverts // [X] when multiple lots are created @@ -30,12 +34,12 @@ contract UniswapV3DirectToLiquidityOnCancelTest is UniswapV3DirectToLiquidityTes vm.expectRevert(err); // Call the function - _performCallback(_lotId); + _performOnCancel(); } function test_success() public givenCallbackIsCreated givenOnCreate { // Call the function - _performCallback(_lotId); + _performOnCancel(); // Check the values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); @@ -52,7 +56,7 @@ contract UniswapV3DirectToLiquidityOnCancelTest is UniswapV3DirectToLiquidityTes // Create a second lot and cancel it uint96 lotIdTwo = _createLot(_NOT_SELLER); - _performCallback(lotIdTwo); + _performOnCancel(lotIdTwo, _REFUND_AMOUNT); // Check the values BaseDirectToLiquidity.DTLConfiguration memory configurationOne = @@ -68,4 +72,55 @@ contract UniswapV3DirectToLiquidityOnCancelTest is UniswapV3DirectToLiquidityTes assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); } + + function test_auctionCancelled_onCreate_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseCallback determines if the lot has already been registered + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_auctionCancelled_onCurate_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + _performOnCurate(0); + } + + function test_auctionCancelled_onCancel_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + _performOnCancel(); + } + + function test_auctionCancelled_onSettle_reverts() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + _performOnSettle(); + } } diff --git a/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol index 15122668..a5657483 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol @@ -3,30 +3,13 @@ pragma solidity 0.8.19; import {UniswapV3DirectToLiquidityTest} from "./UniswapV3DTLTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; import {UniswapV3DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV3DTL.sol"; contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTest { // ============ Modifiers ============ // - function _performCallback(address seller_) internal { - vm.prank(address(_auctionHouse)); - _dtl.onCreate( - _lotId, - seller_, - address(_baseToken), - address(_quoteToken), - _LOT_CAPACITY, - false, - abi.encode(_dtlCreateParams) - ); - } - - function _performCallback() internal { - _performCallback(_SELLER); - } - // ============ Assertions ============ // function _expectTransferFrom() internal { @@ -43,7 +26,7 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes vm.expectRevert(err); } - function _assertBaseTokenBalances() internal { + function _assertBaseTokenBalances() internal view { assertEq(_baseToken.balanceOf(_SELLER), 0, "seller balance"); assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller balance"); assertEq(_baseToken.balanceOf(_dtlAddress), 0, "dtl balance"); @@ -61,10 +44,16 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes // [X] it reverts // [X] when the proceeds utilisation is greater than 100% // [X] it reverts + // [X] when the implParams is not the correct length + // [X] it reverts + // [X] when the max slippage is between 0 and 100% + // [X] it succeeds + // [X] when the max slippage is greater than 100% + // [X] it reverts // [X] given the pool fee is not enabled // [X] it reverts // [X] given uniswap v3 pool already exists - // [X] it reverts + // [X] it succeeds // [X] when the start and expiry timestamps are the same // [X] it reverts // [X] when the start timestamp is after the expiry timestamp @@ -76,6 +65,8 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes // [X] when the start timestamp and expiry timestamp are specified // [X] given the linear vesting module is not installed // [X] it reverts + // [X] given the vesting start timestamp is before the auction conclusion + // [X] it reverts // [X] it records the address of the linear vesting module // [X] when the recipient is the zero address // [X] it reverts @@ -116,39 +107,89 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes } function test_whenLotHasAlreadyBeenRegistered_reverts() public givenCallbackIsCreated { - _performCallback(); + _performOnCreate(); _expectInvalidParams(); - _performCallback(); + _performOnCreate(); } - function test_whenProceedsUtilisationIs0_reverts() - public - givenCallbackIsCreated - givenProceedsUtilisationPercent(0) - { + function test_poolPercent_whenBelowBounds_reverts( + uint24 poolPercent_ + ) public givenCallbackIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 0, 10e2 - 1)); + + // Set pool percent + _setPoolPercent(poolPercent); + // Expect revert bytes memory err = abi.encodeWithSelector( - BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 0, 1, 100e2 + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, + poolPercent, + 10e2, + 100e2 ); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); } - function test_whenProceedsUtilisationIsGreaterThan100Percent_reverts() - public - givenCallbackIsCreated - givenProceedsUtilisationPercent(100e2 + 1) - { + function test_poolPercent_whenAboveBounds_reverts( + uint24 poolPercent_ + ) public givenCallbackIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 100e2 + 1, type(uint24).max)); + + // Set pool percent + _setPoolPercent(poolPercent); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, + poolPercent, + 10e2, + 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_poolPercent_fuzz(uint24 poolPercent_) public givenCallbackIsCreated { + uint24 poolPercent = uint24(bound(poolPercent_, 10e2, 100e2)); + + _setPoolPercent(poolPercent); + + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.poolPercent, poolPercent, "poolPercent"); + } + + function test_paramsIncorrectLength_reverts() public givenCallbackIsCreated { + // Set the implParams to an incorrect length + _dtlCreateParams.implParams = abi.encode(uint256(10)); + + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_maxSlippageGreaterThan100Percent_reverts( + uint24 maxSlippage_ + ) public givenCallbackIsCreated { + uint24 maxSlippage = uint24(bound(maxSlippage_, 100e2 + 1, type(uint24).max)); + _setMaxSlippage(maxSlippage); + // Expect revert bytes memory err = abi.encodeWithSelector( - BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 100e2 + 1, 1, 100e2 + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, maxSlippage, 0, 100e2 ); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); } function test_givenPoolFeeIsNotEnabled_reverts() @@ -162,10 +203,10 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes ); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); } - function test_givenUniswapV3PoolAlreadyExists_reverts() + function test_givenUniswapV3PoolAlreadyExists() public givenCallbackIsCreated givenPoolFee(500) @@ -173,113 +214,139 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes // Create the pool _uniV3Factory.createPool(address(_baseToken), address(_quoteToken), 500); - // Expect revert - bytes memory err = - abi.encodeWithSelector(BaseDirectToLiquidity.Callback_Params_PoolExists.selector); - vm.expectRevert(err); + // Perform the callback + _performOnCreate(); - _performCallback(); + // Assert that the callback was successful + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.active, true, "active"); } function test_whenStartAndExpiryTimestampsAreTheSame_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 1) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 1) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); } function test_whenStartTimestampIsAfterExpiryTimestamp_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 2) - givenVestingExpiry(_START + 1) + givenVestingStart(_AUCTION_CONCLUSION + 2) + givenVestingExpiry(_AUCTION_CONCLUSION + 1) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); } - function test_whenStartTimestampIsBeforeCurrentTimestamp_succeeds() + function test_whenStartTimestampIsBeforeCurrentTimestamp_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled givenVestingStart(_START - 1) givenVestingExpiry(_START + 1) { - _performCallback(); - - // Assert values - BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); - assertEq(configuration.vestingStart, _START - 1, "vestingStart"); - assertEq(configuration.vestingExpiry, _START + 1, "vestingExpiry"); - assertEq( - address(configuration.linearVestingModule), - address(_linearVesting), - "linearVestingModule" + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - // Assert balances - _assertBaseTokenBalances(); + _createLot(address(_SELLER), err); } function test_whenExpiryTimestampIsBeforeCurrentTimestamp_reverts() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 1) - givenVestingExpiry(_START - 1) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION - 1) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); } function test_whenVestingSpecified_givenLinearVestingModuleNotInstalled_reverts() public givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) { // Expect revert bytes memory err = abi.encodeWithSelector( BaseDirectToLiquidity.Callback_LinearVestingModuleNotFound.selector ); - vm.expectRevert(err); - _performCallback(); + _createLot(address(_SELLER), err); + } + + function test_whenVestingSpecified_whenStartTimestampIsBeforeAuctionConclusion_reverts() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_AUCTION_CONCLUSION - 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector + ); + + _createLot(address(_SELLER), err); + } + + function test_whenVestingSpecified_whenVestingStartTimestampIsOnAuctionConclusion() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_AUCTION_CONCLUSION) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) + { + _lotId = _createLot(address(_SELLER)); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.vestingStart, _AUCTION_CONCLUSION, "vestingStart"); + assertEq(configuration.vestingExpiry, _AUCTION_CONCLUSION + 2, "vestingExpiry"); + assertEq( + address(configuration.linearVestingModule), + address(_linearVesting), + "linearVestingModule" + ); + + // Assert balances + _assertBaseTokenBalances(); } function test_whenVestingSpecified() public givenCallbackIsCreated givenLinearVestingModuleIsInstalled - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) { - _performCallback(); + _lotId = _createLot(address(_SELLER)); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); - assertEq(configuration.vestingStart, _START + 1, "vestingStart"); - assertEq(configuration.vestingExpiry, _START + 2, "vestingExpiry"); + assertEq(configuration.vestingStart, _AUCTION_CONCLUSION + 1, "vestingStart"); + assertEq(configuration.vestingExpiry, _AUCTION_CONCLUSION + 2, "vestingExpiry"); assertEq( address(configuration.linearVestingModule), address(_linearVesting), @@ -298,7 +365,7 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes abi.encodeWithSelector(BaseDirectToLiquidity.Callback_Params_InvalidAddress.selector); vm.expectRevert(err); - _performCallback(); + _performOnCreate(); } function test_whenRecipientIsNotSeller_succeeds() @@ -306,7 +373,7 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes givenCallbackIsCreated whenRecipientIsNotSeller { - _performCallback(); + _performOnCreate(); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); @@ -317,18 +384,14 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes } function test_succeeds() public givenCallbackIsCreated { - _performCallback(); + _performOnCreate(); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); assertEq(configuration.recipient, _SELLER, "recipient"); assertEq(configuration.lotCapacity, _LOT_CAPACITY, "lotCapacity"); assertEq(configuration.lotCuratorPayout, 0, "lotCuratorPayout"); - assertEq( - configuration.proceedsUtilisationPercent, - _dtlCreateParams.proceedsUtilisationPercent, - "proceedsUtilisationPercent" - ); + assertEq(configuration.poolPercent, _dtlCreateParams.poolPercent, "poolPercent"); assertEq(configuration.vestingStart, 0, "vestingStart"); assertEq(configuration.vestingExpiry, 0, "vestingExpiry"); assertEq(address(configuration.linearVestingModule), address(0), "linearVestingModule"); @@ -338,36 +401,64 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes assertEq(configurationPoolFee, _poolFee, "poolFee"); assertEq(configuration.implParams, _dtlCreateParams.implParams, "implParams"); + UniswapV3DirectToLiquidity.UniswapV3OnCreateParams memory uniswapV3CreateParams = abi.decode( + configuration.implParams, (UniswapV3DirectToLiquidity.UniswapV3OnCreateParams) + ); + assertEq(uniswapV3CreateParams.poolFee, _uniswapV3CreateParams.poolFee, "poolFee"); + assertEq( + uniswapV3CreateParams.maxSlippage, _uniswapV3CreateParams.maxSlippage, "maxSlippage" + ); + // Assert balances _assertBaseTokenBalances(); } function test_succeeds_multiple() public givenCallbackIsCreated { // Lot one - _performCallback(); + _performOnCreate(); // Lot two _dtlCreateParams.recipient = _NOT_SELLER; _lotId = 2; - _performCallback(_NOT_SELLER); + _performOnCreate(_NOT_SELLER); // Assert values BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); assertEq(configuration.recipient, _NOT_SELLER, "recipient"); assertEq(configuration.lotCapacity, _LOT_CAPACITY, "lotCapacity"); assertEq(configuration.lotCuratorPayout, 0, "lotCuratorPayout"); - assertEq( - configuration.proceedsUtilisationPercent, - _dtlCreateParams.proceedsUtilisationPercent, - "proceedsUtilisationPercent" - ); + assertEq(configuration.poolPercent, _dtlCreateParams.poolPercent, "poolPercent"); assertEq(configuration.vestingStart, 0, "vestingStart"); assertEq(configuration.vestingExpiry, 0, "vestingExpiry"); assertEq(address(configuration.linearVestingModule), address(0), "linearVestingModule"); assertEq(configuration.active, true, "active"); assertEq(configuration.implParams, _dtlCreateParams.implParams, "implParams"); + UniswapV3DirectToLiquidity.UniswapV3OnCreateParams memory uniswapV3CreateParams = abi.decode( + configuration.implParams, (UniswapV3DirectToLiquidity.UniswapV3OnCreateParams) + ); + assertEq(uniswapV3CreateParams.poolFee, _uniswapV3CreateParams.poolFee, "poolFee"); + assertEq( + uniswapV3CreateParams.maxSlippage, _uniswapV3CreateParams.maxSlippage, "maxSlippage" + ); + // Assert balances _assertBaseTokenBalances(); } + + function test_maxSlippage_fuzz(uint24 maxSlippage_) public givenCallbackIsCreated { + uint24 maxSlippage = uint24(bound(maxSlippage_, 0, 100e2)); + _setMaxSlippage(maxSlippage); + + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.implParams, _dtlCreateParams.implParams, "implParams"); + + UniswapV3DirectToLiquidity.UniswapV3OnCreateParams memory uniswapV3CreateParams = abi.decode( + configuration.implParams, (UniswapV3DirectToLiquidity.UniswapV3OnCreateParams) + ); + assertEq(uniswapV3CreateParams.maxSlippage, maxSlippage, "maxSlippage"); + } } diff --git a/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol index 79308ef2..20b30999 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {UniswapV3DirectToLiquidityTest} from "./UniswapV3DTLTest.sol"; -import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; contract UniswapV3DirectToLiquidityOnCurateTest is UniswapV3DirectToLiquidityTest { diff --git a/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol index 4a4b5511..166f86f9 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol @@ -11,26 +11,28 @@ import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; import {IUniswapV3Pool} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; import {SqrtPriceMath} from "../../../../src/lib/uniswap-v3/SqrtPriceMath.sol"; +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; // G-UNI import {GUniPool} from "@g-uni-v1-core-0.9.9/GUniPool.sol"; // AuctionHouse -import {ILinearVesting} from "@axis-core-1.0.0/interfaces/modules/derivatives/ILinearVesting.sol"; +import {ILinearVesting} from "@axis-core-1.0.1/interfaces/modules/derivatives/ILinearVesting.sol"; import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; import {UniswapV3DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV3DTL.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; + +import {console2} from "@forge-std-1.9.1/console2.sol"; contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTest { uint96 internal constant _PROCEEDS = 20e18; uint96 internal constant _REFUND = 0; - uint96 internal _proceeds; - uint96 internal _refund; uint96 internal _capacityUtilised; uint96 internal _quoteTokensToDeposit; uint96 internal _baseTokensToDeposit; uint96 internal _curatorPayout; - uint24 internal _maxSlippage = 1; // 0.01% + uint256 internal _additionalQuoteTokensMinted; uint160 internal constant _SQRT_PRICE_X96_OVERRIDE = 125_270_724_187_523_965_593_206_000_000; // Different to what is normally calculated @@ -63,15 +65,15 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes // ========== Assertions ========== // - function _assertPoolState(uint160 sqrtPriceX96_) internal { + function _assertPoolState(uint160 sqrtPriceX96_) internal view { // Get the pool - address pool = _getPool(); + IUniswapV3Pool pool = _getPool(); - (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); + (uint160 sqrtPriceX96,,,,,,) = pool.slot0(); assertEq(sqrtPriceX96, sqrtPriceX96_, "pool sqrt price"); } - function _assertLpTokenBalance() internal { + function _assertLpTokenBalance() internal view { // Get the pools deployed by the DTL callback GUniPool pool = _getGUniPool(); @@ -136,15 +138,37 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes ); } - function _assertQuoteTokenBalance() internal { + function _assertQuoteTokenBalance() internal view { assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: quote token balance"); + + uint256 nonPoolProceeds = _proceeds - _quoteTokensToDeposit; + assertApproxEqAbs( + _quoteToken.balanceOf(_NOT_SELLER), + _dtlCreateParams.recipient == _NOT_SELLER ? nonPoolProceeds : 0, + ( + _dtlCreateParams.recipient == _NOT_SELLER + ? _uniswapV3CreateParams.maxSlippage * _quoteTokensToDeposit / 100e2 + : 0 + ) + 2, // Rounding errors + "not seller: quote token balance" + ); + assertApproxEqAbs( + _quoteToken.balanceOf(_SELLER), + _dtlCreateParams.recipient == _SELLER ? nonPoolProceeds : 0, + ( + _dtlCreateParams.recipient == _NOT_SELLER + ? _uniswapV3CreateParams.maxSlippage * _quoteTokensToDeposit / 100e2 + : 0 + ) + 2, // Rounding errors + "seller: quote token balance" + ); } - function _assertBaseTokenBalance() internal { + function _assertBaseTokenBalance() internal view { assertEq(_baseToken.balanceOf(_dtlAddress), 0, "DTL: base token balance"); } - function _assertApprovals() internal { + function _assertApprovals() internal view { // Ensure there are no dangling approvals assertEq( _quoteToken.allowance(_dtlAddress, address(_getGUniPool())), @@ -160,20 +184,6 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes // ========== Modifiers ========== // - function _performCallback(uint96 lotId_) internal { - vm.prank(address(_auctionHouse)); - _dtl.onSettle( - lotId_, - _proceeds, - _refund, - abi.encode(UniswapV3DirectToLiquidity.OnSettleParams({maxSlippage: _maxSlippage})) - ); - } - - function _performCallback() internal { - _performCallback(_lotId); - } - function _createPool() internal returns (address) { (address token0, address token1) = address(_baseToken) < address(_quoteToken) ? (address(_baseToken), address(_quoteToken)) @@ -219,20 +229,19 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _capacityUtilised = _LOT_CAPACITY * capacityUtilisationPercent / 100e2; // The proceeds utilisation percent scales the quote tokens and base tokens linearly - _quoteTokensToDeposit = _proceeds * _dtlCreateParams.proceedsUtilisationPercent / 100e2; - _baseTokensToDeposit = - _capacityUtilised * _dtlCreateParams.proceedsUtilisationPercent / 100e2; + _quoteTokensToDeposit = _proceeds * _dtlCreateParams.poolPercent / 100e2; + _baseTokensToDeposit = _capacityUtilised * _dtlCreateParams.poolPercent / 100e2; _sqrtPriceX96 = _calculateSqrtPriceX96(_quoteTokensToDeposit, _baseTokensToDeposit); _; } - modifier givenUnboundedProceedsUtilisationPercent(uint24 percent_) { + modifier givenUnboundedPoolPercent(uint24 percent_) { // Bound the percent - uint24 percent = uint24(bound(percent_, 1, 100e2)); + uint24 percent = uint24(bound(percent_, 10e2, 100e2)); // Set the value on the DTL - _dtlCreateParams.proceedsUtilisationPercent = percent; + _dtlCreateParams.poolPercent = percent; _; } @@ -261,28 +270,37 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _; } - function _getPool() internal view returns (address) { + modifier givenPoolHasDepositMuchHigherPrice() { + _sqrtPriceX96 = _calculateSqrtPriceX96(_PROCEEDS * 10, _LOT_CAPACITY); + _; + } + + function _getPool() internal view returns (IUniswapV3Pool) { (address token0, address token1) = address(_baseToken) < address(_quoteToken) ? (address(_baseToken), address(_quoteToken)) : (address(_quoteToken), address(_baseToken)); - return _uniV3Factory.getPool(token0, token1, _poolFee); - } - - function _setMaxSlippage(uint24 maxSlippage_) internal { - _maxSlippage = maxSlippage_; - } - - modifier givenMaxSlippage(uint24 maxSlippage_) { - _setMaxSlippage(maxSlippage_); - _; + return IUniswapV3Pool(_uniV3Factory.getPool(token0, token1, _poolFee)); } // ========== Tests ========== // + // [X] given the onSettle callback has already been called + // [X] when onSettle is called + // [X] it reverts + // [X] when onCancel is called + // [X] it reverts + // [X] when onCreate is called + // [X] it reverts + // [X] when onCurate is called + // [X] it reverts // [X] given the pool is created // [X] it initializes the pool // [X] given the pool is created and initialized // [X] it succeeds + // [X] given there is liquidity in the pool at a higher tick + // [X] it adjusts the pool price + // [X] given there is liquidity in the pool at a lower tick + // [X] it adjusts the pool price // [X] given the proceeds utilisation percent is set // [X] it calculates the deposit amount correctly // [X] given curation is enabled @@ -315,7 +333,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -325,60 +343,20 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _assertApprovals(); } - function test_givenPoolIsCreatedAndInitialized_givenMaxSlippage() - public - givenCallbackIsCreated - givenOnCreate - setCallbackParameters(_PROCEEDS, _REFUND) - givenPoolIsCreatedAndInitialized(_SQRT_PRICE_X96_OVERRIDE) - givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) - givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) - givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) - givenMaxSlippage(8100) // 81% - { - _performCallback(); - - _assertPoolState(_SQRT_PRICE_X96_OVERRIDE); - _assertLpTokenBalance(); - _assertVestingTokenBalance(); - _assertQuoteTokenBalance(); - _assertBaseTokenBalance(); - _assertApprovals(); - } - - function test_givenPoolIsCreatedAndInitialized_reverts() - public - givenCallbackIsCreated - givenOnCreate - setCallbackParameters(_PROCEEDS, _REFUND) - givenPoolIsCreatedAndInitialized(_SQRT_PRICE_X96_OVERRIDE) - givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) - givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) - givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) - { - // Expect revert - bytes memory err = abi.encodeWithSelector( - UniswapV3DirectToLiquidity.Callback_Slippage.selector, - address(_baseToken), - 7_999_999_999_999_999_999, // Hardcoded - 9_999_000_000_000_000_000 // Hardcoded - ); - vm.expectRevert(err); - - _performCallback(); - } - - function test_givenProceedsUtilisationPercent_fuzz(uint24 percent_) + function test_givenPoolPercent_fuzz( + uint24 percent_ + ) public givenCallbackIsCreated - givenUnboundedProceedsUtilisationPercent(percent_) + givenUnboundedPoolPercent(percent_) + whenRecipientIsNotSeller givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -388,7 +366,9 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _assertApprovals(); } - function test_givenCurationPayout_fuzz(uint96 curationPayout_) + function test_givenCurationPayout_fuzz( + uint96 curationPayout_ + ) public givenCallbackIsCreated givenOnCreate @@ -398,7 +378,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -408,13 +388,13 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _assertApprovals(); } - function test_givenProceedsUtilisationPercent_givenCurationPayout_fuzz( + function test_givenPoolPercent_givenCurationPayout_fuzz( uint24 percent_, uint96 curationPayout_ ) public givenCallbackIsCreated - givenUnboundedProceedsUtilisationPercent(percent_) + givenUnboundedPoolPercent(percent_) givenOnCreate givenUnboundedOnCurate(curationPayout_) setCallbackParameters(_PROCEEDS, _REFUND) @@ -422,7 +402,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -432,7 +412,9 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _assertApprovals(); } - function test_whenRefund_fuzz(uint96 refund_) + function test_whenRefund_fuzz( + uint96 refund_ + ) public givenCallbackIsCreated givenOnCreate @@ -442,7 +424,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -455,18 +437,18 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes function test_givenPoolHasDepositWithLowerPrice() public givenCallbackIsCreated + givenMaxSlippage(200) // 2% givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) givenPoolHasDepositLowerPrice givenPoolIsCreatedAndInitialized(_sqrtPriceX96) - givenMaxSlippage(5100) // 51% givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); - _assertPoolState(_sqrtPriceX96); + _assertPoolState(_calculateSqrtPriceX96(_PROCEEDS, _LOT_CAPACITY)); _assertLpTokenBalance(); _assertVestingTokenBalance(); _assertQuoteTokenBalance(); @@ -474,21 +456,23 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _assertApprovals(); } + // TODO need to add a case where the price is more than 2x the target price and there is too much liquidity to sell through + // the result should be the pool is initialized at a higher price than the target price, but with balanced liquidity function test_givenPoolHasDepositWithHigherPrice() public givenCallbackIsCreated + givenMaxSlippage(200) // 2% givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) givenPoolHasDepositHigherPrice givenPoolIsCreatedAndInitialized(_sqrtPriceX96) - givenMaxSlippage(5100) // 51% givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); - _assertPoolState(_sqrtPriceX96); + _assertPoolState(_calculateSqrtPriceX96(_PROCEEDS, _LOT_CAPACITY)); _assertLpTokenBalance(); _assertVestingTokenBalance(); _assertQuoteTokenBalance(); @@ -499,14 +483,14 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes function test_lessThanMaxSlippage() public givenCallbackIsCreated + givenMaxSlippage(1) // 0.01% givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) - givenMaxSlippage(1) // 0.01% givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -519,9 +503,9 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes function test_greaterThanMaxSlippage_reverts() public givenCallbackIsCreated + givenMaxSlippage(0) // 0% givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) - givenMaxSlippage(0) // 0% givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) @@ -535,22 +519,22 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes ); vm.expectRevert(err); - _performCallback(); + _performOnSettle(); } function test_givenVesting() public givenLinearVestingModuleIsInstalled givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -564,8 +548,8 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes public givenLinearVestingModuleIsInstalled givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) whenRecipientIsNotSeller givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) @@ -573,7 +557,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -587,21 +571,25 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes public givenLinearVestingModuleIsInstalled givenCallbackIsCreated - givenVestingStart(_START + 1) - givenVestingExpiry(_START + 2) + givenVestingStart(_AUCTION_CONCLUSION + 1) + givenVestingExpiry(_AUCTION_CONCLUSION + 2) givenOnCreate setCallbackParameters(_PROCEEDS, _REFUND) givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) { - _performCallback(); + _performOnSettle(); // Warp to the end of the vesting period - vm.warp(_START + 3); + vm.warp(_AUCTION_CONCLUSION + 3); - // Redeem the vesting tokens + // Check that there is a vested token balance uint256 tokenId = _getVestingTokenId(); + uint256 redeemable = _linearVesting.redeemable(_SELLER, tokenId); + assertGt(redeemable, 0, "redeemable"); + + // Redeem the vesting tokens vm.prank(_SELLER); _linearVesting.redeemMax(tokenId); @@ -620,14 +608,14 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); // Get the pools deployed by the DTL callback address[] memory pools = _gUniFactory.getPools(_dtlAddress); assertEq(pools.length, 1, "pools length"); GUniPool pool = GUniPool(pools[0]); - address uniPool = _getPool(); + IUniswapV3Pool uniPool = _getPool(); // Withdraw the LP token uint256 sellerBalance = pool.balanceOf(_SELLER); @@ -643,8 +631,8 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: quote token balance"); assertEq(_baseToken.balanceOf(_dtlAddress), 0, "DTL: base token balance"); // There is a rounding error when burning the LP token, which leaves dust in the pool - assertEq(_quoteToken.balanceOf(uniPool), 1, "uni pool: quote token balance"); - assertEq(_baseToken.balanceOf(uniPool), 1, "uni pool: base token balance"); + assertEq(_quoteToken.balanceOf(address(uniPool)), 1, "uni pool: quote token balance"); + assertEq(_baseToken.balanceOf(address(uniPool)), 1, "uni pool: base token balance"); } function test_givenInsufficientBaseTokenBalance_reverts() @@ -666,7 +654,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes ); vm.expectRevert(err); - _performCallback(); + _performOnSettle(); } function test_givenInsufficientBaseTokenAllowance_reverts() @@ -681,7 +669,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes // Expect revert vm.expectRevert("TRANSFER_FROM_FAILED"); - _performCallback(); + _performOnSettle(); } function test_success() @@ -693,7 +681,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -715,7 +703,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes // Create second lot uint96 lotIdTwo = _createLot(_NOT_SELLER); - _performCallback(lotIdTwo); + _performOnSettle(lotIdTwo); _assertLpTokenBalance(); _assertVestingTokenBalance(); @@ -734,7 +722,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) { - _performCallback(); + _performOnSettle(); _assertPoolState(_sqrtPriceX96); _assertLpTokenBalance(); @@ -743,4 +731,268 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes _assertBaseTokenBalance(); _assertApprovals(); } + + function test_auctionCompleted_onCreate_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseCallback determines if the lot has already been registered + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + // Try to call onCreate again + _performOnCreate(); + } + + function test_auctionCompleted_onCurate_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Try to call onCurate + _performOnCurate(_curatorPayout); + } + + function test_auctionCompleted_onCancel_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Try to call onCancel + _performOnCancel(); + } + + function test_auctionCompleted_onSettle_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Expect revert + // BaseDirectToLiquidity determines if the lot has already been completed + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_AlreadyComplete.selector); + vm.expectRevert(err); + + // Try to call onSettle + _performOnSettle(); + } + + function uniswapV3MintCallback(uint256, uint256 amount1Owed, bytes calldata) external { + console2.log("Minting additional quote tokens", amount1Owed); + _additionalQuoteTokensMinted += amount1Owed; + + // Transfer the quote tokens + _quoteToken.mint(msg.sender, amount1Owed); + } + + function _mintPosition(int24 tickLower_, int24 tickUpper_) internal { + // Using PoC: https://github.com/GuardianAudits/axis-1/pull/4/files + IUniswapV3Pool pool = _getPool(); + + pool.mint(address(this), tickLower_, tickUpper_, 1e18, ""); + } + + function uniswapV3SwapCallback(int256, int256, bytes memory) external pure { + return; + } + + function _swap(uint160 sqrtPrice_) internal { + IUniswapV3Pool pool = _getPool(); + + pool.swap(address(this), true, 1, sqrtPrice_, ""); + } + + function test_existingReservesAtHigherPoolTick() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + { + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after mint"); // Original active tick + + // Swap at a tick higher than the anchor range + IUniswapV3Pool pool = _getPool(); + pool.swap(address(this), false, 1, TickMath.getSqrtRatioAtTick(60_000), ""); + + // Assert that the pool tick has moved higher + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 60_000, "pool tick after swap"); + + // Provide reserve tokens to the pool at a tick higher than the original active tick and lower than the new active tick + _mintPosition(7200, 7200 + _getPool().tickSpacing()); + + // Perform callback + _performOnSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + // _assertQuoteTokenBalance(); // Difficult to calculate the exact balance, given the swaps + // _assertBaseTokenBalance(); // Difficult to calculate the exact balance, given the swaps + _assertApprovals(); + } + + function test_existingReservesAtHigherPoolTick_noLiquidity() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + { + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after mint"); // Original active tick + + // Swap at a tick higher than the active tick + IUniswapV3Pool pool = _getPool(); + pool.swap(address(this), false, 1, TickMath.getSqrtRatioAtTick(60_000), ""); + + // Assert that the pool tick has moved higher + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 60_000, "pool tick after swap"); + + // Do not mint any liquidity above the previous active tick + + // Perform callback + _performOnSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_existingReservesAtLowerPoolTick() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + { + // Provide reserve tokens to the pool at a lower tick + _mintPosition(-60_000 - _getPool().tickSpacing(), -60_000); + + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after mint"); // Original active tick + + // Swap at a tick lower than the active tick + _swap(TickMath.getSqrtRatioAtTick(-60_000)); + + // Assert that the pool price has moved lower + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, -60_001, "pool tick after swap"); + + // Perform callback + _performOnSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_existingReservesAtLowerPoolTick_noLiquidity() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + { + // Don't mint any liquidity + + // Assert the pool price + int24 poolTick; + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after mint"); // Original active tick + + // Swap at a tick lower than the active tick + _swap(TickMath.getSqrtRatioAtTick(-60_000)); + + // Assert that the pool price has moved lower + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, -60_000, "pool tick after swap"); + + // Perform callback + _performOnSettle(); + + // Assert that the pool tick has corrected + (, poolTick,,,,,) = _getPool().slot0(); + assertEq(poolTick, 6931, "pool tick after settlement"); // Ends up rounded to the tick spacing + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } } diff --git a/test/invariant/AxisInvariant.sol b/test/invariant/AxisInvariant.sol new file mode 100644 index 00000000..5b089365 --- /dev/null +++ b/test/invariant/AxisInvariant.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {UniswapV2DTLHandler} from "./handlers/UniswapV2DTLHandler.sol"; +import {UniswapV3DTLHandler} from "./handlers/UniswapV3DTLHandler.sol"; +import {BaselineDTLHandler} from "./handlers/BaselineDTLHandler.sol"; +import {V2PoolHandler} from "./handlers/V2PoolHandler.sol"; +import {V3PoolHandler} from "./handlers/V3PoolHandler.sol"; +import {BaselinePoolHandler} from "./handlers/BaselinePoolHandler.sol"; + +// forgefmt: disable-start +/**************************************************************************************************************/ +/*** AxisInvariant is the highest level contract that contains all setup, handlers, and invariants for ***/ +/*** the Axis Fuzz Suite. ***/ +/**************************************************************************************************************/ +// forgefmt: disable-end +contract AxisInvariant is + UniswapV2DTLHandler, + UniswapV3DTLHandler, + BaselineDTLHandler, + V2PoolHandler, + V3PoolHandler, + BaselinePoolHandler +{ + constructor() payable { + setup(); + } +} diff --git a/test/invariant/InvariantREADME.md b/test/invariant/InvariantREADME.md new file mode 100644 index 00000000..ee0f01c1 --- /dev/null +++ b/test/invariant/InvariantREADME.md @@ -0,0 +1,132 @@ +## Axis Fuzz Suite + +### Overview + +Axis engaged Guardian Audits for an in-depth security review of its periphery contracts containing the callback functionality for the core Axis auctions. This comprehensive evaluation, conducted from July 22nd to July 29th, 2024, included the development of a specialized fuzzing suite to uncover complex logical errors. This suite was created during the review period and successfully delivered upon the audit's conclusion. + +### Contents + +Setup.sol configures the Axis Protocol setup and actors used for fuzzing. + +It also concludes a number of handler contracts for each DTL contract in scope: + +- UniswapV2DTLHandler.sol +- V2PoolHandler.sol +- UniswapV3DTLHandler.sol +- V3PoolHandler.sol +- BaselineDTLHandler.sol +- BaselinePoolHandler.sol + +### Setup And Run Instructions + +First, install dependencies: + +```shell +pnpm install +``` + +If an error occurs while soldeer runs during installation, try running soldeer individually: + +```shell +soldeer install +``` + +Then, install forge dependencies: + +```shell +forge install +``` + +To run invariant tests: + +```shell +echidna . --contract AxisInvariant --config ./test/invariant/echidna.yaml +``` + +If a key error occurs (`KeyError: 'output'`) : + +```shell +forge clean +``` + +then try the echidna command again. + +### Unexpected Selectors + +Due to the issue of the proceeds\_ value being based on the total sold less the fees taken by the protocol and referrer, the handler function `baselineDTL_onSettle` will revert with the error selector `Callback_InvalidCapacityRatio`. Curator fees have been disabled to bypass this issue. + +In test/invariant/handlers/BaselineDTL_Handler.sol: + +```diff +- curatorFee_ = bound(curatorFee_, 0, 5e18); ++ curatorFee_ = 0; +``` + +There is also an issue where donating baseTokens will disrupt accounting in UniswapV2DTL.sol. Base token donations have been disabled. If base token donations are enabled AX-52 will fail. + +In test/invariant/handlers/V2PoolHandler.sol: + +```diff +- address _token = tokenIndexSeed % 2 == 0 ? address(_quoteToken) : address(_baseToken); ++ address _token = address(_quoteToken); +``` + +### Invariants + +## **Axis** + +| **Invariant ID** | **Invariant Description** | **Passed** | **Remediation** | **Run Count** | +| :--------------: | :-------------------------------------------------------------------------------------------------------- | :--------: | :-------------: | :-----------: | ---- | ---- | ----------- | +| **AX-01** | UniswapV2Dtl_onCreate() should set DTL Config recipient | PASS | PASS | 10,000,000+ | +| **AX-02** | UniswapV2Dtl_onCreate() should set DTL Config lotCapacity | PASS | PASS | 10,000,000+ | +| **AX-03** | UniswapV2Dtl_onCreate() should set DTL Config lotCuratorPayout | PASS | PASS | 10,000,000+ | +| **AX-04** | UniswapV2Dtl_onCreate() should set DTL Config proceedsUtilisationPercent | PASS | PASS | 10,000,000+ | +| **AX-05** | UniswapV2Dtl_onCreate() should set DTL Config vestingStart | PASS | PASS | 10,000,000+ | +| **AX-06** | UniswapV2Dtl_onCreate() should set DTL Config vestingExpiry | PASS | PASS | 10,000,000+ | +| **AX-07** | UniswapV2Dtl_onCreate() should set DTL Config linearVestingModule | PASS | PASS | 10,000,000+ | +| **AX-08** | UniswapV2Dtl_onCreate() should set DTL Config active to true | PASS | PASS | 10,000,000+ | +| **AX-09** | DTL Callbacks should not change seller base token balance | PASS | PASS | 10,000,000+ | +| **AX-10** | DTL Callbacks should not change dtl base token balance | PASS | PASS | 10,000,000+ | +| **AX-11** | DTL_onCancel() should set DTL Config active to false | PASS | PASS | 10,000,000+ | +| **AX-12** | DTL_onCurate should set DTL Config lotCuratorPayout | PASS | PASS | 10,000,000+ | +| **AX-13** | When calling DTL_onCurate auction house base token balance should be equal to lot Capacity of each lotId | PASS | PASS | 10,000,000+ | +| **AX-14** | DTL_onSettle should should credit seller the expected LP token balance | PASS | PASS | 10,000,000+ | +| **AX-15** | DTL_onSettle should should credit linearVestingModule the expected LP token balance | PASS | PASS | 10,000,000+ | +| **AX-16** | DTL_onSettle should should credit seller the expected wrapped vesting token balance | PASS | PASS | 10,000,000+ | +| **AX-17** | After DTL_onSettle DTL Address quote token balance should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-18** | After DTL_onSettle DTL Address base token balance should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-19** | After UniswapV2DTL_onSettle DTL Address quote token allowance for the UniswapV2 Router should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-20** | After UniswapV2DTL_onSettle DTL Address base token allowance UniswapV2 Router should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-21** | UniswapV3Dtl_onCreate() should set DTL Config recipient | PASS | PASS | 10,000,000+ | +| **AX-22** | UniswapV3Dtl_onCreate() should set DTL Config lotCapacity | PASS | PASS | 10,000,000+ | +| **AX-23** | UniswapV3Dtl_onCreate() should set DTL Config lotCuratorPayout | PASS | PASS | 10,000,000+ | +| **AX-24** | UniswapV3Dtl_onCreate() should set DTL Config proceedsUtilisationPercent | PASS | PASS | 10,000,000+ | +| **AX-25** | UniswapV3Dtl_onCreate() should set DTL Config vestingStart | PASS | PASS | 10,000,000+ | +| **AX-26** | UniswapV3Dtl_onCreate() should set DTL Config vestingExpiry | PASS | PASS | 10,000,000+ | +| **AX-27** | UniswapV3Dtl_onCreate() should set DTL Config linearVestingModule | PASS | PASS | 10,000,000+ | +| **AX-28** | UniswapV3Dtl_onCreate() should set DTL Config active to true | PASS | PASS | 10,000,000+ | +| **AX-29** | On UniswapV3DTL_OnSettle() calculated sqrt price should equal pool sqrt price | PASS | PASS | 10,000,000+ | +| **AX-30** | After UniswapV3DTL_onSettle DTL Address base token allowance for the GUniPool should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-31** | After UniswapV3DTL_onSettle DTL Address base token allowance for the GUniPool should equal 0 | PASS | PASS | 10,000,000+ | PASS | PASS | 10,000,000+ | +| **AX-32** | When calling BaselineDTL_createLot auction house base token balance should be equal to lot Capacity lotId | PASS | PASS | 10,000,000+ | +| **AX-33** | After DTL_onSettle quote token balance of quote token should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-34** | BaselineDTL_onSettle should credit baseline pool with correct quote token proceeds | PASS | PASS | 10,000,000+ | +| **AX-35** | BaselineDTL_onSettle should credit seller quote token proceeds | PASS | PASS | 10,000,000+ | +| **AX-36** | Baseline token total supply after \_onCancel should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-37** | BaselineDTL_onCancel should mark auction completed | PASS | PASS | 10,000,000+ | +| **AX-38** | When calling BaselineDTL_onCancel DTL base token balance should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-39** | When calling BaselineDTL_onCancel baseline contract base token balance should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-40** | BaselineDTL_onCurate should credit auction house correct base token fees | PASS | PASS | 10,000,000+ | +| **AX-41** | After BaselineDTL_onSettle baseline token base token balance should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-42** | After BaselineDTL_onSettle baseline pool base token balance should equal baseline pool supply | PASS | PASS | 10,000,000+ | +| **AX-43** | After BaselineDTL_onSettle seller baseline token balance should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-44** | circulating supply should equal lot capacity plus curatorFee minus refund | PASS | PASS | 10,000,000+ | +| **AX-45** | BaselineDTL_onSettle should mark auction complete | PASS | PASS | 10,000,000+ | +| **AX-46** | After BaselineDTL_onSettle floor reserves should equal floor proceeds | PASS | PASS | 10,000,000+ | +| **AX-47** | After BaselineDTL_onSettle anchor reserves should equal pool proceeds - floor proceeds | PASS | PASS | 10,000,000+ | +| **AX-48** | After BaselineDTL_onSettle discovery reserves should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-49** | After BaselineDTL_onSettle floor bAssets should equal 0 | PASS | PASS | 10,000,000+ | +| **AX-50** | After BaselineDTL_onSettle anchor bAssets should be greater than 0 | PASS | PASS | 10,000,000+ | +| **AX-51** | After BaselineDTL_onSettle discovery bAssets should be greater than 0 | PASS | PASS | 10,000,000+ | +| **AX-52** | UniswapV2DTL_onSettle should not fail with 'UniswapV2Library: INSUFFICIENT_LIQUIDITY' | **FAIL** | PASS | 10,000,000+ | +| **AX-53** | Profit should not be extractable due to UniswapV3Pool price manipulation | **FAIL** | PASS | 10,000,000+ | diff --git a/test/invariant/Setup.sol b/test/invariant/Setup.sol new file mode 100644 index 00000000..89b37412 --- /dev/null +++ b/test/invariant/Setup.sol @@ -0,0 +1,527 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Test} from "@forge-std-1.9.1/Test.sol"; +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; + +import {IAuction} from "@axis-core-1.0.1/interfaces/modules/IAuction.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; +import {EncryptedMarginalPrice} from "@axis-core-1.0.1/modules/auctions/batch/EMP.sol"; +import {IFixedPriceBatch} from "@axis-core-1.0.1/interfaces/modules/auctions/IFixedPriceBatch.sol"; +import {FixedPriceBatch} from "@axis-core-1.0.1/modules/auctions/batch/FPB.sol"; + +import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Factory.sol"; +import {UniswapV2FactoryClone} from "../lib/uniswap-v2/UniswapV2FactoryClone.sol"; + +import {IUniswapV2Router02} from "@uniswap-v2-periphery-1.0.1/interfaces/IUniswapV2Router02.sol"; +import {UniswapV2Router02} from "@uniswap-v2-periphery-1.0.1/UniswapV2Router02.sol"; + +import {GUniFactory} from "@g-uni-v1-core-0.9.9/GUniFactory.sol"; +import {GUniPool} from "@g-uni-v1-core-0.9.9/GUniPool.sol"; +import {IUniswapV3Factory} from + "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Factory.sol"; + +import {UniswapV3Factory} from "../lib/uniswap-v3/UniswapV3Factory.sol"; +import {WETH9} from "./modules/WETH.sol"; +import {SwapRouter} from "./modules/uniswapv3-periphery/SwapRouter.sol"; +import {FixedPointMathLib} from "@solmate-6.7.0/utils/FixedPointMathLib.sol"; +import {SqrtPriceMath} from "../../../../src/lib/uniswap-v3/SqrtPriceMath.sol"; +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; + +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {UniswapV2DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV2DTL.sol"; +import {UniswapV3DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV3DTL.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; +import {MockBatchAuctionModule} from + "@axis-core-1.0.1-test/modules/Auction/MockBatchAuctionModule.sol"; + +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.1/modules/Keycode.sol"; + +import {BaselineAxisLaunch} from + "../../../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; + +// Baseline +import {Kernel, Actions, Module, toKeycode as toBaselineKeycode} from "@baseline/Kernel.sol"; + +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; +import {BPOOLv1, Range, Position} from "@baseline/modules/BPOOL.v1.sol"; +import {BPOOLMinter} from "./modules/BPOOLMinter.sol"; +import {CREDTMinter} from "./modules/CREDTMinter.sol"; +import {CREDTv1} from "@baseline/modules/CREDT.v1.sol"; +import {LOOPSv1} from "@baseline/modules/LOOPS.v1.sol"; +import {ModuleTester, ModuleTestFixture} from "./modules/ModuleTester.sol"; + +import {WithSalts} from "../lib/WithSalts.sol"; +import {TestConstants} from "../Constants.sol"; +import {console2} from "@forge-std-1.9.1/console2.sol"; + +import {MockBatchAuctionHouse} from "./mocks/MockBatchAuctionHouse.sol"; +import {MockBlast} from "./mocks/MockBlast.sol"; + +abstract contract Setup is Test, Permit2User, WithSalts, TestConstants { + using Callbacks for UniswapV2DirectToLiquidity; + using Callbacks for UniswapV3DirectToLiquidity; + using Callbacks for BaselineAxisLaunch; + + /*////////////////////////////////////////////////////////////////////////// + GLOBAL VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint96[] internal lotIdsV2; + uint96[] internal lotIdsV3; + + address internal user0 = vm.addr(uint256(keccak256("User0"))); + address internal user1 = vm.addr(uint256(keccak256("User1"))); + address internal user2 = vm.addr(uint256(keccak256("User2"))); + address internal user3 = vm.addr(uint256(keccak256("User3"))); + address internal user4 = vm.addr(uint256(keccak256("User4"))); + address internal user5 = vm.addr(uint256(keccak256("User5"))); + address[] internal users = [user0, user1, user2, user3, user4, user5]; + + // address internal constant _SELLER = address(0x2); + address internal constant _PROTOCOL = address(0x3); + + uint96 internal constant _LOT_CAPACITY = 10e18; + uint96 internal constant _REFUND_AMOUNT = 2e18; + uint96 internal constant _PAYOUT_AMOUNT = 1e18; + uint256 internal constant _PROCEEDS_AMOUNT = 24e18; + uint256 internal constant _FIXED_PRICE = 3e18; + uint24 internal constant _FEE_TIER = 10_000; + uint256 internal constant _BASE_SCALE = 1e18; + + uint8 internal _quoteTokenDecimals = 18; + uint8 internal _baseTokenDecimals = 18; + + bool internal _isBaseTokenAddressLower = true; + + uint24 internal _feeTier = _FEE_TIER; + int24 internal _poolInitialTick; + int24 internal _tickSpacing; + + uint48 internal constant _START = 1_000_000; + + uint96 internal _lotId = 1; + + string internal constant UNISWAP_PREFIX = "E6"; + string internal constant BASELINE_PREFIX = "EF"; + + address internal _dtlV2Address; + address internal _dtlV3Address; + address internal _dtlBaselineAddress; + + IFixedPriceBatch.AuctionDataParams internal _fpbParams = + IFixedPriceBatch.AuctionDataParams({price: _FIXED_PRICE, minFillPercent: 100e2}); + + /*////////////////////////////////////////////////////////////////////////// + TEST CONTRACTS + //////////////////////////////////////////////////////////////////////////*/ + + BatchAuctionHouse internal _auctionHouse; + MockBatchAuctionHouse internal _baselineAuctionHouse; + + UniswapV2DirectToLiquidity internal _dtlV2; + + IUniswapV2Factory internal _uniV2Factory; + IUniswapV2Router02 internal _uniV2Router; + + UniswapV3DirectToLiquidity internal _dtlV3; + + IUniswapV3Factory internal _uniV3Factory; + GUniFactory internal _gUniFactory; + WETH9 internal _weth; + SwapRouter internal _v3SwapRouter; + + BaselineAxisLaunch internal _dtlBaseline; + + EncryptedMarginalPrice internal _empModule; + FixedPriceBatch internal _fpbModule; + Kernel internal _kernel; + + LinearVesting internal _linearVesting; + MockBatchAuctionModule internal _batchAuctionModule; + IAuction internal _auctionModule; + + MockBlast internal _blast; + + MockERC20 internal _quoteToken; + MockERC20 internal _baseToken; + BPOOLv1 internal _baselineToken; + CREDTv1 internal _credt; + LOOPSv1 internal _loops; + + BPOOLMinter internal _bpoolMinter; + CREDTMinter internal _credtMinter; + + /*////////////////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////////////////*/ + + event MessageAddress(string a, address b); + event MessageBytes(string a, bytes b); + event MessageBytes32(string a, bytes32 b); + event MessageString(string a, string b); + event MessageNum(string a, uint256 b); + + /*////////////////////////////////////////////////////////////////////////// + SET-UP FUNCTION + //////////////////////////////////////////////////////////////////////////*/ + + function setup() internal { + // Set reasonable timestamp + vm.warp(_START); + + // Create an BatchAuctionHouse at a deterministic address, since it is used as input to callbacks + _auctionHouse = new BatchAuctionHouse(_OWNER, _PROTOCOL, _permit2Address); + _baselineAuctionHouse = new MockBatchAuctionHouse(_OWNER, _PROTOCOL, _permit2Address); + + _uniV2Factory = new UniswapV2FactoryClone(); + + _uniV2Router = new UniswapV2Router02(address(_uniV2Factory), address(0)); + + _linearVesting = new LinearVesting(address(_auctionHouse)); + _batchAuctionModule = new MockBatchAuctionModule(address(_auctionHouse)); + _empModule = new EncryptedMarginalPrice(address(_baselineAuctionHouse)); + _fpbModule = new FixedPriceBatch(address(_baselineAuctionHouse)); + + _auctionModule = _fpbModule; + + // Install a mock batch auction module + vm.prank(_OWNER); + _auctionHouse.installModule(_batchAuctionModule); + vm.prank(_OWNER); + _auctionHouse.installModule(_linearVesting); + + _quoteToken = new MockERC20("Quote Token", "QT", 18); + _baseToken = new MockERC20("Base Token", "BT", 18); + + bytes memory constructorArgs = abi.encodePacked( + type(UniswapV2DirectToLiquidity).creationCode, + abi.encode(address(_auctionHouse), address(_uniV2Factory), address(_uniV2Router)) + ); + + string[] memory uniswapV2Inputs = new string[](7); + uniswapV2Inputs[0] = "./test/invariant/helpers/salt_hash.sh"; + uniswapV2Inputs[1] = "--bytecodeHash"; + uniswapV2Inputs[2] = toHexString(keccak256(constructorArgs)); + uniswapV2Inputs[3] = "--prefix"; + uniswapV2Inputs[4] = UNISWAP_PREFIX; + uniswapV2Inputs[5] = "--deployer"; + uniswapV2Inputs[6] = toString(address(this)); + + bytes memory uniswapV2Res = vm.ffi(uniswapV2Inputs); + bytes32 uniswapV2Salt = abi.decode(uniswapV2Res, (bytes32)); + + _dtlV2 = new UniswapV2DirectToLiquidity{salt: uniswapV2Salt}( + address(_auctionHouse), address(_uniV2Factory), address(_uniV2Router) + ); + + _dtlV2Address = address(_dtlV2); + + _uniV3Factory = new UniswapV3Factory(); + + _weth = new WETH9(); + + _v3SwapRouter = new SwapRouter(address(_uniV3Factory), address(_weth)); + + _gUniFactory = new GUniFactory(address(_uniV3Factory)); + + address payable gelatoAddress = payable(address(0x10)); + GUniPool poolImplementation = new GUniPool(gelatoAddress); + _gUniFactory.initialize(address(poolImplementation), address(0), address(this)); + + bytes memory v3SaltArgs = abi.encodePacked( + type(UniswapV3DirectToLiquidity).creationCode, + abi.encode(address(_auctionHouse), address(_uniV3Factory), address(_gUniFactory)) + ); + + string[] memory uniswapV3Inputs = new string[](7); + uniswapV3Inputs[0] = "./test/invariant/helpers/salt_hash.sh"; + uniswapV3Inputs[1] = "--bytecodeHash"; + uniswapV3Inputs[2] = toHexString(keccak256(v3SaltArgs)); + uniswapV3Inputs[3] = "--prefix"; + uniswapV3Inputs[4] = UNISWAP_PREFIX; + uniswapV3Inputs[5] = "--deployer"; + uniswapV3Inputs[6] = toString(address(this)); + + bytes memory uniswapV3Res = vm.ffi(uniswapV3Inputs); + bytes32 uniswapV3Salt = abi.decode(uniswapV3Res, (bytes32)); + + _dtlV3 = new UniswapV3DirectToLiquidity{salt: uniswapV3Salt}( + address(_auctionHouse), address(_uniV3Factory), address(_gUniFactory) + ); + + _dtlV3Address = address(_dtlV3); + + _tickSpacing = _uniV3Factory.feeAmountTickSpacing(_feeTier); + + _blast = new MockBlast(); + + _updatePoolInitialTick(); + + _kernel = new Kernel(); + + _baselineToken = _deployBPOOL( + _kernel, + "Base Token", + "BT", + _baseTokenDecimals, + address(_uniV3Factory), + address(_quoteToken), + _feeTier, + _poolInitialTick, + address(_blast), + address(0) + ); + + _credt = new CREDTv1(_kernel, address(_blast), address(0)); + + _loops = new LOOPSv1(_kernel, 1); + + _bpoolMinter = new BPOOLMinter(_kernel); + _credtMinter = new CREDTMinter(_kernel); + + _kernel.executeAction(Actions.InstallModule, address(_baselineToken)); + _kernel.executeAction(Actions.ActivatePolicy, address(_bpoolMinter)); + + _kernel.executeAction(Actions.InstallModule, address(_credt)); + _kernel.executeAction(Actions.ActivatePolicy, address(_credtMinter)); + + _kernel.executeAction(Actions.InstallModule, address(_loops)); + + vm.prank(_OWNER); + _baselineAuctionHouse.installModule(_fpbModule); + vm.prank(_OWNER); + _baselineAuctionHouse.installModule(_empModule); + + bytes memory baselineSaltArgs = abi.encodePacked( + type(BaselineAxisLaunch).creationCode, + abi.encode( + address(_baselineAuctionHouse), address(_kernel), address(_quoteToken), _SELLER + ) + ); + + string[] memory baselineInputs = new string[](7); + baselineInputs[0] = "./test/invariant/helpers/salt_hash.sh"; + baselineInputs[1] = "--bytecodeHash"; + baselineInputs[2] = toHexString(keccak256(baselineSaltArgs)); + baselineInputs[3] = "--prefix"; + baselineInputs[4] = BASELINE_PREFIX; + baselineInputs[5] = "--deployer"; + baselineInputs[6] = toString(address(this)); + + bytes memory baselineRes = vm.ffi(baselineInputs); + bytes32 baselineSalt = abi.decode(baselineRes, (bytes32)); + + _dtlBaseline = new BaselineAxisLaunch{salt: baselineSalt}( + address(_baselineAuctionHouse), address(_kernel), address(_quoteToken), _SELLER + ); + + _dtlBaselineAddress = address(_dtlBaseline); + + _bpoolMinter.setTransferLock(false); + + _kernel.executeAction(Actions.ActivatePolicy, _dtlBaselineAddress); + } + + function randomAddress(uint256 seed) internal view returns (address) { + return users[bound(seed, 0, users.length - 1)]; + } + + function randomLotIdV2(uint256 seed) internal view returns (uint96) { + return lotIdsV2[bound(seed, 0, lotIdsV2.length - 1)]; + } + + function randomLotIdV3(uint256 seed) internal view returns (uint96) { + return lotIdsV3[bound(seed, 0, lotIdsV3.length - 1)]; + } + + function _updatePoolInitialTick() internal { + _poolInitialTick = + _getTickFromPrice(_fpbParams.price, _baseTokenDecimals, _isBaseTokenAddressLower); + } + + function _getTickFromPrice( + uint256 price_, + uint8 baseTokenDecimals_, + bool isBaseTokenAddressLower_ + ) internal view returns (int24 tick) { + // Get sqrtPriceX96 + uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96( + address(_quoteToken), + isBaseTokenAddressLower_ + ? address(0x0000000000000000000000000000000000000001) + : address(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF), + price_, + 10 ** baseTokenDecimals_ + ); + + // Convert to tick + return TickMath.getTickAtSqrtRatio(sqrtPriceX96); + } + + function _deployBPOOL( + Kernel kernel_, + string memory _name, + string memory _symbol, + uint8 _decimals, + address _factory, + address _reserve, + uint24 _feeTier, + int24 _initialActiveTick, + address blast, + address blastGovernor + ) internal returns (BPOOLv1) { + bytes32 salt = _getSalt( + kernel_, + _name, + _symbol, + _decimals, + _factory, + _reserve, + _feeTier, + _initialActiveTick, + blast, + blastGovernor + ); + + return new BPOOLv1{salt: salt}( + kernel_, + _name, + _symbol, + _decimals, + _factory, + _reserve, + _feeTier, + _initialActiveTick, + blast, + blastGovernor + ); + } + + // Returns a salt that will result in a BPOOL address less than the reserve address + function _getSalt( + Kernel kernel_, + string memory _name, + string memory _symbol, + uint8 _decimals, + address _factory, + address _reserve, + uint24 _feeTier, + int24 _initialActiveTick, + address blast, + address blastGovernor + ) internal view returns (bytes32) { + uint256 salt; + + while (salt < 100) { + // Calculate the BPOOL bytecode hash + bytes32 BPOOLHash = keccak256( + abi.encodePacked( + type(BPOOLv1).creationCode, + abi.encode( + kernel_, + _name, + _symbol, + _decimals, + _factory, + _reserve, + _feeTier, + _initialActiveTick, + blast, + blastGovernor + ) + ) + ); + + // Calculate the BPOOL CREATE2 address + address BPOOLAddress = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), // deployer address + bytes32(salt), + BPOOLHash + ) + ) + ) + ) + ); + + // Return the salt that will result in a BPOOL address less than the reserve address + if (BPOOLAddress < _reserve) { + return bytes32(salt); + } + + salt++; + } + + revert("No salt found"); + } + + function toString(address _addr) internal pure returns (string memory) { + bytes32 value = bytes32(uint256(uint160(_addr))); + bytes memory alphabet = "0123456789abcdef"; + + bytes memory str = new bytes(42); + str[0] = "0"; + str[1] = "x"; + + for (uint256 i = 0; i < 20; i++) { + str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)]; + str[3 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)]; + } + + return string(str); + } + + function givenAddressHasQuoteTokenBalance(address address_, uint256 amount_) internal { + _quoteToken.mint(address_, amount_); + } + + function givenAddressHasBaseTokenBalance(address address_, uint256 amount_) internal { + _baseToken.mint(address_, amount_); + } + + function givenAddressHasBaselineTokenBalance(address account_, uint256 amount_) internal { + _baselineToken.mint(account_, amount_); + } + + function _transferBaselineTokenRefund(uint256 amount_) internal { + // Transfer refund from auction house to the callback + // We transfer instead of minting to not affect the supply + vm.prank(address(_baselineAuctionHouse)); + _baselineToken.transfer(_dtlBaselineAddress, amount_); + } + + function givenAddressHasBaseTokenAllowance( + address owner_, + address spender_, + uint256 amount_ + ) internal { + vm.prank(owner_); + _baseToken.approve(spender_, type(uint256).max); + } + + function toHexString(bytes32 input) internal pure returns (string memory) { + bytes16 symbols = "0123456789abcdef"; + bytes memory hex_buffer = new bytes(64 + 2); + hex_buffer[0] = "0"; + hex_buffer[1] = "x"; + + uint256 pos = 2; + for (uint256 i = 0; i < 32; ++i) { + uint256 _byte = uint8(input[i]); + hex_buffer[pos++] = symbols[_byte >> 4]; + hex_buffer[pos++] = symbols[_byte & 0xf]; + } + return string(hex_buffer); + } + + function compareStrings(string memory a, string memory b) internal pure returns (bool) { + return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } +} diff --git a/test/invariant/echidna.yaml b/test/invariant/echidna.yaml new file mode 100644 index 00000000..ec32a955 --- /dev/null +++ b/test/invariant/echidna.yaml @@ -0,0 +1,119 @@ +# TODO +#select the mode to test, which can be property, assertion, overflow, exploration, optimization +testMode: "assertion" +#check if some contract was destructed or not +testDestruction: false +#psender is the sender for property transactions; by default intentionally +#the same as contract deployer +psender: "0x10000" +#prefix is the prefix for Boolean functions that are properties to be checked +prefix: "echidna_" +#propMaxGas defines gas cost at which a property fails +propMaxGas: 8000030 +#testMaxGas is a gas limit; does not cause failure, but terminates sequence +testMaxGas: 8000030 +#maxGasprice is the maximum gas price +maxGasprice: 0 +#testLimit is the number of test sequences to run +testLimit: 1000000 +workers: 10 +#stopOnFail makes echidna terminate as soon as any property fails and has been shrunk +stopOnFail: false +#estimateGas makes echidna perform analysis of maximum gas costs for functions (experimental) +estimateGas: false +#seqLen defines how many transactions are in a test sequence +seqLen: 100 +#shrinkLimit determines how much effort is spent shrinking failing sequences +shrinkLimit: 5000 +#coverage controls coverage guided testing +coverage: true +#format can be "text" or "json" for different output (human or machine readable) +# format: "text" +#contractAddr is the address of the contract itself +contractAddr: "0x00a329c0648769a73afac7f9381e08fb43dbea72" +#deployer is address of the contract deployer (who often is privileged owner, etc.) +deployer: "0x30000" +#sender is set of addresses transactions may originate from +sender: ["0x10000", "0x20000", "0x30000"] +#balanceAddr is default balance for addresses +balanceAddr: 0xffffffff +#balanceContract overrides balanceAddr for the contract address +balanceContract: 0 +#codeSize max code size for deployed contratcs (default 0xffffffff) +codeSize: 0xffffffff +#solcArgs allows special args to solc +solcArgs: "" +#solcLibs is solc libraries +solcLibs: [] +#cryticArgs allows special args to crytic +cryticArgs: ["--foundry-compile-all"] +#quiet produces (much) less verbose output +# quiet: false +#initialize the blockchain with some data +# initialize: null +#initialize the blockchain with some predeployed contracts in some addresses +# deployContracts: [] +#initialize the blockchain with some bytecode in some addresses +# deployBytecodes: [] +#whether ot not to fuzz all contracts +allContracts: false +#timeout controls test timeout settings +# timeout: null +#seed not defined by default, is the random seed +#seed: 0 +#dictFreq controls how often to use echidna's internal dictionary vs random +#values +dictFreq: 0.40 +maxTimeDelay: 604800 +#maximum time between generated txs; default is one week +maxBlockDelay: 60480 +#maximum number of blocks elapsed between generated txs; default is expected increment in one week +# timeout: +#campaign timeout (in seconds) +# list of methods to filter +# filterFunctions: [ +# "AxisInvariant.targetSenders()", +# AxisInvariant.excludeArtifacts()" +# ] +# by default, blacklist methods in filterFunctions +# filterBlacklist: true +# enable or disable ffi HEVM cheatcode +allowFFI: true +#directory to save the corpus; by default is disabled +corpusDir: echidna +# list of file formats to save coverage reports in; default is all possible formats +coverageFormats: ["txt", "html", "lcov"] +# constants for corpus mutations (for experimentation only) +# mutConsts: [1, 1, 1, 1] +# maximum value to send to payable functions +# maxValue: 100000000000000000000 # 100 eth +# URL to fetch contracts over RPC +# rpcUrl: null +# block number to use when fetching over RPC +# rpcBlock: null +# Etherscan API key +# etherscanApiKey: null +# number of workers. By default (unset) its value is the clamp of the number cores between 1 and 4 +# workers: null +# events server port +# server: null +# whether to add an additional symbolic execution worker +# symExec: false +# whether symbolic execution will be concolic (vs full symbolic execution) +# only relevant if symExec is true +# symExecConcolic: true +# number of SMT solvers used in symbolic execution +# only relevant if symExec is true +# symExecNSolvers: 1 +# timeout for symbolic execution SMT solver queries +# only relevant if symExec is true +# symExecTimeout: 30 +# Number of times we may revisit a particular branching point +# only relevant if symExec is true and symExecConcolic is false +# symExecMaxIters: 10 +# Number of times we may revisit a particular branching point before we consult the smt solver to check reachability +# only relevant if symExec is true and symExecConcolic is false +# symExecAskSMTIters: 1 +# List of whitelisted functions for using symbolic/concolic exploration +# only relevant if symExec is true +# symExecTargets: null diff --git a/test/invariant/handlers/BaselineDTLHandler.sol b/test/invariant/handlers/BaselineDTLHandler.sol new file mode 100644 index 00000000..8fd951a7 --- /dev/null +++ b/test/invariant/handlers/BaselineDTLHandler.sol @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../helpers/BeforeAfter.sol"; +import {Assertions} from "../helpers/Assertions.sol"; + +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; + +import {IAuction} from "@axis-core-1.0.1/interfaces/modules/IAuction.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; +import {ILinearVesting} from "@axis-core-1.0.1/interfaces/modules/derivatives/ILinearVesting.sol"; +import {IFixedPriceBatch} from "@axis-core-1.0.1/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +import {GUniFactory} from "@g-uni-v1-core-0.9.9/GUniFactory.sol"; +import {GUniPool} from "@g-uni-v1-core-0.9.9/GUniPool.sol"; +import {IUniswapV3Pool} from + "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; +import {IUniswapV3Factory} from + "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Factory.sol"; +import {SqrtPriceMath} from "../../../../src/lib/uniswap-v3/SqrtPriceMath.sol"; + +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {BaselineAxisLaunch} from + "../../../../src/callbacks/liquidity/BaselineV2/BaselineAxisLaunch.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {UniswapV3DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV3DTL.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; +import {MockBatchAuctionModule} from + "@axis-core-1.0.1-test/modules/Auction/MockBatchAuctionModule.sol"; + +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.1/modules/Keycode.sol"; +import {Veecode} from "@axis-core-1.0.1/modules/Modules.sol"; + +import {BPOOLv1, Range, Position} from "@baseline/modules/BPOOL.v1.sol"; + +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; +import {FixedPointMathLib} from "@solmate-6.7.0/utils/FixedPointMathLib.sol"; + +abstract contract BaselineDTLHandler is BeforeAfter, Assertions { + /*////////////////////////////////////////////////////////////////////////// + HANDLER VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint256 internal lotCapacity; + + uint256 internal baselineCuratorFee_; + + BaselineAxisLaunch.CreateData internal _createData; + + address internal sellerBaseline_; + + int24 internal _ANCHOR_TICK_WIDTH; + int24 internal _DISCOVERY_TICK_WIDTH; + uint24 internal _FLOOR_RESERVES_PERCENT; + int24 internal _FLOOR_RANGE_GAP; + int24 internal _ANCHOR_TICK_U; + uint24 internal _POOL_PERCENT; + + /*////////////////////////////////////////////////////////////////////////// + TARGET FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + function baselineDTL_createLot() public { + // PRE-CONDITIONS + if (_dtlBaseline.lotId() != type(uint96).max) return; + sellerBaseline_ = _SELLER; + + lotCapacity = 10 ether; + + _ANCHOR_TICK_WIDTH = int24(int256(bound(uint256(int256(_ANCHOR_TICK_WIDTH)), 10, 50))); + _DISCOVERY_TICK_WIDTH = int24(int256(bound(uint256(int256(_DISCOVERY_TICK_WIDTH)), 1, 500))); + _FLOOR_RESERVES_PERCENT = uint24(bound(uint256(_FLOOR_RESERVES_PERCENT), 10e2, 90e2)); + _FLOOR_RANGE_GAP = int24(int256(bound(uint256(int256(_FLOOR_RANGE_GAP)), 0, 500))); + _ANCHOR_TICK_U = _baselineToken.getActiveTS(); + _POOL_PERCENT = 100e2; + + _createData = BaselineAxisLaunch.CreateData({ + recipient: sellerBaseline_, + poolPercent: 87e2, //_POOL_PERCENT, + floorReservesPercent: 50e2, //_FLOOR_RESERVES_PERCENT, + floorRangeGap: 0, //_FLOOR_RANGE_GAP, + anchorTickU: 11_000, //_ANCHOR_TICK_U, + anchorTickWidth: 10, //_ANCHOR_TICK_WIDTH, + allowlistParams: abi.encode("") + }); + + IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ + start: uint48(block.timestamp), + duration: 1 days, + capacityInQuote: false, + capacity: _scaleBaseTokenAmount(lotCapacity), + implParams: abi.encode(_fpbParams) + }); + + __before(_lotId, sellerBaseline_, _dtlBaselineAddress); + + // ACTION + vm.prank(address(_baselineAuctionHouse)); + try _fpbModule.auction(_lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals) {} + catch { + assert(false); + } + + _baselineAuctionHouse.setLotCounter(_lotId + 1); + _baselineAuctionHouse.setAuctionReference(_lotId, _fpbModule.VEECODE()); + + vm.prank(address(_baselineAuctionHouse)); + try _dtlBaseline.onCreate( + _lotId, + sellerBaseline_, + address(_baselineToken), + address(_quoteToken), + _scaleBaseTokenAmount(lotCapacity), + true, + abi.encode(_createData) + ) { + // POST-CONDITIONS + __after(_lotId, sellerBaseline_, _dtlBaselineAddress); + + _assertBaselineTokenBalances(); + } catch { + assert(false); + } + } + + struct OnCancelBaselineTemps { + address sender; + uint96 lotId; + } + + function baselineDTL_onCancel(uint256 senderIndexSeed, uint256 lotIndexSeed) public { + // PRE-CONDTIONS + if (_dtlBaseline.lotId() == type(uint96).max) return; + OnCancelBaselineTemps memory d; + d.sender = randomAddress(senderIndexSeed); + + __before(_lotId, sellerBaseline_, _dtlBaselineAddress); + + vm.prank(address(_baselineAuctionHouse)); + _baselineToken.transfer(_dtlBaselineAddress, lotCapacity); + + // ACTION + vm.prank(address(_baselineAuctionHouse)); + try _dtlBaseline.onCancel(_lotId, _scaleBaseTokenAmount(lotCapacity), true, abi.encode("")) + { + // POST-CONDITIONS + __after(_lotId, sellerBaseline_, _dtlBaselineAddress); + + equal( + _after.baselineTotalSupply, + 0 + baselineCuratorFee_, + "AX-36: Baseline token total supply after _onCancel should equal 0" + ); + + equal( + _dtlBaseline.auctionComplete(), + true, + "AX-37: BaselineDTL_onCancel should mark auction completed" + ); + + equal( + _after.dtlBaselineBalance, + _before.dtlBaselineBalance, + "AX-38: When calling BaselineDTL_onCancel DTL base token balance should equal 0" + ); + + equal( + _baselineToken.balanceOf(address(_baselineToken)), + 0, + "AX-39: When calling BaselineDTL_onCancel baseline contract base token balance should equal 0" + ); + } catch (bytes memory err) { + bytes4[3] memory errors = [ + BaselineAxisLaunch.Callback_MissingFunds.selector, + BaselineAxisLaunch.Callback_AlreadyComplete.selector, + BaseCallback.Callback_InvalidParams.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnCurateBaselineTemps { + address sender; + uint96 lotId; + } + + function baselineDTL_onCurate( + uint256 senderIndexSeed, + uint256 lotIndexSeed, + uint256 curatorFee_ + ) public { + // PRE-CONDTIONS + if (_dtlBaseline.lotId() == type(uint96).max) return; + OnCurateBaselineTemps memory d; + d.sender = randomAddress(senderIndexSeed); + + __before(_lotId, sellerBaseline_, _dtlBaselineAddress); + + // curatorFee_ = bound(curatorFee_, 0, 5e18); + curatorFee_ = 0; + + // ACTION + vm.prank(address(_baselineAuctionHouse)); + try _dtlBaseline.onCurate(_lotId, curatorFee_, true, abi.encode("")) { + // POST-CONDITIONS + __after(_lotId, sellerBaseline_, _dtlBaselineAddress); + + equal( + _after.auctionHouseBaselineBalance, + _before.auctionHouseBaselineBalance + curatorFee_, + "AX-40: BaselineDTL_onCurate should credit auction house correct base token fees" + ); + + baselineCuratorFee_ += curatorFee_; + } catch (bytes memory err) { + bytes4[3] memory errors = [ + BaselineAxisLaunch.Callback_MissingFunds.selector, + BaselineAxisLaunch.Callback_AlreadyComplete.selector, + BaseCallback.Callback_InvalidParams.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnSettleBaselineTemps { + address sender; + uint96 lotId; + } + + function baselineDTL_onSettle( + uint256 senderIndexSeed, + uint256 lotIndexSeed, + uint256 proceeds_, + uint256 refund_ + ) public { + // PRE-CONDTIONS + if (_dtlBaseline.lotId() == type(uint96).max) return; + refund_ = ((lotCapacity * 4e2) / 100e2); + uint256 proceeds = lotCapacity - refund_; + + OnSettleBaselineTemps memory d; + d.sender = randomAddress(senderIndexSeed); + + __before(_lotId, sellerBaseline_, _dtlBaselineAddress); + + givenAddressHasQuoteTokenBalance(_dtlBaselineAddress, _PROCEEDS_AMOUNT); + _transferBaselineTokenRefund(_REFUND_AMOUNT); + + // ACTION + vm.prank(address(_baselineAuctionHouse)); + try _dtlBaseline.onSettle( + _lotId, _PROCEEDS_AMOUNT, _scaleBaseTokenAmount(_REFUND_AMOUNT), abi.encode("") + ) { + // POST-CONDITIONS + __after(_lotId, sellerBaseline_, _dtlBaselineAddress); + + _assertQuoteTokenBalances(); + _assertBaseTokenBalances(); + _assertCirculatingSupply(); + _assertAuctionComplete(); + _assertPoolReserves(); + } catch (bytes memory err) { + bytes4[3] memory errors = [ + BaselineAxisLaunch.Callback_MissingFunds.selector, + BaselineAxisLaunch.Callback_AlreadyComplete.selector, + BaseCallback.Callback_InvalidParams.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + function _scaleBaseTokenAmount(uint256 amount_) internal view returns (uint256) { + return FixedPointMathLib.mulDivDown(amount_, 10 ** _baseTokenDecimals, _BASE_SCALE); + } + + function _getRangeBAssets(Range range_) internal returns (uint256) { + Position memory position = _baselineToken.getPosition(range_); + + return position.bAssets; + } + + function _getRangeReserves(Range range_) internal returns (uint256) { + Position memory position = _baselineToken.getPosition(range_); + + return position.reserves; + } + + function _assertBaselineTokenBalances() internal { + equal( + _after.sellerBaselineBalance, + _before.sellerBaselineBalance, + "AX-09: DTL Callbacks should not change seller base token balance" + ); + equal( + _after.dtlBaselineBalance, + _before.dtlBaselineBalance, + "AX-10: DTL Callbacks should not change dtl base token balance" + ); + equal( + _after.auctionHouseBaselineBalance, + _scaleBaseTokenAmount(lotCapacity), + "AX-32: When calling BaselineDTL_createLot auction house base token balance should be equal to lot Capacity lotId" + ); + } + + function _assertQuoteTokenBalances() internal { + equal( + _quoteToken.balanceOf(_dtlBaselineAddress), + 0, + "AX-17: After DTL_onSettle DTL Address quote token balance should equal 0" + ); + equal( + _quoteToken.balanceOf(address(_quoteToken)), + 0, + "AX-33: After DTL_onSettle quote token balance of quote token should equal 0" + ); + uint256 poolProceeds = _PROCEEDS_AMOUNT * _createData.poolPercent / 100e2; + equal( + _quoteToken.balanceOf(address(_baselineToken.pool())), + poolProceeds, + "AX-34: BaselineDTL_onSettle should credit baseline pool with correct quote token proceeds" + ); + equal( + _after.sellerQuoteBalance, + _before.sellerQuoteBalance + _PROCEEDS_AMOUNT - poolProceeds, + "AX-35: BaselineDTL_onSettle should credit seller quote token proceeds" + ); + } + + function _assertBaseTokenBalances() internal { + equal( + _baselineToken.balanceOf(_dtlBaselineAddress), + 0, + "AX-18: After DTL_onSettle DTL Address base token balance should equal 0" + ); + equal( + _baselineToken.balanceOf(address(_baselineToken)), + 0, + "AX-41: After BaselineDTL_onSettle baseline token base token balance should equal 0" + ); + + uint256 totalSupply = _baselineToken.totalSupply(); + + // No payout distributed to "bidders", so don't account for it here + uint256 spotSupply = totalSupply - _baselineToken.getPosition(Range.FLOOR).bAssets + - _baselineToken.getPosition(Range.ANCHOR).bAssets + - _baselineToken.getPosition(Range.DISCOVERY).bAssets; + + uint256 poolSupply = totalSupply - spotSupply; + + assertApproxEq( + _baselineToken.balanceOf(address(_baselineToken.pool())), + poolSupply, + 2, + "AX-42: After BaselineDTL_onSettle baseline pool base token balance should equal baseline pool supply" + ); + equal( + _baselineToken.balanceOf(sellerBaseline_), + 0, + "AX-43: After BaselineDTL_onSettle seller baseline token balance should equal 0" + ); + } + + function _assertCirculatingSupply() internal { + uint256 totalSupply = _baselineToken.totalSupply(); + + assertApproxEq( + totalSupply - _baselineToken.getPosition(Range.FLOOR).bAssets + - _baselineToken.getPosition(Range.ANCHOR).bAssets + - _baselineToken.getPosition(Range.DISCOVERY).bAssets - _credt.totalCreditIssued(), // totalCreditIssued would affect supply, totalCollateralized will not + lotCapacity - _REFUND_AMOUNT + baselineCuratorFee_, + 2, // There is a difference (rounding error?) of 2 + "AX-44: circulating supply should equal lot capacity plus curatorFee minus refund" + ); + } + + function _assertAuctionComplete() internal { + equal( + _dtlBaseline.auctionComplete(), + true, + "AX-45: BaselineDTL_onSettle should mark auction complete" + ); + } + + function _assertPoolReserves() internal { + uint256 poolProceeds = _PROCEEDS_AMOUNT * _createData.poolPercent / 100e2; + uint256 floorProceeds = poolProceeds * _createData.floorReservesPercent / 100e2; + assertApproxEq( + _getRangeReserves(Range.FLOOR), + floorProceeds, + 1, // There is a difference (rounding error?) of 1 + "AX-46: After BaselineDTL_onSettle floor reserves should equal floor proceeds" + ); + assertApproxEq( + _getRangeReserves(Range.ANCHOR), + poolProceeds - floorProceeds, + 1, // There is a difference (rounding error?) of 1 + "AX-47: After BaselineDTL_onSettle anchor reserves should equal pool proceeds - floor proceeds" + ); + equal( + _getRangeReserves(Range.DISCOVERY), + 0, + "AX-48: After BaselineDTL_onSettle discovery reserves should equal 0" + ); + + // BAssets deployed into the pool + equal( + _getRangeBAssets(Range.FLOOR), + 0, + "AX-49: After BaselineDTL_onSettle floor bAssets should equal 0" + ); + equal( + _getRangeBAssets(Range.ANCHOR), + 0, + "AX-50: After BaselineDTL_onSettle anchor bAssets should be greater than 0" + ); + gt( + _getRangeBAssets(Range.DISCOVERY), + 0, + "AX-51: After BaselineDTL_onSettle discovery bAssets should be greater than 0" + ); + } +} diff --git a/test/invariant/handlers/BaselinePoolHandler.sol b/test/invariant/handlers/BaselinePoolHandler.sol new file mode 100644 index 00000000..25bdd21c --- /dev/null +++ b/test/invariant/handlers/BaselinePoolHandler.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../helpers/BeforeAfter.sol"; +import {Assertions} from "../helpers/Assertions.sol"; + +import {IUniswapV3Pool} from + "../../../lib/baseline-v2/lib/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import {ISwapRouter} from "../modules/uniswapv3-periphery/interfaces/ISwapRouter.sol"; + +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; + +abstract contract BaselinePoolHandler is BeforeAfter, Assertions { + function BaselinePoolHandler_donate(uint256 tokenIndexSeed, uint256 amount) public { + address _token = tokenIndexSeed % 2 == 0 ? address(_quoteToken) : address(_baselineToken); + + MockERC20(_token).mint(address(this), amount); + MockERC20(_token).transfer(address(_baselineToken), amount); + } + + function BaselinePoolHandler_swapToken0(uint256 recipientIndexSeed, uint256 amountIn) public { + address recipient = randomAddress(recipientIndexSeed); + if (_quoteToken.balanceOf(recipient) < 1e14) return; + amountIn = bound(amountIn, 1e14, _quoteToken.balanceOf(recipient)); + + (address token0, address token1) = address(_baselineToken) < address(_quoteToken) + ? (address(_baselineToken), address(_quoteToken)) + : (address(_quoteToken), address(_baselineToken)); + + IUniswapV3Pool pool = IUniswapV3Pool(address(_baselineToken.pool())); + if (address(pool) == address(0)) return; + + vm.prank(recipient); + _quoteToken.approve(address(_v3SwapRouter), amountIn); + + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(_quoteToken), + tokenOut: address(_baselineToken), + fee: 500, + recipient: recipient, + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + + vm.prank(recipient); + try _v3SwapRouter.exactInputSingle(params) {} catch {} + } + + function BaselinePoolHandler_swapToken1(uint256 recipientIndexSeed, uint256 amountIn) public { + address recipient = randomAddress(recipientIndexSeed); + if (_baselineToken.balanceOf(recipient) < 1e14) return; + amountIn = bound(amountIn, 1e14, _baselineToken.balanceOf(recipient)); + + (address token0, address token1) = address(_baselineToken) < address(_quoteToken) + ? (address(_baselineToken), address(_quoteToken)) + : (address(_quoteToken), address(_baselineToken)); + + IUniswapV3Pool pool = IUniswapV3Pool(address(_baselineToken.pool())); + if (address(pool) == address(0)) return; + + vm.prank(recipient); + _baselineToken.approve(address(_v3SwapRouter), amountIn); + + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(_baselineToken), + tokenOut: address(_quoteToken), + fee: 500, + recipient: recipient, + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + + vm.prank(recipient); + try _v3SwapRouter.exactInputSingle(params) {} catch {} + } +} diff --git a/test/invariant/handlers/UniswapV2DTLHandler.sol b/test/invariant/handlers/UniswapV2DTLHandler.sol new file mode 100644 index 00000000..d0036705 --- /dev/null +++ b/test/invariant/handlers/UniswapV2DTLHandler.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../helpers/BeforeAfter.sol"; +import {Assertions} from "../helpers/Assertions.sol"; + +import {UniswapV2DirectToLiquidity} from "../../../../../src/callbacks/liquidity/UniswapV2DTL.sol"; +import {BaseDirectToLiquidity} from "../../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Pair.sol"; +import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; + +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.1/modules/Keycode.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {IAuction} from "@axis-core-1.0.1/interfaces/modules/IAuction.sol"; +import {ILinearVesting} from "@axis-core-1.0.1/interfaces/modules/derivatives/ILinearVesting.sol"; +import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Pair.sol"; +import {FixedPointMathLib} from "@solmate-6.7.0/utils/FixedPointMathLib.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; + +abstract contract UniswapV2DTLHandler is BeforeAfter, Assertions { + /*////////////////////////////////////////////////////////////////////////// + HANDLER VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint256 internal constant _MINIMUM_LIQUIDITY = 10 ** 3; + + uint96 internal _proceeds; + uint96 internal _refund; + uint96 internal _capacityUtilised; + uint96 internal _quoteTokensToDeposit; + uint96 internal _baseTokensToDeposit; + uint96 internal _curatorPayout; + uint256 internal _auctionPrice; + uint256 internal _quoteTokensDonated; + + uint24 internal _maxSlippage = 1; + + BaseDirectToLiquidity.OnCreateParams internal _dtlCreateParamsV2; + UniswapV2DirectToLiquidity.UniswapV2OnCreateParams internal _uniswapV2CreateParams; + + mapping(uint96 => bool) internal isLotFinished; + + mapping(uint96 => BaseDirectToLiquidity.OnCreateParams) internal lotIdCreationParams; + + /*////////////////////////////////////////////////////////////////////////// + TARGET FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + function uniswapV2DTL_createLot( + uint256 sellerIndexSeed, + uint24 poolPercent, + uint48 vestingStart, + uint48 vestingExpiry + ) public { + // PRE-CONDITIONS + if (lotIdsV2.length == 1) return; + address seller_ = randomAddress(sellerIndexSeed); + __before(0, seller_, _dtlV2Address); + + poolPercent = uint24(bound(uint256(poolPercent), 10e2, 100e2)); + vestingStart = uint48( + bound( + uint256(vestingStart), uint48(block.timestamp) + 2 days, uint256(type(uint48).max) + ) + ); + vestingExpiry = uint48( + bound(uint256(vestingStart), uint256(vestingStart + 1), uint256(type(uint48).max)) + ); + _maxSlippage = uint24(bound(uint256(_maxSlippage), 1, 100e2)); + + if (vestingStart == vestingExpiry) return; + + _uniswapV2CreateParams = + UniswapV2DirectToLiquidity.UniswapV2OnCreateParams({maxSlippage: _maxSlippage}); + + _dtlCreateParamsV2 = BaseDirectToLiquidity.OnCreateParams({ + poolPercent: 100e2, //poolPercent, + vestingStart: vestingStart, + vestingExpiry: vestingExpiry, + recipient: seller_, + implParams: abi.encode(_uniswapV2CreateParams) + }); + + // Mint and approve the capacity to the owner + _baseToken.mint(seller_, _LOT_CAPACITY); + vm.prank(seller_); + _baseToken.approve(address(_auctionHouse), _LOT_CAPACITY); + + // Prep the lot arguments + IAuctionHouse.RoutingParams memory routingParams = IAuctionHouse.RoutingParams({ + auctionType: keycodeFromVeecode(_batchAuctionModule.VEECODE()), + baseToken: address(_baseToken), + quoteToken: address(_quoteToken), + referrerFee: 0, // No referrer fee + curator: address(0), + callbacks: _dtlV2, + callbackData: abi.encode(_dtlCreateParamsV2), + derivativeType: toKeycode(""), + derivativeParams: abi.encode(""), + wrapDerivative: false + }); + + IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ + start: uint48(block.timestamp) + 1, + duration: 1 days, + capacityInQuote: false, + capacity: _LOT_CAPACITY, + implParams: abi.encode("") + }); + + // Create a new lot + vm.prank(seller_); + try _auctionHouse.auction(routingParams, auctionParams, "") returns (uint96 lotIdCreated) { + // POST-CONDITIONS + __after(lotIdCreated, seller_, _dtlV2Address); + + equal( + _after.dtlConfigV2.recipient, + seller_, + "AX-01: UniswapV2Dtl_onCreate() should set DTL Config recipient" + ); + equal( + _after.dtlConfigV2.lotCapacity, + _LOT_CAPACITY, + "AX-02: UniswapV2Dtl_onCreate() should set DTL Config lotCapacity" + ); + equal( + _after.dtlConfigV2.lotCuratorPayout, + 0, + "AX-03: UniswapV2Dtl_onCreate() should set DTL Config lotCuratorPayout" + ); + equal( + _after.dtlConfigV2.poolPercent, + _dtlCreateParamsV2.poolPercent, + "AX-04: UniswapV2Dtl_onCreate() should set DTL Config poolPercent" + ); + equal( + _after.dtlConfigV2.vestingStart, + vestingStart, + "AX-05: UniswapV2Dtl_onCreate() should set DTL Config vestingStart" + ); + equal( + _after.dtlConfigV2.vestingExpiry, + vestingExpiry, + "AX-06: UniswapV2Dtl_onCreate() should set DTL Config vestingExpiry" + ); + equal( + address(_after.dtlConfigV2.linearVestingModule), + vestingStart == 0 ? address(0) : address(_linearVesting), + "AX-07: UniswapV2Dtl_onCreate() should set DTL Config linearVestingModule" + ); + equal( + _after.dtlConfigV2.active, + true, + "AX-08: UniswapV2Dtl_onCreate() should set DTL Config active" + ); + + // Assert balances + _assertBaseTokenBalancesV2(); + + lotIdsV2.push(lotIdCreated); + lotIdCreationParams[lotIdCreated] = _dtlCreateParamsV2; + } catch (bytes memory err) { + bytes4[1] memory errors = [BaseDirectToLiquidity.Callback_Params_PoolExists.selector]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnCancelTemps { + address seller; + address sender; + uint96 lotId; + } + + function uniswapV2DTL_onCancel(uint256 senderIndexSeed, uint256 lotIndexSeed) public { + // PRE-CONDITIONS + OnCancelTemps memory d; + d.sender = randomAddress(senderIndexSeed); + d.lotId = randomLotIdV2(lotIndexSeed); + (d.seller,,,,,,,,) = _auctionHouse.lotRouting(d.lotId); + + __before(d.lotId, d.seller, _dtlV2Address); + if (_before.seller == address(0)) return; + + // ACTION + vm.prank(address(_auctionHouse)); + try _dtlV2.onCancel(d.lotId, _REFUND_AMOUNT, false, abi.encode("")) { + // POST-CONDITIONS + __after(d.lotId, d.seller, _dtlV2Address); + + equal( + _after.dtlConfigV2.active, + false, + "AX-11: DTL_onCancel() should set DTL Config active to false" + ); + + _assertBaseTokenBalancesV2(); + + isLotFinished[d.lotId] = true; + } catch (bytes memory err) { + bytes4[2] memory errors = [ + BaseDirectToLiquidity.Callback_Params_PoolExists.selector, + BaseDirectToLiquidity.Callback_AlreadyComplete.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnCurateTemps { + address seller; + address sender; + uint96 lotId; + } + + function uniswapV2DTL_onCurate(uint256 senderIndexSeed, uint256 lotIndexSeed) public { + // PRE-CONDITIONS + OnCurateTemps memory d; + d.sender = randomAddress(senderIndexSeed); + d.lotId = randomLotIdV2(lotIndexSeed); + (d.seller,,,,,,,,) = _auctionHouse.lotRouting(d.lotId); + + __before(d.lotId, d.seller, _dtlV2Address); + if (_before.seller == address(0)) return; + + // ACTION + vm.prank(address(_auctionHouse)); + try _dtlV2.onCurate(d.lotId, _PAYOUT_AMOUNT, false, abi.encode("")) { + // POST-CONDITIONS + __after(d.lotId, d.seller, _dtlV2Address); + + equal( + _after.dtlConfigV2.lotCuratorPayout, + _PAYOUT_AMOUNT, + "AX-12: DTL_onCurate should set DTL Config lotCuratorPayout" + ); + + equal( + _after.auctionHouseBaseBalance, + (_LOT_CAPACITY * lotIdsV2.length) + (_LOT_CAPACITY * lotIdsV3.length), + "AX-13: When calling DTL_onCurate auction house base token balance should be equal to lot Capacity of each lotId" + ); + + _assertBaseTokenBalancesV2(); + + _curatorPayout = _PAYOUT_AMOUNT; + } catch (bytes memory err) { + bytes4[2] memory errors = [ + BaseDirectToLiquidity.Callback_Params_PoolExists.selector, + BaseDirectToLiquidity.Callback_AlreadyComplete.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnSettleTemps { + address seller; + address sender; + uint96 lotId; + } + + function uniswapV2DTL_onSettle( + uint256 senderIndexSeed, + uint256 lotIndexSeed, + uint96 proceeds_, + uint96 refund + ) public { + // PRE-CONDITIONS + proceeds_ = uint96(bound(uint256(proceeds_), 2e18, 10e18)); + refund = uint96(bound(uint256(refund), 0, 1e18)); + + address pairAddress = _uniV2Factory.getPair(address(_baseToken), address(_quoteToken)); + if (pairAddress == address(0)) { + _createPool(); + } + + OnSettleTemps memory d; + d.sender = randomAddress(senderIndexSeed); + d.lotId = randomLotIdV2(lotIndexSeed); + (d.seller,,,,,,,,) = _auctionHouse.lotRouting(d.lotId); + + setV2CallbackParameters(d.lotId, proceeds_, refund); + + __before(d.lotId, d.seller, _dtlV2Address); + if (_before.seller == address(0)) return; + if (isLotFinished[d.lotId] == true) return; + + givenAddressHasQuoteTokenBalance(_dtlV2Address, _quoteTokensToDeposit); + givenAddressHasBaseTokenBalance(_before.seller, _baseTokensToDeposit); + givenAddressHasBaseTokenAllowance(_before.seller, _dtlV2Address, _baseTokensToDeposit); + + // ACTION + vm.prank(address(_auctionHouse)); + try _dtlV2.onSettle(d.lotId, _proceeds, _refund, abi.encode("")) { + // POST-CONDITIONS + __after(d.lotId, d.seller, _dtlV2Address); + + _assertLpTokenBalance(d.lotId, d.seller); + if (_before.dtlConfigV2.vestingStart != 0) { + _assertVestingTokenBalance(d.lotId, d.seller); + } + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + + isLotFinished[d.lotId] = true; + } catch (bytes memory err) { + bytes4[1] memory errors = [BaseDirectToLiquidity.Callback_InsufficientBalance.selector]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } catch Error(string memory reason) { + string[3] memory stringErrors = [ + "UniswapV2Library: INSUFFICIENT_LIQUIDITY", + "UniswapV2Router: INSUFFICIENT_A_AMOUNT", + "UniswapV2Router: INSUFFICIENT_B_AMOUNT" + ]; + for (uint256 i = 0; i < stringErrors.length; i++) { + if (compareStrings(stringErrors[i], reason)) { + t( + false, + "AX-52: UniswapV2DTL_onSettle should not fail with 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'" + ); + } + } + } + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + function _createPool() internal returns (address) { + return _uniV2Factory.createPair(address(_quoteToken), address(_baseToken)); + } + + function _getUniswapV2Pool() internal view returns (IUniswapV2Pair) { + return IUniswapV2Pair(_uniV2Factory.getPair(address(_quoteToken), address(_baseToken))); + } + + function setV2CallbackParameters(uint96 lotId, uint96 proceeds_, uint96 refund_) internal { + _proceeds = proceeds_; + _refund = refund_; + + // Calculate the capacity utilised + // Any unspent curator payout is included in the refund + // However, curator payouts are linear to the capacity utilised + // Calculate the percent utilisation + uint96 capacityUtilisationPercent = 100e2 + - uint96(FixedPointMathLib.mulDivDown(_refund, 100e2, _LOT_CAPACITY + _curatorPayout)); + _capacityUtilised = _LOT_CAPACITY * capacityUtilisationPercent / 100e2; + + // The proceeds utilisation percent scales the quote tokens and base tokens linearly + _quoteTokensToDeposit = _proceeds * lotIdCreationParams[lotId].poolPercent / 100e2; + _baseTokensToDeposit = _capacityUtilised * lotIdCreationParams[lotId].poolPercent / 100e2; + + _auctionPrice = _proceeds * 10 ** _baseToken.decimals() / (_LOT_CAPACITY - _refund); + } + + function _assertBaseTokenBalancesV2() internal { + equal( + _after.sellerBaseBalance, + _before.sellerBaseBalance, + "AX-09: DTL Callbacks should not change seller base token balance" + ); + equal( + _after.dtlBaseBalance, + _before.dtlBaseBalance, + "AX-10: DTL Callbacks should not change dtl base token balance" + ); + } + + function _assertLpTokenBalance(uint96 lotId, address seller) internal { + // Get the pools deployed by the DTL callback + IUniswapV2Pair pool = _getUniswapV2Pool(); + + // Exclude the LP token balance on this contract + uint256 testBalance = pool.balanceOf(address(this)); + + uint256 sellerExpectedBalance; + uint256 linearVestingExpectedBalance; + // Only has a balance if not vesting + if (lotIdCreationParams[lotId].vestingStart == 0) { + sellerExpectedBalance = pool.totalSupply() - testBalance - _MINIMUM_LIQUIDITY; + } else { + linearVestingExpectedBalance = pool.totalSupply() - testBalance - _MINIMUM_LIQUIDITY; + } + + equal( + pool.balanceOf(seller), + lotIdCreationParams[lotId].recipient == seller ? sellerExpectedBalance : 0, + "AX-14: DTL_onSettle should should credit seller the expected LP token balance" + ); + equal( + pool.balanceOf(address(_linearVesting)), + linearVestingExpectedBalance, + "AX-15: DTL_onSettle should should credit linearVestingModule the expected LP token balance" + ); + } + + function _assertVestingTokenBalance(uint96 lotId, address seller) internal { + // Exit if not vesting + if (lotIdCreationParams[lotId].vestingStart == 0) { + return; + } + + // Get the pools deployed by the DTL callback + address pool = address(_getUniswapV2Pool()); + + // Get the wrapped address + (, address wrappedVestingTokenAddress) = _linearVesting.deploy( + pool, + abi.encode( + ILinearVesting.VestingParams({ + start: lotIdCreationParams[lotId].vestingStart, + expiry: lotIdCreationParams[lotId].vestingExpiry + }) + ), + true + ); + ERC20 wrappedVestingToken = ERC20(wrappedVestingTokenAddress); + uint256 sellerExpectedBalance = wrappedVestingToken.totalSupply(); + + equal( + wrappedVestingToken.balanceOf(seller), + sellerExpectedBalance, + "AX-16: DTL_onSettle should should credit seller the expected wrapped vesting token balance" + ); + } + + function _assertQuoteTokenBalance() internal { + equal( + _quoteToken.balanceOf(_dtlV2Address), + 0, + "AX-17: After DTL_onSettle DTL Address quote token balance should equal 0" + ); + } + + function _assertBaseTokenBalance() internal { + equal( + _baseToken.balanceOf(_dtlV2Address), + 0, + "AX-18: After DTL_onSettle DTL Address base token balance should equal 0" + ); + } + + function _assertApprovals() internal { + // Ensure there are no dangling approvals + equal( + _quoteToken.allowance(_dtlV2Address, address(_uniV2Router)), + 0, + "AX-19: After UniswapV2DTL_onSettle DTL Address quote token allowance for the UniswapV2 Router should equal 0" + ); + equal( + _baseToken.allowance(_dtlV2Address, address(_uniV2Router)), + 0, + "AX-20: After UniswapV2DTL_onSettle DTL Address base token allowance for the UniswapV2 Router should equal 0" + ); + } +} diff --git a/test/invariant/handlers/UniswapV3DTLHandler.sol b/test/invariant/handlers/UniswapV3DTLHandler.sol new file mode 100644 index 00000000..3dad5d7d --- /dev/null +++ b/test/invariant/handlers/UniswapV3DTLHandler.sol @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../helpers/BeforeAfter.sol"; +import {Assertions} from "../helpers/Assertions.sol"; + +import {Callbacks} from "@axis-core-1.0.1/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.1-test/lib/permit2/Permit2User.sol"; + +import {IAuction} from "@axis-core-1.0.1/interfaces/modules/IAuction.sol"; +import {IAuctionHouse} from "@axis-core-1.0.1/interfaces/IAuctionHouse.sol"; +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; +import {ILinearVesting} from "@axis-core-1.0.1/interfaces/modules/derivatives/ILinearVesting.sol"; + +import {GUniFactory} from "@g-uni-v1-core-0.9.9/GUniFactory.sol"; +import {GUniPool} from "@g-uni-v1-core-0.9.9/GUniPool.sol"; +import {IUniswapV3Pool} from + "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; +import {ISwapRouter} from "../modules/uniswapv3-periphery/interfaces/ISwapRouter.sol"; +import {IUniswapV3Factory} from + "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Factory.sol"; +import {SqrtPriceMath} from "../../../../src/lib/uniswap-v3/SqrtPriceMath.sol"; + +import {BaseDirectToLiquidity} from "../../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {BaseCallback} from "@axis-core-1.0.1/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {UniswapV3DirectToLiquidity} from "../../../../src/callbacks/liquidity/UniswapV3DTL.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; +import {MockBatchAuctionModule} from + "@axis-core-1.0.1-test/modules/Auction/MockBatchAuctionModule.sol"; + +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.1/modules/Keycode.sol"; + +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; +import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; +import {FixedPointMathLib} from "@solmate-6.7.0/utils/FixedPointMathLib.sol"; + +abstract contract UniswapV3DTLHandler is BeforeAfter, Assertions { + /*////////////////////////////////////////////////////////////////////////// + HANDLER VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + uint24 internal _poolFee = 500; + uint96 internal constant _PROCEEDS = 20e18; + uint96 internal constant _REFUND = 0; + + uint96 internal _proceedsV3; + uint96 internal _refundV3; + uint96 internal _capacityUtilisedV3; + uint96 internal _quoteTokensToDepositV3; + uint96 internal _baseTokensToDepositV3; + uint96 internal _curatorPayoutV3; + uint24 internal _maxSlippageV3 = 1; + uint256 internal _additionalQuoteTokensMinted; + + uint160 internal _sqrtPriceX96; + + BaseDirectToLiquidity.OnCreateParams internal _dtlCreateParamsV3; + UniswapV3DirectToLiquidity.UniswapV3OnCreateParams internal _uniswapV3CreateParams; + + mapping(uint96 => bool) internal isLotFinishedV3; + + mapping(uint96 => BaseDirectToLiquidity.OnCreateParams) internal lotIdCreationParamsV3; + + /*////////////////////////////////////////////////////////////////////////// + TARGET FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + function uniswapV3DTL_createLot( + uint256 sellerIndexSeed, + uint24 poolPercent, + uint48 vestingStart, + uint48 vestingExpiry + ) public { + // PRE-CONDITIONS + address seller_ = randomAddress(sellerIndexSeed); + + __before(0, seller_, _dtlV3Address); + + poolPercent = uint24(bound(uint256(poolPercent), 10e2, 100e2)); + vestingStart = uint48( + bound( + uint256(vestingStart), uint48(block.timestamp) + 2 days, uint256(type(uint48).max) + ) + ); + vestingExpiry = uint48( + bound(uint256(vestingStart), uint256(vestingStart + 1), uint256(type(uint48).max)) + ); + _maxSlippageV3 = uint24(bound(uint256(_maxSlippageV3), 1, 100e2)); + + if (vestingStart == vestingExpiry) return; + + _uniswapV3CreateParams = UniswapV3DirectToLiquidity.UniswapV3OnCreateParams({ + poolFee: _poolFee, + maxSlippage: _maxSlippageV3 // 0.01%, to handle rounding errors + }); + + _dtlCreateParamsV3 = BaseDirectToLiquidity.OnCreateParams({ + poolPercent: poolPercent, + vestingStart: vestingStart, + vestingExpiry: vestingExpiry, + recipient: seller_, + implParams: abi.encode(_uniswapV3CreateParams) + }); + + // Mint and approve the capacity to the owner + _baseToken.mint(seller_, _LOT_CAPACITY); + vm.prank(seller_); + _baseToken.approve(address(_auctionHouse), _LOT_CAPACITY); + + // Prep the lot arguments + IAuctionHouse.RoutingParams memory routingParams = IAuctionHouse.RoutingParams({ + auctionType: keycodeFromVeecode(_batchAuctionModule.VEECODE()), + baseToken: address(_baseToken), + quoteToken: address(_quoteToken), + referrerFee: 0, // No referrer fee + curator: address(0), + callbacks: _dtlV3, + callbackData: abi.encode(_dtlCreateParamsV3), + derivativeType: toKeycode(""), + derivativeParams: abi.encode(""), + wrapDerivative: false + }); + + IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ + start: uint48(block.timestamp) + 1, + duration: 1 days, + capacityInQuote: false, + capacity: _LOT_CAPACITY, + implParams: abi.encode("") + }); + + // Create a new lot + vm.prank(seller_); + try _auctionHouse.auction(routingParams, auctionParams, "") returns (uint96 lotIdCreated) { + // POST-CONDITIONS + __after(lotIdCreated, seller_, _dtlV2Address); + + equal( + _after.dtlConfigV3.recipient, + seller_, + "AX-21: UniswapV3Dtl_onCreate() should set DTL Config recipient" + ); + equal( + _after.dtlConfigV3.lotCapacity, + _LOT_CAPACITY, + "AX-22: UniswapV3Dtl_onCreate() should set DTL Config lotCapacity" + ); + equal( + _after.dtlConfigV3.lotCuratorPayout, + 0, + "AX-23: UniswapV3Dtl_onCreate() should set DTL Config lotCuratorPayout" + ); + equal( + _after.dtlConfigV3.poolPercent, + _dtlCreateParamsV3.poolPercent, + "AX-24: UniswapV3Dtl_onCreate() should set DTL Config poolPercent" + ); + equal( + _after.dtlConfigV3.vestingStart, + vestingStart, + "AX-25: UniswapV3Dtl_onCreate() should set DTL Config vestingStart" + ); + equal( + _after.dtlConfigV3.vestingExpiry, + vestingExpiry, + "AX-26: UniswapV3Dtl_onCreate() should set DTL Config vestingExpiry" + ); + equal( + address(_after.dtlConfigV3.linearVestingModule), + vestingStart == 0 ? address(0) : address(_linearVesting), + "AX-27: UniswapV3Dtl_onCreate() should set DTL Config linearVestingModule" + ); + equal( + _after.dtlConfigV3.active, + true, + "AX-28: UniswapV3Dtl_onCreate() should set DTL Config active to true" + ); + + _assertBaseTokenBalancesV3(); + + lotIdsV3.push(lotIdCreated); + lotIdCreationParamsV3[lotIdCreated] = _dtlCreateParamsV3; + } catch (bytes memory err) { + bytes4[1] memory errors = [BaseDirectToLiquidity.Callback_Params_PoolExists.selector]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnCancelV3Temps { + address seller; + address sender; + uint96 lotId; + } + + function uniswapV3DTL_onCancel(uint256 senderIndexSeed, uint256 lotIndexSeed) public { + // PRE-CONDITIONS + OnCancelV3Temps memory d; + d.sender = randomAddress(senderIndexSeed); + d.lotId = randomLotIdV3(lotIndexSeed); + (d.seller,,,,,,,,) = _auctionHouse.lotRouting(d.lotId); + + __before(d.lotId, d.seller, _dtlV3Address); + if (_before.seller == address(0)) return; + + // ACTION + vm.prank(address(_auctionHouse)); + try _dtlV3.onCancel(d.lotId, _REFUND_AMOUNT, false, abi.encode("")) { + // POST-CONDITIONS + __after(d.lotId, d.seller, _dtlV3Address); + + equal( + _after.dtlConfigV3.active, + false, + "AX-11: DTL_onCancel() should set DTL Config active to false" + ); + + _assertBaseTokenBalancesV3(); + + isLotFinishedV3[d.lotId] = true; + } catch (bytes memory err) { + bytes4[2] memory errors = [ + BaseDirectToLiquidity.Callback_Params_PoolExists.selector, + BaseDirectToLiquidity.Callback_AlreadyComplete.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnCurateV3Temps { + address seller; + address sender; + uint96 lotId; + } + + function uniswapV3DTL_onCurate(uint256 senderIndexSeed, uint256 lotIndexSeed) public { + // PRE-CONDITIONS + OnCurateV3Temps memory d; + d.sender = randomAddress(senderIndexSeed); + d.lotId = randomLotIdV3(lotIndexSeed); + (d.seller,,,,,,,,) = _auctionHouse.lotRouting(d.lotId); + + __before(d.lotId, d.seller, _dtlV3Address); + if (_before.seller == address(0)) return; + + // ACTION + vm.prank(address(_auctionHouse)); + try _dtlV3.onCurate(d.lotId, _PAYOUT_AMOUNT, false, abi.encode("")) { + // POST-CONDITIONS + __after(d.lotId, d.seller, _dtlV3Address); + + equal( + _after.dtlConfigV3.lotCuratorPayout, + _PAYOUT_AMOUNT, + "AX-12: DTL_onCurate should set DTL Config lotCuratorPayout" + ); + + equal( + _after.auctionHouseBaseBalance, + (_LOT_CAPACITY * lotIdsV2.length) + (_LOT_CAPACITY * lotIdsV3.length), + "AX-13: When calling DTL_onCurate auction house base token balance should be equal to lot Capacity of each lotId" + ); + + _assertBaseTokenBalancesV3(); + + _capacityUtilisedV3 = _PAYOUT_AMOUNT; + } catch (bytes memory err) { + bytes4[2] memory errors = [ + BaseDirectToLiquidity.Callback_Params_PoolExists.selector, + BaseDirectToLiquidity.Callback_AlreadyComplete.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + struct OnSettleV3Temps { + address seller; + address sender; + uint96 lotId; + } + + function uniswapV3DTL_onSettle( + uint256 senderIndexSeed, + uint256 lotIndexSeed, + uint96 proceeds_, + uint96 refund + ) public { + // PRE-CONDITIONS + proceeds_ = uint96(bound(uint256(proceeds_), 2e18, 10e18)); + refund = uint96(bound(uint256(refund), 0, 1e18)); + _maxSlippageV3 = uint24(bound(uint256(_maxSlippageV3), 1, 100e2)); + + OnSettleV3Temps memory d; + d.sender = randomAddress(senderIndexSeed); + d.lotId = randomLotIdV3(lotIndexSeed); + (d.seller,,,,,,,,) = _auctionHouse.lotRouting(d.lotId); + + address pool = _createV3Pool(); + setV3CallbackParameters(d.lotId, _PROCEEDS, _REFUND); + // _initializePool(pool, _sqrtPriceX96); + + __before(d.lotId, d.seller, _dtlV3Address); + if (_before.seller == address(0)) return; + if (isLotFinishedV3[d.lotId] == true) return; + + givenAddressHasQuoteTokenBalance(_dtlV3Address, _proceedsV3); + givenAddressHasBaseTokenBalance(_before.seller, _capacityUtilisedV3); + givenAddressHasBaseTokenAllowance(_before.seller, _dtlV3Address, _capacityUtilisedV3); + + // ACTION + vm.prank(address(_auctionHouse)); + try _dtlV3.onSettle(d.lotId, _proceedsV3, _refundV3, abi.encode("")) { + // POST-CONDITIONS + __after(d.lotId, d.seller, _dtlV3Address); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalanceV3(d.lotId, d.seller); + if (_before.dtlConfigV3.vestingStart != 0) { + _assertVestingTokenBalanceV3(d.lotId, d.seller); + } + _assertQuoteTokenBalanceV3(); + _assertBaseTokenBalanceV3(); + _assertApprovalsV3(); + + isLotFinishedV3[d.lotId] = true; + } catch (bytes memory err) { + bytes4[2] memory errors = [ + UniswapV3DirectToLiquidity.Callback_Slippage.selector, + BaseDirectToLiquidity.Callback_InsufficientBalance.selector + ]; + bool expected = false; + for (uint256 i = 0; i < errors.length; i++) { + if (errors[i] == bytes4(err)) { + expected = true; + break; + } + } + assert(expected); + return; + } + } + + /*////////////////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////////////////*/ + + function _createV3Pool() internal returns (address) { + (address token0, address token1) = address(_baseToken) < address(_quoteToken) + ? (address(_baseToken), address(_quoteToken)) + : (address(_quoteToken), address(_baseToken)); + + return _uniV3Factory.createPool(token0, token1, _poolFee); + } + + function _getGUniPool() internal view returns (GUniPool) { + // Get the pools deployed by the DTL callback + address[] memory pools = _gUniFactory.getPools(_dtlV3Address); + + return GUniPool(pools[0]); + } + + function _initializePool(address pool_, uint160 sqrtPriceX96_) internal { + IUniswapV3Pool(pool_).initialize(sqrtPriceX96_); + } + + function _calculateSqrtPriceX96( + uint256 quoteTokenAmount_, + uint256 baseTokenAmount_ + ) internal view returns (uint160) { + return SqrtPriceMath.getSqrtPriceX96( + address(_quoteToken), address(_baseToken), quoteTokenAmount_, baseTokenAmount_ + ); + } + + function setV3CallbackParameters(uint96 lotId, uint96 proceeds_, uint96 refund_) internal { + _proceedsV3 = proceeds_; + _refundV3 = refund_; + + // Calculate the capacity utilised + // Any unspent curator payout is included in the refund + // However, curator payouts are linear to the capacity utilised + // Calculate the percent utilisation + uint96 capacityUtilisationPercent = 100e2 + - uint96(FixedPointMathLib.mulDivDown(_refundV3, 100e2, _LOT_CAPACITY + _curatorPayoutV3)); + _capacityUtilisedV3 = _LOT_CAPACITY * capacityUtilisationPercent / 100e2; + + // The proceeds utilisation percent scales the quote tokens and base tokens linearly + _quoteTokensToDepositV3 = _proceedsV3 * lotIdCreationParamsV3[lotId].poolPercent / 100e2; + _baseTokensToDepositV3 = + _capacityUtilisedV3 * lotIdCreationParamsV3[lotId].poolPercent / 100e2; + + _sqrtPriceX96 = _calculateSqrtPriceX96(_quoteTokensToDepositV3, _baseTokensToDepositV3); + } + + function _getPool() internal view returns (address) { + (address token0, address token1) = address(_baseToken) < address(_quoteToken) + ? (address(_baseToken), address(_quoteToken)) + : (address(_quoteToken), address(_baseToken)); + return _uniV3Factory.getPool(token0, token1, _poolFee); + } + + function _assertBaseTokenBalancesV3() internal { + equal( + _after.sellerBaseBalance, + _before.sellerBaseBalance, + "AX-09: DTL Callbacks should not change seller base token balance" + ); + equal( + _after.dtlBaseBalance, + _before.dtlBaseBalance, + "AX-10: DTL Callbacks should not change dtl base token balance" + ); + } + + function _assertPoolState(uint160 sqrtPriceX96_) internal { + // Get the pool + address pool = _getPool(); + + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); + equal( + sqrtPriceX96, + sqrtPriceX96_, + "AX-29: On UniswapV3DTL_OnSettle() calculated sqrt price should equal pool sqrt price" + ); + } + + function _assertLpTokenBalanceV3(uint96 lotId, address seller) internal { + // Get the pools deployed by the DTL callback + GUniPool pool = _getGUniPool(); + + uint256 sellerExpectedBalance; + uint256 linearVestingExpectedBalance; + // Only has a balance if not vesting + if (lotIdCreationParamsV3[lotId].vestingStart == 0) { + sellerExpectedBalance = pool.totalSupply(); + } else { + linearVestingExpectedBalance = pool.totalSupply(); + } + + equal( + pool.balanceOf(seller), + lotIdCreationParamsV3[lotId].recipient == _SELLER ? sellerExpectedBalance : 0, + "AX-14: DTL_onSettle should should credit seller the expected LP token balance" + ); + equal( + pool.balanceOf(address(_linearVesting)), + linearVestingExpectedBalance, + "AX-15: DTL_onSettle should should credit linearVestingModule the expected LP token balance" + ); + } + + function _assertVestingTokenBalanceV3(uint96 lotId, address seller) internal { + // Exit if not vesting + if (lotIdCreationParamsV3[lotId].vestingStart == 0) { + return; + } + + // Get the pools deployed by the DTL callback + address pool = address(_getGUniPool()); + + // Get the wrapped address + (, address wrappedVestingTokenAddress) = _linearVesting.deploy( + pool, + abi.encode( + ILinearVesting.VestingParams({ + start: lotIdCreationParamsV3[lotId].vestingStart, + expiry: lotIdCreationParamsV3[lotId].vestingExpiry + }) + ), + true + ); + ERC20 wrappedVestingToken = ERC20(wrappedVestingTokenAddress); + uint256 sellerExpectedBalance = wrappedVestingToken.totalSupply(); + + equal( + wrappedVestingToken.balanceOf(seller), + sellerExpectedBalance, + "AX-16: DTL_onSettle should should credit seller the expected wrapped vesting token balance" + ); + } + + function _assertQuoteTokenBalanceV3() internal { + equal( + _quoteToken.balanceOf(_dtlV3Address), + 0, + "AX-17: After DTL_onSettle DTL Address quote token balance should equal 0" + ); + } + + function _assertBaseTokenBalanceV3() internal { + equal( + _baseToken.balanceOf(_dtlV3Address), + 0, + "AX-18: After DTL_onSettle DTL Address base token balance should equal 0" + ); + } + + function _assertApprovalsV3() internal { + // Ensure there are no dangling approvals + equal( + _quoteToken.allowance(_dtlV3Address, address(_getGUniPool())), + 0, + "AX-30: After UniswapV3DTL_onSettle DTL Address quote token allowance for GUniPool should equal 0" + ); + equal( + _baseToken.allowance(_dtlV3Address, address(_getGUniPool())), + 0, + "AX-31: After UniswapV3DTL_onSettle DTL Address base token allowance for GUniPool should equal 0" + ); + } +} diff --git a/test/invariant/handlers/V2PoolHandler.sol b/test/invariant/handlers/V2PoolHandler.sol new file mode 100644 index 00000000..a7d9bd1b --- /dev/null +++ b/test/invariant/handlers/V2PoolHandler.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../helpers/BeforeAfter.sol"; +import {Assertions} from "../helpers/Assertions.sol"; + +import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Pair.sol"; + +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; + +abstract contract V2PoolHandler is BeforeAfter, Assertions { + function V2PoolHandler_donate(uint256 tokenIndexSeed, uint256 amount) public { + // address _token = tokenIndexSeed % 2 == 0 ? address(_quoteToken) : address(_baseToken); + address _token = address(_quoteToken); + + address pairAddress = _uniV2Factory.getPair(address(_baseToken), address(_quoteToken)); + if (pairAddress == address(0)) { + pairAddress = _uniV2Factory.createPair(address(_baseToken), address(_quoteToken)); + } + IUniswapV2Pair pool = IUniswapV2Pair(pairAddress); + + amount = bound(amount, 1, 10_000 ether); + + MockERC20(_token).mint(address(this), amount); + MockERC20(_token).transfer(address(pool), amount); + } + + function V2PoolHandler_sync() public { + address pairAddress = _uniV2Factory.getPair(address(_baseToken), address(_quoteToken)); + if (pairAddress == address(0)) return; + IUniswapV2Pair pool = IUniswapV2Pair(pairAddress); + + pool.sync(); + } + + function V2PoolHandler_skim(uint256 userIndexSeed) public { + address to = randomAddress(userIndexSeed); + address pairAddress = _uniV2Factory.getPair(address(_baseToken), address(_quoteToken)); + if (pairAddress == address(0)) return; + IUniswapV2Pair pool = IUniswapV2Pair(pairAddress); + + pool.skim(to); + } + + function V2PoolHandler_swapToken0(uint256 senderIndexSeed, uint256 amount) public { + address to = randomAddress(senderIndexSeed); + if (_quoteToken.balanceOf(to) < 1e14) return; + amount = bound(amount, 1e14, _quoteToken.balanceOf(to)); + + IUniswapV2Pair pool = + IUniswapV2Pair(_uniV2Factory.getPair(address(_quoteToken), address(_baseToken))); + if (address(pool) == address(0)) return; + + vm.prank(to); + _quoteToken.approve(address(_uniV2Router), amount); + + address[] memory path = new address[](2); + path[0] = address(_quoteToken); + path[1] = address(_baseToken); + + vm.prank(to); + try _uniV2Router.swapExactTokensForTokens(amount, 0, path, to, block.timestamp) {} catch {} + } + + function V2PoolHandler_swapToken1(uint256 senderIndexSeed, uint256 amount) public { + address to = randomAddress(senderIndexSeed); + if (_baseToken.balanceOf(to) < 1e14) return; + amount = bound(amount, 1e14, _baseToken.balanceOf(to)); + + IUniswapV2Pair pool = + IUniswapV2Pair(_uniV2Factory.getPair(address(_quoteToken), address(_baseToken))); + if (address(pool) == address(0)) return; + + vm.prank(to); + _baseToken.approve(address(_uniV2Router), amount); + + address[] memory path = new address[](2); + path[0] = address(_baseToken); + path[1] = address(_quoteToken); + + vm.prank(to); + try _uniV2Router.swapExactTokensForTokens(amount, 0, path, to, block.timestamp) {} catch {} + } +} diff --git a/test/invariant/handlers/V3PoolHandler.sol b/test/invariant/handlers/V3PoolHandler.sol new file mode 100644 index 00000000..97b44267 --- /dev/null +++ b/test/invariant/handlers/V3PoolHandler.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../helpers/BeforeAfter.sol"; +import {Assertions} from "../helpers/Assertions.sol"; + +import {IUniswapV3Pool} from + "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; +import {ISwapRouter} from "../modules/uniswapv3-periphery/interfaces/ISwapRouter.sol"; +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; + +abstract contract V3PoolHandler is BeforeAfter, Assertions { + function V3PoolHandler_donate(uint256 tokenIndexSeed, uint256 amount) public { + address _token = tokenIndexSeed % 2 == 0 ? address(_quoteToken) : address(_baseToken); + (address token0, address token1) = address(_baseToken) < address(_quoteToken) + ? (address(_baseToken), address(_quoteToken)) + : (address(_quoteToken), address(_baseToken)); + address pool = _uniV3Factory.getPool(token0, token1, 500); + + amount = bound(amount, 1, 10_000 ether); + + MockERC20(_token).mint(address(this), amount); + MockERC20(_token).transfer(address(pool), amount); + } + + function V3PoolHandler_swapToken0(uint256 recipientIndexSeed, uint256 amountIn) public { + address recipient = randomAddress(recipientIndexSeed); + if (_quoteToken.balanceOf(recipient) < 1e14) return; + amountIn = bound(amountIn, 1e14, _quoteToken.balanceOf(recipient)); + + (address token0, address token1) = address(_baseToken) < address(_quoteToken) + ? (address(_baseToken), address(_quoteToken)) + : (address(_quoteToken), address(_baseToken)); + + IUniswapV3Pool pool = IUniswapV3Pool(_uniV3Factory.getPool(token0, token1, 500)); + if (address(pool) == address(0)) return; + + vm.prank(recipient); + _quoteToken.approve(address(_v3SwapRouter), amountIn); + + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(_quoteToken), + tokenOut: address(_baseToken), + fee: 500, + recipient: recipient, + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + + vm.prank(recipient); + try _v3SwapRouter.exactInputSingle(params) {} catch {} + } + + function V3PoolHandler_swapToken1(uint256 recipientIndexSeed, uint256 amountIn) public { + address recipient = randomAddress(recipientIndexSeed); + if (_baseToken.balanceOf(recipient) < 1e14) return; + amountIn = bound(amountIn, 1e14, _baseToken.balanceOf(recipient)); + + (address token0, address token1) = address(_baseToken) < address(_quoteToken) + ? (address(_baseToken), address(_quoteToken)) + : (address(_quoteToken), address(_baseToken)); + + IUniswapV3Pool pool = IUniswapV3Pool(_uniV3Factory.getPool(token0, token1, 500)); + if (address(pool) == address(0)) return; + + vm.prank(recipient); + _baseToken.approve(address(_v3SwapRouter), amountIn); + + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(_baseToken), + tokenOut: address(_quoteToken), + fee: 500, + recipient: recipient, + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + + vm.prank(recipient); + try _v3SwapRouter.exactInputSingle(params) {} catch {} + } +} diff --git a/test/invariant/helpers/Assertions.sol b/test/invariant/helpers/Assertions.sol new file mode 100644 index 00000000..6d774aeb --- /dev/null +++ b/test/invariant/helpers/Assertions.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./String.sol"; + +/// @author Based on Crytic PropertiesHelper (https://github.com/crytic/properties/blob/main/contracts/util/PropertiesHelper.sol) +abstract contract Assertions { + event AssertFail(string); + event assertEqualFail(string); + event AssertNeqFail(string); + event AssertGteFail(string); + event AssertGtFail(string); + event AssertLteFail(string); + event AssertLtFail(string); + event Message(string a); + event MessageUint(string a, uint256 b); + + function t(bool b, string memory reason) internal { + if (!b) { + emit AssertFail(reason); + assert(false); + } + } + + /// @notice asserts that a is equal to b. Violations are logged using reason. + function equal(uint256 a, uint256 b, string memory reason) internal { + if (a != b) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "!=", reason); + emit assertEqualFail(assertMsg); + assert(false); + } + } + + /// @notice int256 version of equal + function equal(int256 a, int256 b, string memory reason) internal { + if (a != b) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "!=", reason); + emit assertEqualFail(assertMsg); + assert(false); + } + } + + /// @notice bool version of equal + function equal(bool a, bool b, string memory reason) internal { + if (a != b) { + string memory aStr = a ? "true" : "false"; + string memory bStr = b ? "true" : "false"; + string memory assertMsg = createAssertFailMessage(aStr, bStr, "!=", reason); + emit assertEqualFail(assertMsg); + assert(false); + } + } + + /// @notice address version of equal + function equal(address a, address b, string memory reason) internal { + if (a != b) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "!=", reason); + emit assertEqualFail(assertMsg); + assert(false); + } + } + + /// @notice bytes4 version of equal + function equal(bytes4 a, bytes4 b, string memory reason) internal { + if (a != b) { + bytes memory aBytes = abi.encodePacked(a); + bytes memory bBytes = abi.encodePacked(b); + string memory aStr = String.toHexString(aBytes); + string memory bStr = String.toHexString(bBytes); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "!=", reason); + emit assertEqualFail(assertMsg); + assert(false); + } + } + + /// @notice asserts that a is not equal to b. Violations are logged using reason. + function neq(uint256 a, uint256 b, string memory reason) internal { + if (a == b) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "==", reason); + emit AssertNeqFail(assertMsg); + assert(false); + } + } + + /// @notice int256 version of neq + function neq(int256 a, int256 b, string memory reason) internal { + if (a == b) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "==", reason); + emit AssertNeqFail(assertMsg); + assert(false); + } + } + + /// @notice asserts that a is greater than or equal to b. Violations are logged using reason. + function gte(uint256 a, uint256 b, string memory reason) internal { + if (!(a >= b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "<", reason); + emit AssertGteFail(assertMsg); + assert(false); + } + } + + /// @notice int256 version of gte + function gte(int256 a, int256 b, string memory reason) internal { + if (!(a >= b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "<", reason); + emit AssertGteFail(assertMsg); + assert(false); + } + } + + /// @notice asserts that a is greater than b. Violations are logged using reason. + function gt(uint256 a, uint256 b, string memory reason) internal { + if (!(a > b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "<=", reason); + emit AssertGtFail(assertMsg); + assert(false); + } + } + + /// @notice int256 version of gt + function gt(int256 a, int256 b, string memory reason) internal { + if (!(a > b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, "<=", reason); + emit AssertGtFail(assertMsg); + assert(false); + } + } + + /// @notice asserts that a is less than or equal to b. Violations are logged using reason. + function lte(uint256 a, uint256 b, string memory reason) internal { + if (!(a <= b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, ">", reason); + emit AssertLteFail(assertMsg); + assert(false); + } + } + + /// @notice int256 version of lte + function lte(int256 a, int256 b, string memory reason) internal { + if (!(a <= b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, ">", reason); + emit AssertLteFail(assertMsg); + assert(false); + } + } + + /// @notice asserts that a is less than b. Violations are logged using reason. + function lt(uint256 a, uint256 b, string memory reason) internal { + if (!(a < b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, ">=", reason); + emit AssertLtFail(assertMsg); + assert(false); + } + } + + /// @notice int256 version of lt + function lt(int256 a, int256 b, string memory reason) internal { + if (!(a < b)) { + string memory aStr = String.toString(a); + string memory bStr = String.toString(b); + string memory assertMsg = createAssertFailMessage(aStr, bStr, ">=", reason); + emit AssertLtFail(assertMsg); + assert(false); + } + } + + function assertApproxEq( + uint256 a, + uint256 b, + uint256 maxDelta, + string memory reason + ) internal { + if (!(a == b)) { + uint256 dt = b > a ? b - a : a - b; + if (dt > maxDelta) { + emit Message("Error: a =~ b not satisfied [uint]"); + emit MessageUint(" Value a", a); + emit MessageUint(" Value b", b); + emit MessageUint(" Max Delta", maxDelta); + emit MessageUint(" Delta", dt); + t(false, reason); + } + } else { + t(true, "a == b"); + } + } + + function assertRevertReasonNotEqual(bytes memory returnData, string memory reason) internal { + bool isEqual = String.isRevertReasonEqual(returnData, reason); + t(!isEqual, reason); + } + + function assertRevertReasonEqual(bytes memory returnData, string memory reason) internal { + bool isEqual = String.isRevertReasonEqual(returnData, reason); + t(isEqual, reason); + } + + function assertRevertReasonEqual( + bytes memory returnData, + string memory reason1, + string memory reason2 + ) internal { + bool isEqual = String.isRevertReasonEqual(returnData, reason1) + || String.isRevertReasonEqual(returnData, reason2); + string memory assertMsg = string(abi.encodePacked(reason1, " OR ", reason2)); + t(isEqual, assertMsg); + } + + function assertRevertReasonEqual( + bytes memory returnData, + string memory reason1, + string memory reason2, + string memory reason3 + ) internal { + bool isEqual = String.isRevertReasonEqual(returnData, reason1) + || String.isRevertReasonEqual(returnData, reason2) + || String.isRevertReasonEqual(returnData, reason3); + string memory assertMsg = + string(abi.encodePacked(reason1, " OR ", reason2, " OR ", reason3)); + t(isEqual, assertMsg); + } + + function assertRevertReasonEqual( + bytes memory returnData, + string memory reason1, + string memory reason2, + string memory reason3, + string memory reason4 + ) internal { + bool isEqual = String.isRevertReasonEqual(returnData, reason1) + || String.isRevertReasonEqual(returnData, reason2) + || String.isRevertReasonEqual(returnData, reason3) + || String.isRevertReasonEqual(returnData, reason4); + string memory assertMsg = + string(abi.encodePacked(reason1, " OR ", reason2, " OR ", reason3, " OR ", reason4)); + t(isEqual, assertMsg); + } + + function errAllow( + bytes4 errorSelector, + bytes4[] memory allowedErrors, + string memory message + ) internal { + bool allowed = false; + for (uint256 i = 0; i < allowedErrors.length; i++) { + if (errorSelector == allowedErrors[i]) { + allowed = true; + break; + } + } + t(allowed, message); + } + + function errsAllow( + bytes4 errorSelector, + bytes4[] memory allowedErrors, + string[] memory messages + ) internal { + bool allowed = false; + uint256 passIndex = 0; + for (uint256 i = 0; i < allowedErrors.length; i++) { + if (errorSelector == allowedErrors[i]) { + allowed = true; + passIndex = i; + break; + } + } + t(allowed, messages[passIndex]); + } + + function createAssertFailMessage( + string memory aStr, + string memory bStr, + string memory operator, + string memory reason + ) internal pure returns (string memory) { + return string(abi.encodePacked("Invalid: ", aStr, operator, bStr, ", reason: ", reason)); + } +} diff --git a/test/invariant/helpers/BeforeAfter.sol b/test/invariant/helpers/BeforeAfter.sol new file mode 100644 index 00000000..7b87dda3 --- /dev/null +++ b/test/invariant/helpers/BeforeAfter.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Setup} from "../Setup.sol"; + +import {Veecode} from "@axis-core-1.0.1/modules/Keycode.sol"; +import {ICallback} from "@axis-core-1.0.1/interfaces/ICallback.sol"; +import {BaseDirectToLiquidity} from "../../../src/callbacks/liquidity/BaseDTL.sol"; +import {LinearVesting} from "@axis-core-1.0.1/modules/derivatives/LinearVesting.sol"; + +abstract contract BeforeAfter is Setup { + struct Vars { + address seller; + uint256 sellerBaseBalance; + uint256 sellerBaselineBalance; + uint256 sellerQuoteBalance; + uint256 dtlBaseBalance; + uint256 dtlBaselineBalance; + uint256 auctionHouseBaseBalance; + uint256 auctionHouseBaselineBalance; + uint256 baselineTotalSupply; + BaseDirectToLiquidity.DTLConfiguration dtlConfigV2; + BaseDirectToLiquidity.DTLConfiguration dtlConfigV3; + } + + Vars internal _before; + Vars internal _after; + + function __before(uint96 lotId, address seller, address _dtlAddress) internal { + _before.dtlConfigV2 = _getDTLConfigurationV2(lotId); + _before.dtlConfigV3 = _getDTLConfigurationV3(lotId); + (_before.seller,,,,,,,,) = _auctionHouse.lotRouting(lotId); + _before.sellerBaseBalance = _baseToken.balanceOf(seller); + _before.sellerQuoteBalance = _quoteToken.balanceOf(seller); + _before.dtlBaseBalance = _baseToken.balanceOf(_dtlAddress); + _before.auctionHouseBaseBalance = _baseToken.balanceOf(address(_auctionHouse)); + _before.sellerBaselineBalance = _baselineToken.balanceOf(seller); + _before.dtlBaselineBalance = _baselineToken.balanceOf(_dtlAddress); + _before.auctionHouseBaselineBalance = + _baselineToken.balanceOf(address(_baselineAuctionHouse)); + _before.baselineTotalSupply = _baselineToken.totalSupply(); + } + + function __after(uint96 lotId, address seller, address _dtlAddress) internal { + _after.dtlConfigV2 = _getDTLConfigurationV2(lotId); + _after.dtlConfigV3 = _getDTLConfigurationV3(lotId); + (_after.seller,,,,,,,,) = _auctionHouse.lotRouting(lotId); + _after.sellerBaseBalance = _baseToken.balanceOf(seller); + _after.sellerQuoteBalance = _quoteToken.balanceOf(seller); + _after.dtlBaseBalance = _baseToken.balanceOf(_dtlAddress); + _after.auctionHouseBaseBalance = _baseToken.balanceOf(address(_auctionHouse)); + _after.sellerBaselineBalance = _baselineToken.balanceOf(seller); + _after.dtlBaselineBalance = _baselineToken.balanceOf(_dtlAddress); + _after.auctionHouseBaselineBalance = + _baselineToken.balanceOf(address(_baselineAuctionHouse)); + _after.baselineTotalSupply = _baselineToken.totalSupply(); + } + + function _getDTLConfigurationV2( + uint96 lotId_ + ) internal view returns (BaseDirectToLiquidity.DTLConfiguration memory) { + ( + address recipient_, + uint256 lotCapacity_, + uint256 lotCuratorPayout_, + uint24 poolPercent_, + uint48 vestingStart_, + uint48 vestingExpiry_, + LinearVesting linearVestingModule_, + bool active_, + bytes memory implParams_ + ) = _dtlV2.lotConfiguration(lotId_); + + return BaseDirectToLiquidity.DTLConfiguration({ + recipient: recipient_, + lotCapacity: lotCapacity_, + lotCuratorPayout: lotCuratorPayout_, + poolPercent: poolPercent_, + vestingStart: vestingStart_, + vestingExpiry: vestingExpiry_, + linearVestingModule: linearVestingModule_, + active: active_, + implParams: implParams_ + }); + } + + function _getDTLConfigurationV3( + uint96 lotId_ + ) internal view returns (BaseDirectToLiquidity.DTLConfiguration memory) { + ( + address recipient_, + uint256 lotCapacity_, + uint256 lotCuratorPayout_, + uint24 poolPercent_, + uint48 vestingStart_, + uint48 vestingExpiry_, + LinearVesting linearVestingModule_, + bool active_, + bytes memory implParams_ + ) = _dtlV3.lotConfiguration(lotId_); + + return BaseDirectToLiquidity.DTLConfiguration({ + recipient: recipient_, + lotCapacity: lotCapacity_, + lotCuratorPayout: lotCuratorPayout_, + poolPercent: poolPercent_, + vestingStart: vestingStart_, + vestingExpiry: vestingExpiry_, + linearVestingModule: linearVestingModule_, + active: active_, + implParams: implParams_ + }); + } +} diff --git a/test/invariant/helpers/GuardianTester.sol b/test/invariant/helpers/GuardianTester.sol new file mode 100644 index 00000000..612d0eb3 --- /dev/null +++ b/test/invariant/helpers/GuardianTester.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from "@forge-std-1.9.1/Test.sol"; +import {console2} from "@forge-std-1.9.1/console2.sol"; +import {UniswapV2DTLHandler} from "../handlers/UniswapV2DTLHandler.sol"; +import {UniswapV3DTLHandler} from "../handlers/UniswapV3DTLHandler.sol"; +import {BaselineDTLHandler} from "../handlers/BaselineDTLHandler.sol"; +import {V2PoolHandler} from "../handlers/V2PoolHandler.sol"; +import {V3PoolHandler} from "../handlers/V3PoolHandler.sol"; +import {BaselinePoolHandler} from "../handlers/BaselinePoolHandler.sol"; + +contract GuardianTester is + UniswapV2DTLHandler, + UniswapV3DTLHandler, + BaselineDTLHandler, + V2PoolHandler, + V3PoolHandler, + BaselinePoolHandler +{ + function setUp() public { + setup(); + } + + function test_replay() public { + baselineDTL_createLot(); + } + + function test_AX_52() public { + V2PoolHandler_donate(1, 0); + uniswapV2DTL_createLot(0, 0, 0, 0); + V2PoolHandler_sync(); + uniswapV2DTL_onSettle(0, 0, 0, 0); + } + + function test_uni_v3() public { + // baselineDTL_createLot(32834951442023372761398138891550170); + // uniswapV2DTL_createLot(0,0,0,0); + // baselineDTL_onSettle(0,0,0,0); + // uniswapV2DTL_onSettle(0,0,0,0); + // V2PoolHandler_swapToken0(24989087196341648061875939510755979066808,0); + uniswapV3DTL_createLot(0, 0, 0, 0); + uniswapV3DTL_onCurate(0, 0); + uniswapV3DTL_onSettle(0, 813_287_864_469_051_073_156, 0, 1_005_841_494_018_651); + } + + function test_baseline() public { + uniswapV3DTL_createLot( + 437_216_507_706_592_282_876_171_117_618_922_826_437_786_610_427_871_130_838_586_195_282_963_363, + 0, + 0, + 0 + ); + uniswapV3DTL_onSettle( + 636_005_900_715_977_586_173_874_473_073_298_288_870_076_909_992_124_878_864_819, + 416_450_195_249_299_281_987, + 12_825, + 19_237_704_000 + ); + baselineDTL_createLot(); + baselineDTL_onSettle( + 35_823_517_419_589_735_724_585_236_993_570_690_661_427_686_147_832_812, + 547_625_172_962_746_778_936_920_119_754_048_998_529_185_575_659_961_730_593_116, + 413_846_032_223_724_352_952_166_507_982_050_954_163_269_860_227_480_398_133, + 2_378_700_954_196_402_680_978_671_333_632_440_717_118_146_550_494 + ); + } +} diff --git a/test/invariant/helpers/String.sol b/test/invariant/helpers/String.sol new file mode 100644 index 00000000..45f4b141 --- /dev/null +++ b/test/invariant/helpers/String.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Efficient library for creating string representations of integers. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/LibString.sol) +/// @author Modified from Solady (https://github.com/Vectorized/solady/blob/main/src/utils/LibString.sol) +/// @author Modified from Crytic Properties (https://github.com/crytic/properties/blob/main/contracts/util/PropertiesHelper.sol) +library String { + bytes16 internal constant HEX_DIGITS = "0123456789abcdef"; + + function toString(int256 value) internal pure returns (string memory str) { + uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); + str = toString(absValue); + + if (value < 0) { + str = string(abi.encodePacked("-", str)); + } + } + + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes + // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the + // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. + let newFreeMemoryPointer := add(mload(0x40), 160) + + // Update the free memory pointer to avoid overriding our string. + mstore(0x40, newFreeMemoryPointer) + + // Assign str to the end of the zone of newly allocated memory. + str := sub(newFreeMemoryPointer, 32) + + // Clean the last word of memory it may not be overwritten. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + // Move the pointer 1 byte to the left. + str := sub(str, 1) + + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + + // Keep dividing temp until zero. + temp := div(temp, 10) + + // prettier-ignore + if iszero(temp) { break } + } + + // Compute and cache the final total length of the string. + let length := sub(end, str) + + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 32) + + // Store the string's length at the start of memory allocated for our string. + mstore(str, length) + } + } + + function toString(address value) internal pure returns (string memory str) { + bytes memory s = new bytes(40); + for (uint256 i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint256(uint160(value)) / (2 ** (8 * (19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); + } + return string(s); + } + + function char(bytes1 b) internal pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } + + // based on OZ's toHexString + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol + function toHexString(bytes memory value) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * value.length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 0; i < value.length; i++) { + uint8 valueByte = uint8(value[i]); + buffer[2 * i + 2] = HEX_DIGITS[valueByte >> 4]; + buffer[2 * i + 3] = HEX_DIGITS[valueByte & 0xf]; + } + return string(buffer); + } + + // https://ethereum.stackexchange.com/a/83577 + function getRevertMsg(bytes memory returnData) internal pure returns (string memory) { + // Check that the data has the right size: 4 bytes for signature + 32 bytes for panic code + if (returnData.length == 4 + 32) { + // Check that the data starts with the Panic signature + bytes4 panicSignature = bytes4(keccak256(bytes("Panic(uint256)"))); + for (uint256 i = 0; i < 4; i++) { + if (returnData[i] != panicSignature[i]) { + return "Undefined signature"; + } + } + + uint256 panicCode; + for (uint256 i = 4; i < 36; i++) { + panicCode = panicCode << 8; + panicCode |= uint8(returnData[i]); + } + + // Now convert the panic code into its string representation + if (panicCode == 17) { + return "Panic(17)"; + } + + // Add other panic codes as needed or return a generic "Unknown panic" + return "Undefined panic code"; + } + + // If the returnData length is less than 68, then the transaction failed silently (without a revert message) + if (returnData.length < 68) return "Transaction reverted silently"; + + assembly { + // Slice the sighash. + returnData := add(returnData, 0x04) + } + return abi.decode(returnData, (string)); // All that remains is the revert string + } + + function isRevertReasonEqual( + bytes memory returnData, + string memory reason + ) internal pure returns (bool) { + return ( + keccak256(abi.encodePacked(getRevertMsg(returnData))) + == keccak256(abi.encodePacked(reason)) + ); + } +} diff --git a/test/invariant/helpers/salt_hash.sh b/test/invariant/helpers/salt_hash.sh new file mode 100755 index 00000000..0f3349ec --- /dev/null +++ b/test/invariant/helpers/salt_hash.sh @@ -0,0 +1,29 @@ +#!/bin/bash +while [ $# -gt 0 ]; do + if [[ $1 == *"--"* ]]; then + v="${1/--/}" + declare $v="$2" + fi + + shift +done + +# Check if bytecodeFile exists +if [ -z "$bytecodeHash" ] +then + echo "Bytecode not found. Provide the correct bytecode after the command." + exit 1 +fi + +# Check if prefix is set +if [ -z "$prefix" ] +then + echo "No prefix specified. Provide the prefix after the bytecode file." + exit 1 +fi + +output=$(cast create2 --case-sensitive --starts-with $prefix --init-code-hash $bytecodeHash --deployer $deployer) + +salt=$(echo "$output" | grep "Salt:" | awk '{print $2}' | tr -d '\n' | sed 's/\r//g') + +printf "%s" "$salt" \ No newline at end of file diff --git a/test/invariant/mocks/MockBatchAuctionHouse.sol b/test/invariant/mocks/MockBatchAuctionHouse.sol new file mode 100644 index 00000000..23c6860b --- /dev/null +++ b/test/invariant/mocks/MockBatchAuctionHouse.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {BatchAuctionHouse} from "@axis-core-1.0.1/BatchAuctionHouse.sol"; + +import {Veecode} from "@axis-core-1.0.1/modules/Modules.sol"; + +contract MockBatchAuctionHouse is BatchAuctionHouse { + constructor( + address owner_, + address protocol_, + address permit2_ + ) BatchAuctionHouse(owner_, protocol_, permit2_) {} + + function setLotCounter(uint96 newLotCounter) public { + lotCounter = newLotCounter; + } + + function setAuctionReference(uint96 lotId_, Veecode auctionReference) public { + lotRouting[lotId_].auctionReference = auctionReference; + } +} diff --git a/test/invariant/mocks/MockBlast.sol b/test/invariant/mocks/MockBlast.sol new file mode 100644 index 00000000..dcee3838 --- /dev/null +++ b/test/invariant/mocks/MockBlast.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +enum YieldMode { + AUTOMATIC, + VOID, + CLAIMABLE +} + +enum GasMode { + VOID, + CLAIMABLE +} + +contract MockBlast { + function configure(YieldMode _yield, GasMode gasMode, address governor) external {} +} diff --git a/test/invariant/modules/BPOOLMinter.sol b/test/invariant/modules/BPOOLMinter.sol new file mode 100644 index 00000000..7f1f86ba --- /dev/null +++ b/test/invariant/modules/BPOOLMinter.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Owned} from "@solmate-6.7.0/auth/Owned.sol"; + +import {Kernel, Keycode, toKeycode, Policy, Permissions} from "@baseline/Kernel.sol"; +import {BPOOLv1} from "@baseline/modules/BPOOL.v1.sol"; + +contract BPOOLMinter is Policy, Owned { + BPOOLv1 public BPOOL; + + constructor(Kernel kernel_) Policy(kernel_) Owned(kernel_.executor()) {} + + function configureDependencies() external override returns (Keycode[] memory dependencies) { + dependencies = new Keycode[](1); + dependencies[0] = toKeycode("BPOOL"); + + BPOOL = BPOOLv1(getModuleAddress(toKeycode("BPOOL"))); + } + + function requestPermissions() external view override returns (Permissions[] memory requests) { + requests = new Permissions[](2); + requests[0] = Permissions(toKeycode("BPOOL"), BPOOL.mint.selector); + requests[1] = Permissions(toKeycode("BPOOL"), BPOOL.setTransferLock.selector); + } + + function mint(address to_, uint256 amount_) external onlyOwner { + BPOOL.mint(to_, amount_); + } + + function setTransferLock(bool lock_) external onlyOwner { + BPOOL.setTransferLock(lock_); + } +} diff --git a/test/invariant/modules/CREDTMinter.sol b/test/invariant/modules/CREDTMinter.sol new file mode 100644 index 00000000..89b60bec --- /dev/null +++ b/test/invariant/modules/CREDTMinter.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Owned} from "@solmate-6.7.0/auth/Owned.sol"; + +import {Kernel, Keycode, toKeycode, Policy} from "@baseline/Kernel.sol"; +import {CREDTv1} from "@baseline/modules/CREDT.v1.sol"; + +contract CREDTMinter is Policy, Owned { + CREDTv1 public CREDT; + + constructor(Kernel kernel_) Policy(kernel_) Owned(kernel_.executor()) {} + + function configureDependencies() external override returns (Keycode[] memory dependencies) { + dependencies = new Keycode[](1); + dependencies[0] = toKeycode("CREDT"); + + CREDT = CREDTv1(getModuleAddress(toKeycode("CREDT"))); + } +} diff --git a/test/invariant/modules/ModuleTester.sol b/test/invariant/modules/ModuleTester.sol new file mode 100644 index 00000000..bb304c39 --- /dev/null +++ b/test/invariant/modules/ModuleTester.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import "../../../../../src/callbacks/liquidity/BaselineV2/lib/Kernel.sol"; + +// Generate an UNACTIVATED test fixture policy for a module. Must be activated separately. +library ModuleTester { + // Generate a test fixture policy for a module with all permissions passed in + function generateFixture( + Module module_, + Permissions[] memory requests_ + ) internal returns (address) { + return address(new ModuleTestFixture(module_.kernel(), module_, requests_)); + } + + // Generate a test fixture policy authorized for a single module function + function generateMultiFunctionFixture( + Module module_, + bytes4[] calldata funcSelectors_ + ) internal returns (address) { + uint256 len = funcSelectors_.length; + Keycode keycode = module_.KEYCODE(); + + Permissions[] memory requests = new Permissions[](len); + for (uint256 i; i < len; ++i) { + requests[i] = Permissions(keycode, funcSelectors_[i]); + } + + return generateFixture(module_, requests); + } + + // Generate a test fixture policy authorized for a single module function + function generateFunctionFixture( + Module module_, + bytes4 funcSelector_ + ) internal returns (address) { + Permissions[] memory requests = new Permissions[](1); + requests[0] = Permissions(module_.KEYCODE(), funcSelector_); + return generateFixture(module_, requests); + } + + // Generate a test fixture policy with NO permissions + function generateDummyFixture(Module module_) internal returns (address) { + Permissions[] memory requests = new Permissions[](0); + return generateFixture(module_, requests); + } +} + +/// @notice Mock policy to allow testing gated module functions +contract ModuleTestFixture is Policy { + Module internal _module; + Permissions[] internal _requests; + + constructor(Kernel kernel_, Module module_, Permissions[] memory requests_) Policy(kernel_) { + _module = module_; + uint256 len = requests_.length; + for (uint256 i; i < len; i++) { + _requests.push(requests_[i]); + } + } + + // ========= FRAMEWORK CONFIFURATION ========= // + function configureDependencies() + external + view + override + returns (Keycode[] memory dependencies) + { + dependencies = new Keycode[](1); + dependencies[0] = _module.KEYCODE(); + } + + function requestPermissions() external view override returns (Permissions[] memory requests) { + uint256 len = _requests.length; + requests = new Permissions[](len); + for (uint256 i; i < len; i++) { + requests[i] = _requests[i]; + } + } + + function call(bytes memory data) external { + (bool success,) = address(_module).call(data); + require(success, "ModuleTestFixture: call failed"); + } +} diff --git a/test/invariant/modules/WETH.sol b/test/invariant/modules/WETH.sol new file mode 100644 index 00000000..bdc3d483 --- /dev/null +++ b/test/invariant/modules/WETH.sol @@ -0,0 +1,758 @@ +/** + * Submitted for verification at Etherscan.io on 2017-12-12 + */ + +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + msg.sender.call{value: wad}(""); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} + +/* + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +*/ diff --git a/test/invariant/modules/uniswapv3-periphery/SwapRouter.sol b/test/invariant/modules/uniswapv3-periphery/SwapRouter.sol new file mode 100644 index 00000000..ca150509 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/SwapRouter.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/SafeCast.sol"; +import "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; +import "@uniswap-v3-core-1.0.1-solc-0.8-simulate/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; + +import "./interfaces/ISwapRouter.sol"; +import "./base/PeripheryImmutableState.sol"; +import "./base/PeripheryValidation.sol"; +import "./base/PeripheryPaymentsWithFee.sol"; +import "./base/Multicall.sol"; +import "./base/SelfPermit.sol"; +import "./libraries/Path.sol"; +import "./libraries/CallbackValidation.sol"; +import "./interfaces/external/IWETH9.sol"; + +/// @title Uniswap V3 Swap Router +/// @notice Router for stateless execution of swaps against Uniswap V3 +contract SwapRouter is + ISwapRouter, + PeripheryImmutableState, + PeripheryValidation, + PeripheryPaymentsWithFee, + Multicall, + SelfPermit +{ + using Path for bytes; + using SafeCast for uint256; + + /// @dev Used as the placeholder value for amountInCached, because the computed amount in for an exact output swap + /// can never actually be this value + uint256 private constant DEFAULT_AMOUNT_IN_CACHED = type(uint256).max; + + /// @dev Transient storage variable used for returning the computed amount in for an exact output swap. + uint256 private amountInCached = DEFAULT_AMOUNT_IN_CACHED; + + constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {} + + /// @dev Returns the pool for the given token pair and fee. The pool contract may or may not exist. + function getPool( + address tokenA, + address tokenB, + uint24 fee + ) private view returns (IUniswapV3Pool) { + return IUniswapV3Pool(IUniswapV3Factory(factory).getPool(tokenA, tokenB, fee)); + } + + struct SwapCallbackData { + bytes path; + address payer; + } + + /// @inheritdoc IUniswapV3SwapCallback + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata _data + ) external override { + require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported + SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData)); + (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); + CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); + + (bool isExactInput, uint256 amountToPay) = amount0Delta > 0 + ? (tokenIn < tokenOut, uint256(amount0Delta)) + : (tokenOut < tokenIn, uint256(amount1Delta)); + if (isExactInput) { + pay(tokenIn, data.payer, msg.sender, amountToPay); + } else { + // either initiate the next swap or pay + if (data.path.hasMultiplePools()) { + data.path = data.path.skipToken(); + exactOutputInternal(amountToPay, msg.sender, 0, data); + } else { + amountInCached = amountToPay; + tokenIn = tokenOut; // swap in/out because exact output swaps are reversed + pay(tokenIn, data.payer, msg.sender, amountToPay); + } + } + } + + /// @dev Performs a single exact input swap + function exactInputInternal( + uint256 amountIn, + address recipient, + uint160 sqrtPriceLimitX96, + SwapCallbackData memory data + ) private returns (uint256 amountOut) { + // allow swapping to the router address with address 0 + if (recipient == address(0)) recipient = address(this); + + (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); + + bool zeroForOne = tokenIn < tokenOut; + + (int256 amount0, int256 amount1) = getPool(tokenIn, tokenOut, fee).swap( + recipient, + zeroForOne, + amountIn.toInt256(), + sqrtPriceLimitX96 == 0 + ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) + : sqrtPriceLimitX96, + abi.encode(data) + ); + + return uint256(-(zeroForOne ? amount1 : amount0)); + } + + /// @inheritdoc ISwapRouter + function exactInputSingle( + ExactInputSingleParams calldata params + ) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { + amountOut = exactInputInternal( + params.amountIn, + params.recipient, + params.sqrtPriceLimitX96, + SwapCallbackData({ + path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), + payer: msg.sender + }) + ); + require(amountOut >= params.amountOutMinimum, "Too little received"); + } + + /// @inheritdoc ISwapRouter + function exactInput( + ExactInputParams memory params + ) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { + address payer = msg.sender; // msg.sender pays for the first hop + + while (true) { + bool hasMultiplePools = params.path.hasMultiplePools(); + + // the outputs of prior swaps become the inputs to subsequent ones + params.amountIn = exactInputInternal( + params.amountIn, + hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies + 0, + SwapCallbackData({ + path: params.path.getFirstPool(), // only the first pool in the path is necessary + payer: payer + }) + ); + + // decide whether to continue or terminate + if (hasMultiplePools) { + payer = address(this); // at this point, the caller has paid + params.path = params.path.skipToken(); + } else { + amountOut = params.amountIn; + break; + } + } + + require(amountOut >= params.amountOutMinimum, "Too little received"); + } + + /// @dev Performs a single exact output swap + function exactOutputInternal( + uint256 amountOut, + address recipient, + uint160 sqrtPriceLimitX96, + SwapCallbackData memory data + ) private returns (uint256 amountIn) { + // allow swapping to the router address with address 0 + if (recipient == address(0)) recipient = address(this); + + (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool(); + + bool zeroForOne = tokenIn < tokenOut; + + (int256 amount0Delta, int256 amount1Delta) = getPool(tokenIn, tokenOut, fee).swap( + recipient, + zeroForOne, + -amountOut.toInt256(), + sqrtPriceLimitX96 == 0 + ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) + : sqrtPriceLimitX96, + abi.encode(data) + ); + + uint256 amountOutReceived; + (amountIn, amountOutReceived) = zeroForOne + ? (uint256(amount0Delta), uint256(-amount1Delta)) + : (uint256(amount1Delta), uint256(-amount0Delta)); + // it's technically possible to not receive the full output amount, + // so if no price limit has been specified, require this possibility away + if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut); + } + + /// @inheritdoc ISwapRouter + function exactOutputSingle( + ExactOutputSingleParams calldata params + ) external payable override checkDeadline(params.deadline) returns (uint256 amountIn) { + // avoid an SLOAD by using the swap return data + amountIn = exactOutputInternal( + params.amountOut, + params.recipient, + params.sqrtPriceLimitX96, + SwapCallbackData({ + path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), + payer: msg.sender + }) + ); + + require(amountIn <= params.amountInMaximum, "Too much requested"); + // has to be reset even though we don't use it in the single hop case + amountInCached = DEFAULT_AMOUNT_IN_CACHED; + } + + /// @inheritdoc ISwapRouter + function exactOutput( + ExactOutputParams calldata params + ) external payable override checkDeadline(params.deadline) returns (uint256 amountIn) { + // it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output + // swap, which happens first, and subsequent swaps are paid for within nested callback frames + exactOutputInternal( + params.amountOut, + params.recipient, + 0, + SwapCallbackData({path: params.path, payer: msg.sender}) + ); + + amountIn = amountInCached; + require(amountIn <= params.amountInMaximum, "Too much requested"); + amountInCached = DEFAULT_AMOUNT_IN_CACHED; + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/base/BlockTimestamp.sol b/test/invariant/modules/uniswapv3-periphery/base/BlockTimestamp.sol new file mode 100644 index 00000000..d7c6cf2f --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/base/BlockTimestamp.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title Function for getting block timestamp +/// @dev Base contract that is overridden for tests +abstract contract BlockTimestamp { + /// @dev Method that exists purely to be overridden for tests + /// @return The current block timestamp + function _blockTimestamp() internal view virtual returns (uint256) { + return block.timestamp; + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/base/Multicall.sol b/test/invariant/modules/uniswapv3-periphery/base/Multicall.sol new file mode 100644 index 00000000..1181ffbe --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/base/Multicall.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "../interfaces/IMulticall.sol"; + +/// @title Multicall +/// @notice Enables calling multiple methods in a single call to the contract +abstract contract Multicall is IMulticall { + /// @inheritdoc IMulticall + function multicall( + bytes[] calldata data + ) public payable override returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + + if (!success) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert(); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + + results[i] = result; + } + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/base/PeripheryImmutableState.sol b/test/invariant/modules/uniswapv3-periphery/base/PeripheryImmutableState.sol new file mode 100644 index 00000000..d8f0d81a --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/base/PeripheryImmutableState.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../interfaces/IPeripheryImmutableState.sol"; + +/// @title Immutable state +/// @notice Immutable state used by periphery contracts +abstract contract PeripheryImmutableState is IPeripheryImmutableState { + /// @inheritdoc IPeripheryImmutableState + address public immutable override factory; + /// @inheritdoc IPeripheryImmutableState + address public immutable override WETH9; + + constructor(address _factory, address _WETH9) { + factory = _factory; + WETH9 = _WETH9; + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/base/PeripheryPayments.sol b/test/invariant/modules/uniswapv3-periphery/base/PeripheryPayments.sol new file mode 100644 index 00000000..a587537f --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/base/PeripheryPayments.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "../interfaces/IPeripheryPayments.sol"; +import "../interfaces/external/IWETH9.sol"; + +import "../libraries/TransferHelper.sol"; + +import "./PeripheryImmutableState.sol"; + +abstract contract PeripheryPayments is IPeripheryPayments, PeripheryImmutableState { + receive() external payable { + require(msg.sender == WETH9, "Not WETH9"); + } + + /// @inheritdoc IPeripheryPayments + function unwrapWETH9(uint256 amountMinimum, address recipient) public payable override { + uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this)); + require(balanceWETH9 >= amountMinimum, "Insufficient WETH9"); + + if (balanceWETH9 > 0) { + IWETH9(WETH9).withdraw(balanceWETH9); + TransferHelper.safeTransferETH(recipient, balanceWETH9); + } + } + + /// @inheritdoc IPeripheryPayments + function sweepToken( + address token, + uint256 amountMinimum, + address recipient + ) public payable override { + uint256 balanceToken = IERC20(token).balanceOf(address(this)); + require(balanceToken >= amountMinimum, "Insufficient token"); + + if (balanceToken > 0) { + TransferHelper.safeTransfer(token, recipient, balanceToken); + } + } + + /// @inheritdoc IPeripheryPayments + function refundETH() external payable override { + if (address(this).balance > 0) { + TransferHelper.safeTransferETH(msg.sender, address(this).balance); + } + } + + /// @param token The token to pay + /// @param payer The entity that must pay + /// @param recipient The entity that will receive payment + /// @param value The amount to pay + function pay(address token, address payer, address recipient, uint256 value) internal { + if (token == WETH9 && address(this).balance >= value) { + // pay with WETH9 + IWETH9(WETH9).deposit{value: value}(); // wrap only what is needed to pay + IWETH9(WETH9).transfer(recipient, value); + } else if (payer == address(this)) { + // pay with tokens already in the contract (for the exact input multihop case) + TransferHelper.safeTransfer(token, recipient, value); + } else { + // pull payment + TransferHelper.safeTransferFrom(token, payer, recipient, value); + } + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/base/PeripheryPaymentsWithFee.sol b/test/invariant/modules/uniswapv3-periphery/base/PeripheryPaymentsWithFee.sol new file mode 100644 index 00000000..074583d1 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/base/PeripheryPaymentsWithFee.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin-contracts-4.9.2/token/ERC20/IERC20.sol"; +import "../libraries/LowGasSafeMath.sol"; + +import "./PeripheryPayments.sol"; +import "../interfaces/IPeripheryPaymentsWithFee.sol"; + +import "../interfaces/external/IWETH9.sol"; +import "../libraries/TransferHelper.sol"; + +abstract contract PeripheryPaymentsWithFee is PeripheryPayments, IPeripheryPaymentsWithFee { + using LowGasSafeMath for uint256; + + /// @inheritdoc IPeripheryPaymentsWithFee + function unwrapWETH9WithFee( + uint256 amountMinimum, + address recipient, + uint256 feeBips, + address feeRecipient + ) public payable override { + require(feeBips > 0 && feeBips <= 100); + + uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this)); + require(balanceWETH9 >= amountMinimum, "Insufficient WETH9"); + + if (balanceWETH9 > 0) { + IWETH9(WETH9).withdraw(balanceWETH9); + uint256 feeAmount = balanceWETH9.mul(feeBips) / 10_000; + if (feeAmount > 0) TransferHelper.safeTransferETH(feeRecipient, feeAmount); + TransferHelper.safeTransferETH(recipient, balanceWETH9 - feeAmount); + } + } + + /// @inheritdoc IPeripheryPaymentsWithFee + function sweepTokenWithFee( + address token, + uint256 amountMinimum, + address recipient, + uint256 feeBips, + address feeRecipient + ) public payable override { + require(feeBips > 0 && feeBips <= 100); + + uint256 balanceToken = IERC20(token).balanceOf(address(this)); + require(balanceToken >= amountMinimum, "Insufficient token"); + + if (balanceToken > 0) { + uint256 feeAmount = balanceToken.mul(feeBips) / 10_000; + if (feeAmount > 0) TransferHelper.safeTransfer(token, feeRecipient, feeAmount); + TransferHelper.safeTransfer(token, recipient, balanceToken - feeAmount); + } + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/base/PeripheryValidation.sol b/test/invariant/modules/uniswapv3-periphery/base/PeripheryValidation.sol new file mode 100644 index 00000000..4671b425 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/base/PeripheryValidation.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./BlockTimestamp.sol"; + +abstract contract PeripheryValidation is BlockTimestamp { + modifier checkDeadline(uint256 deadline) { + require(_blockTimestamp() <= deadline, "Transaction too old"); + _; + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/base/SelfPermit.sol b/test/invariant/modules/uniswapv3-periphery/base/SelfPermit.sol new file mode 100644 index 00000000..60228b3a --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/base/SelfPermit.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +import "../interfaces/ISelfPermit.sol"; +import "../interfaces/external/IERC20PermitAllowed.sol"; + +/// @title Self Permit +/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route +/// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function +/// that requires an approval in a single transaction. +abstract contract SelfPermit is ISelfPermit { + /// @inheritdoc ISelfPermit + function selfPermit( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public payable override { + IERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s); + } + + /// @inheritdoc ISelfPermit + function selfPermitIfNecessary( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable override { + if (IERC20(token).allowance(msg.sender, address(this)) < value) { + selfPermit(token, value, deadline, v, r, s); + } + } + + /// @inheritdoc ISelfPermit + function selfPermitAllowed( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public payable override { + IERC20PermitAllowed(token).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); + } + + /// @inheritdoc ISelfPermit + function selfPermitAllowedIfNecessary( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external payable override { + if (IERC20(token).allowance(msg.sender, address(this)) < type(uint256).max) { + selfPermitAllowed(token, nonce, expiry, v, r, s); + } + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/IMulticall.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/IMulticall.sol new file mode 100644 index 00000000..8b59815c --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/IMulticall.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; +pragma abicoder v2; + +/// @title Multicall interface +/// @notice Enables calling multiple methods in a single call to the contract +interface IMulticall { + /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed + /// @dev The `msg.value` should not be trusted for any method callable from multicall. + /// @param data The encoded function data for each of the calls to make to this contract + /// @return results The results from each of the calls passed in via data + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryImmutableState.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryImmutableState.sol new file mode 100644 index 00000000..80d20882 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryImmutableState.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title Immutable state +/// @notice Functions that return immutable state of the router +interface IPeripheryImmutableState { + /// @return Returns the address of the Uniswap V3 factory + function factory() external view returns (address); + + /// @return Returns the address of WETH9 + function WETH9() external view returns (address); +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryPayments.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryPayments.sol new file mode 100644 index 00000000..f1c19b23 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryPayments.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title Periphery Payments +/// @notice Functions to ease deposits and withdrawals of ETH +interface IPeripheryPayments { + /// @notice Unwraps the contract's WETH9 balance and sends it to recipient as ETH. + /// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users. + /// @param amountMinimum The minimum amount of WETH9 to unwrap + /// @param recipient The address receiving ETH + function unwrapWETH9(uint256 amountMinimum, address recipient) external payable; + + /// @notice Refunds any ETH balance held by this contract to the `msg.sender` + /// @dev Useful for bundling with mint or increase liquidity that uses ether, or exact output swaps + /// that use ether for the input amount + function refundETH() external payable; + + /// @notice Transfers the full amount of a token held by this contract to recipient + /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users + /// @param token The contract address of the token which will be transferred to `recipient` + /// @param amountMinimum The minimum amount of token required for a transfer + /// @param recipient The destination address of the token + function sweepToken(address token, uint256 amountMinimum, address recipient) external payable; +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryPaymentsWithFee.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryPaymentsWithFee.sol new file mode 100644 index 00000000..9b7f7746 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/IPeripheryPaymentsWithFee.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./IPeripheryPayments.sol"; + +/// @title Periphery Payments +/// @notice Functions to ease deposits and withdrawals of ETH +interface IPeripheryPaymentsWithFee is IPeripheryPayments { + /// @notice Unwraps the contract's WETH9 balance and sends it to recipient as ETH, with a percentage between + /// 0 (exclusive), and 1 (inclusive) going to feeRecipient + /// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users. + function unwrapWETH9WithFee( + uint256 amountMinimum, + address recipient, + uint256 feeBips, + address feeRecipient + ) external payable; + + /// @notice Transfers the full amount of a token held by this contract to recipient, with a percentage between + /// 0 (exclusive) and 1 (inclusive) going to feeRecipient + /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users + function sweepTokenWithFee( + address token, + uint256 amountMinimum, + address recipient, + uint256 feeBips, + address feeRecipient + ) external payable; +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/ISelfPermit.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/ISelfPermit.sol new file mode 100644 index 00000000..65b9c76d --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/ISelfPermit.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title Self Permit +/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route +interface ISelfPermit { + /// @notice Permits this contract to spend a given token from `msg.sender` + /// @dev The `owner` is always msg.sender and the `spender` is always address(this). + /// @param token The address of the token spent + /// @param value The amount that can be spent of token + /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function selfPermit( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + /// @notice Permits this contract to spend a given token from `msg.sender` + /// @dev The `owner` is always msg.sender and the `spender` is always address(this). + /// Can be used instead of #selfPermit to prevent calls from failing due to a frontrun of a call to #selfPermit + /// @param token The address of the token spent + /// @param value The amount that can be spent of token + /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function selfPermitIfNecessary( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter + /// @dev The `owner` is always msg.sender and the `spender` is always address(this) + /// @param token The address of the token spent + /// @param nonce The current nonce of the owner + /// @param expiry The timestamp at which the permit is no longer valid + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function selfPermitAllowed( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + + /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter + /// @dev The `owner` is always msg.sender and the `spender` is always address(this) + /// Can be used instead of #selfPermitAllowed to prevent calls from failing due to a frontrun of a call to #selfPermitAllowed. + /// @param token The address of the token spent + /// @param nonce The current nonce of the owner + /// @param expiry The timestamp at which the permit is no longer valid + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function selfPermitAllowedIfNecessary( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/ISwapRouter.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/ISwapRouter.sol new file mode 100644 index 00000000..d5609cd8 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/ISwapRouter.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; +pragma abicoder v2; + +import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface ISwapRouter is IUniswapV3SwapCallback { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle( + ExactInputSingleParams calldata params + ) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput( + ExactInputParams calldata params + ) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle( + ExactOutputSingleParams calldata params + ) external payable returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata + /// @return amountIn The amount of the input token + function exactOutput( + ExactOutputParams calldata params + ) external payable returns (uint256 amountIn); +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/external/IERC20PermitAllowed.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/external/IERC20PermitAllowed.sol new file mode 100644 index 00000000..1b715939 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/external/IERC20PermitAllowed.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title Interface for permit +/// @notice Interface used by DAI/CHAI for permit +interface IERC20PermitAllowed { + /// @notice Approve the spender to spend some tokens via the holder signature + /// @dev This is the permit interface used by DAI and CHAI + /// @param holder The address of the token holder, the token owner + /// @param spender The address of the token spender + /// @param nonce The holder's nonce, increases at each call to permit + /// @param expiry The timestamp at which the permit is no longer valid + /// @param allowed Boolean that sets approval amount, true for type(uint256).max and false for 0 + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/test/invariant/modules/uniswapv3-periphery/interfaces/external/IWETH9.sol b/test/invariant/modules/uniswapv3-periphery/interfaces/external/IWETH9.sol new file mode 100644 index 00000000..77eb5742 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/interfaces/external/IWETH9.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Interface for WETH9 +interface IWETH9 is IERC20 { + /// @notice Deposit ether to get wrapped ether + function deposit() external payable; + + /// @notice Withdraw wrapped ether to get ether + function withdraw(uint256) external; +} diff --git a/test/invariant/modules/uniswapv3-periphery/libraries/BytesLib.sol b/test/invariant/modules/uniswapv3-periphery/libraries/BytesLib.sol new file mode 100644 index 00000000..b19b6dee --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/libraries/BytesLib.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity ^0.8.0; + +library BytesLib { + function slice( + bytes memory _bytes, + uint256 _start, + uint256 _length + ) internal pure returns (bytes memory) { + require(_length + 31 >= _length, "slice_overflow"); + require(_start + _length >= _start, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { mstore(mc, mload(cc)) } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_start + 20 >= _start, "toAddress_overflow"); + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { + require(_start + 3 >= _start, "toUint24_overflow"); + require(_bytes.length >= _start + 3, "toUint24_outOfBounds"); + uint24 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x3), _start)) + } + + return tempUint; + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/libraries/CallbackValidation.sol b/test/invariant/modules/uniswapv3-periphery/libraries/CallbackValidation.sol new file mode 100644 index 00000000..8ad3ff62 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/libraries/CallbackValidation.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "./PoolAddress.sol"; + +/// @notice Provides validation for callbacks from Uniswap V3 Pools +library CallbackValidation { + /// @notice Returns the address of a valid Uniswap V3 Pool + /// @param factory The contract address of the Uniswap V3 factory + /// @param tokenA The contract address of either token0 or token1 + /// @param tokenB The contract address of the other token + /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip + /// @return pool The V3 pool contract address + function verifyCallback( + address factory, + address tokenA, + address tokenB, + uint24 fee + ) internal view returns (IUniswapV3Pool pool) { + pool = IUniswapV3Pool(IUniswapV3Factory(factory).getPool(tokenA, tokenB, fee)); + require(msg.sender == address(pool)); + } + + /// @notice Returns the address of a valid Uniswap V3 Pool + /// @param factory The contract address of the Uniswap V3 factory + /// @param poolKey The identifying key of the V3 pool + /// @return pool The V3 pool contract address + function verifyCallback( + address factory, + PoolAddress.PoolKey memory poolKey + ) internal view returns (IUniswapV3Pool pool) { + pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); + require(msg.sender == address(pool)); + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/libraries/LowGasSafeMath.sol b/test/invariant/modules/uniswapv3-periphery/libraries/LowGasSafeMath.sol new file mode 100644 index 00000000..53846498 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/libraries/LowGasSafeMath.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title Optimized overflow and underflow safe math operations +/// @notice Contains methods for doing math operations that revert on overflow or underflow for minimal gas cost +library LowGasSafeMath { + /// @notice Returns x + y, reverts if sum overflows uint256 + /// @param x The augend + /// @param y The addend + /// @return z The sum of x and y + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + + /// @notice Returns x - y, reverts if underflows + /// @param x The minuend + /// @param y The subtrahend + /// @return z The difference of x and y + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + /// @notice Returns x * y, reverts if overflows + /// @param x The multiplicand + /// @param y The multiplier + /// @return z The product of x and y + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(x == 0 || (z = x * y) / x == y); + } + + /// @notice Returns x + y, reverts if overflows or underflows + /// @param x The augend + /// @param y The addend + /// @return z The sum of x and y + function add(int256 x, int256 y) internal pure returns (int256 z) { + require((z = x + y) >= x == (y >= 0)); + } + + /// @notice Returns x - y, reverts if overflows or underflows + /// @param x The minuend + /// @param y The subtrahend + /// @return z The difference of x and y + function sub(int256 x, int256 y) internal pure returns (int256 z) { + require((z = x - y) <= x == (y >= 0)); + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/libraries/Path.sol b/test/invariant/modules/uniswapv3-periphery/libraries/Path.sol new file mode 100644 index 00000000..da1dc423 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/libraries/Path.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./BytesLib.sol"; + +/// @title Functions for manipulating path data for multihop swaps +library Path { + using BytesLib for bytes; + + /// @dev The length of the bytes encoded address + uint256 private constant ADDR_SIZE = 20; + /// @dev The length of the bytes encoded fee + uint256 private constant FEE_SIZE = 3; + + /// @dev The offset of a single token address and pool fee + uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE; + /// @dev The offset of an encoded pool key + uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET; + + /// @notice Returns true iff the path contains two or more pools + /// @param path The encoded swap path + /// @return True if path contains two or more pools, otherwise false + function hasMultiplePools(bytes memory path) internal pure returns (bool) { + return path.length >= MULTIPLE_POOLS_MIN_LENGTH; + } + + /// @notice Returns the number of pools in the path + /// @param path The encoded swap path + /// @return The number of pools in the path + function numPools(bytes memory path) internal pure returns (uint256) { + // Ignore the first token address. From then on every fee and token offset indicates a pool. + return ((path.length - ADDR_SIZE) / NEXT_OFFSET); + } + + /// @notice Decodes the first pool in path + /// @param path The bytes encoded swap path + /// @return tokenA The first token of the given pool + /// @return tokenB The second token of the given pool + /// @return fee The fee level of the pool + function decodeFirstPool( + bytes memory path + ) internal pure returns (address tokenA, address tokenB, uint24 fee) { + tokenA = path.toAddress(0); + fee = path.toUint24(ADDR_SIZE); + tokenB = path.toAddress(NEXT_OFFSET); + } + + /// @notice Gets the segment corresponding to the first pool in the path + /// @param path The bytes encoded swap path + /// @return The segment containing all data necessary to target the first pool in the path + function getFirstPool(bytes memory path) internal pure returns (bytes memory) { + return path.slice(0, POP_OFFSET); + } + + /// @notice Skips a token + fee element from the buffer and returns the remainder + /// @param path The swap path + /// @return The remaining token + fee elements in the path + function skipToken(bytes memory path) internal pure returns (bytes memory) { + return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/libraries/PoolAddress.sol b/test/invariant/modules/uniswapv3-periphery/libraries/PoolAddress.sol new file mode 100644 index 00000000..9c3c9518 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/libraries/PoolAddress.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @title Provides functions for deriving a pool address from the factory, tokens, and the fee +library PoolAddress { + bytes32 internal constant POOL_INIT_CODE_HASH = + 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + + /// @notice The identifying key of the pool + struct PoolKey { + address token0; + address token1; + uint24 fee; + } + + /// @notice Returns PoolKey: the ordered tokens with the matched fee levels + /// @param tokenA The first token of a pool, unsorted + /// @param tokenB The second token of a pool, unsorted + /// @param fee The fee level of the pool + /// @return Poolkey The pool details with ordered token0 and token1 assignments + function getPoolKey( + address tokenA, + address tokenB, + uint24 fee + ) internal pure returns (PoolKey memory) { + if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); + } + + /// @notice Deterministically computes the pool address given the factory and PoolKey + /// @param factory The Uniswap V3 factory contract address + /// @param key The PoolKey + /// @return pool The contract address of the V3 pool + function computeAddress( + address factory, + PoolKey memory key + ) internal pure returns (address pool) { + require(key.token0 < key.token1); + pool = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encode(key.token0, key.token1, key.fee)), + POOL_INIT_CODE_HASH + ) + ) + ) + ) + ); + } +} diff --git a/test/invariant/modules/uniswapv3-periphery/libraries/TransferHelper.sol b/test/invariant/modules/uniswapv3-periphery/libraries/TransferHelper.sol new file mode 100644 index 00000000..0f2260a1 --- /dev/null +++ b/test/invariant/modules/uniswapv3-periphery/libraries/TransferHelper.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +library TransferHelper { + /// @notice Transfers tokens from the targeted address to the given destination + /// @notice Errors with 'STF' if transfer fails + /// @param token The contract address of the token to be transferred + /// @param from The originating address from which the tokens will be transferred + /// @param to The destination address of the transfer + /// @param value The amount to be transferred + function safeTransferFrom(address token, address from, address to, uint256 value) internal { + (bool success, bytes memory data) = + token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "STF"); + } + + /// @notice Transfers tokens from msg.sender to a recipient + /// @dev Errors with ST if transfer fails + /// @param token The contract address of the token which will be transferred + /// @param to The recipient of the transfer + /// @param value The value of the transfer + function safeTransfer(address token, address to, uint256 value) internal { + (bool success, bytes memory data) = + token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "ST"); + } + + /// @notice Approves the stipulated contract to spend the given allowance in the given token + /// @dev Errors with 'SA' if transfer fails + /// @param token The contract address of the token to be approved + /// @param to The target of the approval + /// @param value The amount of the given token the target will be allowed to spend + function safeApprove(address token, address to, uint256 value) internal { + (bool success, bytes memory data) = + token.call(abi.encodeWithSelector(IERC20.approve.selector, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "SA"); + } + + /// @notice Transfers ETH to the recipient address + /// @dev Fails with `STE` + /// @param to The destination of the transfer + /// @param value The value to be transferred + function safeTransferETH(address to, uint256 value) internal { + (bool success,) = to.call{value: value}(new bytes(0)); + require(success, "STE"); + } +} diff --git a/test/invariant/modules/utils/TimeslotLib.sol b/test/invariant/modules/utils/TimeslotLib.sol new file mode 100644 index 00000000..04185f1f --- /dev/null +++ b/test/invariant/modules/utils/TimeslotLib.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +library TimeslotLib { + /// @notice uint256 is the unix timestamp at the end of the day + /// @param timestamp_ Unix timestamp + function getTimeslot(uint256 timestamp_) internal pure returns (uint256) { + return timestamp_ - (timestamp_ % 1 days) + 1 days - 1; + } + + function today() internal view returns (uint256) { + return getTimeslot(block.timestamp); + } + + function addDays(uint256 oldTimeslot_, uint256 numDays_) internal pure returns (uint256) { + return getTimeslot(oldTimeslot_ + numDays_ * 1 days); + } + + function subDays(uint256 oldTimeslot_, uint256 numDays_) internal pure returns (uint256) { + return getTimeslot(oldTimeslot_ - numDays_ * 1 days); + } + + // NOTE: Exclusive + function diffDays(uint256 from_, uint256 to_) internal pure returns (uint256) { + if (from_ > to_) { + return (from_ - to_) / 1 days; + } else { + return (to_ - from_) / 1 days; + } + } +}