diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c3dbf7d..8d004d0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,6 +52,8 @@ jobs: run: | forge test -vvv id: test + env: + ARBITRUM_RPC_URL: ${{ secrets.ARBITRUM_RPC_URL }} - name: Contract Sizes run: | diff --git a/.solhintignore b/.solhintignore index afa64c5b..6a51dd00 100644 --- a/.solhintignore +++ b/.solhintignore @@ -5,3 +5,4 @@ test/lib/uniswap-v2/** test/lib/uniswap-v3/** src/callbacks/liquidity/BaselineV2/lib/** +src/callbacks/liquidity/Cleopatra/lib/** diff --git a/script/deploy/Deploy.s.sol b/script/deploy/Deploy.s.sol index 4360e1fc..ff2a6c6e 100644 --- a/script/deploy/Deploy.s.sol +++ b/script/deploy/Deploy.s.sol @@ -33,6 +33,16 @@ import {BALwithCappedAllowlist} from "../../src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol"; import {BALwithTokenAllowlist} from "../../src/callbacks/liquidity/BaselineV2/BALwithTokenAllowlist.sol"; +import {CleopatraV1DirectToLiquidity} from + "../../src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol"; +import {CleopatraV2DirectToLiquidity} from + "../../src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol"; + +// Cleopatra +import {ICleopatraV1Router} from + "../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Router.sol"; +import {ICleopatraV2PositionManager} from + "../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2PositionManager.sol"; // Baseline import { @@ -972,6 +982,95 @@ contract Deploy is Script, WithDeploySequence, WithSalts { return (address(batchAllowlist), _PREFIX_CALLBACKS, deploymentKey); } + function deployBatchCleopatraV1DirectToLiquidity(string memory sequenceName_) + public + returns (address, string memory, string memory) + { + // Get configuration variables + address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + address cleopatraV1PairFactory = _getEnvAddressOrOverride( + "constants.cleopatraV1.pairFactory", sequenceName_, "args.pairFactory" + ); + address payable cleopatraV1Router = payable( + _getEnvAddressOrOverride("constants.cleopatraV1.router", sequenceName_, "args.router") + ); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); + + // Check that the router and factory match + require( + ICleopatraV1Router(cleopatraV1Router).factory() == cleopatraV1PairFactory, + "ICleopatraV1Router.factory() does not match given Cleopatra V1 pair factory address" + ); + + // Get the salt + bytes32 salt_ = _getSalt( + deploymentKey, + type(CleopatraV1DirectToLiquidity).creationCode, + abi.encode(batchAuctionHouse, cleopatraV1PairFactory, cleopatraV1Router) + ); + + // 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(); + CleopatraV1DirectToLiquidity dtl = new CleopatraV1DirectToLiquidity{salt: salt_}( + batchAuctionHouse, cleopatraV1PairFactory, cleopatraV1Router + ); + console2.log(""); + console2.log(" deployed at:", address(dtl)); + + return (address(dtl), _PREFIX_CALLBACKS, deploymentKey); + } + + function deployBatchCleopatraV2DirectToLiquidity(string memory sequenceName_) + public + returns (address, string memory, string memory) + { + // Get configuration variables + address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); + address cleopatraV2Factory = + _getEnvAddressOrOverride("constants.cleopatraV2.factory", sequenceName_, "args.factory"); + address payable cleopatraV2PositionManager = payable( + _getEnvAddressOrOverride( + "constants.cleopatraV2.positionManager", sequenceName_, "args.positionManager" + ) + ); + string memory deploymentKey = _getDeploymentKey(sequenceName_); + console2.log(" deploymentKey:", deploymentKey); + + // Check that the router and factory match + require( + ICleopatraV2PositionManager(cleopatraV2PositionManager).factory() == cleopatraV2Factory, + "ICleopatraV2PositionManager.factory() does not match given Cleopatra V2 factory address" + ); + + // Get the salt + bytes32 salt_ = _getSalt( + deploymentKey, + type(CleopatraV2DirectToLiquidity).creationCode, + abi.encode(batchAuctionHouse, cleopatraV2Factory, cleopatraV2PositionManager) + ); + + // 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(); + CleopatraV2DirectToLiquidity dtl = new CleopatraV2DirectToLiquidity{salt: salt_}( + batchAuctionHouse, cleopatraV2Factory, cleopatraV2PositionManager + ); + console2.log(""); + console2.log(" deployed at:", address(dtl)); + + return (address(dtl), _PREFIX_CALLBACKS, deploymentKey); + } + // ========== HELPER FUNCTIONS ========== // function _configureDeployment(string memory data_, string memory name_) internal { diff --git a/script/deploy/sequences/cleopatra-dtl.json b/script/deploy/sequences/cleopatra-dtl.json new file mode 100644 index 00000000..0c33b672 --- /dev/null +++ b/script/deploy/sequences/cleopatra-dtl.json @@ -0,0 +1,10 @@ +{ + "sequence": [ + { + "name": "BatchCleopatraV1DirectToLiquidity" + }, + { + "name": "BatchCleopatraV2DirectToLiquidity" + } + ] +} diff --git a/script/env.json b/script/env.json index fde6c41a..98407d10 100644 --- a/script/env.json +++ b/script/env.json @@ -160,15 +160,13 @@ }, "mantle": { "constants": { - "gUni": { - "factory": "0x0000000000000000000000000000000000000000" - }, - "uniswapV2": { - "factory": "0x0000000000000000000000000000000000000000", - "router": "0x0000000000000000000000000000000000000000" + "cleopatraV1": { + "pairFactory": "0xAAA16c016BF556fcD620328f0759252E29b1AB57", + "router": "0xAAA45c8F5ef92a000a121d102F4e89278a711Faa" }, - "uniswapV3": { - "factory": "0x0000000000000000000000000000000000000000" + "cleopatraV2": { + "factory": "0xAAA32926fcE6bE95ea2c51cB4Fcb60836D320C42", + "positionManager": "0xAAA78E8C4241990B4ce159E105dA08129345946A" } } }, diff --git a/script/salts/dtl-cleopatra/CleopatraDTLSalts.s.sol b/script/salts/dtl-cleopatra/CleopatraDTLSalts.s.sol new file mode 100644 index 00000000..64c300ae --- /dev/null +++ b/script/salts/dtl-cleopatra/CleopatraDTLSalts.s.sol @@ -0,0 +1,122 @@ +/// 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"; + +// Cleopatra +import {CleopatraV1DirectToLiquidity} from + "../../../src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol"; +import {CleopatraV2DirectToLiquidity} from + "../../../src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol"; + +contract CleopatraDTLSalts is Script, WithDeploySequence, WithSalts { + string internal constant _ADDRESS_PREFIX = "E6"; + + function _setUp(string calldata chain_, string calldata sequenceFilePath_) internal { + _loadSequence(chain_, sequenceFilePath_); + _createBytecodeDirectory(); + } + + 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 Cleopatra V1 + if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicCleopatraV1DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.AtomicAuctionHouse"); + + _generateV1(sequenceName, auctionHouse, deploymentKey); + } + // Batch Cleopatra V1 + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchCleopatraV1DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); + + _generateV1(sequenceName, auctionHouse, deploymentKey); + } + // Atomic Cleopatra V2 + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("AtomicCleopatraV2DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.AtomicAuctionHouse"); + + _generateV2(sequenceName, auctionHouse, deploymentKey); + } + // Batch Cleopatra V2 + else if ( + keccak256(abi.encodePacked(sequenceName)) + == keccak256(abi.encodePacked("BatchCleopatraV2DirectToLiquidity")) + ) { + address auctionHouse = _envAddressNotZero("deployments.BatchAuctionHouse"); + + _generateV2(sequenceName, auctionHouse, deploymentKey); + } + // Something else + else { + console2.log(" Skipping unknown sequence: %s", sequenceName); + } + } + } + + function _generateV1( + string memory sequenceName_, + address auctionHouse_, + string memory deploymentKey_ + ) internal { + // Get input variables or overrides + address envCleopatraV1PairFactory = _getEnvAddressOrOverride( + "constants.cleopatraV1.pairFactory", sequenceName_, "args.pairFactory" + ); + address envCleopatraV1Router = + _getEnvAddressOrOverride("constants.cleopatraV1.router", sequenceName_, "args.router"); + + // Calculate salt for the CleopatraV1DirectToLiquidity + bytes memory contractCode = type(CleopatraV1DirectToLiquidity).creationCode; + (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( + deploymentKey_, + contractCode, + abi.encode(auctionHouse_, envCleopatraV1PairFactory, envCleopatraV1Router) + ); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } + + function _generateV2( + string memory sequenceName_, + address auctionHouse_, + string memory deploymentKey_ + ) internal { + // Get input variables or overrides + address envCleopatraV2Factory = + _getEnvAddressOrOverride("constants.cleopatraV2.factory", sequenceName_, "args.factory"); + address envCleopatraV2PositionManager = _getEnvAddressOrOverride( + "constants.cleopatraV2.positionManager", sequenceName_, "args.positionManager" + ); + + // Calculate salt for the CleopatraV2DirectToLiquidity + bytes memory contractCode = type(CleopatraV2DirectToLiquidity).creationCode; + (string memory bytecodePath, bytes32 bytecodeHash) = _writeBytecode( + deploymentKey_, + contractCode, + abi.encode(auctionHouse_, envCleopatraV2Factory, envCleopatraV2PositionManager) + ); + _setSalt(bytecodePath, _ADDRESS_PREFIX, deploymentKey_, bytecodeHash); + } +} diff --git a/script/salts/dtl-cleopatra/dtl_salts.sh b/script/salts/dtl-cleopatra/dtl_salts.sh new file mode 100755 index 00000000..40cc5567 --- /dev/null +++ b/script/salts/dtl-cleopatra/dtl_salts.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Usage: +# ./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. + +# 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-cleopatra/CleopatraDTLSalts.s.sol:CleopatraDTLSalts --sig "generate(string,string)()" $CHAIN $DEPLOY_FILE diff --git a/script/salts/salts.json b/script/salts/salts.json index 32707af5..e3c3a88d 100644 --- a/script/salts/salts.json +++ b/script/salts/salts.json @@ -13,6 +13,12 @@ "0xaa5c1bd02f7b04980f0e4ef0778e9165f8404f41c1c2bc372f2902f80dc646b0": "0x3c781e03bd8272c21052708923b6b00b60d976b84bc792a25a8eeb527d166944", "0xc329d36cea27b1f8e044c99a5eda27503fd6087f50b6d27b7bb12ac4493507e1": "0xb724ad108002e85a8144c2b305013b4d302fb1ef4a39d477f6d18e96bc219a3d" }, + "BatchCleopatraV1DirectToLiquidity": { + "0x214daf0edf11bf09aaa737f0eb7d75e2a8d78e8f84cd0fc355c51e0fb3471de1": "0xb5ded029c8eef0136f18c6898b79b9a38972352ba7f2223e4e8d826ac35e5373" + }, + "BatchCleopatraV2DirectToLiquidity": { + "0xde101247ba561eba6901aa19a1c718f71765f182b32e1be09a7f8cb664253de4": "0x59c7481d2d7cd383969401c474b1a88f2a3073387c97877752766392988d1c7c" + }, "CappedMerkleAllowlist": { "0x0249dded8310d17581f166a526ea9ded65b81112ccac0ee8a144e9770d2b8432": "0xf5746fa34aeff9866dc5ec58712f7f47045aea15db39a0aabf4dadc9d35c8854", "0x0ae7777d88dd21a8b9ca3dd9212307295a83aed438fef9dad0b62d57fbaf1025": "0x1de5ae5b126bd2cee8eb4f083080f5c30baa692580cf823fa5f382a7bfc70ac5", @@ -53,24 +59,30 @@ "0x3e786da8306fcbed7cac84942338a1270f92283628b6c00f68be021274a9398c": "0x0f25886775a191f8793d3da1493fe07c98943f0d34227e3ceebae3dfb555b56a" }, "Test_BaselineAllocatedAllowlist": { - "0x5d746474bd6f58dea871b468d53e3ab576b19043d224904750600a2d128829a1": "0x8452e66de7fac583f0fc44105edcef467d77d8edcf4c938a71aa76596ecfb67c" + "0x5d746474bd6f58dea871b468d53e3ab576b19043d224904750600a2d128829a1": "0x54b0050dbb650e1e968be88b75dc8354fd668ba1c9d404303f33dac5ffa0d9fa" }, "Test_BaselineAllowlist": { - "0x5994dd0cafe772dd48a75ac3eba409d0654bf2cac96c9f15c8096296038e2a00": "0xf6689b20de39181dd95dcd925b9bc3b10e545bf381bedaaa54dbac2d54a8334b" + "0x5994dd0cafe772dd48a75ac3eba409d0654bf2cac96c9f15c8096296038e2a00": "0x9fefe6be328d53df69523f980edaf65730c4938eac67dd0ab3aa32fde0c6d24f" }, "Test_BaselineAxisLaunch": { - "0x52496df1c08f8ccb2ff3d5890297b3619ae3d670581df798367699ac94530d12": "0x9a04d55297d9a3348f8fe51141f2c75825152eb31e306f1411ddbd741d4d14dc" + "0x52496df1c08f8ccb2ff3d5890297b3619ae3d670581df798367699ac94530d12": "0x2041068fbbad4940b256c7e16cf6a283248a550bb0b524e30c58ddf22e1662de" }, "Test_BaselineCappedAllowlist": { - "0x1ed7b5269875d6fd24f19a8b6a1906cca2ca94bba1d23cf1fb3e95877b8b0338": "0x6f2e4acbd33ef40152b0e5679bd694d0f60f4a9af611cded049d04f3598d8f10" + "0x1ed7b5269875d6fd24f19a8b6a1906cca2ca94bba1d23cf1fb3e95877b8b0338": "0xaba1c15b4006c6584a63f6ee625f9486ce3edf34139f6b9b52289837d32d6f95" }, "Test_BaselineTokenAllowlist": { - "0x3f8ca4e10bd4e9daa2aee9c323586dc24d4a3358d7599a3668eed1dd1860a829": "0xee3f8d4ffa9ffed5fa846f99d80c6080722f07cca9e388c72bfd5c49b007d8b4" + "0x3f8ca4e10bd4e9daa2aee9c323586dc24d4a3358d7599a3668eed1dd1860a829": "0xe5893ce7eff3f88fda53743fa9b06fda694a726bcdcc76b23ab0974559d70342" }, "Test_CappedMerkleAllowlist": { "0x1dc038ba91f15889eedf059525478d760c3b01dbf5288c0c995ef5c8f1395e8b": "0x6bfac83b21063468377650c02a19f8b39750ffa72f1763596c48d62833e54e12", "0xb092f03e11d329d47afaec052626436946facd0fa3cb8821145d3dcfc13f6dff": "0x89152c018a4041b7ae10dacb9da894f32cdb91bd07774c0b61ac105db77f12ba" }, + "Test_CleopatraV1DirectToLiquidity": { + "0xc5fdd09bf3deb40dadc05bcec48fce4be48191e7f2413d537c8f52b27549a1d6": "0xc34811ce2ea63c87da00e769e3963413fb9140d8d05c518168e3a20d918b1fd0" + }, + "Test_CleopatraV2DirectToLiquidity": { + "0x26c36d9cca50ed8dd922d52f9237db0e6b79696b1bdd94faaa39ad9f12e3a589": "0x873b1c35209448361c305aa3d658b7b6361e0897668adc2770b286bc3b7b6919" + }, "Test_GUniFactory": { "0x049627578a379e876af59b3ba6d1971a2095f048ea7dafb5449c84380e8bfd15": "0x32842bb4a2d9dcf8ae6970d43167191b5de3e3faca6c89d40cc1376d75ab61f0" }, @@ -102,13 +114,13 @@ "0xdb5689fd93b97ad35ee469bbcb38d4472c005f1ae951b154f2437bd1207a03f3": "0xf35c7565f04f89db7d1783343067728c4db35f70d7aea4d7dbd732fa462bb956" }, "Test_UniswapV2DirectToLiquidity": { - "0x44d9f5974232d8dc047dadc9a93a66dde3b1938d906428d1c52dc9e356988d87": "0xd3cddc8f40b0d22205b1eaf044af0e3131bbd63922c5c14a38e2440552c8fd5f" + "0xc6457ead866939ba79fbe24976c01de6a4e5350b66873ae2444500fcf3d0a68a": "0x34d7d49c713c6454e5d96bb09e9496895a1faf94d45e3a6c8550b81c9ba15117" }, "Test_UniswapV2Router": { "0x3aeb5c743a058e3c9d871533d2537013b819c4e401acaca3619f046cd9422258": "0x4c096423447dffd4ffce23f8e15e2d1b08619f72abc81536b5492d95b7c8f03f" }, "Test_UniswapV3DirectToLiquidity": { - "0x5bd1c45b9f8ee81f6de61284469b1e580289694e339da3bf7f89422b2f6acee2": "0xf4188c3dde8973a334f65d1f532e5b4e022e75a69140173cd2ee38fa05f1d789" + "0xfea4b3d341404a1e1c342f18c0eef626604a03f4293ad8eeabe8e66df65f89b5": "0xa4fad84e0fc223785c278b66da03e1ded6ad816e80401a05f24840367aa11846" }, "Test_UniswapV3Factory": { "0x33479a3955b5801da918af0aa61f4501368d483ac02e8ae036d4e6f9c56d2038": "0x715638753ed78c13d0d385c1758204daad3b308083604fb0d555eb285199ed30" diff --git a/script/salts/test/TestSalts.s.sol b/script/salts/test/TestSalts.s.sol index b451a678..ebe5f7c3 100644 --- a/script/salts/test/TestSalts.s.sol +++ b/script/salts/test/TestSalts.s.sol @@ -34,6 +34,10 @@ import {BALwithCappedAllowlist} from "../../../src/callbacks/liquidity/BaselineV2/BALwithCappedAllowlist.sol"; import {BALwithTokenAllowlist} from "../../../src/callbacks/liquidity/BaselineV2/BALwithTokenAllowlist.sol"; +import {CleopatraV1DirectToLiquidity} from + "../../../src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol"; +import {CleopatraV2DirectToLiquidity} from + "../../../src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol"; contract TestSalts is Script, WithEnvironment, Permit2User, WithSalts, TestConstants { string internal constant _CAPPED_MERKLE_ALLOWLIST = "CappedMerkleAllowlist"; @@ -312,4 +316,21 @@ contract TestSalts is Script, WithEnvironment, Permit2User, WithSalts, TestConst ); _setTestSalt(callbackBytecodePath, "EF", "BaselineTokenAllowlist", callbackBytecodeHash); } + + function generateCleopatraV1DirectToLiquidity() public { + bytes memory args = abi.encode(_AUCTION_HOUSE, _CLEOPATRA_V1_FACTORY, _CLEOPATRA_V1_ROUTER); + bytes memory contractCode = type(CleopatraV1DirectToLiquidity).creationCode; + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode("CleopatraV1DirectToLiquidity", contractCode, args); + _setTestSalt(bytecodePath, "E6", "CleopatraV1DirectToLiquidity", bytecodeHash); + } + + function generateCleopatraV2DirectToLiquidity() public { + bytes memory args = + abi.encode(_AUCTION_HOUSE, _CLEOPATRA_V2_FACTORY, _CLEOPATRA_V2_POSITION_MANAGER); + bytes memory contractCode = type(CleopatraV2DirectToLiquidity).creationCode; + (string memory bytecodePath, bytes32 bytecodeHash) = + _writeBytecode("CleopatraV2DirectToLiquidity", contractCode, args); + _setTestSalt(bytecodePath, "E6", "CleopatraV2DirectToLiquidity", bytecodeHash); + } } diff --git a/src/callbacks/liquidity/BaseDTL.sol b/src/callbacks/liquidity/BaseDTL.sol index 379a9b7f..f015b2b7 100644 --- a/src/callbacks/liquidity/BaseDTL.sol +++ b/src/callbacks/liquidity/BaseDTL.sol @@ -15,6 +15,12 @@ import {LinearVesting} from "@axis-core-1.0.0/modules/derivatives/LinearVesting. import {AuctionHouse} from "@axis-core-1.0.0/bases/AuctionHouse.sol"; import {Keycode, wrapVeecode} from "@axis-core-1.0.0/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,6 +40,14 @@ abstract contract BaseDirectToLiquidity is BaseCallback { error Callback_LinearVestingModuleNotFound(); + error Callback_PoolTokenNotFound(); + + /// @notice The auction lot has already been completed + error Callback_AlreadyComplete(); + + /// @notice Indicates that the feature is not supported by the contract + error Callback_NotSupported(); + // ========== STRUCTS ========== // /// @notice Configuration for the DTL callback @@ -68,6 +82,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// @param implParams The implementation-specific parameters struct OnCreateParams { uint24 proceedsUtilisationPercent; + // TODO ideally move the vesting parameters into an inheriting contract that accesses implParams uint48 vestingStart; uint48 vestingExpiry; address recipient; @@ -148,6 +163,11 @@ abstract contract BaseDirectToLiquidity is BaseCallback { // If vesting is enabled if (params.vestingStart != 0 || params.vestingExpiry != 0) { + // Check if linear vesting is supported + if (!_isLinearVestingSupported()) { + revert Callback_NotSupported(); + } + // Get the linear vesting module (or revert) linearVestingModule = LinearVesting(_getLatestLinearVestingModule()); @@ -216,9 +236,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 +256,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 +266,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 +311,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 +323,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; @@ -323,7 +365,18 @@ abstract contract BaseDirectToLiquidity is BaseCallback { _tokensRequiredForPool(proceeds_, config.proceedsUtilisationPercent); } - // Ensure the required tokens are present before minting + // Ensure the required quote tokens are present in this contract before minting + { + // Check that sufficient balance exists + uint256 quoteTokenBalance = ERC20(quoteToken).balanceOf(address(this)); + if (quoteTokenBalance < quoteTokensRequired) { + revert Callback_InsufficientBalance( + quoteToken, address(this), quoteTokensRequired, quoteTokenBalance + ); + } + } + + // Ensure the required base tokens are present on the seller address before minting { // Check that sufficient balance exists uint256 baseTokenBalance = ERC20(baseToken).balanceOf(seller); @@ -333,33 +386,17 @@ abstract contract BaseDirectToLiquidity is BaseCallback { ); } + // Transfer the base tokens from the seller to this contract ERC20(baseToken).safeTransferFrom(seller, address(this), baseTokensRequired); } // Mint and deposit into the pool - (ERC20 poolToken) = _mintAndDeposit( + _mintAndDeposit( lotId_, quoteToken, quoteTokensRequired, baseToken, baseTokensRequired, callbackData_ ); - uint256 poolTokenQuantity = poolToken.balanceOf(address(this)); - - // 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); - - // Mint the vesting tokens (it will deploy if necessary) - config.linearVestingModule.mint( - config.recipient, - address(poolToken), - _getEncodedVestingParams(config.vestingStart, config.vestingExpiry), - poolTokenQuantity, - true // Wrap vesting LP tokens so they are easily visible - ); - } - // Send the LP tokens to the seller - else { - poolToken.safeTransfer(config.recipient, poolTokenQuantity); - } + + // Transfer the pool token to the recipient + _transferPoolToken(lotId_); // Send any remaining quote tokens to the seller { @@ -385,7 +422,7 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// - Create and initialize the pool /// - Deposit the quote and base tokens into the pool /// - The pool tokens should be received by this contract - /// - Return the ERC20 pool token + /// - Store the details of the pool token (ERC20 or ERC721) in the state /// /// @param lotId_ The lot ID /// @param quoteToken_ The quote token address @@ -393,7 +430,6 @@ abstract contract BaseDirectToLiquidity is BaseCallback { /// @param baseToken_ The base token address /// @param baseTokenAmount_ The amount of base tokens to deposit /// @param callbackData_ Implementation-specific data - /// @return poolToken The ERC20 pool token function _mintAndDeposit( uint96 lotId_, address quoteToken_, @@ -401,7 +437,16 @@ abstract contract BaseDirectToLiquidity is BaseCallback { address baseToken_, uint256 baseTokenAmount_, bytes memory callbackData_ - ) internal virtual returns (ERC20 poolToken); + ) internal virtual; + + /// @notice Transfer the pool token to the recipient + /// @dev This function should be implemented by the Uniswap-specific callback + /// + /// It is expected to: + /// - Transfer the pool token (ERC20 or ERC721) to the recipient + /// + /// @param lotId_ The lot ID + function _transferPoolToken(uint96 lotId_) internal virtual; // ========== INTERNAL FUNCTIONS ========== // @@ -445,4 +490,41 @@ abstract contract BaseDirectToLiquidity is BaseCallback { ) internal pure returns (bytes memory) { return abi.encode(ILinearVesting.VestingParams({start: start_, expiry: expiry_})); } + + /// @notice Mint vesting tokens from an ERC20 token + /// + /// @param token_ The ERC20 token to mint the vesting tokens from + /// @param quantity_ The quantity of tokens to mint + /// @param module_ The LinearVesting module to mint the tokens with + /// @param recipient_ The recipient of the vesting tokens + /// @param vestingStart_ The start of the vesting period + /// @param vestingExpiry_ The end of the vesting period + function _mintVestingTokens( + ERC20 token_, + uint256 quantity_, + LinearVesting module_, + address recipient_, + uint48 vestingStart_, + uint48 vestingExpiry_ + ) internal { + // Approve spending of the tokens + token_.approve(address(module_), quantity_); + + // Mint the vesting tokens (it will deploy if necessary) + module_.mint( + recipient_, + address(token_), + _getEncodedVestingParams(vestingStart_, vestingExpiry_), + quantity_, + true // Wrap vesting LP tokens so they are easily visible + ); + + // The LinearVesting module will use all of `poolTokenQuantity`, so there is no need to clean up dangling approvals + } + + /// @notice Indicates if linear vesting is supported by the callback + /// @dev Implementing contracts can opt to override this in order to disable linear vesting + function _isLinearVestingSupported() internal pure virtual returns (bool) { + return true; + } } diff --git a/src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol b/src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol new file mode 100644 index 00000000..4fe20a78 --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +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"; + +// Callbacks +import {BaseDirectToLiquidity} from "../BaseDTL.sol"; + +// Cleopatra +import {ICleopatraV1Factory} from "./lib/ICleopatraV1Factory.sol"; +import {ICleopatraV1Router} from "./lib/ICleopatraV1Router.sol"; + +/// @title CleopatraV1DirectToLiquidity +/// @notice This Callback contract deposits the proceeds from a batch auction into a Cleopatra V1 pool +/// in order to create liquidity immediately. +/// +/// The LP tokens are transferred to `DTLConfiguration.recipient`, or can optionally vest to the auction seller. +/// +/// An important risk to consider: if the auction's base token is available and liquid, a third-party +/// could front-run the auction by creating the pool before the auction ends. This would allow them to +/// manipulate the price of the pool and potentially profit from the eventual deposit of the auction proceeds. +/// +/// @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 CleopatraV1DirectToLiquidity is BaseDirectToLiquidity { + using SafeTransferLib for ERC20; + + // ========== STRUCTS ========== // + + /// @notice Parameters for the onCreate callback + /// @dev This will be encoded in the `callbackData_` parameter + /// + /// @param stable Whether the pool will be stable or volatile + /// @param maxSlippage The maximum slippage allowed when adding liquidity (in terms of basis points, where 1% = 1e2) + struct CleopatraV1OnCreateParams { + bool stable; + uint24 maxSlippage; + } + + // ========== STATE VARIABLES ========== // + + /// @notice The Cleopatra PairFactory contract + /// @dev This contract is used to create Cleopatra pairs + ICleopatraV1Factory public pairFactory; + + /// @notice The Cleopatra Router contract + /// @dev This contract is used to add liquidity to Cleopatra pairs + ICleopatraV1Router public router; + + /// @notice Mapping of lot ID to pool token + mapping(uint96 => address) public lotIdToPoolToken; + + // ========== CONSTRUCTOR ========== // + + constructor( + address auctionHouse_, + address pairFactory_, + address payable router_ + ) BaseDirectToLiquidity(auctionHouse_) { + if (pairFactory_ == address(0)) { + revert Callback_Params_InvalidAddress(); + } + if (router_ == address(0)) { + revert Callback_Params_InvalidAddress(); + } + pairFactory = ICleopatraV1Factory(pairFactory_); + router = ICleopatraV1Router(router_); + } + + // ========== CALLBACK FUNCTIONS ========== // + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This function implements the following: + /// - Validates the parameters + /// + /// This function reverts if: + /// - The callback data is of the incorrect length + /// - `CleopatraV1OnCreateParams.maxSlippage` is out of bounds + function __onCreate( + uint96 lotId_, + address, + address, + address, + uint256, + bool, + bytes calldata + ) internal virtual override { + CleopatraV1OnCreateParams memory params = _decodeParameters(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: + /// - Validates the parameters + /// - Creates the pool if necessary + /// - Deposits the tokens into the pool + function _mintAndDeposit( + uint96 lotId_, + address quoteToken_, + uint256 quoteTokenAmount_, + address baseToken_, + uint256 baseTokenAmount_, + bytes memory + ) internal virtual override { + CleopatraV1OnCreateParams memory params = _decodeParameters(lotId_); + + // Create and initialize the pool if necessary + // Token orientation is irrelevant + address pairAddress = pairFactory.getPair(baseToken_, quoteToken_, params.stable); + if (pairAddress == address(0)) { + pairAddress = pairFactory.createPair(baseToken_, quoteToken_, params.stable); + } + + // Calculate the minimum amount out for each token + uint256 quoteTokenAmountMin = _getAmountWithSlippage(quoteTokenAmount_, params.maxSlippage); + uint256 baseTokenAmountMin = _getAmountWithSlippage(baseTokenAmount_, params.maxSlippage); + + // Approve the router to spend the tokens + ERC20(quoteToken_).approve(address(router), quoteTokenAmount_); + ERC20(baseToken_).approve(address(router), baseTokenAmount_); + + // Deposit into the pool + // Token orientation is irrelevant + // If the pool is liquid and initialised at a price different to the auction, this will revert + // The auction would fail to settle, and bidders could be refunded by an abort() call + router.addLiquidity( + quoteToken_, + baseToken_, + params.stable, + quoteTokenAmount_, + baseTokenAmount_, + quoteTokenAmountMin, + baseTokenAmountMin, + address(this), + block.timestamp + ); + + // Remove any dangling approvals + // This is necessary, since the router may not spend all available tokens + ERC20(quoteToken_).approve(address(router), 0); + ERC20(baseToken_).approve(address(router), 0); + + // Store the pool token for later + lotIdToPoolToken[lotId_] = pairAddress; + } + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This function implements the following: + /// - If LinearVesting is enabled, mints derivative tokens + /// - Otherwise, transfers the pool tokens to the recipient + function _transferPoolToken(uint96 lotId_) internal virtual override { + address poolTokenAddress = lotIdToPoolToken[lotId_]; + if (poolTokenAddress == address(0)) { + revert Callback_PoolTokenNotFound(); + } + + ERC20 poolToken = ERC20(poolTokenAddress); + uint256 poolTokenQuantity = poolToken.balanceOf(address(this)); + DTLConfiguration memory config = lotConfiguration[lotId_]; + + // If vesting is enabled, create the vesting tokens + if (address(config.linearVestingModule) != address(0)) { + _mintVestingTokens( + poolToken, + poolTokenQuantity, + config.linearVestingModule, + config.recipient, + config.vestingStart, + config.vestingExpiry + ); + } + // Otherwise, send the LP tokens to the seller + else { + poolToken.safeTransfer(config.recipient, poolTokenQuantity); + } + } + + function _decodeParameters(uint96 lotId_) + internal + view + returns (CleopatraV1OnCreateParams 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, (CleopatraV1OnCreateParams)); + } +} diff --git a/src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol b/src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol new file mode 100644 index 00000000..88f6cc7d --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Libraries +import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; + +// Callbacks +import {BaseDirectToLiquidity} from "../BaseDTL.sol"; + +// Cleopatra +import {ICleopatraV2Pool} from "./lib/ICleopatraV2Pool.sol"; +import {ICleopatraV2Factory} from "./lib/ICleopatraV2Factory.sol"; +import {ICleopatraV2PositionManager} from "./lib/ICleopatraV2PositionManager.sol"; + +// Uniswap +import {TickMath} from "@uniswap-v3-core-1.0.1-solc-0.8-simulate/libraries/TickMath.sol"; +import {SqrtPriceMath} from "../../../lib/uniswap-v3/SqrtPriceMath.sol"; + +/// @title CleopatraV2DirectToLiquidity +/// @notice This Callback contract deposits the proceeds from a batch auction into a Cleopatra V2 pool +/// in order to create full-range liquidity immediately. +/// +/// The LP tokens are transferred to `DTLConfiguration.recipient`, which must be an EOA or a contract that can receive ERC721 tokens. +/// +/// An important risk to consider: if the auction's base token is available and liquid, a third-party +/// could front-run the auction by creating the pool before the auction ends. This would allow them to +/// manipulate the price of the pool and potentially profit from the eventual deposit of the auction proceeds. +/// +/// @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 CleopatraV2DirectToLiquidity is BaseDirectToLiquidity { + // ========== ERRORS ========== // + + /// @notice The specified pool fee is not enabled in the Cleopatra V2 Factory + error Callback_Params_PoolFeeNotEnabled(); + + // ========== STRUCTS ========== // + + /// @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 basis points, where 1% = 1e2) + struct CleopatraV2OnCreateParams { + uint24 poolFee; + uint24 maxSlippage; + } + + // ========== STATE VARIABLES ========== // + + /// @notice The Cleopatra V2 factory + ICleopatraV2Factory public cleopatraV2Factory; + + /// @notice The Cleopatra V2 position manager + ICleopatraV2PositionManager public cleopatraV2PositionManager; + + /// @notice Mapping of lot ID to Cleopatra V2 token ID + mapping(uint96 => uint256) public lotIdToTokenId; + + // ========== CONSTRUCTOR ========== // + + constructor( + address auctionHouse_, + address cleopatraV2Factory_, + address payable cleopatraV2PositionManager_ + ) BaseDirectToLiquidity(auctionHouse_) { + if (cleopatraV2Factory_ == address(0)) { + revert Callback_Params_InvalidAddress(); + } + cleopatraV2Factory = ICleopatraV2Factory(cleopatraV2Factory_); + + if (cleopatraV2PositionManager_ == address(0)) { + revert Callback_Params_InvalidAddress(); + } + cleopatraV2PositionManager = ICleopatraV2PositionManager(cleopatraV2PositionManager_); + } + + // ========== CALLBACK FUNCTIONS ========== // + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This function performs the following: + /// - Validates the input data + /// + /// This function reverts if: + /// - `CleopatraV2OnCreateParams.poolFee` is not enabled + /// - `CleopatraV2OnCreateParams.maxSlippage` is out of bounds + function __onCreate( + uint96 lotId_, + address, + address, + address, + uint256, + bool, + bytes calldata + ) internal virtual override { + CleopatraV2OnCreateParams memory params = _decodeParameters(lotId_); + + // Validate the parameters + // Pool fee + // Fee not enabled + if (cleopatraV2Factory.feeAmountTickSpacing(params.poolFee) == 0) { + revert Callback_Params_PoolFeeNotEnabled(); + } + + // 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); + } + } + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This function implements the following: + /// - Creates and initializes the pool, if necessary + /// - Creates a new position and adds liquidity + /// - Transfers the ERC721 pool token to the recipient + /// + /// The assumptions are: + /// - the callback has `quoteTokenAmount_` quantity of quote tokens (as `receiveQuoteTokens` flag is set) + /// - the callback has `baseTokenAmount_` quantity of base tokens + function _mintAndDeposit( + uint96 lotId_, + address quoteToken_, + uint256 quoteTokenAmount_, + address baseToken_, + uint256 baseTokenAmount_, + bytes memory + ) internal virtual override { + // Determine the ordering of tokens + bool quoteTokenIsToken0 = quoteToken_ < baseToken_; + + // Create and initialize the pool if necessary + { + // Determine sqrtPriceX96 + uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96( + quoteToken_, baseToken_, quoteTokenAmount_, baseTokenAmount_ + ); + + // 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 + _createAndInitializePoolIfNecessary( + quoteTokenIsToken0 ? quoteToken_ : baseToken_, + quoteTokenIsToken0 ? baseToken_ : quoteToken_, + _decodeParameters(lotId_).poolFee, + sqrtPriceX96 + ); + } + + // Mint the position and add liquidity + { + ICleopatraV2PositionManager.MintParams memory mintParams = + _getMintParams(lotId_, quoteToken_, quoteTokenAmount_, baseToken_, baseTokenAmount_); + + // Approve spending + ERC20(quoteToken_).approve(address(cleopatraV2PositionManager), quoteTokenAmount_); + ERC20(baseToken_).approve(address(cleopatraV2PositionManager), baseTokenAmount_); + + // Mint the position + (uint256 tokenId,,,) = cleopatraV2PositionManager.mint(mintParams); + lotIdToTokenId[lotId_] = tokenId; + + // Reset dangling approvals + // The position manager may not spend all tokens + ERC20(quoteToken_).approve(address(cleopatraV2PositionManager), 0); + ERC20(baseToken_).approve(address(cleopatraV2PositionManager), 0); + } + } + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This function does not perform any actions, + /// as `_mintAndDeposit()` directly transfers the token to the recipient + function _transferPoolToken(uint96) internal virtual override { + // Nothing to do + } + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This implementation disables linear vesting + function _isLinearVestingSupported() internal pure virtual override returns (bool) { + return false; + } + + // ========== INTERNAL FUNCTIONS ========== // + + /// @dev Copied from UniswapV3's PoolInitializer (which is GPL >= 2) + function _createAndInitializePoolIfNecessary( + address token0, + address token1, + uint24 fee, + uint160 sqrtPriceX96 + ) internal returns (address pool) { + require(token0 < token1); + pool = cleopatraV2Factory.getPool(token0, token1, fee); + + if (pool == address(0)) { + pool = cleopatraV2Factory.createPool(token0, token1, fee, sqrtPriceX96); + } else { + (uint160 sqrtPriceX96Existing,,,,,,) = ICleopatraV2Pool(pool).slot0(); + if (sqrtPriceX96Existing == 0) { + ICleopatraV2Pool(pool).initialize(sqrtPriceX96); + } + } + } + + function _getMintParams( + uint96 lotId_, + address quoteToken_, + uint256 quoteTokenAmount_, + address baseToken_, + uint256 baseTokenAmount_ + ) internal view returns (ICleopatraV2PositionManager.MintParams memory) { + CleopatraV2OnCreateParams memory params = _decodeParameters(lotId_); + + int24 tickSpacing = cleopatraV2Factory.feeAmountTickSpacing(params.poolFee); + + // Determine the ordering of tokens + bool quoteTokenIsToken0 = quoteToken_ < baseToken_; + + // Calculate the minimum amount out for each token + uint256 amount0 = quoteTokenIsToken0 ? quoteTokenAmount_ : baseTokenAmount_; + uint256 amount1 = quoteTokenIsToken0 ? baseTokenAmount_ : quoteTokenAmount_; + + return ICleopatraV2PositionManager.MintParams({ + token0: quoteTokenIsToken0 ? quoteToken_ : baseToken_, + token1: quoteTokenIsToken0 ? baseToken_ : quoteToken_, + fee: params.poolFee, + tickLower: (TickMath.MIN_TICK / tickSpacing) * tickSpacing, + tickUpper: (TickMath.MAX_TICK / tickSpacing) * tickSpacing, + amount0Desired: amount0, + amount1Desired: amount1, + amount0Min: _getAmountWithSlippage(amount0, params.maxSlippage), + amount1Min: _getAmountWithSlippage(amount1, params.maxSlippage), + recipient: lotConfiguration[lotId_].recipient, + deadline: block.timestamp, + veNFTTokenId: 0 // Not supported at the moment + }); + } + + /// @notice Decodes the configuration parameters from the DTLConfiguration + /// @dev The configuration parameters are stored in `DTLConfiguration.implParams` + function _decodeParameters(uint96 lotId_) + internal + view + returns (CleopatraV2OnCreateParams 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, (CleopatraV2OnCreateParams)); + } +} diff --git a/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Factory.sol b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Factory.sol new file mode 100644 index 00000000..8e5d780a --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Factory.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICleopatraV1Factory { + event ImplementationChanged( + address indexed oldImplementation, address indexed newImplementation + ); + event Initialized(uint8 version); + event OwnerChanged(address indexed oldOwner, address indexed newOwner); + event PairCreated( + address indexed token0, address indexed token1, bool stable, address pair, uint256 + ); + event SetFeeSplit(uint8 toFeesOld, uint8 toTreasuryOld, uint8 toFeesNew, uint8 toTreasuryNew); + event SetPoolFeeSplit( + address pool, uint8 toFeesOld, uint8 toTreasuryOld, uint8 toFeesNew, uint8 toTreasuryNew + ); + + function MAX_FEE() external view returns (uint256); + function _pairFee(address pair) external view returns (uint256 fee); + function acceptFeeManager() external; + function acceptPauser() external; + function allPairs(uint256) external view returns (address); + function allPairsLength() external view returns (uint256); + function createPair( + address tokenA, + address tokenB, + bool stable + ) external returns (address pair); + function feeManager() external view returns (address); + function feeSplit() external view returns (uint8); + function getFee(bool _stable) external view returns (uint256); + function getPair(address, address, bool) external view returns (address); + function getPairFee(address _pair, bool _stable) external view returns (uint256); + function getPoolFeeSplit(address _pool) external view returns (uint8 _poolFeeSplit); + function implementation() external view returns (address); + function initialize( + address _voter, + address msig, + address _owner, + address _implementation + ) external; + function isPair(address) external view returns (bool); + function isPaused() external view returns (bool); + function owner() external view returns (address); + function pairCodeHash() external pure returns (bytes32); + function pairFee(address _pool) external view returns (uint256 fee); + function pauser() external view returns (address); + function pendingFeeManager() external view returns (address); + function pendingPauser() external view returns (address); + function setFee(bool _stable, uint256 _fee) external; + function setFeeManager(address _feeManager) external; + function setFeeSplit(uint8 _toFees, uint8 _toTreasury) external; + function setImplementation(address _implementation) external; + function setOwner(address _owner) external; + function setPairFee(address _pair, uint256 _fee) external; + function setPause(bool _state) external; + function setPauser(address _pauser) external; + function setPoolFeeSplit(address _pool, uint8 _toFees, uint8 _toTreasury) external; + function setTreasury(address _treasury) external; + function stableFee() external view returns (uint256); + function treasury() external view returns (address); + function volatileFee() external view returns (uint256); + function voter() external view returns (address); +} diff --git a/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Pool.sol b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Pool.sol new file mode 100644 index 00000000..bcc3df99 --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Pool.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICleopatraV1Pool { + struct Observation { + uint256 timestamp; + uint256 reserve0Cumulative; + uint256 reserve1Cumulative; + } + + event Approval(address indexed owner, address indexed spender, uint256 amount); + event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); + event Claim( + address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1 + ); + event Fees(address indexed sender, uint256 amount0, uint256 amount1); + event Initialized(uint8 version); + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event SetFeeSplit(uint8 toFeesOld, uint8 toTreasuryOld, uint8 toFeesNew, uint8 toTreasuryNew); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(uint256 reserve0, uint256 reserve1); + event Transfer(address indexed from, address indexed to, uint256 amount); + + function allowance(address, address) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address) external view returns (uint256); + function blockTimestampLast() external view returns (uint256); + function burn(address to) external returns (uint256 amount0, uint256 amount1); + function claimFees() external returns (uint256, uint256); + function current(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut); + function currentCumulativePrices() + external + view + returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); + function decimals() external view returns (uint8); + function feeSplit() external view returns (uint8); + function fees() external view returns (address); + function getAmountOut(uint256 amountIn, address tokenIn) external view returns (uint256); + function getReserves() + external + view + returns (uint256 _reserve0, uint256 _reserve1, uint256 _blockTimestampLast); + function initialize(address _token0, address _token1, bool _stable) external; + function initializeVoter() external; + function lastObservation() external view returns (Observation memory); + function metadata() + external + view + returns ( + uint256 dec0, + uint256 dec1, + uint256 r0, + uint256 r1, + bool st, + address t0, + address t1 + ); + function mint(address to) external returns (uint256 liquidity); + function name() external view returns (string memory); + function nonces(address) external view returns (uint256); + function observationLength() external view returns (uint256); + function observations(uint256) + external + view + returns (uint256 timestamp, uint256 reserve0Cumulative, uint256 reserve1Cumulative); + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + function prices( + address tokenIn, + uint256 amountIn, + uint256 points + ) external view returns (uint256[] memory); + function quote( + address tokenIn, + uint256 amountIn, + uint256 granularity + ) external view returns (uint256 amountOut); + function reserve0() external view returns (uint256); + function reserve0CumulativeLast() external view returns (uint256); + function reserve1() external view returns (uint256); + function reserve1CumulativeLast() external view returns (uint256); + function sample( + address tokenIn, + uint256 amountIn, + uint256 points, + uint256 window + ) external view returns (uint256[] memory); + function setActiveGauge(bool isActive) external; + function setFeeSplit() external; + function skim(address to) external; + function stable() external view returns (bool); + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes memory data) external; + function symbol() external view returns (string memory); + function sync() external; + function token0() external view returns (address); + function token1() external view returns (address); + function tokens() external view returns (address, address); + function totalSupply() external view returns (uint256); + function transfer(address dst, uint256 amount) external returns (bool); + function transferFrom(address src, address dst, uint256 amount) external returns (bool); + function voter() external view returns (address); +} diff --git a/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Router.sol b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Router.sol new file mode 100644 index 00000000..19521f9c --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Router.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICleopatraV1Router { + struct route { + address from; + address to; + bool stable; + } + + event Initialized(uint8 version); + + receive() external payable; + + function UNSAFE_swapExactTokensForTokens( + uint256[] memory amounts, + route[] memory routes, + address to, + uint256 deadline + ) external returns (uint256[] memory); + function addLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); + function addLiquidityETH( + address token, + bool stable, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); + function factory() external view returns (address); + function getAmountOut( + uint256 amountIn, + address tokenIn, + address tokenOut + ) external view returns (uint256 amount, bool stable); + function getAmountsOut( + uint256 amountIn, + route[] memory routes + ) external view returns (uint256[] memory amounts); + function getReserves( + address tokenA, + address tokenB, + bool stable + ) external view returns (uint256 reserveA, uint256 reserveB); + function initialize(address _factory, address _weth, address _timelock) external; + function isPair(address pair) external view returns (bool); + function pairFor( + address tokenA, + address tokenB, + bool stable + ) external view returns (address pair); + function quoteAddLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 amountADesired, + uint256 amountBDesired + ) external view returns (uint256 amountA, uint256 amountB, uint256 liquidity); + function quoteRemoveLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 liquidity + ) external view returns (uint256 amountA, uint256 amountB); + function recoverFunds(address token, address to, uint256 amount) external; + function removeLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + function removeLiquidityETH( + address token, + bool stable, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + bool stable, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + function removeLiquidityETHWithPermit( + address token, + bool stable, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountToken, uint256 amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + bool stable, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountToken, uint256 amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + bool stable, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountA, uint256 amountB); + function sortTokens( + address tokenA, + address tokenB + ) external pure returns (address token0, address token1); + function swapExactETHForTokens( + uint256 amountOutMin, + route[] memory routes, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint256 amountOutMin, + route[] memory routes, + address to, + uint256 deadline + ) external payable; + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + route[] memory routes, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + route[] memory routes, + address to, + uint256 deadline + ) external; + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + route[] memory routes, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + function swapExactTokensForTokensSimple( + uint256 amountIn, + uint256 amountOutMin, + address tokenFrom, + address tokenTo, + bool stable, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + route[] memory routes, + address to, + uint256 deadline + ) external; + function timelock() external view returns (address); + function weth() external view returns (address); +} diff --git a/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Factory.sol b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Factory.sol new file mode 100644 index 00000000..47d9559f --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Factory.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICleopatraV2Factory { + event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); + event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector); + event FeeSetterChanged(address indexed oldSetter, address indexed newSetter); + event ImplementationChanged( + address indexed oldImplementation, address indexed newImplementation + ); + event OwnerChanged(address indexed oldOwner, address indexed newOwner); + event PoolCreated( + address indexed token0, + address indexed token1, + uint24 indexed fee, + int24 tickSpacing, + address pool + ); + event SetFeeProtocol( + uint8 feeProtocol0Old, uint8 feeProtocol1Old, uint8 feeProtocol0New, uint8 feeProtocol1New + ); + event SetPoolFeeProtocol( + address pool, + uint8 feeProtocol0Old, + uint8 feeProtocol1Old, + uint8 feeProtocol0New, + uint8 feeProtocol1New + ); + + function POOL_INIT_CODE_HASH() external view returns (bytes32); + function createPool( + address tokenA, + address tokenB, + uint24 fee, + uint160 sqrtPriceX96 + ) external returns (address pool); + function enableFeeAmount(uint24 fee, int24 tickSpacing) external; + function feeAmountTickSpacing(uint24) external view returns (int24); + function feeCollector() external view returns (address); + function feeProtocol() external view returns (uint8); + function feeSetter() external view returns (address); + function getPool(address, address, uint24) external view returns (address); + function implementation() external view returns (address); + function initialize( + address _nfpManager, + address _votingEscrow, + address _voter, + address _implementation + ) external; + function initializeFeeSetter() external; + function initializePool( + address token0, + address token1, + uint24 fee, + uint160 sqrtPriceX96 + ) external returns (address pool); + function nfpManager() external view returns (address); + function owner() external view returns (address); + function poolFeeProtocol(address pool) external view returns (uint8 __poolFeeProtocol); + function setFee(address _pool, uint24 _fee) external; + function setFeeCollector(address _feeCollector) external; + function setFeeProtocol(uint8 _feeProtocol) external; + function setFeeSetter(address _newFeeSetter) external; + function setImplementation(address _implementation) external; + function setOwner(address _owner) external; + function setPoolFeeProtocol(address pool, uint8 feeProtocol0, uint8 feeProtocol1) external; + function voter() external view returns (address); + function votingEscrow() external view returns (address); +} diff --git a/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Pool.sol b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Pool.sol new file mode 100644 index 00000000..c7bffdae --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Pool.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICleopatraV2Pool { + event Burn( + address indexed owner, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount, + uint256 amount0, + uint256 amount1 + ); + event Collect( + address indexed owner, + address recipient, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount0, + uint128 amount1 + ); + event CollectProtocol( + address indexed sender, address indexed recipient, uint128 amount0, uint128 amount1 + ); + event Flash( + address indexed sender, + address indexed recipient, + uint256 amount0, + uint256 amount1, + uint256 paid0, + uint256 paid1 + ); + event IncreaseObservationCardinalityNext( + uint16 observationCardinalityNextOld, uint16 observationCardinalityNextNew + ); + event Initialize(uint160 sqrtPriceX96, int24 tick); + event Mint( + address sender, + address indexed owner, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount, + uint256 amount0, + uint256 amount1 + ); + event SetFeeProtocol( + uint8 feeProtocol0Old, uint8 feeProtocol1Old, uint8 feeProtocol0New, uint8 feeProtocol1New + ); + event Swap( + address indexed sender, + address indexed recipient, + int256 amount0, + int256 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick + ); + + function _advancePeriod() external; + function boostInfos(uint256 period) + external + view + returns (uint128 totalBoostAmount, int128 totalVeRamAmount); + function boostInfos( + uint256 period, + bytes32 key + ) + external + view + returns ( + uint128 boostAmount, + int128 veRamAmount, + int256 secondsDebtX96, + int256 boostedSecondsDebtX96 + ); + function boostedLiquidity() external view returns (uint128); + function burn( + uint256 index, + int24 tickLower, + int24 tickUpper, + uint128 amount + ) external returns (uint256 amount0, uint256 amount1); + function burn( + int24 tickLower, + int24 tickUpper, + uint128 amount + ) external returns (uint256 amount0, uint256 amount1); + function burn( + uint256 index, + int24 tickLower, + int24 tickUpper, + uint128 amount, + uint256 veRamTokenId + ) external returns (uint256 amount0, uint256 amount1); + function collect( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount0Requested, + uint128 amount1Requested + ) external returns (uint128 amount0, uint128 amount1); + function collect( + address recipient, + uint256 index, + int24 tickLower, + int24 tickUpper, + uint128 amount0Requested, + uint128 amount1Requested + ) external returns (uint128 amount0, uint128 amount1); + function collectProtocol( + address recipient, + uint128 amount0Requested, + uint128 amount1Requested + ) external returns (uint128 amount0, uint128 amount1); + function currentFee() external view returns (uint24); + function factory() external view returns (address); + function fee() external view returns (uint24); + function feeGrowthGlobal0X128() external view returns (uint256); + function feeGrowthGlobal1X128() external view returns (uint256); + function flash( + address recipient, + uint256 amount0, + uint256 amount1, + bytes memory data + ) external; + function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; + function initialize( + address _factory, + address _nfpManager, + address _veRam, + address _voter, + address _token0, + address _token1, + uint24 _fee, + int24 _tickSpacing + ) external; + function initialize(uint160 sqrtPriceX96) external; + function lastPeriod() external view returns (uint256); + function liquidity() external view returns (uint128); + function maxLiquidityPerTick() external view returns (uint128); + function mint( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount, + bytes memory data + ) external returns (uint256 amount0, uint256 amount1); + function mint( + address recipient, + uint256 index, + int24 tickLower, + int24 tickUpper, + uint128 amount, + uint256 veRamTokenId, + bytes memory data + ) external returns (uint256 amount0, uint256 amount1); + function nfpManager() external view returns (address); + function observations(uint256 index) + external + view + returns ( + uint32 blockTimestamp, + int56 tickCumulative, + uint160 secondsPerLiquidityCumulativeX128, + bool initialized, + uint160 secondsPerBoostedLiquidityPeriodX128 + ); + function observe(uint32[] memory secondsAgos) + external + view + returns ( + int56[] memory tickCumulatives, + uint160[] memory secondsPerLiquidityCumulativeX128s, + uint160[] memory secondsPerBoostedLiquidityPeriodX128s + ); + function periodCumulativesInside( + uint32 period, + int24 tickLower, + int24 tickUpper + ) + external + view + returns ( + uint160 secondsPerLiquidityInsideX128, + uint160 secondsPerBoostedLiquidityInsideX128 + ); + function periods(uint256 period) + external + view + returns ( + uint32 previousPeriod, + int24 startTick, + int24 lastTick, + uint160 endSecondsPerLiquidityPeriodX128, + uint160 endSecondsPerBoostedLiquidityPeriodX128, + uint32 boostedInRange + ); + function positionPeriodDebt( + uint256 period, + address owner, + uint256 index, + int24 tickLower, + int24 tickUpper + ) external view returns (int256 secondsDebtX96, int256 boostedSecondsDebtX96); + function positionPeriodSecondsInRange( + uint256 period, + address owner, + uint256 index, + int24 tickLower, + int24 tickUpper + ) + external + view + returns (uint256 periodSecondsInsideX96, uint256 periodBoostedSecondsInsideX96); + function positions(bytes32 key) + external + view + returns ( + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1, + uint256 attachedVeRamId + ); + function protocolFees() external view returns (uint128 token0, uint128 token1); + function readStorage(bytes32[] memory slots) + external + view + returns (bytes32[] memory returnData); + function setFee(uint24 _fee) external; + function setFeeProtocol() external; + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ); + function snapshotCumulativesInside( + int24 tickLower, + int24 tickUpper + ) + external + view + returns ( + int56 tickCumulativeInside, + uint160 secondsPerLiquidityInsideX128, + uint160 secondsPerBoostedLiquidityInsideX128, + uint32 secondsInside + ); + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes memory data + ) external returns (int256 amount0, int256 amount1); + function tickBitmap(int16 tick) external view returns (uint256); + function tickSpacing() external view returns (int24); + function ticks(int24 tick) + external + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint128 boostedLiquidityGross, + int128 boostedLiquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128, + int56 tickCumulativeOutside, + uint160 secondsPerLiquidityOutsideX128, + uint32 secondsOutside, + bool initialized + ); + function token0() external view returns (address); + function token1() external view returns (address); + function veRam() external view returns (address); + function voter() external view returns (address); +} diff --git a/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2PositionManager.sol b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2PositionManager.sol new file mode 100644 index 00000000..c2ad566c --- /dev/null +++ b/src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2PositionManager.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICleopatraV2PositionManager { + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + uint256 veNFTTokenId; + } + + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1); + event DecreaseLiquidity( + uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1 + ); + event IncreaseLiquidity( + uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1 + ); + event SwitchAttachment(uint256 indexed tokenId, uint256 oldVeTokenId, uint256 newVeTokenId); + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + receive() external payable; + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external view returns (bytes32); + function WETH9() external view returns (address); + function approve(address to, uint256 tokenId) external; + function balanceOf(address owner) external view returns (uint256); + function baseURI() external pure returns (string memory); + function batchSwitchAttachment(uint256[] memory tokenIds, uint256 veRamTokenId) external; + function burn(uint256 tokenId) external payable; + function collect(CollectParams memory params) + external + payable + returns (uint256 amount0, uint256 amount1); + function createAndInitializePoolIfNecessary( + address token0, + address token1, + uint24 fee, + uint160 sqrtPriceX96 + ) external payable returns (address pool); + function decreaseLiquidity(DecreaseLiquidityParams memory params) + external + payable + returns (uint256 amount0, uint256 amount1); + function factory() external view returns (address); + function getApproved(uint256 tokenId) external view returns (address); + function getReward(uint256 tokenId, address[] memory tokens) external; + function increaseLiquidity(IncreaseLiquidityParams memory params) + external + payable + returns (uint128 liquidity, uint256 amount0, uint256 amount1); + function isApprovedForAll(address owner, address operator) external view returns (bool); + function isApprovedOrOwner(address spender, uint256 tokenId) external view returns (bool); + function mint(MintParams memory params) + external + payable + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + function multicall(bytes[] memory data) external payable returns (bytes[] memory results); + function name() external view returns (string memory); + function ownerOf(uint256 tokenId) external view returns (address); + function permit( + address spender, + uint256 tokenId, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + function ramsesV2MintCallback( + uint256 amount0Owed, + uint256 amount1Owed, + bytes memory data + ) external; + function refundETH() external payable; + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) external; + function selfPermit( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + function selfPermitAllowed( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + function selfPermitAllowedIfNecessary( + address token, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + function selfPermitIfNecessary( + address token, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; + function setApprovalForAll(address operator, bool approved) external; + function supportsInterface(bytes4 interfaceId) external view returns (bool); + function sweepToken(address token, uint256 amountMinimum, address recipient) external payable; + function switchAttachment(uint256 tokenId, uint256 veNftTokenId) external; + function symbol() external view returns (string memory); + function tokenByIndex(uint256 index) external view returns (uint256); + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); + function tokenURI(uint256 tokenId) external view returns (string memory); + function totalSupply() external view returns (uint256); + function transferFrom(address from, address to, uint256 tokenId) external; + function unwrapWETH9(uint256 amountMinimum, address recipient) external payable; + function voter() external view returns (address); + function votingEscrow() external view returns (address); +} diff --git a/src/callbacks/liquidity/UniswapV2DTL.sol b/src/callbacks/liquidity/UniswapV2DTL.sol index 3dfb43b5..ba8489ac 100644 --- a/src/callbacks/liquidity/UniswapV2DTL.sol +++ b/src/callbacks/liquidity/UniswapV2DTL.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 {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Factory.sol"; @@ -24,9 +25,11 @@ 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 onSettle 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`) @@ -44,6 +47,10 @@ contract UniswapV2DirectToLiquidity is BaseDirectToLiquidity { /// @dev This contract is used to add liquidity to Uniswap V2 pools IUniswapV2Router02 public uniV2Router; + /// @notice Mapping of lot ID to pool token + /// @dev This is used to track the pool token for each lot + mapping(uint96 lotId => address poolToken) public lotIdToPoolToken; + // ========== CONSTRUCTOR ========== // constructor( @@ -90,13 +97,13 @@ contract UniswapV2DirectToLiquidity is BaseDirectToLiquidity { /// - Creates the pool if necessary /// - Deposits the tokens into the pool function _mintAndDeposit( - uint96, + uint96 lotId_, address quoteToken_, uint256 quoteTokenAmount_, address baseToken_, uint256 baseTokenAmount_, bytes memory callbackData_ - ) internal virtual override returns (ERC20 poolToken) { + ) internal virtual override { // Decode the callback data OnSettleParams memory params = abi.decode(callbackData_, (OnSettleParams)); @@ -132,6 +139,38 @@ contract UniswapV2DirectToLiquidity is BaseDirectToLiquidity { ERC20(quoteToken_).approve(address(uniV2Router), 0); ERC20(baseToken_).approve(address(uniV2Router), 0); - return ERC20(pairAddress); + // Store the pool token for later + lotIdToPoolToken[lotId_] = pairAddress; + } + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This function implements the following: + /// - If LinearVesting is enabled, mints derivative tokens + /// - Otherwise, transfers the pool tokens to the recipient + function _transferPoolToken(uint96 lotId_) internal virtual override { + address poolTokenAddress = lotIdToPoolToken[lotId_]; + if (poolTokenAddress == address(0)) { + revert Callback_PoolTokenNotFound(); + } + + ERC20 poolToken = ERC20(poolTokenAddress); + uint256 poolTokenQuantity = poolToken.balanceOf(address(this)); + DTLConfiguration memory config = lotConfiguration[lotId_]; + + // If vesting is enabled, create the vesting tokens + if (address(config.linearVestingModule) != address(0)) { + _mintVestingTokens( + poolToken, + poolTokenQuantity, + config.linearVestingModule, + config.recipient, + config.vestingStart, + config.vestingExpiry + ); + } + // Otherwise, send the LP tokens to the seller + else { + poolToken.safeTransfer(config.recipient, poolTokenQuantity); + } } } diff --git a/src/callbacks/liquidity/UniswapV3DTL.sol b/src/callbacks/liquidity/UniswapV3DTL.sol index 4efdfeec..504164b6 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 @@ -21,7 +22,7 @@ import {BaseDirectToLiquidity} from "./BaseDTL.sol"; /// @title UniswapV3DirectToLiquidity /// @notice This Callback contract deposits the proceeds from a batch auction into a Uniswap V3 pool -/// in order to create liquidity immediately. +/// in order to create full-range liquidity immediately. /// /// The Uniswap V3 position is tokenised as an ERC-20 using [G-UNI](https://github.com/gelatodigital/g-uni-v1-core). /// @@ -34,6 +35,8 @@ 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(); @@ -60,6 +63,11 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { /// @dev This contract is used to create the ERC20 LP tokens IGUniFactory public gUniFactory; + /// @notice Mapping of lot ID to pool token + /// @dev This is used to track the pool token for each lot. + /// As this contract uses G-UNI to tokenise the Uniswap V3 position, the pool token is an ERC-20. + mapping(uint96 => address) public lotIdToPoolToken; + // ========== CONSTRUCTOR ========== // constructor( @@ -129,7 +137,7 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { address baseToken_, uint256 baseTokenAmount_, bytes memory callbackData_ - ) internal virtual override returns (ERC20 poolToken) { + ) internal virtual override { // Decode the callback data OnSettleParams memory params = abi.decode(callbackData_, (OnSettleParams)); @@ -214,9 +222,46 @@ contract UniswapV3DirectToLiquidity is BaseDirectToLiquidity { // Mint the LP tokens // The parent callback is responsible for transferring any leftover quote and base tokens gUniPoolToken.mint(poolTokenQuantity, address(this)); + + // Remove any dangling approvals + // This is necessary, since the G-UNI pool may not spend all available tokens + ERC20(quoteToken_).approve(address(poolTokenAddress), 0); + ERC20(baseToken_).approve(address(poolTokenAddress), 0); + } + + // Store the pool token for later + lotIdToPoolToken[lotId_] = poolTokenAddress; + } + + /// @inheritdoc BaseDirectToLiquidity + /// @dev This function implements the following: + /// - If LinearVesting is enabled, mints derivative tokens + /// - Otherwise, transfers the pool tokens to the recipient + function _transferPoolToken(uint96 lotId_) internal virtual override { + address poolTokenAddress = lotIdToPoolToken[lotId_]; + if (poolTokenAddress == address(0)) { + revert Callback_PoolTokenNotFound(); } - poolToken = ERC20(poolTokenAddress); + ERC20 poolToken = ERC20(poolTokenAddress); + uint256 poolTokenQuantity = poolToken.balanceOf(address(this)); + DTLConfiguration memory config = lotConfiguration[lotId_]; + + // If vesting is enabled, create the vesting tokens + if (address(config.linearVestingModule) != address(0)) { + _mintVestingTokens( + poolToken, + poolTokenQuantity, + config.linearVestingModule, + config.recipient, + config.vestingStart, + config.vestingExpiry + ); + } + // Otherwise, send the LP tokens to the seller + else { + poolToken.safeTransfer(config.recipient, poolTokenQuantity); + } } // ========== INTERNAL FUNCTIONS ========== // diff --git a/test/Constants.sol b/test/Constants.sol index 1f37435d..45983e84 100644 --- a/test/Constants.sol +++ b/test/Constants.sol @@ -16,4 +16,14 @@ abstract contract TestConstants is TestConstantsCore { address(0xAA22883d39ea4e42f7033e3e931aA476DEe30b73); address internal constant _CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + + // Cleopatra addresses on Mantle + address internal constant _CLEOPATRA_V1_FACTORY = + address(0xAAA16c016BF556fcD620328f0759252E29b1AB57); + address internal constant _CLEOPATRA_V1_ROUTER = + address(0xAAA45c8F5ef92a000a121d102F4e89278a711Faa); + address internal constant _CLEOPATRA_V2_FACTORY = + address(0xAAA32926fcE6bE95ea2c51cB4Fcb60836D320C42); + address internal constant _CLEOPATRA_V2_POSITION_MANAGER = + address(0xAAA78E8C4241990B4ce159E105dA08129345946A); } diff --git a/test/callbacks/liquidity/BaselineV2/onCreate.t.sol b/test/callbacks/liquidity/BaselineV2/onCreate.t.sol index 75d59e51..4604f11c 100644 --- a/test/callbacks/liquidity/BaselineV2/onCreate.t.sol +++ b/test/callbacks/liquidity/BaselineV2/onCreate.t.sol @@ -29,7 +29,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"); @@ -130,7 +130,7 @@ contract BaselineOnCreateTest is BaselineAxisLaunchTest { return roundedTick; } - function _assertTicks(int24 fixedPriceTick_) internal { + function _assertTicks(int24 fixedPriceTick_) internal view { assertEq(_baseToken.activeTick(), fixedPriceTick_, "active tick"); console2.log("Active tick: ", _baseToken.activeTick()); diff --git a/test/callbacks/liquidity/CleopatraV1/CleopatraV1DTLTest.sol b/test/callbacks/liquidity/CleopatraV1/CleopatraV1DTLTest.sol new file mode 100644 index 00000000..d6d2463b --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV1/CleopatraV1DTLTest.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Test scaffolding +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 {WithSalts} from "../../../lib/WithSalts.sol"; +import {TestConstants} from "../../../Constants.sol"; + +// Mocks +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; +import {MockBatchAuctionModule} from + "@axis-core-1.0.0-test/modules/Auction/MockBatchAuctionModule.sol"; + +// Callbacks +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; + +// Cleopatra +import {CleopatraV1DirectToLiquidity} from + "../../../../src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol"; +import {ICleopatraV1Factory} from + "../../../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Factory.sol"; +import {ICleopatraV1Router} from + "../../../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Router.sol"; + +// Axis core +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.0/modules/Keycode.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 {LinearVesting} from "@axis-core-1.0.0/modules/derivatives/LinearVesting.sol"; + +abstract contract CleopatraV1DirectToLiquidityTest is + Test, + Permit2User, + WithSalts, + TestConstants +{ + using Callbacks for CleopatraV1DirectToLiquidity; + + 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; + + uint48 internal _initialTimestamp; + + uint96 internal _lotId = 1; + + BatchAuctionHouse internal _auctionHouse; + CleopatraV1DirectToLiquidity internal _dtl; + address internal _dtlAddress; + + ICleopatraV1Factory internal _factory; + ICleopatraV1Router internal _router; + LinearVesting internal _linearVesting; + MockBatchAuctionModule internal _batchAuctionModule; + + MockERC20 internal _quoteToken; + MockERC20 internal _baseToken; + + uint96 internal _proceeds; + uint96 internal _refund; + + // Inputs + CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams internal _cleopatraCreateParams = + CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams({stable: false, maxSlippage: uint24(0)}); + BaseDirectToLiquidity.OnCreateParams internal _dtlCreateParams = BaseDirectToLiquidity + .OnCreateParams({ + proceedsUtilisationPercent: 100e2, + vestingStart: 0, + vestingExpiry: 0, + recipient: _SELLER, + implParams: abi.encode(_cleopatraCreateParams) + }); + + function setUp() public { + // Create a fork on Mantle + string memory mantleRpcUrl = vm.envString("MANTLE_RPC_URL"); + vm.createSelectFork(mantleRpcUrl); + require(block.chainid == 5000, "Must be on Mantle"); + + _initialTimestamp = uint48(block.timestamp); + + // Create an BatchAuctionHouse at a deterministic address, since it is used as input to callbacks + BatchAuctionHouse auctionHouse = new BatchAuctionHouse(_OWNER, _PROTOCOL, _permit2Address); + _auctionHouse = BatchAuctionHouse(_AUCTION_HOUSE); + vm.etch(address(_auctionHouse), address(auctionHouse).code); + vm.store(address(_auctionHouse), bytes32(uint256(0)), bytes32(abi.encode(_OWNER))); // Owner + vm.store(address(_auctionHouse), bytes32(uint256(6)), bytes32(abi.encode(1))); // Reentrancy + vm.store(address(_auctionHouse), bytes32(uint256(10)), bytes32(abi.encode(_PROTOCOL))); // Protocol + + _factory = ICleopatraV1Factory(_CLEOPATRA_V1_FACTORY); + _router = ICleopatraV1Router(payable(_CLEOPATRA_V1_ROUTER)); + + _linearVesting = new LinearVesting(address(_auctionHouse)); + _batchAuctionModule = new MockBatchAuctionModule(address(_auctionHouse)); + + // Install a mock batch auction module + vm.prank(_OWNER); + _auctionHouse.installModule(_batchAuctionModule); + + _quoteToken = new MockERC20("Quote Token", "QT", 18); + _baseToken = new MockERC20("Base Token", "BT", 18); + } + + // ========== MODIFIERS ========== // + + modifier givenLinearVestingModuleIsInstalled() { + vm.prank(_OWNER); + _auctionHouse.installModule(_linearVesting); + _; + } + + modifier givenCallbackIsCreated() { + // Get the salt + bytes memory args = abi.encode(address(_auctionHouse), address(_factory), address(_router)); + bytes32 salt = _getTestSalt( + "CleopatraV1DirectToLiquidity", type(CleopatraV1DirectToLiquidity).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 CleopatraV1DirectToLiquidity{salt: salt}( + address(_auctionHouse), address(_factory), payable(_router) + ); + vm.stopBroadcast(); + + _dtlAddress = address(_dtl); + _; + } + + modifier givenAddressHasQuoteTokenBalance(address address_, uint256 amount_) { + _quoteToken.mint(address_, amount_); + _; + } + + modifier givenAddressHasBaseTokenBalance(address address_, uint256 amount_) { + _baseToken.mint(address_, amount_); + _; + } + + modifier givenAddressHasQuoteTokenAllowance(address owner_, address spender_, uint256 amount_) { + vm.prank(owner_); + _quoteToken.approve(spender_, amount_); + _; + } + + modifier givenAddressHasBaseTokenAllowance(address owner_, address spender_, uint256 amount_) { + vm.prank(owner_); + _baseToken.approve(spender_, amount_); + _; + } + + function _createLot(address seller_) internal returns (uint96 lotId) { + // 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: _dtl, + callbackData: abi.encode(_dtlCreateParams), + 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_); + return _auctionHouse.auction(routingParams, auctionParams, ""); + } + + 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 _performOnCancel(uint96 lotId_, uint256 refundAmount_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCancel(lotId_, refundAmount_, false, abi.encode("")); + } + + function _performOnCancel() internal { + _performOnCancel(_lotId, 0); + } + + function _performOnCurate(uint96 curatorPayout_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCurate(_lotId, curatorPayout_, false, abi.encode("")); + } + + modifier givenOnCurate(uint96 curatorPayout_) { + _performOnCurate(curatorPayout_); + _; + } + + function _performOnSettle(uint96 lotId_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onSettle(lotId_, _proceeds, _refund, abi.encode("")); + } + + function _performOnSettle() internal { + _performOnSettle(_lotId); + } + + modifier givenProceedsUtilisationPercent(uint24 percent_) { + _dtlCreateParams.proceedsUtilisationPercent = percent_; + _; + } + + modifier givenVestingStart(uint48 start_) { + _dtlCreateParams.vestingStart = start_; + _; + } + + modifier givenVestingExpiry(uint48 end_) { + _dtlCreateParams.vestingExpiry = end_; + _; + } + + modifier whenRecipientIsNotSeller() { + _dtlCreateParams.recipient = _NOT_SELLER; + _; + } + + modifier givenStable(bool stable_) { + _cleopatraCreateParams.stable = stable_; + + // Update the callback data + _dtlCreateParams.implParams = abi.encode(_cleopatraCreateParams); + _; + } + + function _setMaxSlippage(uint24 maxSlippage_) internal { + _cleopatraCreateParams.maxSlippage = maxSlippage_; + _dtlCreateParams.implParams = abi.encode(_cleopatraCreateParams); + } + + modifier givenMaxSlippage(uint24 maxSlippage_) { + _setMaxSlippage(maxSlippage_); + _; + } + + // ========== FUNCTIONS ========== // + + function _getDTLConfiguration(uint96 lotId_) + internal + view + returns (BaseDirectToLiquidity.DTLConfiguration memory) + { + ( + address recipient_, + uint256 lotCapacity_, + uint256 lotCuratorPayout_, + uint24 proceedsUtilisationPercent_, + uint48 vestingStart_, + uint48 vestingExpiry_, + LinearVesting linearVestingModule_, + bool active_, + bytes memory implParams_ + ) = _dtl.lotConfiguration(lotId_); + + return BaseDirectToLiquidity.DTLConfiguration({ + recipient: recipient_, + lotCapacity: lotCapacity_, + lotCuratorPayout: lotCuratorPayout_, + proceedsUtilisationPercent: proceedsUtilisationPercent_, + vestingStart: vestingStart_, + vestingExpiry: vestingExpiry_, + linearVestingModule: linearVestingModule_, + active: active_, + implParams: implParams_ + }); + } + + // ========== ASSERTIONS ========== // + + function _assertApprovals() internal view { + // Router + assertEq( + _quoteToken.allowance(address(_dtl), address(_router)), + 0, + "allowance: quote token: router" + ); + assertEq( + _baseToken.allowance(address(_dtl), address(_router)), + 0, + "allowance: base token: router" + ); + + // LinearVesting + assertEq( + _quoteToken.allowance(address(_dtl), address(_linearVesting)), + 0, + "allowance: quote token: linear vesting" + ); + assertEq( + _baseToken.allowance(address(_dtl), address(_linearVesting)), + 0, + "allowance: base token: linear vesting" + ); + } +} diff --git a/test/callbacks/liquidity/CleopatraV1/onCancel.t.sol b/test/callbacks/liquidity/CleopatraV1/onCancel.t.sol new file mode 100644 index 00000000..67d8649d --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV1/onCancel.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV1DirectToLiquidityTest} from "./CleopatraV1DTLTest.sol"; + +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; + +contract CleopatraV1DTLOnCancelForkTest is CleopatraV1DirectToLiquidityTest { + uint96 internal constant _REFUND_AMOUNT = 2e18; + + // ============ Modifiers ============ // + + function _performCallback(uint96 lotId_) internal { + _performOnCancel(lotId_, _REFUND_AMOUNT); + } + + // ============ 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 + // [X] it marks the correct lot as inactive + // [X] it marks the lot as inactive + + function test_whenLotNotRegistered_reverts() public givenCallbackIsCreated { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the function + _performCallback(_lotId); + } + + function test_success() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performCallback(_lotId); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.active, false, "active"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); + assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); + } + + function test_success_multiple() public givenCallbackIsCreated givenOnCreate { + uint96 lotIdOne = _lotId; + + // Create a second lot and cancel it + uint96 lotIdTwo = _createLot(_NOT_SELLER); + _performCallback(lotIdTwo); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configurationOne = + _getDTLConfiguration(lotIdOne); + assertEq(configurationOne.active, true, "lot one: active"); + + BaseDirectToLiquidity.DTLConfiguration memory configurationTwo = + _getDTLConfiguration(lotIdTwo); + assertEq(configurationTwo.active, false, "lot two: active"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + 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 + _performCallback(_lotId); + + // 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 + _performCallback(_lotId); + + // 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 + _performCallback(_lotId); + + // 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 + _performCallback(_lotId); + + // 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/CleopatraV1/onCreate.t.sol b/test/callbacks/liquidity/CleopatraV1/onCreate.t.sol new file mode 100644 index 00000000..12743cb2 --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV1/onCreate.t.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV1DirectToLiquidityTest} from "./CleopatraV1DTLTest.sol"; + +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {CleopatraV1DirectToLiquidity} from + "../../../../src/callbacks/liquidity/Cleopatra/CleopatraV1DTL.sol"; + +contract CleopatraV1DTLOnCreateForkTest is CleopatraV1DirectToLiquidityTest { + // ============ Modifiers ============ // + + // ============ Assertions ============ // + + function _expectTransferFrom() internal { + vm.expectRevert("TRANSFER_FROM_FAILED"); + } + + function _expectInvalidParams() internal { + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + } + + function _expectNotAuthorized() internal { + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + } + + 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"); + } + + // ============ Tests ============ // + + // [X] when the callback data is incorrect + // [ 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 + // [X] it reverts + // [X] when the proceeds utilisation is 0 + // [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 cleopatra v1 pool stable already exists + // [X] it succeeds + // [X] given cleopatra v1 pool volatile 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 + // [X] it reverts + // [X] when the start timestamp is before the current timestamp + // [X] it succeeds + // [X] when the expiry timestamp is before the current timestamp + // [X] it reverts + // [X] when the start timestamp and expiry timestamp are specified + // [X] given the linear vesting module is not installed + // [X] it reverts + // [X] it records the address of the linear vesting module + // [X] when the recipient is the zero address + // [X] it reverts + // [X] when the recipient is not the seller + // [X] it records the recipient + // [X] when multiple lots are created + // [X] it registers each lot + // [X] it registers the lot, stores the parameters + + function test_whenCallbackDataIsIncorrect_reverts() public givenCallbackIsCreated { + // Expect revert + vm.expectRevert(); + + vm.prank(address(_auctionHouse)); + _dtl.onCreate( + _lotId, + _SELLER, + address(_baseToken), + address(_quoteToken), + _LOT_CAPACITY, + false, + abi.encode(uint256(10)) + ); + } + + function test_whenCallbackIsNotCalledByAuctionHouse_reverts() public givenCallbackIsCreated { + _expectNotAuthorized(); + + _dtl.onCreate( + _lotId, + _SELLER, + address(_baseToken), + address(_quoteToken), + _LOT_CAPACITY, + false, + abi.encode(_dtlCreateParams) + ); + } + + function test_whenLotHasAlreadyBeenRegistered_reverts() public givenCallbackIsCreated { + _performOnCreate(); + + _expectInvalidParams(); + + _performOnCreate(); + } + + function test_whenProceedsUtilisationIs0_reverts() + public + givenCallbackIsCreated + givenProceedsUtilisationPercent(0) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 0, 1, 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenProceedsUtilisationIsGreaterThan100Percent_reverts() + public + givenCallbackIsCreated + givenProceedsUtilisationPercent(100e2 + 1) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 100e2 + 1, 1, 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + 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, maxSlippage, 0, 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenStartAndExpiryTimestampsAreTheSame_reverts() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp + 1) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenStartTimestampIsAfterExpiryTimestamp_reverts() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_initialTimestamp + 2) + givenVestingExpiry(_initialTimestamp + 1) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenStartTimestampIsBeforeCurrentTimestamp_succeeds() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_initialTimestamp - 1) + givenVestingExpiry(_initialTimestamp + 1) + { + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.vestingStart, _initialTimestamp - 1, "vestingStart"); + assertEq(configuration.vestingExpiry, _initialTimestamp + 1, "vestingExpiry"); + assertEq( + address(configuration.linearVestingModule), + address(_linearVesting), + "linearVestingModule" + ); + + // Assert balances + _assertBaseTokenBalances(); + } + + function test_whenExpiryTimestampIsBeforeCurrentTimestamp_reverts() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp - 1) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_InvalidVestingParams.selector + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenVestingSpecified_givenLinearVestingModuleNotInstalled_reverts() + public + givenCallbackIsCreated + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp + 2) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_LinearVestingModuleNotFound.selector + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenVestingSpecified() + public + givenCallbackIsCreated + givenLinearVestingModuleIsInstalled + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp + 2) + { + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.vestingStart, _initialTimestamp + 1, "vestingStart"); + assertEq(configuration.vestingExpiry, _initialTimestamp + 2, "vestingExpiry"); + assertEq( + address(configuration.linearVestingModule), + address(_linearVesting), + "linearVestingModule" + ); + + // Assert balances + _assertBaseTokenBalances(); + + _assertApprovals(); + } + + function test_whenRecipientIsZeroAddress_reverts() public givenCallbackIsCreated { + _dtlCreateParams.recipient = address(0); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_Params_InvalidAddress.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenRecipientIsNotSeller_succeeds() + public + givenCallbackIsCreated + whenRecipientIsNotSeller + { + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.recipient, _NOT_SELLER, "recipient"); + + // Assert balances + _assertBaseTokenBalances(); + } + + function test_succeeds() public givenCallbackIsCreated { + _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.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"); + + CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams memory cleopatraCreateParams = abi + .decode( + _dtlCreateParams.implParams, (CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams) + ); + assertEq(cleopatraCreateParams.stable, _cleopatraCreateParams.stable, "stable"); + assertEq( + cleopatraCreateParams.maxSlippage, _cleopatraCreateParams.maxSlippage, "maxSlippage" + ); + + // Assert balances + _assertBaseTokenBalances(); + + _assertApprovals(); + } + + function test_succeeds_multiple() public givenCallbackIsCreated { + // Lot one + _performOnCreate(); + + // Lot two + _dtlCreateParams.recipient = _NOT_SELLER; + _lotId = 2; + _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.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"); + + CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams memory cleopatraCreateParams = abi + .decode( + _dtlCreateParams.implParams, (CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams) + ); + assertEq(cleopatraCreateParams.stable, _cleopatraCreateParams.stable, "stable"); + assertEq( + cleopatraCreateParams.maxSlippage, _cleopatraCreateParams.maxSlippage, "maxSlippage" + ); + + // Assert balances + _assertBaseTokenBalances(); + + _assertApprovals(); + } + + 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"); + + CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams memory cleopatraCreateParams = abi + .decode( + _dtlCreateParams.implParams, (CleopatraV1DirectToLiquidity.CleopatraV1OnCreateParams) + ); + assertEq(cleopatraCreateParams.stable, _cleopatraCreateParams.stable, "stable"); + assertEq(cleopatraCreateParams.maxSlippage, maxSlippage, "maxSlippage"); + } + + function test_givenStablePoolExists() public givenCallbackIsCreated { + // Create the pool + _factory.createPair(address(_baseToken), address(_quoteToken), true); + + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.active, true, "active"); + } + + function test_givenVolatilePoolExists() public givenCallbackIsCreated { + // Create the pool + _factory.createPair(address(_baseToken), address(_quoteToken), false); + + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.active, true, "active"); + } +} diff --git a/test/callbacks/liquidity/CleopatraV1/onCurate.t.sol b/test/callbacks/liquidity/CleopatraV1/onCurate.t.sol new file mode 100644 index 00000000..4e22a009 --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV1/onCurate.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV1DirectToLiquidityTest} from "./CleopatraV1DTLTest.sol"; + +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; + +contract CleopatraV1DTLOnCurateForkTest is CleopatraV1DirectToLiquidityTest { + uint96 internal constant _PAYOUT_AMOUNT = 1e18; + + // ============ Modifiers ============ // + + function _performCallback(uint96 lotId_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCurate(lotId_, _PAYOUT_AMOUNT, false, abi.encode("")); + } + + // ============ Tests ============ // + + // [X] when the lot has not been registered + // [X] it reverts + // [X] when multiple lots are created + // [X] it marks the correct lot as inactive + // [X] it registers the curator payout + + function test_whenLotNotRegistered_reverts() public givenCallbackIsCreated { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the function + _performCallback(_lotId); + } + + function test_success() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performCallback(_lotId); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.lotCuratorPayout, _PAYOUT_AMOUNT, "lotCuratorPayout"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); + assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); + assertEq( + _baseToken.balanceOf(address(_auctionHouse)), + _LOT_CAPACITY, + "auction house base token balance" + ); + + _assertApprovals(); + } + + function test_success_multiple() public givenCallbackIsCreated givenOnCreate { + uint96 lotIdOne = _lotId; + + // Create a second lot + uint96 lotIdTwo = _createLot(_NOT_SELLER); + + // Call the function + _performCallback(lotIdTwo); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configurationOne = + _getDTLConfiguration(lotIdOne); + assertEq(configurationOne.lotCuratorPayout, 0, "lot one: lotCuratorPayout"); + + BaseDirectToLiquidity.DTLConfiguration memory configurationTwo = + _getDTLConfiguration(lotIdTwo); + assertEq(configurationTwo.lotCuratorPayout, _PAYOUT_AMOUNT, "lot two: lotCuratorPayout"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); + assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); + assertEq( + _baseToken.balanceOf(address(_auctionHouse)), + _LOT_CAPACITY * 2, + "auction house base token balance" + ); + + _assertApprovals(); + } +} diff --git a/test/callbacks/liquidity/CleopatraV1/onSettle.t.sol b/test/callbacks/liquidity/CleopatraV1/onSettle.t.sol new file mode 100644 index 00000000..7ddc2f56 --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV1/onSettle.t.sol @@ -0,0 +1,805 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV1DirectToLiquidityTest} from "./CleopatraV1DTLTest.sol"; + +// Libraries +import {FixedPointMathLib} from "@solmate-6.7.0/utils/FixedPointMathLib.sol"; +import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; + +// Cleopatra +import {ICleopatraV1Pool} from + "../../../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV1Pool.sol"; + +// AuctionHouse +import {ILinearVesting} from "@axis-core-1.0.0/interfaces/modules/derivatives/ILinearVesting.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; + +contract CleopatraV1OnSettleForkTest is CleopatraV1DirectToLiquidityTest { + uint96 internal constant _PROCEEDS = 20e18; + uint96 internal constant _REFUND = 0; + + /// @dev The minimum amount of liquidity retained in the pool + uint256 internal constant _MINIMUM_LIQUIDITY = 10 ** 3; + + uint96 internal _capacityUtilised; + uint96 internal _quoteTokensToDeposit; + uint96 internal _baseTokensToDeposit; + uint96 internal _curatorPayout; + + uint24 internal _maxSlippage = 1; // 0.01% + + // ========== Internal functions ========== // + + function _getCleopatraV1Pool(bool stable_) internal view returns (ICleopatraV1Pool) { + return + ICleopatraV1Pool(_factory.getPair(address(_quoteToken), address(_baseToken), stable_)); + } + + function _getCleopatraV1Pool() internal view returns (ICleopatraV1Pool) { + return _getCleopatraV1Pool(_cleopatraCreateParams.stable); + } + + function _getVestingTokenId() internal view returns (uint256) { + // Get the pools deployed by the DTL callback + address pool = address(_getCleopatraV1Pool()); + + return _linearVesting.computeId( + pool, + abi.encode( + ILinearVesting.VestingParams({ + start: _dtlCreateParams.vestingStart, + expiry: _dtlCreateParams.vestingExpiry + }) + ) + ); + } + + // ========== Assertions ========== // + + function _assertLpTokenBalance() internal view { + // Get the pools deployed by the DTL callback + ICleopatraV1Pool pool = _getCleopatraV1Pool(); + + // 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 (_dtlCreateParams.vestingStart == 0) { + sellerExpectedBalance = pool.totalSupply() - testBalance - _MINIMUM_LIQUIDITY; + } else { + linearVestingExpectedBalance = pool.totalSupply() - testBalance - _MINIMUM_LIQUIDITY; + } + + assertEq( + pool.balanceOf(_SELLER), + _dtlCreateParams.recipient == _SELLER ? sellerExpectedBalance : 0, + "seller: LP token balance" + ); + assertEq( + pool.balanceOf(_NOT_SELLER), + _dtlCreateParams.recipient == _NOT_SELLER ? sellerExpectedBalance : 0, + "not seller: LP token balance" + ); + assertEq( + pool.balanceOf(address(_linearVesting)), + linearVestingExpectedBalance, + "linear vesting: LP token balance" + ); + } + + function _assertVestingTokenBalance() internal { + // Exit if not vesting + if (_dtlCreateParams.vestingStart == 0) { + return; + } + + // Get the pools deployed by the DTL callback + address pool = address(_getCleopatraV1Pool()); + + // Get the wrapped address + (, address wrappedVestingTokenAddress) = _linearVesting.deploy( + pool, + abi.encode( + ILinearVesting.VestingParams({ + start: _dtlCreateParams.vestingStart, + expiry: _dtlCreateParams.vestingExpiry + }) + ), + true + ); + ERC20 wrappedVestingToken = ERC20(wrappedVestingTokenAddress); + uint256 sellerExpectedBalance = wrappedVestingToken.totalSupply(); + + assertEq( + wrappedVestingToken.balanceOf(_SELLER), + _dtlCreateParams.recipient == _SELLER ? sellerExpectedBalance : 0, + "seller: vesting token balance" + ); + assertEq( + wrappedVestingToken.balanceOf(_NOT_SELLER), + _dtlCreateParams.recipient == _NOT_SELLER ? sellerExpectedBalance : 0, + "not seller: vesting token balance" + ); + } + + function _assertQuoteTokenBalance() internal view { + assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: quote token balance"); + } + + function _assertBaseTokenBalance() internal view { + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "DTL: base token balance"); + } + + // ========== Modifiers ========== // + + function _createPool() internal returns (address) { + return _factory.createPair( + address(_quoteToken), address(_baseToken), _cleopatraCreateParams.stable + ); + } + + modifier givenPoolIsCreated() { + _createPool(); + _; + } + + modifier setCallbackParameters(uint96 proceeds_, uint96 refund_) { + _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 * _dtlCreateParams.proceedsUtilisationPercent / 100e2; + _baseTokensToDeposit = + _capacityUtilised * _dtlCreateParams.proceedsUtilisationPercent / 100e2; + _; + } + + modifier givenUnboundedProceedsUtilisationPercent(uint24 percent_) { + // Bound the percent + uint24 percent = uint24(bound(percent_, 1, 100e2)); + + // Set the value on the DTL + _dtlCreateParams.proceedsUtilisationPercent = percent; + _; + } + + modifier givenUnboundedOnCurate(uint96 curationPayout_) { + // Bound the value + _curatorPayout = uint96(bound(curationPayout_, 1e17, _LOT_CAPACITY)); + + // Call the onCurate callback + _performOnCurate(_curatorPayout); + _; + } + + modifier whenRefundIsBounded(uint96 refund_) { + // Bound the refund + _refund = uint96(bound(refund_, 1e17, 5e18)); + _; + } + + modifier givenPoolHasDepositLowerPrice() { + uint256 quoteTokensToDeposit = _quoteTokensToDeposit * 105 / 100; + uint256 baseTokensToDeposit = _baseTokensToDeposit; + + // Mint additional tokens + _quoteToken.mint(address(this), quoteTokensToDeposit); + _baseToken.mint(address(this), baseTokensToDeposit); + + // Approve spending + _quoteToken.approve(address(_router), quoteTokensToDeposit); + _baseToken.approve(address(_router), baseTokensToDeposit); + + // Deposit tokens into the pool + _router.addLiquidity( + address(_quoteToken), + address(_baseToken), + _cleopatraCreateParams.stable, + quoteTokensToDeposit, + baseTokensToDeposit, + quoteTokensToDeposit, + baseTokensToDeposit, + address(this), + block.timestamp + ); + _; + } + + modifier givenPoolHasDepositHigherPrice() { + 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(_router), quoteTokensToDeposit); + _baseToken.approve(address(_router), baseTokensToDeposit); + + // Deposit tokens into the pool + _router.addLiquidity( + address(_quoteToken), + address(_baseToken), + _cleopatraCreateParams.stable, + 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 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 the proceeds utilisation percent is set + // [X] it calculates the deposit amount correctly + // [X] given curation is enabled + // [X] the utilisation percent considers this + // [X] when the refund amount changes + // [X] the utilisation percent considers this + // [X] given minting pool tokens utilises less than the available amount of base tokens + // [X] the excess base tokens are returned + // [X] given minting pool tokens utilises less than the available amount of quote tokens + // [X] the excess quote tokens are returned + // [X] given the send base tokens flag is false + // [X] it transfers the base tokens from the seller + // [X] given vesting is enabled + // [X] given the recipient is not the seller + // [X] it mints the vesting tokens to the seller + // [X] it mints the vesting tokens to the seller + // [X] given the recipient is not the seller + // [X] it mints the LP token to the recipient + // [X] when multiple lots are created + // [X] it performs actions on the correct pool + // [X] given the stable parameter is true + // [X] it creates a stable pool + // [X] given the stable parameter is false + // [X] it creates a volatile pool + // [X] it creates and initializes the pool, creates a pool token, deposits into the pool token, transfers the LP token to the seller and transfers any excess back to the seller + + function test_givenPoolIsCreated() + public + givenCallbackIsCreated + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenProceedsUtilisationPercent_fuzz(uint24 percent_) + public + givenCallbackIsCreated + givenUnboundedProceedsUtilisationPercent(percent_) + 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_givenProceedsUtilisationPercent_givenCurationPayout_fuzz( + uint24 percent_, + uint96 curationPayout_ + ) + public + givenCallbackIsCreated + givenUnboundedProceedsUtilisationPercent(percent_) + givenOnCreate + givenUnboundedOnCurate(curationPayout_) + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_whenRefund_fuzz(uint96 refund_) + public + givenCallbackIsCreated + givenOnCreate + whenRefundIsBounded(refund_) + setCallbackParameters(_PROCEEDS, _refund) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenPoolHasDepositWithLowerPrice_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreated + givenPoolHasDepositLowerPrice + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + // Expect revert + vm.expectRevert("INSUFFICIENT B"); + + _performOnSettle(); + } + + function test_givenPoolHasDepositWithLowerPrice_whenMaxSlippageIsSet() + public + givenCallbackIsCreated + givenMaxSlippage(500) // 5% + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreated + givenPoolHasDepositLowerPrice + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenPoolHasDepositWithHigherPrice_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreated + givenPoolHasDepositHigherPrice + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + // Expect revert + vm.expectRevert("INSUFFICIENT A"); + + _performOnSettle(); + } + + function test_givenPoolHasDepositWithHigherPrice_whenMaxSlippageIsSet() + public + givenCallbackIsCreated + givenMaxSlippage(500) // 5% + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreated + givenPoolHasDepositHigherPrice + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenVesting() + public + givenLinearVestingModuleIsInstalled + givenCallbackIsCreated + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp + 2) + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenVesting_whenRecipientIsNotSeller() + public + givenLinearVestingModuleIsInstalled + givenCallbackIsCreated + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp + 2) + whenRecipientIsNotSeller + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenVesting_redemption() + public + givenLinearVestingModuleIsInstalled + givenCallbackIsCreated + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp + 2) + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + // Warp to the end of the vesting period + vm.warp(_initialTimestamp + 3); + + // Redeem the vesting tokens + uint256 tokenId = _getVestingTokenId(); + vm.prank(_SELLER); + _linearVesting.redeemMax(tokenId); + + // Assert that the LP token has been transferred to the seller + ICleopatraV1Pool pool = _getCleopatraV1Pool(); + assertEq( + pool.balanceOf(_SELLER), + pool.totalSupply() - _MINIMUM_LIQUIDITY, + "seller: LP token balance" + ); + } + + function test_withdrawLpToken() + public + givenCallbackIsCreated + givenOnCreate + givenPoolIsCreated + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + // Get the pools deployed by the DTL callback + ICleopatraV1Pool pool = _getCleopatraV1Pool(); + + // Approve the spending of the LP token + uint256 lpTokenAmount = pool.balanceOf(_SELLER); + vm.prank(_SELLER); + pool.approve(address(_router), lpTokenAmount); + + // Withdraw the LP token + vm.prank(_SELLER); + _router.removeLiquidity( + address(_quoteToken), + address(_baseToken), + _cleopatraCreateParams.stable, + lpTokenAmount, + _quoteTokensToDeposit * 99 / 100, + _baseTokensToDeposit * 99 / 100, + _SELLER, + block.timestamp + ); + + // Get the minimum liquidity retained in the pool + uint256 quoteTokenPoolAmount = _quoteToken.balanceOf(address(pool)); + uint256 baseTokenPoolAmount = _baseToken.balanceOf(address(pool)); + + // Check the balances + assertEq(pool.balanceOf(_SELLER), 0, "seller: LP token balance"); + assertEq( + _quoteToken.balanceOf(_SELLER), + _proceeds - quoteTokenPoolAmount, + "seller: quote token balance" + ); + assertEq( + _baseToken.balanceOf(_SELLER), + _capacityUtilised - baseTokenPoolAmount, + "seller: base token balance" + ); + assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: quote token balance"); + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "DTL: base token balance"); + } + + function test_givenInsufficientBaseTokenBalance_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised - 1) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_InsufficientBalance.selector, + address(_baseToken), + _SELLER, + _baseTokensToDeposit, + _baseTokensToDeposit - 1 + ); + vm.expectRevert(err); + + _performOnSettle(); + } + + function test_givenInsufficientBaseTokenAllowance_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised - 1) + { + // Expect revert + vm.expectRevert("TRANSFER_FROM_FAILED"); + + _performOnSettle(); + } + + function test_success() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_success_multiple() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_NOT_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_NOT_SELLER, _dtlAddress, _capacityUtilised) + { + // Create second lot + uint96 lotIdTwo = _createLot(_NOT_SELLER); + + _performOnSettle(lotIdTwo); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_stablePool() + public + givenCallbackIsCreated + givenStable(true) + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + address stablePool = address(_getCleopatraV1Pool(true)); + address volatilePool = address(_getCleopatraV1Pool(false)); + + assertNotEq(stablePool, address(0), "stable pool address"); + assertEq(volatilePool, address(0), "volatile pool address"); + } + + function test_volatilePool() + public + givenCallbackIsCreated + givenStable(false) + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + address stablePool = address(_getCleopatraV1Pool(true)); + address volatilePool = address(_getCleopatraV1Pool(false)); + + assertEq(stablePool, address(0), "stable pool address"); + assertNotEq(volatilePool, address(0), "volatile pool address"); + } + + function test_whenRecipientIsNotSeller() + public + givenCallbackIsCreated + whenRecipientIsNotSeller + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertLpTokenBalance(); + _assertVestingTokenBalance(); + _assertQuoteTokenBalance(); + _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/CleopatraV2/CleopatraV2DTLTest.sol b/test/callbacks/liquidity/CleopatraV2/CleopatraV2DTLTest.sol new file mode 100644 index 00000000..811afc33 --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV2/CleopatraV2DTLTest.sol @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Test scaffolding +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 {WithSalts} from "../../../lib/WithSalts.sol"; +import {TestConstants} from "../../../Constants.sol"; + +// Mocks +import {MockERC20} from "@solmate-6.7.0/test/utils/mocks/MockERC20.sol"; +import {MockBatchAuctionModule} from + "@axis-core-1.0.0-test/modules/Auction/MockBatchAuctionModule.sol"; + +// Callbacks +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; + +// Cleopatra +import {CleopatraV2DirectToLiquidity} from + "../../../../src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol"; +import {ICleopatraV2Factory} from + "../../../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Factory.sol"; +import {ICleopatraV2PositionManager} from + "../../../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2PositionManager.sol"; +// import {IVotingEscrow} from "../../../../src/callbacks/liquidity/Cleopatra/lib/IVotingEscrow.sol"; +// import {IRAM} from "../../../../src/callbacks/liquidity/Cleopatra/lib/IRAM.sol"; + +// Axis core +import {keycodeFromVeecode, toKeycode} from "@axis-core-1.0.0/modules/Keycode.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 {LinearVesting} from "@axis-core-1.0.0/modules/derivatives/LinearVesting.sol"; + +abstract contract CleopatraV2DirectToLiquidityTest is + Test, + Permit2User, + WithSalts, + TestConstants +{ + using Callbacks for CleopatraV2DirectToLiquidity; + + 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; + + uint48 internal _initialTimestamp; + + uint96 internal _lotId = 1; + + BatchAuctionHouse internal _auctionHouse; + CleopatraV2DirectToLiquidity internal _dtl; + address internal _dtlAddress; + ICleopatraV2Factory internal _factory; + ICleopatraV2PositionManager internal _positionManager; + // IRAM internal _ram; + MockBatchAuctionModule internal _batchAuctionModule; + + MockERC20 internal _quoteToken; + MockERC20 internal _baseToken; + + uint96 internal _proceeds; + uint96 internal _refund; + + // Inputs + CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams internal _cleopatraCreateParams = + CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams({ + poolFee: 500, + maxSlippage: 1 // 0.01%, to handle rounding errors + }); + BaseDirectToLiquidity.OnCreateParams internal _dtlCreateParams = BaseDirectToLiquidity + .OnCreateParams({ + proceedsUtilisationPercent: 100e2, + vestingStart: 0, + vestingExpiry: 0, + recipient: _SELLER, + implParams: abi.encode(_cleopatraCreateParams) + }); + + function setUp() public { + // Create a fork on Mantle + string memory mantleRpcUrl = vm.envString("MANTLE_RPC_URL"); + vm.createSelectFork(mantleRpcUrl); + require(block.chainid == 5000, "Must be on Mantle"); + + _initialTimestamp = uint48(block.timestamp); + + // Create an AuctionHouse at a deterministic address, since it is used as input to callbacks + BatchAuctionHouse auctionHouse = new BatchAuctionHouse(_OWNER, _PROTOCOL, _permit2Address); + _auctionHouse = BatchAuctionHouse(_AUCTION_HOUSE); + vm.etch(address(_auctionHouse), address(auctionHouse).code); + vm.store(address(_auctionHouse), bytes32(uint256(0)), bytes32(abi.encode(_OWNER))); // Owner + vm.store(address(_auctionHouse), bytes32(uint256(6)), bytes32(abi.encode(1))); // Reentrancy + vm.store(address(_auctionHouse), bytes32(uint256(10)), bytes32(abi.encode(_PROTOCOL))); // Protocol + + _factory = ICleopatraV2Factory(_CLEOPATRA_V2_FACTORY); + _positionManager = ICleopatraV2PositionManager(payable(_CLEOPATRA_V2_POSITION_MANAGER)); + // _ram = IRAM(IVotingEscrow(_positionManager.veRam()).token()); + + _batchAuctionModule = new MockBatchAuctionModule(address(_auctionHouse)); + + // Install a mock batch auction module + vm.prank(_OWNER); + _auctionHouse.installModule(_batchAuctionModule); + + _quoteToken = new MockERC20("Quote Token", "QT", 18); + _baseToken = new MockERC20("Base Token", "BT", 18); + } + + // ========== MODIFIERS ========== // + + modifier givenCallbackIsCreated() { + // Get the salt + bytes memory args = + abi.encode(address(_auctionHouse), address(_factory), address(_positionManager)); + bytes32 salt = _getTestSalt( + "CleopatraV2DirectToLiquidity", type(CleopatraV2DirectToLiquidity).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 CleopatraV2DirectToLiquidity{salt: salt}( + address(_auctionHouse), address(_factory), payable(_positionManager) + ); + vm.stopBroadcast(); + + _dtlAddress = address(_dtl); + _; + } + + modifier givenAddressHasQuoteTokenBalance(address address_, uint256 amount_) { + _quoteToken.mint(address_, amount_); + _; + } + + modifier givenAddressHasBaseTokenBalance(address address_, uint256 amount_) { + _baseToken.mint(address_, amount_); + _; + } + + modifier givenAddressHasQuoteTokenAllowance(address owner_, address spender_, uint256 amount_) { + vm.prank(owner_); + _quoteToken.approve(spender_, amount_); + _; + } + + modifier givenAddressHasBaseTokenAllowance(address owner_, address spender_, uint256 amount_) { + vm.prank(owner_); + _baseToken.approve(spender_, amount_); + _; + } + + function _createLot(address seller_) internal returns (uint96 lotId) { + // 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: _dtl, + callbackData: abi.encode(_dtlCreateParams), + 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_); + return _auctionHouse.auction(routingParams, auctionParams, ""); + } + + 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("")); + } + + modifier givenOnCurate(uint96 curatorPayout_) { + _performOnCurate(curatorPayout_); + _; + } + + 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); + } + + modifier givenProceedsUtilisationPercent(uint24 percent_) { + _dtlCreateParams.proceedsUtilisationPercent = percent_; + _; + } + + modifier givenPoolFee(uint24 fee_) { + _cleopatraCreateParams.poolFee = fee_; + + // Update the callback data + _dtlCreateParams.implParams = abi.encode(_cleopatraCreateParams); + _; + } + + function _setMaxSlippage(uint24 maxSlippage_) internal { + _cleopatraCreateParams.maxSlippage = maxSlippage_; + _dtlCreateParams.implParams = abi.encode(_cleopatraCreateParams); + } + + modifier givenMaxSlippage(uint24 maxSlippage_) { + _setMaxSlippage(maxSlippage_); + _; + } + + // function _setVeRamTokenId(uint256 veRamTokenId_) internal { + // _cleopatraCreateParams.veRamTokenId = veRamTokenId_; + // _dtlCreateParams.implParams = abi.encode(_cleopatraCreateParams); + // } + + // modifier givenVeRamTokenId() { + // _createVeRamDeposit(); + // _; + // } + + modifier givenVestingStart(uint48 start_) { + _dtlCreateParams.vestingStart = start_; + _; + } + + modifier givenVestingExpiry(uint48 end_) { + _dtlCreateParams.vestingExpiry = end_; + _; + } + + modifier whenRecipientIsNotSeller() { + _dtlCreateParams.recipient = _NOT_SELLER; + _; + } + + function _createPool(uint160 sqrtPriceX96_) internal returns (address) { + (address token0, address token1) = address(_baseToken) < address(_quoteToken) + ? (address(_baseToken), address(_quoteToken)) + : (address(_quoteToken), address(_baseToken)); + + return _factory.createPool(token0, token1, _cleopatraCreateParams.poolFee, sqrtPriceX96_); + } + + modifier givenPoolIsCreatedAndInitialized(uint160 sqrtPriceX96_) { + address pool = _createPool(sqrtPriceX96_); + _; + } + + // function _createVeRamDeposit() internal { + // // Mint RAM + // vm.prank(address(_ram.minter())); + // _ram.mint(_SELLER, 1e18); + + // // Approve spending + // address veRam = address(_positionManager.veRam()); + // vm.prank(_SELLER); + // _ram.approve(veRam, 1e18); + + // // Deposit into the voting escrow + // vm.startPrank(_SELLER); + // uint256 tokenId = IVotingEscrow(veRam).create_lock_for( + // 1e18, 24 hours * 365, _SELLER + // ); + // vm.stopPrank(); + + // // Update the callback + // _setVeRamTokenId(tokenId); + // } + + // function _mockVeRamTokenIdApproved(uint256 veRamTokenId_, bool approved_) internal { + // // TODO this could be shifted to an actual approval, but I couldn't figure out how + // vm.mockCall( + // address(_positionManager.veRam()), + // abi.encodeWithSelector( + // IVotingEscrow.isApprovedOrOwner.selector, address(_dtl), veRamTokenId_ + // ), + // abi.encode(approved_) + // ); + // } + + // modifier givenVeRamTokenIdApproval(bool approved_) { + // _mockVeRamTokenIdApproved(_cleopatraCreateParams.veRamTokenId, approved_); + // _; + // } + + // ========== FUNCTIONS ========== // + + function _getDTLConfiguration(uint96 lotId_) + internal + view + returns (BaseDirectToLiquidity.DTLConfiguration memory) + { + ( + address recipient_, + uint256 lotCapacity_, + uint256 lotCuratorPayout_, + uint24 proceedsUtilisationPercent_, + uint48 vestingStart_, + uint48 vestingExpiry_, + LinearVesting linearVestingModule_, + bool active_, + bytes memory implParams_ + ) = _dtl.lotConfiguration(lotId_); + + return BaseDirectToLiquidity.DTLConfiguration({ + recipient: recipient_, + lotCapacity: lotCapacity_, + lotCuratorPayout: lotCuratorPayout_, + proceedsUtilisationPercent: proceedsUtilisationPercent_, + vestingStart: vestingStart_, + vestingExpiry: vestingExpiry_, + linearVestingModule: linearVestingModule_, + active: active_, + implParams: implParams_ + }); + } +} diff --git a/test/callbacks/liquidity/CleopatraV2/onCancel.t.sol b/test/callbacks/liquidity/CleopatraV2/onCancel.t.sol new file mode 100644 index 00000000..568d2130 --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV2/onCancel.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV2DirectToLiquidityTest} from "./CleopatraV2DTLTest.sol"; + +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; + +contract CleopatraV2DTLOnCancelForkTest is CleopatraV2DirectToLiquidityTest { + uint96 internal constant _REFUND_AMOUNT = 2e18; + + // ============ Modifiers ============ // + + function _performOnCancel(uint96 lotId_) internal { + _performOnCancel(lotId_, _REFUND_AMOUNT); + } + + // ============ 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 + // [X] it marks the correct lot as inactive + // [X] it marks the lot as inactive + + function test_whenLotNotRegistered_reverts() public givenCallbackIsCreated { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the function + _performOnCancel(_lotId); + } + + function test_success() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performOnCancel(_lotId); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.active, false, "active"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); + assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); + } + + function test_success_multiple() public givenCallbackIsCreated givenOnCreate { + uint96 lotIdOne = _lotId; + + // Create a second lot and cancel it + uint96 lotIdTwo = _createLot(_NOT_SELLER); + _performOnCancel(lotIdTwo); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configurationOne = + _getDTLConfiguration(lotIdOne); + assertEq(configurationOne.active, true, "lot one: active"); + + BaseDirectToLiquidity.DTLConfiguration memory configurationTwo = + _getDTLConfiguration(lotIdTwo); + assertEq(configurationTwo.active, false, "lot two: active"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + 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(_lotId); + + // 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(_lotId); + + // 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(_lotId); + + // 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(_lotId); + + // 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/CleopatraV2/onCreate.t.sol b/test/callbacks/liquidity/CleopatraV2/onCreate.t.sol new file mode 100644 index 00000000..67b4b514 --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV2/onCreate.t.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV2DirectToLiquidityTest} from "./CleopatraV2DTLTest.sol"; + +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; +import {CleopatraV2DirectToLiquidity} from + "../../../../src/callbacks/liquidity/Cleopatra/CleopatraV2DTL.sol"; + +contract CleopatraV2DTLOnCreateForkTest is CleopatraV2DirectToLiquidityTest { + // ============ Modifiers ============ // + + // ============ Assertions ============ // + + function _expectTransferFrom() internal { + vm.expectRevert("TRANSFER_FROM_FAILED"); + } + + function _expectInvalidParams() internal { + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + } + + function _expectNotAuthorized() internal { + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + } + + 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"); + } + + // ============ Tests ============ // + + // [X] when the callback data is incorrect + // [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 + // [X] it reverts + // [X] when the proceeds utilisation is 0 + // [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 cleopatra v2 pool already exists + // [X] it succeeds + // [X] when the vesting start timestamp is set + // [X] it reverts + // [X] when the vesting expiry timestamp is set + // [X] it reverts + // [X] when the recipient is the zero address + // [X] it reverts + // [X] when the recipient is not the seller + // [X] it records the recipient + // [ ] when the veRamTokenId is set + // [ ] given the DTL contract is approved to use the veRamTokenId + // [ ] it succeeds + // [ ] it reverts + // [X] when multiple lots are created + // [X] it registers each lot + // [X] it registers the lot + + function test_whenCallbackDataIsIncorrect_reverts() public givenCallbackIsCreated { + // Expect revert + vm.expectRevert(); + + vm.prank(address(_auctionHouse)); + _dtl.onCreate( + _lotId, + _SELLER, + address(_baseToken), + address(_quoteToken), + _LOT_CAPACITY, + false, + abi.encode(uint256(10)) + ); + } + + function test_whenCallbackIsNotCalledByAuctionHouse_reverts() public givenCallbackIsCreated { + _expectNotAuthorized(); + + _dtl.onCreate( + _lotId, + _SELLER, + address(_baseToken), + address(_quoteToken), + _LOT_CAPACITY, + false, + abi.encode(_dtlCreateParams) + ); + } + + function test_whenLotHasAlreadyBeenRegistered_reverts() public givenCallbackIsCreated { + _performOnCreate(); + + _expectInvalidParams(); + + _performOnCreate(); + } + + function test_whenProceedsUtilisationIs0_reverts() + public + givenCallbackIsCreated + givenProceedsUtilisationPercent(0) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 0, 1, 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenProceedsUtilisationIsGreaterThan100Percent_reverts() + public + givenCallbackIsCreated + givenProceedsUtilisationPercent(100e2 + 1) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_Params_PercentOutOfBounds.selector, 100e2 + 1, 1, 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + 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, maxSlippage, 0, 100e2 + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_givenPoolExists() + public + givenCallbackIsCreated + givenPoolIsCreatedAndInitialized(0) + { + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.active, true, "active"); + } + + function test_givenVestingStartParameterIsSet() + public + givenCallbackIsCreated + givenVestingStart(_initialTimestamp + 1) + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_NotSupported.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_givenVestingExpiryParameterIsSet() + public + givenCallbackIsCreated + givenVestingExpiry(_initialTimestamp + 1) + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_NotSupported.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_givenVestingStartAndExpiryParameterIsSet() + public + givenCallbackIsCreated + givenVestingStart(_initialTimestamp + 1) + givenVestingExpiry(_initialTimestamp + 2) + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_NotSupported.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_givenPoolFeeIsNotEnabled_reverts() + public + givenCallbackIsCreated + givenPoolFee(0) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + CleopatraV2DirectToLiquidity.Callback_Params_PoolFeeNotEnabled.selector + ); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenRecipientIsZeroAddress_reverts() public givenCallbackIsCreated { + _dtlCreateParams.recipient = address(0); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(BaseDirectToLiquidity.Callback_Params_InvalidAddress.selector); + vm.expectRevert(err); + + _performOnCreate(); + } + + function test_whenRecipientIsNotSeller_succeeds() + public + givenCallbackIsCreated + whenRecipientIsNotSeller + { + _performOnCreate(); + + // Assert values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.recipient, _NOT_SELLER, "recipient"); + + // Assert balances + _assertBaseTokenBalances(); + } + + function test_succeeds() public givenCallbackIsCreated { + _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.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"); + + CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams memory _cleopatraCreateParams = abi + .decode(configuration.implParams, (CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams)); + assertEq(_cleopatraCreateParams.poolFee, _cleopatraCreateParams.poolFee, "poolFee"); + assertEq( + _cleopatraCreateParams.maxSlippage, _cleopatraCreateParams.maxSlippage, "maxSlippage" + ); + // assertEq(_cleopatraCreateParams.veRamTokenId, _cleopatraCreateParams.veRamTokenId, "veRamTokenId"); + + // Assert balances + _assertBaseTokenBalances(); + } + + function test_succeeds_multiple() public givenCallbackIsCreated { + // Lot one + _performOnCreate(); + + // Lot two + _dtlCreateParams.recipient = _NOT_SELLER; + _lotId = 2; + _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.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"); + + // 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"); + + CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams memory cleopatraCreateParams = abi + .decode(configuration.implParams, (CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams)); + assertEq(cleopatraCreateParams.maxSlippage, maxSlippage, "maxSlippage"); + } + + // function test_veRamTokenId() + // public + // givenCallbackIsCreated + // givenVeRamTokenId + // givenVeRamTokenIdApproval(true) + // { + // _performOnCreate(); + + // // Assert values + // BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + + // CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams memory cleopatraCreateParams = + // abi.decode(configuration.implParams, (CleopatraV2DirectToLiquidity.CleopatraV2OnCreateParams)); + // assertEq(cleopatraCreateParams.veRamTokenId, 1000, "veRamTokenId"); + // } + + // function test_veRamTokenId_notApproved_reverts() + // public + // givenCallbackIsCreated + // givenVeRamTokenId + // givenVeRamTokenIdApproval(false) + // { + // // Expect revert + // bytes memory err = abi.encodeWithSelector( + // CleopatraV2DirectToLiquidity.Callback_Params_VeRamTokenIdNotApproved.selector + // ); + // vm.expectRevert(err); + + // _performOnCreate(); + // } +} diff --git a/test/callbacks/liquidity/CleopatraV2/onCurate.t.sol b/test/callbacks/liquidity/CleopatraV2/onCurate.t.sol new file mode 100644 index 00000000..1a6d1a6d --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV2/onCurate.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV2DirectToLiquidityTest} from "./CleopatraV2DTLTest.sol"; + +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; + +contract CleopatraV2DTLOnCurateForkTest is CleopatraV2DirectToLiquidityTest { + uint96 internal constant _PAYOUT_AMOUNT = 1e18; + + // ============ Modifiers ============ // + + function _performCallback(uint96 lotId_) internal { + vm.prank(address(_auctionHouse)); + _dtl.onCurate(lotId_, _PAYOUT_AMOUNT, false, abi.encode("")); + } + + // ============ Tests ============ // + + // [X] when the lot has not been registered + // [X] it reverts + // [X] when multiple lots are created + // [X] it marks the correct lot as inactive + // [X] it registers the curator payout + + function test_whenLotNotRegistered_reverts() public givenCallbackIsCreated { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + // Call the function + _performCallback(_lotId); + } + + function test_success() public givenCallbackIsCreated givenOnCreate { + // Call the function + _performCallback(_lotId); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configuration = _getDTLConfiguration(_lotId); + assertEq(configuration.lotCuratorPayout, _PAYOUT_AMOUNT, "lotCuratorPayout"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); + assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); + assertEq( + _baseToken.balanceOf(address(_auctionHouse)), + _LOT_CAPACITY, + "auction house base token balance" + ); + } + + function test_success_multiple() public givenCallbackIsCreated givenOnCreate { + uint96 lotIdOne = _lotId; + + // Create a second lot + uint96 lotIdTwo = _createLot(_NOT_SELLER); + + // Call the function + _performCallback(lotIdTwo); + + // Check the values + BaseDirectToLiquidity.DTLConfiguration memory configurationOne = + _getDTLConfiguration(lotIdOne); + assertEq(configurationOne.lotCuratorPayout, 0, "lot one: lotCuratorPayout"); + + BaseDirectToLiquidity.DTLConfiguration memory configurationTwo = + _getDTLConfiguration(lotIdTwo); + assertEq(configurationTwo.lotCuratorPayout, _PAYOUT_AMOUNT, "lot two: lotCuratorPayout"); + + // Check the balances + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "base token balance"); + assertEq(_baseToken.balanceOf(_SELLER), 0, "seller base token balance"); + assertEq(_baseToken.balanceOf(_NOT_SELLER), 0, "not seller base token balance"); + assertEq( + _baseToken.balanceOf(address(_auctionHouse)), + _LOT_CAPACITY * 2, + "auction house base token balance" + ); + } +} diff --git a/test/callbacks/liquidity/CleopatraV2/onSettle.t.sol b/test/callbacks/liquidity/CleopatraV2/onSettle.t.sol new file mode 100644 index 00000000..eba9bc92 --- /dev/null +++ b/test/callbacks/liquidity/CleopatraV2/onSettle.t.sol @@ -0,0 +1,604 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {CleopatraV2DirectToLiquidityTest} from "./CleopatraV2DTLTest.sol"; + +// Libraries +import {FixedPointMathLib} from "@solmate-6.7.0/utils/FixedPointMathLib.sol"; + +// Cleopatra +import {ICleopatraV2Pool} from + "../../../../src/callbacks/liquidity/Cleopatra/lib/ICleopatraV2Pool.sol"; +import {SqrtPriceMath} from "../../../../src/lib/uniswap-v3/SqrtPriceMath.sol"; + +// AuctionHouse +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {BaseDirectToLiquidity} from "../../../../src/callbacks/liquidity/BaseDTL.sol"; + +contract CleopatraV2DTLOnSettleForkTest is CleopatraV2DirectToLiquidityTest { + uint96 internal constant _PROCEEDS = 20e18; + uint96 internal constant _REFUND = 0; + + uint96 internal _capacityUtilised; + uint96 internal _quoteTokensToDeposit; + uint96 internal _baseTokensToDeposit; + uint96 internal _curatorPayout; + uint24 internal _maxSlippage = 1; // 0.01% + + uint160 internal constant _SQRT_PRICE_X96_OVERRIDE = 125_270_724_187_523_965_593_206_000_000; // Different to what is normally calculated + + /// @dev Set via `setCallbackParameters` modifier + uint160 internal _sqrtPriceX96; + + // ========== Internal functions ========== // + + function _getTokenId(uint96 lotId_) internal view returns (uint256) { + return _dtl.lotIdToTokenId(lotId_); + } + + function _getTokenId() internal view returns (uint256) { + return _getTokenId(_lotId); + } + + // ========== Assertions ========== // + + function _assertPoolState(uint160 sqrtPriceX96_) internal view { + // Get the pool + address pool = _getPool(); + + (uint160 sqrtPriceX96,,,,,,) = ICleopatraV2Pool(pool).slot0(); + assertEq(sqrtPriceX96, sqrtPriceX96_, "pool sqrt price"); + } + + function _assertLpTokenBalance(uint96 lotId_, address recipient_) internal view { + uint256 tokenId = _getTokenId(lotId_); + + assertEq(_positionManager.ownerOf(tokenId), recipient_, "LP token owner"); + } + + function _assertLpTokenBalance() internal view { + _assertLpTokenBalance(_lotId, _dtlCreateParams.recipient); + } + + function _assertQuoteTokenBalance() internal view { + assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: quote token balance"); + } + + function _assertBaseTokenBalance() internal view { + assertEq(_baseToken.balanceOf(_dtlAddress), 0, "DTL: base token balance"); + } + + function _assertApprovals() internal view { + // Router + assertEq( + _quoteToken.allowance(address(_dtl), address(_positionManager)), + 0, + "allowance: quote token: position manager" + ); + assertEq( + _baseToken.allowance(address(_dtl), address(_positionManager)), + 0, + "allowance: base token: position manager" + ); + } + + // ========== Modifiers ========== // + + function _calculateSqrtPriceX96( + uint256 quoteTokenAmount_, + uint256 baseTokenAmount_ + ) internal view returns (uint160) { + return SqrtPriceMath.getSqrtPriceX96( + address(_quoteToken), address(_baseToken), quoteTokenAmount_, baseTokenAmount_ + ); + } + + modifier setCallbackParameters(uint96 proceeds_, uint96 refund_) { + _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 * _dtlCreateParams.proceedsUtilisationPercent / 100e2; + _baseTokensToDeposit = + _capacityUtilised * _dtlCreateParams.proceedsUtilisationPercent / 100e2; + + _sqrtPriceX96 = _calculateSqrtPriceX96(_quoteTokensToDeposit, _baseTokensToDeposit); + _; + } + + modifier givenUnboundedProceedsUtilisationPercent(uint24 percent_) { + // Bound the percent + uint24 percent = uint24(bound(percent_, 1, 100e2)); + + // Set the value on the DTL + _dtlCreateParams.proceedsUtilisationPercent = percent; + _; + } + + modifier givenUnboundedOnCurate(uint96 curationPayout_) { + // Bound the value + _curatorPayout = uint96(bound(curationPayout_, 1e17, _LOT_CAPACITY)); + + // Call the onCurate callback + _performOnCurate(_curatorPayout); + _; + } + + modifier whenRefundIsBounded(uint96 refund_) { + // Bound the refund + _refund = uint96(bound(refund_, 1e17, 5e18)); + _; + } + + modifier givenPoolHasDepositLowerPrice() { + _sqrtPriceX96 = _calculateSqrtPriceX96(_PROCEEDS / 2, _LOT_CAPACITY); + _; + } + + modifier givenPoolHasDepositHigherPrice() { + _sqrtPriceX96 = _calculateSqrtPriceX96(_PROCEEDS * 2, _LOT_CAPACITY); + _; + } + + function _getPool() internal view returns (address) { + (address token0, address token1) = address(_baseToken) < address(_quoteToken) + ? (address(_baseToken), address(_quoteToken)) + : (address(_quoteToken), address(_baseToken)); + return _factory.getPool(token0, token1, _cleopatraCreateParams.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 the proceeds utilisation percent is set + // [X] it calculates the deposit amount correctly + // [X] given curation is enabled + // [X] the utilisation percent considers this + // [X] when the refund amount changes + // [X] the utilisation percent considers this + // [X] given minting pool tokens utilises less than the available amount of base tokens + // [X] the excess base tokens are returned + // [X] given minting pool tokens utilises less than the available amount of quote tokens + // [X] the excess quote tokens are returned + // [X] given the send base tokens flag is false + // [X] it transfers the base tokens from the seller + // [X] given the recipient is not the seller + // [X] it mints the LP token to the recipient + // [X] when multiple lots are created + // [X] it performs actions on the correct pool + // [ ] given the veRamTokenId is set + // [ ] it succeeds + // [X] it creates and initializes the pool, mints the position, transfers the LP token to the seller and transfers any excess back to the seller + + function test_givenPoolIsCreated() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenPoolIsCreatedAndInitialized_givenMaxSlippage() + public + givenCallbackIsCreated + givenMaxSlippage(8100) // 81% + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolIsCreatedAndInitialized(_SQRT_PRICE_X96_OVERRIDE) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertPoolState(_SQRT_PRICE_X96_OVERRIDE); + _assertLpTokenBalance(); + _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 + vm.expectRevert("Price slippage check"); + + _performOnSettle(); + } + + function test_givenProceedsUtilisationPercent_fuzz(uint24 percent_) + public + givenCallbackIsCreated + givenUnboundedProceedsUtilisationPercent(percent_) + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _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(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenProceedsUtilisationPercent_givenCurationPayout_fuzz( + uint24 percent_, + uint96 curationPayout_ + ) + public + givenCallbackIsCreated + givenUnboundedProceedsUtilisationPercent(percent_) + givenOnCreate + givenUnboundedOnCurate(curationPayout_) + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_whenRefund_fuzz(uint96 refund_) + public + givenCallbackIsCreated + givenOnCreate + whenRefundIsBounded(refund_) + setCallbackParameters(_PROCEEDS, _refund) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenPoolHasDepositWithLowerPrice() + public + givenCallbackIsCreated + givenMaxSlippage(5100) // 51% + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolHasDepositLowerPrice + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_givenPoolHasDepositWithHigherPrice() + public + givenCallbackIsCreated + givenMaxSlippage(5100) // 51% + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolHasDepositHigherPrice + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_lessThanMaxSlippage() + public + givenCallbackIsCreated + givenMaxSlippage(1) // 0.01% + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_greaterThanMaxSlippage_reverts() + public + givenCallbackIsCreated + givenMaxSlippage(0) // 0% + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenPoolHasDepositHigherPrice + givenPoolIsCreatedAndInitialized(_sqrtPriceX96) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _baseTokensToDeposit) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _baseTokensToDeposit) + { + // Expect revert + vm.expectRevert("Price slippage check"); + + _performOnSettle(); + } + + function test_givenInsufficientBaseTokenBalance_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised - 1) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + BaseDirectToLiquidity.Callback_InsufficientBalance.selector, + address(_baseToken), + _SELLER, + _baseTokensToDeposit, + _baseTokensToDeposit - 1 + ); + vm.expectRevert(err); + + _performOnSettle(); + } + + function test_givenInsufficientBaseTokenAllowance_reverts() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised - 1) + { + // Expect revert + vm.expectRevert("TRANSFER_FROM_FAILED"); + + _performOnSettle(); + } + + function test_success() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_success_multiple() + public + givenCallbackIsCreated + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_NOT_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_NOT_SELLER, _dtlAddress, _capacityUtilised) + whenRecipientIsNotSeller // Affects the second lot + { + // Create second lot + uint96 lotIdTwo = _createLot(_NOT_SELLER); + + _performOnSettle(lotIdTwo); + + _assertLpTokenBalance(lotIdTwo, _NOT_SELLER); + _assertQuoteTokenBalance(); + _assertBaseTokenBalance(); + _assertApprovals(); + } + + function test_whenRecipientIsNotSeller() + public + givenCallbackIsCreated + whenRecipientIsNotSeller + givenOnCreate + setCallbackParameters(_PROCEEDS, _REFUND) + givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + { + _performOnSettle(); + + _assertPoolState(_sqrtPriceX96); + _assertLpTokenBalance(); + _assertQuoteTokenBalance(); + _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 test_veRamTokenId() + // public + // givenCallbackIsCreated + // givenVeRamTokenId + // givenVeRamTokenIdApproval(true) + // givenOnCreate + // setCallbackParameters(_PROCEEDS, _REFUND) + // givenAddressHasQuoteTokenBalance(_dtlAddress, _proceeds) + // givenAddressHasBaseTokenBalance(_SELLER, _capacityUtilised) + // givenAddressHasBaseTokenAllowance(_SELLER, _dtlAddress, _capacityUtilised) + // { + // _performOnSettle(); + + // _assertPoolState(_sqrtPriceX96); + // _assertLpTokenBalance(); + // _assertQuoteTokenBalance(); + // _assertBaseTokenBalance(); + // _assertApprovals(); + // } +} diff --git a/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol index c2ccd09e..27290b53 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onCancel.t.sol @@ -18,6 +18,15 @@ contract UniswapV2DirectToLiquidityOnCancelTest is UniswapV2DirectToLiquidityTes // ============ Tests ============ // + // [ ] given the onCancel callback has already been called + // [ ] when onSettle is called + // [ ] it reverts + // [ ] when onCancel is called + // [ ] it reverts + // [ ] when onCurate is called + // [ ] it reverts + // [ ] when onCreate is called + // [ ] it reverts // [X] when the lot has not been registered // [X] it reverts // [X] when multiple lots are created diff --git a/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol index d65cae54..5b31664f 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onCreate.t.sol @@ -42,7 +42,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"); @@ -50,6 +50,8 @@ contract UniswapV2DirectToLiquidityOnCreateTest is UniswapV2DirectToLiquidityTes // ============ Tests ============ // + // [ ] when the auction lot has already been completed + // [ ] it reverts // [X] when the callback data is incorrect // [X] it reverts // [X] when the callback is not called by the auction house diff --git a/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol index 7078fc5e..8d43ced2 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onCurate.t.sol @@ -18,6 +18,8 @@ contract UniswapV2DirectToLiquidityOnCurateTest is UniswapV2DirectToLiquidityTes // ============ Tests ============ // + // [ ] when the auction lot has already been completed + // [ ] it reverts // [X] when the lot has not been registered // [X] it reverts // [X] when multiple lots are created diff --git a/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol b/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol index f073c4a0..80e07c25 100644 --- a/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol +++ b/test/callbacks/liquidity/UniswapV2DTL/onSettle.t.sol @@ -54,7 +54,7 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes // ========== Assertions ========== // - function _assertLpTokenBalance() internal { + function _assertLpTokenBalance() internal view { // Get the pools deployed by the DTL callback IUniswapV2Pair pool = _getUniswapV2Pool(); @@ -122,15 +122,15 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes ); } - function _assertQuoteTokenBalance() internal { + function _assertQuoteTokenBalance() internal view { assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: 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(_uniV2Router)), @@ -269,6 +269,15 @@ contract UniswapV2DirectToLiquidityOnSettleTest is UniswapV2DirectToLiquidityTes // ========== Tests ========== // + // [ ] given the onSettle callback has already been called + // [ ] when onSettle is called + // [ ] it reverts + // [ ] when onCancel is called + // [ ] it reverts + // [ ] when onCurate is called + // [ ] it reverts + // [ ] when onCreate is called + // [ ] it reverts // [X] given the pool is created // [X] it initializes the pool // [X] given the pool is created and initialized diff --git a/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol index 1383af27..1fec2951 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onCancel.t.sol @@ -18,6 +18,15 @@ contract UniswapV3DirectToLiquidityOnCancelTest is UniswapV3DirectToLiquidityTes // ============ Tests ============ // + // [ ] given the onCancel callback has already been called + // [ ] when onSettle is called + // [ ] it reverts + // [ ] when onCancel is called + // [ ] it reverts + // [ ] when onCurate is called + // [ ] it reverts + // [ ] when onCreate is called + // [ ] it reverts // [X] when the lot has not been registered // [X] it reverts // [X] when multiple lots are created diff --git a/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol index 15122668..a23ad8b4 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onCreate.t.sol @@ -43,7 +43,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"); @@ -57,6 +57,8 @@ contract UniswapV3DirectToLiquidityOnCreateTest is UniswapV3DirectToLiquidityTes // [X] it reverts // [X] when the lot has already been registered // [X] it reverts + // [ ] when the auction lot has already been completed + // [ ] it reverts // [X] when the proceeds utilisation is 0 // [X] it reverts // [X] when the proceeds utilisation is greater than 100% diff --git a/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol index 79308ef2..0140d62f 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onCurate.t.sol @@ -20,6 +20,8 @@ contract UniswapV3DirectToLiquidityOnCurateTest is UniswapV3DirectToLiquidityTes // [X] when the lot has not been registered // [X] it reverts + // [ ] when the auction lot has already been completed + // [ ] it reverts // [X] when multiple lots are created // [X] it marks the correct lot as inactive // [X] it registers the curator payout diff --git a/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol b/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol index 4a4b5511..fe1dd4ba 100644 --- a/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol +++ b/test/callbacks/liquidity/UniswapV3DTL/onSettle.t.sol @@ -63,7 +63,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes // ========== Assertions ========== // - function _assertPoolState(uint160 sqrtPriceX96_) internal { + function _assertPoolState(uint160 sqrtPriceX96_) internal view { // Get the pool address pool = _getPool(); @@ -71,7 +71,7 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes 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 +136,15 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes ); } - function _assertQuoteTokenBalance() internal { + function _assertQuoteTokenBalance() internal view { assertEq(_quoteToken.balanceOf(_dtlAddress), 0, "DTL: 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())), @@ -279,6 +279,15 @@ contract UniswapV3DirectToLiquidityOnSettleTest is UniswapV3DirectToLiquidityTes // ========== Tests ========== // + // [ ] given the onSettle callback has already been called + // [ ] when onSettle is called + // [ ] it reverts + // [ ] when onCancel is called + // [ ] it reverts + // [ ] when onCurate is called + // [ ] it reverts + // [ ] when onCreate is called + // [ ] it reverts // [X] given the pool is created // [X] it initializes the pool // [X] given the pool is created and initialized diff --git a/test/lib/SqrtPriceMath.t.sol b/test/lib/SqrtPriceMath.t.sol index 5fa175b2..35854c43 100644 --- a/test/lib/SqrtPriceMath.t.sol +++ b/test/lib/SqrtPriceMath.t.sol @@ -28,12 +28,12 @@ contract SqrtPriceMathTest is Test { // [X] when tokenA decimals is less than tokenB decimals // [X] it calculates the correct sqrtPriceX96 - function test_whenTokenAIs_TOKEN0() public { + function test_whenTokenAIs_TOKEN0() public pure { uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96(_TOKEN0, _TOKEN1, _AMOUNT0, _AMOUNT1); assertEq(sqrtPriceX96, _SQRTPRICEX96, "SqrtPriceX96"); } - function test_whenTokenAIs_TOKEN1() public { + function test_whenTokenAIs_TOKEN1() public pure { uint160 sqrtPriceX96 = SqrtPriceMath.getSqrtPriceX96(_TOKEN1, _TOKEN0, _AMOUNT1, _AMOUNT0); assertEq(sqrtPriceX96, _SQRTPRICEX96, "SqrtPriceX96"); }