diff --git a/.gitignore b/.gitignore index 05a805c264..847bb491de 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ contracts/.localKeyValueStorage.mainnet contracts/.localKeyValueStorage.holesky contracts/scripts/defender-actions/dist/ +contracts/lib/defender-actions/dist/ + todo.txt brownie/env-brownie/ diff --git a/brownie/abi/maverick_v2_pool.json b/brownie/abi/maverick_v2_pool.json new file mode 100644 index 0000000000..c252d5b401 --- /dev/null +++ b/brownie/abi/maverick_v2_pool.json @@ -0,0 +1,1168 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "binIdsLength", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountsLength", + "type": "uint256" + } + ], + "name": "PoolBinIdsAmountsLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int32", + "name": "startingTick", + "type": "int32" + } + ], + "name": "PoolCurrentTickBeyondSwapLimit", + "type": "error" + }, + { + "inputs": [], + "name": "PoolFunctionNotImplemented", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "deltaLpAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "accountBalance", + "type": "uint256" + } + ], + "name": "PoolInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "PoolInvalidFee", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "kinds", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "kind", + "type": "uint256" + } + ], + "name": "PoolKindNotSupported", + "type": "error" + }, + { + "inputs": [], + "name": "PoolLocked", + "type": "error" + }, + { + "inputs": [], + "name": "PoolMigrateBinFirst", + "type": "error" + }, + { + "inputs": [], + "name": "PoolMinimumLiquidityNotMet", + "type": "error" + }, + { + "inputs": [], + "name": "PoolNoProtocolFeeReceiverSet", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "PoolReservesExceedMaximum", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender_", + "type": "address" + }, + { + "internalType": "address", + "name": "accessor", + "type": "address" + } + ], + "name": "PoolSenderNotAccessor", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender_", + "type": "address" + }, + { + "internalType": "address", + "name": "accessor", + "type": "address" + } + ], + "name": "PoolSenderNotFactory", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tick", + "type": "uint256" + } + ], + "name": "PoolTickMaxExceeded", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "ticksLength", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountsLength", + "type": "uint256" + } + ], + "name": "PoolTicksAmountsLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "previousTick", + "type": "int256" + }, + { + "internalType": "int256", + "name": "tick", + "type": "int256" + } + ], + "name": "PoolTicksNotSorted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "internalReserve", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenBalance", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "PoolTokenNotSolvent", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bits", + "type": "uint256" + } + ], + "name": "PoolValueExceedsBits", + "type": "error" + }, + { + "inputs": [], + "name": "PoolZeroLiquidityAdded", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "int32[]", + "name": "ticks", + "type": "int32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "indexed": false, + "internalType": "struct IMaverickV2Pool.AddLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenAAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenBAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + } + ], + "name": "PoolAddLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "name": "PoolFlashLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "binId", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "maxRecursion", + "type": "uint32" + } + ], + "name": "PoolMigrateBinsUpStack", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "protocolFee", + "type": "uint256" + } + ], + "name": "PoolProtocolFeeCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "indexed": false, + "internalType": "struct IMaverickV2Pool.RemoveLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenAOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenBOut", + "type": "uint256" + } + ], + "name": "PoolRemoveLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sqrtPrice", + "type": "uint256" + } + ], + "name": "PoolSqrtPrice", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "tokenAIn", + "type": "bool" + }, + { + "internalType": "bool", + "name": "exactOutput", + "type": "bool" + }, + { + "internalType": "int32", + "name": "tickLimit", + "type": "int32" + } + ], + "indexed": false, + "internalType": "struct IMaverickV2Pool.SwapParams", + "name": "params", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "name": "PoolSwap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "binId", + "type": "uint32" + } + ], + "name": "PoolTickBinUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reserveA", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reserveB", + "type": "uint256" + } + ], + "name": "PoolTickState", + "type": "event" + }, + { + "inputs": [], + "name": "accessor", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "int32[]", + "name": "ticks", + "type": "int32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "internalType": "struct IMaverickV2Pool.AddLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "addLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "tokenAAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenBAmount", + "type": "uint256" + }, + { + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "binId", + "type": "uint32" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint128", + "name": "lpToken", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "internalType": "uint256", + "name": "kind", + "type": "uint256" + } + ], + "name": "binIdByTickKind", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "isTokenA", + "type": "bool" + } + ], + "name": "distributeFees", + "outputs": [ + { + "internalType": "uint256", + "name": "protocolFee", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "contract IMaverickV2Factory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "tokenAIn", + "type": "bool" + } + ], + "name": "fee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "flashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "binId", + "type": "uint32" + } + ], + "name": "getBin", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "mergeBinBalance", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "tickBalance", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "totalSupply", + "type": "uint128" + }, + { + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "internalType": "uint32", + "name": "mergeId", + "type": "uint32" + } + ], + "internalType": "struct IMaverickV2Pool.BinState", + "name": "bin", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentTwa", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getState", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "reserveA", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "reserveB", + "type": "uint128" + }, + { + "internalType": "int64", + "name": "lastTwaD8", + "type": "int64" + }, + { + "internalType": "int64", + "name": "lastLogPriceD8", + "type": "int64" + }, + { + "internalType": "uint40", + "name": "lastTimestamp", + "type": "uint40" + }, + { + "internalType": "int32", + "name": "activeTick", + "type": "int32" + }, + { + "internalType": "bool", + "name": "isLocked", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "binCounter", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "protocolFeeRatioD3", + "type": "uint8" + } + ], + "internalType": "struct IMaverickV2Pool.State", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int32", + "name": "tick", + "type": "int32" + } + ], + "name": "getTick", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "reserveA", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "reserveB", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "totalSupply", + "type": "uint128" + }, + { + "internalType": "uint32[4]", + "name": "binIdsByTick", + "type": "uint32[4]" + } + ], + "internalType": "struct IMaverickV2Pool.TickState", + "name": "tickState", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "kinds", + "outputs": [ + { + "internalType": "uint8", + "name": "_kinds", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lendingFeeRateD18", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lookback", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "binId", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxRecursion", + "type": "uint32" + } + ], + "name": "migrateBinUpStack", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFeeA", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFeeB", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "internalType": "struct IMaverickV2Pool.RemoveLiquidityParams", + "name": "params", + "type": "tuple" + } + ], + "name": "removeLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "tokenAOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenBOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "tokenAIn", + "type": "bool" + }, + { + "internalType": "bool", + "name": "exactOutput", + "type": "bool" + }, + { + "internalType": "int32", + "name": "tickLimit", + "type": "int32" + } + ], + "internalType": "struct IMaverickV2Pool.SwapParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tickSpacing", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenA", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenAScale", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenB", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenBScale", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] \ No newline at end of file diff --git a/brownie/abi/rooster_amo_strat.json b/brownie/abi/rooster_amo_strat.json new file mode 100644 index 0000000000..90696d3170 --- /dev/null +++ b/brownie/abi/rooster_amo_strat.json @@ -0,0 +1,1179 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "platformAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "vaultAddress", + "type": "address" + } + ], + "internalType": "struct InitializableAbstractStrategy.BaseStrategyConfig", + "name": "_stratConfig", + "type": "tuple" + }, + { + "internalType": "address", + "name": "_wethAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_oethpAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_liquidityManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_poolLens", + "type": "address" + }, + { + "internalType": "address", + "name": "_maverickPosition", + "type": "address" + }, + { + "internalType": "address", + "name": "_maverickQuoter", + "type": "address" + }, + { + "internalType": "address", + "name": "_mPool", + "type": "address" + }, + { + "internalType": "bool", + "name": "_upperTickAtParity", + "type": "bool" + }, + { + "internalType": "address", + "name": "_votingDistributor", + "type": "address" + }, + { + "internalType": "address", + "name": "_poolDistributor", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenOffered", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenRequired", + "type": "uint256" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "InsufficientTokenBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wethBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requiredWeth", + "type": "uint256" + } + ], + "name": "NotEnoughWethForSwap", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wethBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requiredWeth", + "type": "uint256" + } + ], + "name": "NotEnoughWethLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "OutsideExpectedTickRange", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "currentPoolWethShare", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "allowedWethShareStart", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "allowedWethShareEnd", + "type": "uint256" + } + ], + "name": "PoolRebalanceOutOfBounds", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenReceived", + "type": "uint256" + } + ], + "name": "SlippageCheck", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "tick", + "type": "int256" + } + ], + "name": "TickMaxExceeded", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousGovernor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newGovernor", + "type": "address" + } + ], + "name": "GovernorshipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_oldHarvesterAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_newHarvesterAddress", + "type": "address" + } + ], + "name": "HarvesterAddressesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "wethAmountDesired", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oethbAmountDesired", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wethAmountSupplied", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oethbAmountSupplied", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "underlyingAssets", + "type": "uint256" + } + ], + "name": "LiquidityAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "withdrawLiquidityShare", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "removedWETHAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "removedOETHbAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "underlyingAssets", + "type": "uint256" + } + ], + "name": "LiquidityRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + } + ], + "name": "PTokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + } + ], + "name": "PTokenRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousGovernor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newGovernor", + "type": "address" + } + ], + "name": "PendingGovernorshipTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "currentPoolWethShare", + "type": "uint256" + } + ], + "name": "PoolRebalanced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "allowedWethShareStart", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "allowedWethShareEnd", + "type": "uint256" + } + ], + "name": "PoolWethShareIntervalUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address[]", + "name": "_oldAddresses", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "_newAddresses", + "type": "address[]" + } + ], + "name": "RewardTokenAddressesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "rewardToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RewardTokenCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "underlyingAssets", + "type": "uint256" + } + ], + "name": "UnderlyingAssetsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_pToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "inputs": [], + "name": "OETHp", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SOLVENCY_THRESHOLD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allowedWethShareEnd", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allowedWethShareStart", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "assetToPToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + } + ], + "name": "checkBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "collectRewardTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentTradingTick", + "outputs": [ + { + "internalType": "int32", + "name": "_currentTick", + "type": "int32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPoolSqrtPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPositionPrincipal", + "outputs": [ + { + "internalType": "uint256", + "name": "_amountWeth", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_amountOethp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRewardTokenAddresses", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getWETHShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governor", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "harvesterAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "isGovernor", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidityManager", + "outputs": [ + { + "internalType": "contract IMaverickV2LiquidityManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mPool", + "outputs": [ + { + "internalType": "contract IMaverickV2Pool", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maverickPosition", + "outputs": [ + { + "internalType": "contract IMaverickV2Position", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mintInitialPosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "platformAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolDistributor", + "outputs": [ + { + "internalType": "contract IPoolDistributor", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolLens", + "outputs": [ + { + "internalType": "contract IMaverickV2PoolLens", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "quoter", + "outputs": [ + { + "internalType": "contract IMaverickV2Quoter", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amountToSwap", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_swapWeth", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "_minTokenReceived", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_liquidityToRemovePct", + "type": "uint256" + } + ], + "name": "rebalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "removePToken", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "rewardTokenAddresses", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "safeApproveAllTokens", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_allowedWethShareStart", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_allowedWethShareEnd", + "type": "uint256" + } + ], + "name": "setAllowedPoolWethShareInterval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_harvesterAddress", + "type": "address" + } + ], + "name": "setHarvesterAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "setPTokenAddress", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_rewardTokenAddresses", + "type": "address[]" + } + ], + "name": "setRewardTokenAddresses", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "sqrtPriceAtParity", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "sqrtPriceTickHigher", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "sqrtPriceTickLower", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + } + ], + "name": "supportsAsset", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tickDominance", + "outputs": [ + { + "internalType": "uint256", + "name": "_tickDominance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tickNumber", + "outputs": [ + { + "internalType": "int32", + "name": "", + "type": "int32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tickSpacing", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newGovernor", + "type": "address" + } + ], + "name": "transferGovernance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "transferToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "underlyingAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vaultAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "votingDistributor", + "outputs": [ + { + "internalType": "contract IVotingDistributor", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "address", + "name": "_asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] \ No newline at end of file diff --git a/brownie/abi/rooster_maverick_pool.json b/brownie/abi/rooster_maverick_pool.json new file mode 100644 index 0000000000..c252d5b401 --- /dev/null +++ b/brownie/abi/rooster_maverick_pool.json @@ -0,0 +1,1168 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "binIdsLength", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountsLength", + "type": "uint256" + } + ], + "name": "PoolBinIdsAmountsLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int32", + "name": "startingTick", + "type": "int32" + } + ], + "name": "PoolCurrentTickBeyondSwapLimit", + "type": "error" + }, + { + "inputs": [], + "name": "PoolFunctionNotImplemented", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "deltaLpAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "accountBalance", + "type": "uint256" + } + ], + "name": "PoolInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "PoolInvalidFee", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "kinds", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "kind", + "type": "uint256" + } + ], + "name": "PoolKindNotSupported", + "type": "error" + }, + { + "inputs": [], + "name": "PoolLocked", + "type": "error" + }, + { + "inputs": [], + "name": "PoolMigrateBinFirst", + "type": "error" + }, + { + "inputs": [], + "name": "PoolMinimumLiquidityNotMet", + "type": "error" + }, + { + "inputs": [], + "name": "PoolNoProtocolFeeReceiverSet", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "PoolReservesExceedMaximum", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender_", + "type": "address" + }, + { + "internalType": "address", + "name": "accessor", + "type": "address" + } + ], + "name": "PoolSenderNotAccessor", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender_", + "type": "address" + }, + { + "internalType": "address", + "name": "accessor", + "type": "address" + } + ], + "name": "PoolSenderNotFactory", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tick", + "type": "uint256" + } + ], + "name": "PoolTickMaxExceeded", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "ticksLength", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountsLength", + "type": "uint256" + } + ], + "name": "PoolTicksAmountsLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "previousTick", + "type": "int256" + }, + { + "internalType": "int256", + "name": "tick", + "type": "int256" + } + ], + "name": "PoolTicksNotSorted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "internalReserve", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenBalance", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "PoolTokenNotSolvent", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bits", + "type": "uint256" + } + ], + "name": "PoolValueExceedsBits", + "type": "error" + }, + { + "inputs": [], + "name": "PoolZeroLiquidityAdded", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "int32[]", + "name": "ticks", + "type": "int32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "indexed": false, + "internalType": "struct IMaverickV2Pool.AddLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenAAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenBAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + } + ], + "name": "PoolAddLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "name": "PoolFlashLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "binId", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "maxRecursion", + "type": "uint32" + } + ], + "name": "PoolMigrateBinsUpStack", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "protocolFee", + "type": "uint256" + } + ], + "name": "PoolProtocolFeeCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "indexed": false, + "internalType": "struct IMaverickV2Pool.RemoveLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenAOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenBOut", + "type": "uint256" + } + ], + "name": "PoolRemoveLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sqrtPrice", + "type": "uint256" + } + ], + "name": "PoolSqrtPrice", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "tokenAIn", + "type": "bool" + }, + { + "internalType": "bool", + "name": "exactOutput", + "type": "bool" + }, + { + "internalType": "int32", + "name": "tickLimit", + "type": "int32" + } + ], + "indexed": false, + "internalType": "struct IMaverickV2Pool.SwapParams", + "name": "params", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "name": "PoolSwap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "binId", + "type": "uint32" + } + ], + "name": "PoolTickBinUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reserveA", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reserveB", + "type": "uint256" + } + ], + "name": "PoolTickState", + "type": "event" + }, + { + "inputs": [], + "name": "accessor", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "int32[]", + "name": "ticks", + "type": "int32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "internalType": "struct IMaverickV2Pool.AddLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "addLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "tokenAAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenBAmount", + "type": "uint256" + }, + { + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "binId", + "type": "uint32" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint128", + "name": "lpToken", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "internalType": "uint256", + "name": "kind", + "type": "uint256" + } + ], + "name": "binIdByTickKind", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "isTokenA", + "type": "bool" + } + ], + "name": "distributeFees", + "outputs": [ + { + "internalType": "uint256", + "name": "protocolFee", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "contract IMaverickV2Factory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "tokenAIn", + "type": "bool" + } + ], + "name": "fee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "flashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "binId", + "type": "uint32" + } + ], + "name": "getBin", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "mergeBinBalance", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "tickBalance", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "totalSupply", + "type": "uint128" + }, + { + "internalType": "uint8", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "int32", + "name": "tick", + "type": "int32" + }, + { + "internalType": "uint32", + "name": "mergeId", + "type": "uint32" + } + ], + "internalType": "struct IMaverickV2Pool.BinState", + "name": "bin", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentTwa", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getState", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "reserveA", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "reserveB", + "type": "uint128" + }, + { + "internalType": "int64", + "name": "lastTwaD8", + "type": "int64" + }, + { + "internalType": "int64", + "name": "lastLogPriceD8", + "type": "int64" + }, + { + "internalType": "uint40", + "name": "lastTimestamp", + "type": "uint40" + }, + { + "internalType": "int32", + "name": "activeTick", + "type": "int32" + }, + { + "internalType": "bool", + "name": "isLocked", + "type": "bool" + }, + { + "internalType": "uint32", + "name": "binCounter", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "protocolFeeRatioD3", + "type": "uint8" + } + ], + "internalType": "struct IMaverickV2Pool.State", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int32", + "name": "tick", + "type": "int32" + } + ], + "name": "getTick", + "outputs": [ + { + "components": [ + { + "internalType": "uint128", + "name": "reserveA", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "reserveB", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "totalSupply", + "type": "uint128" + }, + { + "internalType": "uint32[4]", + "name": "binIdsByTick", + "type": "uint32[4]" + } + ], + "internalType": "struct IMaverickV2Pool.TickState", + "name": "tickState", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "kinds", + "outputs": [ + { + "internalType": "uint8", + "name": "_kinds", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lendingFeeRateD18", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lookback", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "binId", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxRecursion", + "type": "uint32" + } + ], + "name": "migrateBinUpStack", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFeeA", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFeeB", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subaccount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32[]", + "name": "binIds", + "type": "uint32[]" + }, + { + "internalType": "uint128[]", + "name": "amounts", + "type": "uint128[]" + } + ], + "internalType": "struct IMaverickV2Pool.RemoveLiquidityParams", + "name": "params", + "type": "tuple" + } + ], + "name": "removeLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "tokenAOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenBOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "tokenAIn", + "type": "bool" + }, + { + "internalType": "bool", + "name": "exactOutput", + "type": "bool" + }, + { + "internalType": "int32", + "name": "tickLimit", + "type": "int32" + } + ], + "internalType": "struct IMaverickV2Pool.SwapParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tickSpacing", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenA", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenAScale", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenB", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenBScale", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] \ No newline at end of file diff --git a/brownie/scripts/strategy_report_amo.py b/brownie/scripts/strategy_report_amo.py index 7434adc67c..2ba6e79f63 100644 --- a/brownie/scripts/strategy_report_amo.py +++ b/brownie/scripts/strategy_report_amo.py @@ -7,7 +7,8 @@ # NOTICE: un-comment the below import depending on the chain in which the test is ran # from world import * # from world_base import * -from world_sonic import * +# from world_sonic import * +from world_plume import * WSTETH_WHALE = "0x176f3dab24a159341c0509bb36b833e7fdd0a132" @@ -22,6 +23,11 @@ # the WOS contract SONIC_OS_WHALE = "0x9F0dF7799f6FDAd409300080cfF680f5A23df4b1" +# Plume +# Account holds 440 tokens on block number 3507372 +PLUME_WETH_FISH = "0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971" + + MASTER_SIZE = 60 NUM_DEPOSIT_TESTS_EACH = MASTER_SIZE NUM_BALANCE_TESTS_EACH = MASTER_SIZE @@ -54,6 +60,9 @@ def pool_balances(self): balances = self._balancer_vault.getPoolTokens(self._pool_pid)[0:2] return dict(zip(balances[0], balances[1])) + def print_debug_data(self): + pass + def tilt_pool(self, size): print("Tilt pool", size) amount = abs(size) * self.base_size * int(1e18) @@ -107,6 +116,9 @@ def pool_balances(self): balances = self._balancer_vault.getPoolTokens(self._pool_pid) return dict(zip(balances[0][1:3], balances[1][1:3])) + def print_debug_data(self): + pass + def tilt_pool(self, size): amount = abs(size) * self.base_size * int(1e18) print("Tilt pool", size, amount) @@ -151,7 +163,6 @@ def __init__(self): self.amo_base = weth def setup(self): - print(weth.address) weth.approve(self.vault_core, 1e70, {"from": BASE_WETH_WHALE}) weth.approve(self.pool, 1e70, {"from": BASE_WETH_WHALE}) oethb.approve(self.pool, 1e70, {"from": BASE_WETH_WHALE}) @@ -164,6 +175,9 @@ def pool_balances(self): oethb.address: self.pool.balances(1), } + def print_debug_data(self): + pass + def tilt_pool(self, size): vault_core.mint(weth, 1000*10**18, 0, {"from": BASE_WETH_WHALE}) amount = abs(size) * self.base_size * int(1e18) @@ -207,6 +221,9 @@ def setup(self): ws.approve(self.pool, 1e70, {"from": SONIC_WS_WHALE}) os.approve(self.pool, 1e70, {"from": SONIC_WS_WHALE}) + deposit_amount = self.base_size * 0.1 * int(1e18) + self.amo_base.transfer(vault_admin, deposit_amount, {"from": SONIC_WS_WHALE}) + def pool_balances(self): print("⚱︎ pool_balances") @@ -216,6 +233,9 @@ def pool_balances(self): "os": self.pool.reserve1(), } + def print_debug_data(self): + pass + def tilt_pool(self, size): # 10m OS vault_core.mint(ws, 10 * 10**24, 0, {"from": SONIC_WS_WHALE}) @@ -243,6 +263,121 @@ def pool_create_mix(self, tilt=0.5, size=1): } return mix +class RoosterWETHOethp: + def __init__(self): + # TODO: the strat address will change once the pool is live + strat_address = "0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c" + pool_address = "0x3F86B564A9B530207876d2752948268b9Bf04F71" + self.strat = load_contract("rooster_amo_strat", strat_address) + self.pool = load_contract("rooster_maverick_pool", pool_address) + self.name = "Rooster Pool AMO" + self.vault_core = vault_core + self.vault_admin = vault_admin + self.otoken = oethp + # for check balance tests 200 base + #self.base_size = int(200) + # for deposit tests use 50 base + self.base_size = int(50) + self.STRATEGIST = vault_core.strategistAddr() + self.amo_base = weth + self.deposit_debug_data = [] + + def setup(self): + weth.approve(self.vault_core, 1e70, {"from": PLUME_WETH_FISH}) + oethAmountNeeded = 200 * 10**18 + + if oethp.balanceOf(PLUME_WETH_FISH) < oethAmountNeeded: + vault_core.mint(weth, 200 * 10**18, 0, {"from": PLUME_WETH_FISH}) + + def pool_balances(self): + print("⚱︎ pool_balances") + + return { + "weth": self.pool.getState()[0], # reserveA + "oethp": self.pool.getState()[0] # reserveB, + } + + def print_debug_data(self): + for data in self.deposit_debug_data: + print(data) + + def tilt_pool(self, size): + amount = abs(size) * self.base_size * int(1e18) + print("Tilt pool", size, amount) + if size > -0.00001 and size < 0.00001: + print("skip tilt") + pass + + self.swap_pool(amount, size > 0) + + def swap_pool(self, amount, is_weth): + if is_weth: + weth.transfer(self.pool.address, amount, {"from": PLUME_WETH_FISH}) + self.pool.swap( + PLUME_WETH_FISH, + [ + amount, #amount + True, #tokenAIn + False, #exactOutput + 1000 #tickLimit + ], + b'', + {"from": PLUME_WETH_FISH} + ); + else: + oethp.transfer(self.pool.address, amount, {"from": PLUME_WETH_FISH}) + self.pool.swap( + PLUME_WETH_FISH, + [ + amount, #amount + False, #tokenAIn + False, #exactOutput + -1000 #tickLimit + ], + b'', + {"from": PLUME_WETH_FISH} + ); + + def estimate_swap_amount_to_reach_weth_ratio(self, wethRatio): + tickInfo = self.pool.getTick(-1); + wethReserve = tickInfo[0] + oethpReserve = tickInfo[1] + total = wethReserve + oethpReserve + + if wethReserve == 0 or oethpReserve == 0: + print("wethReserve: ", wethReserve, " oethpReserve:", oethpReserve) + raise Exception("Not in the -1 trading tick. Perform a swap to move the trading in the pool to -1 tick") + + + currentWethRatio = wethReserve * 1e18 / total + if wethRatio > currentWethRatio: + diff = wethRatio - currentWethRatio + swapWeth = True + else: + diff = currentWethRatio - wethRatio + swapWeth = False + + return (diff * total / 1e18, swapWeth) + + + def write_debug_data(self, size): + self.deposit_debug_data.append( + ( + round(size, 2), + self.strat.getCurrentTradingTick(), + round(self.strat.checkBalance(self.amo_base) / 1e18, 2), + round(self.strat.getWETHShare() / 1e16, 2) + ) + ) + + def pool_create_mix(self, tilt=0.5, size=1): + print("⚱︎ pool_create_mix") + mix = { + weth.address: 2 + int(size * self.base_size * (int(1e18) - (int(1e18) * tilt))), + oethp.address: 2 + int(size * self.base_size * int(1e18) * tilt), + } + return mix + # -------------- @@ -256,6 +391,8 @@ def _getHarness(name): return CurveSuperOETHbWETH() elif name == "SwapxOsWS": return SwapxOsWS() + elif name == "RoosterWETHOethp": + return RoosterWETHOethp() return @@ -301,6 +438,8 @@ def run_simulations_amo(strategy_name): workspace = _getWorkspace(strategy_name) harness = _getHarness(strategy_name) + harness_rooster = strategy_name == "RoosterWETHOethp" + # Run setup harness.setup() try: @@ -310,58 +449,61 @@ def run_simulations_amo(strategy_name): pass - # Test CheckBalance - print("Test Check Balance") - check_balance_stats = [] + # # Test CheckBalance + # print("Test Check Balance") + # check_balance_stats = [] - for tilt in np.linspace(-1, 1, 41): - with TemporaryFork(): - stat = {} - stat["action"] = "checkBalance" - stat["action_mix"] = tilt + # for tilt in np.linspace(-1, 1, 41): + # with TemporaryFork(): + # stat = {} + # stat["action"] = "checkBalance" + # stat["action_mix"] = tilt - deposit_amount = harness.base_size * 0.5 * int(1e18) - ws.transfer(vault_admin, deposit_amount * 2, {"from": SONIC_WS_WHALE}) + # deposit_amount = harness.base_size * 0.1 * 10**18 - print("deposit to strategy") - harness.vault_admin.depositToStrategy( - harness.strat, - [harness.amo_base], - #[deposit_amount / 5], - # deposit a smaller amount for an extreme tilt - [deposit_amount / 50], - {"from": harness.STRATEGIST}, - ) + # if harness_rooster: + # # manually correct the current pool price to put it into -1 tick + # harness.tilt_pool(0.1) - pb = list(harness.pool_balances().values()) - stat["pre_pool_0"] = pb[0] - stat["pre_pool_1"] = pb[1] - stat["before_pool_0"] = pb[0] - stat["before_pool_1"] = pb[1] - stat["pre_vault"] = harness.vault_core.totalValue() - stat["before_vault"] = harness.vault_core.totalValue() - stat["before_otoken"] = harness.otoken.totalSupply() - stat["pool_before_check_balance"] = harness.strat.checkBalance(ws) + # (amount_to_swap, swap_weth) = harness.estimate_swap_amount_to_reach_weth_ratio(0.2e18) # 20% + # harness.swap_pool(amount_to_swap, swap_weth) + + # print("deposit to strategy") + # harness.vault_admin.depositToStrategy( + # harness.strat, + # [harness.amo_base], + # [deposit_amount], + # {"from": harness.STRATEGIST}, + # ) + + # pb = list(harness.pool_balances().values()) + # stat["pre_pool_0"] = pb[0] + # stat["pre_pool_1"] = pb[1] + # stat["before_pool_0"] = pb[0] + # stat["before_pool_1"] = pb[1] + # stat["pre_vault"] = harness.vault_core.totalValue() + # stat["before_vault"] = harness.vault_core.totalValue() + # stat["before_otoken"] = harness.otoken.totalSupply() + # stat["pool_before_check_balance"] = harness.strat.checkBalance(harness.amo_base) - harness.tilt_pool(tilt) + # harness.tilt_pool(tilt) - pb = list(harness.pool_balances().values()) - stat["after_pool_0"] = pb[0] - stat["after_pool_1"] = pb[1] - stat["after_vault"] = harness.vault_core.totalValue() - stat["after_otoken"] = harness.otoken.totalSupply() - stat["pool_after_check_balance"] = harness.strat.checkBalance(ws) + # pb = list(harness.pool_balances().values()) + # stat["after_pool_0"] = pb[0] + # stat["after_pool_1"] = pb[1] + # stat["after_vault"] = harness.vault_core.totalValue() + # stat["after_otoken"] = harness.otoken.totalSupply() + # stat["pool_after_check_balance"] = harness.strat.checkBalance(harness.amo_base) - check_balance_stats.append(stat) + # check_balance_stats.append(stat) - pd.DataFrame.from_records(check_balance_stats).to_csv(workspace + "check_balance_stats.csv") + # pd.DataFrame.from_records(check_balance_stats).to_csv(workspace + "check_balance_stats.csv") # # Test Deposits # print("# Test Deposits") # deposit_stats = [] - # vault_core.mint(ws, harness.base_size * 1e18, 0, {"from": SONIC_WS_WHALE}) # for initial_tilt in np.linspace(-0.1, 0.32, 41): # with TemporaryFork(): # stat = {} @@ -369,13 +511,10 @@ def run_simulations_amo(strategy_name): # stat["action"] = "deposit" # stat["action_mix"] = initial_tilt - # # mint some OS in case vault doesn't have enough WS liquidity - # # vault_core.mint(ws, 5 * 10**24, 0, {"from": SONIC_WS_WHALE}) - # harness.vault_admin.depositToStrategy( # harness.strat, # [harness.amo_base], - # [harness.base_size * 0.5], + # [harness.base_size * 0.5 * 10**18], # {"from": harness.STRATEGIST}, # ) @@ -396,10 +535,12 @@ def run_simulations_amo(strategy_name): # harness.vault_admin.depositToStrategy( # harness.strat, # [harness.amo_base], - # [harness.base_size * 0.2], + # [harness.base_size * 0.2 * 10**18], # {"from": harness.STRATEGIST}, # ) + # harness.write_debug_data(initial_tilt) + # stat["after_vault"] = harness.vault_core.totalValue() # stat["after_otoken"] = harness.otoken.totalSupply() # pb = list(harness.pool_balances().values()) @@ -408,77 +549,32 @@ def run_simulations_amo(strategy_name): # deposit_stats.append(stat) # pd.DataFrame.from_records(deposit_stats).to_csv(workspace + "deposit_stats.csv") - # # Test Withdraws # withdraw_stats = [] # for initial_tilt in np.linspace(-0.7, 0.7, 41): - # with TemporaryFork(): - # stat = {} - - # stat["action"] = "withdraw" - # stat["action_mix"] = initial_tilt - # print("w Deposit") - # pb = list(harness.pool_balances().values()) - # stat["pre_pool_0"] = pb[0] - # stat["pre_pool_1"] = pb[1] - # print(pb[0]/10**18,pb[1]/10**18) - - # harness.vault_admin.depositToStrategy( - # harness.strat, - # [harness.amo_base], - # [harness.base_size * 1 * 10**18], - # {"from": harness.STRATEGIST}, - # ) - - # stat["pre_vault"] = harness.vault_core.totalValue() - # pb = list(harness.pool_balances().values()) - # stat["pre_pool_0"] = pb[0] - # stat["pre_pool_1"] = pb[1] - # print(pb[0]/10**18,pb[1]/10**18) - - # print("Withdraw Tilt") - # harness.tilt_pool(initial_tilt) - - # stat["before_vault"] = harness.vault_core.totalValue() - # stat["before_otoken"] = harness.otoken.totalSupply() - # pb = list(harness.pool_balances().values()) - # stat["before_pool_0"] = pb[0] - # stat["before_pool_1"] = pb[1] - # print(pb[0]/10**18,pb[1]/10**18) - - # print("Withdraw Withdraw") - # harness.vault_admin.withdrawFromStrategy( - # harness.strat, - # [harness.amo_base], - # [1e18], - # {"from": harness.STRATEGIST, "allow_revert": True}, - # ) - - # stat["after_vault"] = harness.vault_core.totalValue() - # stat["after_otoken"] = harness.otoken.totalSupply() - # pb = list(harness.pool_balances().values()) - # stat["after_pool_0"] = pb[0] - # stat["after_pool_1"] = pb[1] - - # withdraw_stats.append(stat) - - # pd.DataFrame.from_records(withdraw_stats).to_csv(workspace + "withdraw_stats.csv") - - # Test WithdrawAll - - # withdrawall_stats = [] - # for initial_tilt in np.linspace(-1.5, 1.5, 41): # with TemporaryFork(): # stat = {} - # stat["action"] = "withdrawall" + # if harness_rooster: + # # manually correct the current pool price to put it into -1 tick + # harness.tilt_pool(0.1) + + # (amount_to_swap, swap_weth) = harness.estimate_swap_amount_to_reach_weth_ratio(0.2e18) # 20% + # harness.swap_pool(amount_to_swap, swap_weth) + + # stat["action"] = "withdraw" # stat["action_mix"] = initial_tilt + # print("w Deposit") + # pb = list(harness.pool_balances().values()) + # stat["pre_pool_0"] = pb[0] + # stat["pre_pool_1"] = pb[1] + # print(pb[0]/10**18,pb[1]/10**18) # harness.vault_admin.depositToStrategy( # harness.strat, # [harness.amo_base], - # [harness.base_size * 2.0], + # [harness.base_size * 1 * 10**18], # {"from": harness.STRATEGIST}, # ) @@ -486,7 +582,9 @@ def run_simulations_amo(strategy_name): # pb = list(harness.pool_balances().values()) # stat["pre_pool_0"] = pb[0] # stat["pre_pool_1"] = pb[1] + # print(pb[0]/10**18,pb[1]/10**18) + # print("Withdraw Tilt") # harness.tilt_pool(initial_tilt) # stat["before_vault"] = harness.vault_core.totalValue() @@ -494,9 +592,16 @@ def run_simulations_amo(strategy_name): # pb = list(harness.pool_balances().values()) # stat["before_pool_0"] = pb[0] # stat["before_pool_1"] = pb[1] + # print(pb[0]/10**18,pb[1]/10**18) + + # harness.write_debug_data(initial_tilt) - # harness.vault_admin.withdrawAllFromStrategy( - # harness.strat, {"from": harness.STRATEGIST} + # print("Withdraw Withdraw") + # harness.vault_admin.withdrawFromStrategy( + # harness.strat, + # [harness.amo_base], + # [1e18], + # {"from": harness.STRATEGIST, "allow_revert": True}, # ) # stat["after_vault"] = harness.vault_core.totalValue() @@ -505,12 +610,65 @@ def run_simulations_amo(strategy_name): # stat["after_pool_0"] = pb[0] # stat["after_pool_1"] = pb[1] - # withdrawall_stats.append(stat) + # withdraw_stats.append(stat) + + # pd.DataFrame.from_records(withdraw_stats).to_csv(workspace + "withdraw_stats.csv") - # pd.DataFrame.from_records(withdrawall_stats).to_csv( - # workspace + "withdrawall_stats.csv" - # ) + # Test WithdrawAll + withdrawall_stats = [] + for initial_tilt in np.linspace(-1.5, 1.5, 41): + with TemporaryFork(): + stat = {} + + stat["action"] = "withdrawall" + stat["action_mix"] = initial_tilt + + if harness_rooster: + # manually correct the current pool price to put it into -1 tick + harness.tilt_pool(0.1) + + (amount_to_swap, swap_weth) = harness.estimate_swap_amount_to_reach_weth_ratio(0.2e18) # 20% + harness.swap_pool(amount_to_swap, swap_weth) + + harness.vault_admin.depositToStrategy( + harness.strat, + [harness.amo_base], + [harness.base_size * 1 * 10**18], + {"from": harness.STRATEGIST}, + ) + + stat["pre_vault"] = harness.vault_core.totalValue() + pb = list(harness.pool_balances().values()) + stat["pre_pool_0"] = pb[0] + stat["pre_pool_1"] = pb[1] + + harness.tilt_pool(initial_tilt) + + stat["before_vault"] = harness.vault_core.totalValue() + stat["before_otoken"] = harness.otoken.totalSupply() + pb = list(harness.pool_balances().values()) + stat["before_pool_0"] = pb[0] + stat["before_pool_1"] = pb[1] + + harness.write_debug_data(initial_tilt) + + harness.vault_admin.withdrawAllFromStrategy( + harness.strat, {"from": harness.STRATEGIST} + ) + + stat["after_vault"] = harness.vault_core.totalValue() + stat["after_otoken"] = harness.otoken.totalSupply() + pb = list(harness.pool_balances().values()) + stat["after_pool_0"] = pb[0] + stat["after_pool_1"] = pb[1] + + withdrawall_stats.append(stat) + + pd.DataFrame.from_records(withdrawall_stats).to_csv( + workspace + "withdrawall_stats.csv" + ) + harness.print_debug_data(); @@ -519,9 +677,9 @@ def run_report(strategy_name): workspace = _getWorkspace(strategy_name) harness = _getHarness(strategy_name) - # deposit_base = _load_data(workspace + "/deposit_stats.csv") - # withdraw_base = _load_data(workspace + "/withdraw_stats.csv") - # withdrawall_base = _load_data(workspace + "/withdrawall_stats.csv") + deposit_base = _load_data(workspace + "/deposit_stats.csv") + withdraw_base = _load_data(workspace + "/withdraw_stats.csv") + withdrawall_base = _load_data(workspace + "/withdrawall_stats.csv") checkbalance_base = _load_data(workspace + "/check_balance_stats.csv") sections = [] @@ -542,53 +700,54 @@ def run_report(strategy_name): plt.close() sections.append('

Check Balance

') - # # Deposit Section - # df = deposit_base.sort_values('action_mix') - # plt.title("Deposit profit") - # plt.axhline(0, c="black", linewidth=0.4) - # # for before_mix, rows in df.groupby(df["before_mix"]): - # plt.plot( - # df["action_mix"] * 100, - # df["after_profit"], - # ) - # # plt.ylim([-1e18, 1e18]) - # plt.xlabel("Pool before deposit") - # plt.ylabel("Deposit Profit") - # plt.legend() - # plt.savefig(workspace + "deposit.svg") - # plt.close() - # sections.append('

Deposit

') - - # # # Withdraw Section - # df = withdraw_base - # # df = df[df.before_mix != df.after_mix] - # plt.title("Withdraw profit") - # plt.axhline(0, c="black", linewidth=0.4) - # print(df['after_profit']) - # plt.plot( - # df["action_mix"] * 100, - # df["after_profit"], - # ) - # # plt.ylim([-1e18, 1e18]) - # plt.xlabel("Pool Mix") - # plt.ylabel("Withdraw Profit") - # plt.legend() - # plt.savefig(workspace + "withdraw.svg") - # plt.close() - # sections.append('

Withdraw

') - - # # # Withdraw All - # df = withdrawall_base - # plt.axhline(0, c="black", linewidth=0.4) - # plt.scatter(df["action_mix"]/1.5+0.5, df["after_profit"]) - # plt.scatter(df["before_mix"], df["after_profit"]) - # plt.plot(df["before_mix"], df["after_profit"]) - # # plt.ylim([-1e18,1e18]) - # plt.xlabel("Pool Mix") - # plt.ylabel("Withdraw All Profit") - # plt.savefig(workspace + "withdrawall.svg") - # plt.close() - # sections.append('

Withdraw All

') + # Deposit Section + df = deposit_base.sort_values('action_mix') + plt.title("Deposit profit") + plt.axhline(0, c="black", linewidth=0.4) + # for before_mix, rows in df.groupby(df["before_mix"]): + plt.plot( + df["action_mix"] * 100, + df["after_profit"], + ) + # plt.ylim([-1e18, 1e18]) + plt.xlabel("Pool before deposit") + plt.ylabel("Deposit Profit") + plt.legend() + plt.savefig(workspace + "deposit.svg") + plt.close() + sections.append('

Deposit

') + + # Withdraw Section + df = withdraw_base + # df = df[df.before_mix != df.after_mix] + plt.title("Withdraw profit") + plt.axhline(0, c="black", linewidth=0.4) + print(df['after_profit']) + plt.plot( + df["action_mix"] * 100, + df["after_profit"], + ) + # plt.ylim([-1e18, 1e18]) + plt.xlabel("Pool Mix") + plt.ylabel("Withdraw Profit") + plt.legend() + plt.savefig(workspace + "withdraw.svg") + plt.close() + sections.append('

Withdraw

') + + # # Withdraw All + df = withdrawall_base + plt.axhline(0, c="black", linewidth=0.4) + #plt.scatter(df["action_mix"]/1.5+0.5, df["after_profit"]) + plt.scatter(df["action_mix"] * 100, df["after_profit"]) + plt.scatter(df["before_mix"], df["after_profit"]) + plt.plot(df["before_mix"], df["after_profit"]) + # plt.ylim([-1e18,1e18]) + plt.xlabel("Pool Mix") + plt.ylabel("Withdraw All Profit") + plt.savefig(workspace + "withdrawall.svg") + plt.close() + sections.append('

Withdraw All

') template = """ @@ -627,5 +786,5 @@ def run_report(strategy_name): with open(filename, "w") as f: f.write(html) -#run_report("SwapxOsWS") -#run_complete("SwapxOsWS") \ No newline at end of file +#run_report("RoosterWETHOethp") +run_complete("RoosterWETHOethp") \ No newline at end of file diff --git a/brownie/world_plume.py b/brownie/world_plume.py index 5cd70d4f04..4aa72cf56c 100644 --- a/brownie/world_plume.py +++ b/brownie/world_plume.py @@ -14,6 +14,5 @@ woeth_strat = load_contract('woeth_strategy', OETHP_WOETH_STRATEGY) +oethpWETHpool = load_contract('maverick_v2_pool', "0x3F86B564A9B530207876d2752948268b9Bf04F71") plume_woeth_omnichain_adapter = load_contract('omnichain_l2_adapter', PLUME_WOETH_OMNICHAIN_ADAPTER) - -oethp = load_contract('ERC20', OETHP) diff --git a/contracts/contracts/interfaces/plume/IFeeRegistry.sol b/contracts/contracts/interfaces/plume/IFeeRegistry.sol new file mode 100644 index 0000000000..93dc2eb42e --- /dev/null +++ b/contracts/contracts/interfaces/plume/IFeeRegistry.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +interface IFeeRegistry { + function registerFee( + bool isTokenA, + uint32 binId, + uint256 binFeeInQuote + ) external; +} diff --git a/contracts/contracts/interfaces/plume/ILiquidityRegistry.sol b/contracts/contracts/interfaces/plume/ILiquidityRegistry.sol new file mode 100644 index 0000000000..65e9c9eeb3 --- /dev/null +++ b/contracts/contracts/interfaces/plume/ILiquidityRegistry.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +import { IMaverickV2Pool } from "./IMaverickV2Pool.sol"; + +interface ILiquidityRegistry { + function notifyBinLiquidity( + IMaverickV2Pool pool, + uint256 tokenId, + uint32 binId, + uint256 currentBinLpBalance + ) external; +} diff --git a/contracts/contracts/interfaces/plume/IMaverickV2Factory.sol b/contracts/contracts/interfaces/plume/IMaverickV2Factory.sol new file mode 100644 index 0000000000..9003c224e9 --- /dev/null +++ b/contracts/contracts/interfaces/plume/IMaverickV2Factory.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IFeeRegistry } from "./IFeeRegistry.sol"; +import { IMaverickV2Pool } from "./IMaverickV2Pool.sol"; + +interface IMaverickV2Factory { + error FactorAlreadyInitialized(); + error FactorNotInitialized(); + error FactoryInvalidTokenOrder(IERC20 _tokenA, IERC20 _tokenB); + error FactoryInvalidFee(); + error FactoryInvalidKinds(uint8 kinds); + error FactoryInvalidTickSpacing(uint256 tickSpacing); + error FactoryInvalidLookback(uint256 lookback); + error FactoryInvalidTokenDecimals(uint8 decimalsA, uint8 decimalsB); + error FactoryPoolAlreadyExists( + uint256 feeAIn, + uint256 feeBIn, + uint256 tickSpacing, + uint256 lookback, + IERC20 tokenA, + IERC20 tokenB, + uint8 kinds, + address accessor + ); + error FactoryAccessorMustBeNonZero(); + error NotImplemented(); + + event PoolCreated( + IMaverickV2Pool poolAddress, + uint8 protocolFeeRatio, + uint256 feeAIn, + uint256 feeBIn, + uint256 tickSpacing, + uint256 lookback, + int32 activeTick, + IERC20 tokenA, + IERC20 tokenB, + uint8 kinds, + address accessor + ); + event SetFactoryProtocolFeeReceiver(address receiver); + event SetFactoryProtocolFeeRegistry(IFeeRegistry registry); + + struct DeployParameters { + uint64 feeAIn; + uint64 feeBIn; + uint32 lookback; + int32 activeTick; + uint64 tokenAScale; + uint64 tokenBScale; + // slot + IERC20 tokenA; + // slot + IERC20 tokenB; + // slot + uint16 tickSpacing; + uint8 options; + address accessor; + } + + /** + * @notice Called by deployer library to initialize a pool. + */ + function deployParameters() + external + view + returns ( + uint64 feeAIn, + uint64 feeBIn, + uint32 lookback, + int32 activeTick, + uint64 tokenAScale, + uint64 tokenBScale, + // slot + IERC20 tokenA, + // slot + IERC20 tokenB, + // slot + uint16 tickSpacing, + uint8 options, + address accessor + ); + + /** + * @notice Create a new MaverickV2Pool with symmetric swap fees. + * @param fee Fraction of the pool swap amount that is retained as an LP in + * D18 scale. + * @param tickSpacing Tick spacing of pool where 1.0001^tickSpacing is the + * bin width. + * @param lookback Pool lookback in seconds. + * @param tokenA Address of tokenA. + * @param tokenB Address of tokenB. + * @param activeTick Tick position that contains the active bins. + * @param kinds 1-15 number to represent the active kinds + * 0b0001 = static; + * 0b0010 = right; + * 0b0100 = left; + * 0b1000 = both. + * E.g. a pool with all 4 modes will have kinds = b1111 = 15 + */ + function create( + uint64 fee, + uint16 tickSpacing, + uint32 lookback, + IERC20 tokenA, + IERC20 tokenB, + int32 activeTick, + uint8 kinds + ) external returns (IMaverickV2Pool); + + /** + * @notice Create a new MaverickV2Pool. + * @param feeAIn Fraction of the pool swap amount for tokenA-input swaps + * that is retained as an LP in D18 scale. + * @param feeBIn Fraction of the pool swap amount for tokenB-input swaps + * that is retained as an LP in D18 scale. + * @param tickSpacing Tick spacing of pool where 1.0001^tickSpacing is the + * bin width. + * @param lookback Pool lookback in seconds. + * @param tokenA Address of tokenA. + * @param tokenB Address of tokenB. + * @param activeTick Tick position that contains the active bins. + * @param kinds 1-15 number to represent the active kinds + * 0b0001 = static; + * 0b0010 = right; + * 0b0100 = left; + * 0b1000 = both. + * e.g. a pool with all 4 modes will have kinds = b1111 = 15 + */ + function create( + uint64 feeAIn, + uint64 feeBIn, + uint16 tickSpacing, + uint32 lookback, + IERC20 tokenA, + IERC20 tokenB, + int32 activeTick, + uint8 kinds + ) external returns (IMaverickV2Pool); + + /** + * @notice Bool indicating whether the pool was deployed from this factory. + */ + function isFactoryPool(IMaverickV2Pool pool) external view returns (bool); + + /** + * @notice Address that receives the protocol fee + */ + function protocolFeeReceiver() external view returns (address); + + /** + * @notice Address notified on swaps of the protocol fee + */ + function protocolFeeRegistry() external view returns (IFeeRegistry); + + /** + * @notice Lookup a pool for given parameters. + */ + function lookup( + uint256 feeAIn, + uint256 feeBIn, + uint256 tickSpacing, + uint256 lookback, + IERC20 tokenA, + IERC20 tokenB, + uint8 kinds + ) external view returns (IMaverickV2Pool); + + /** + * @notice Lookup a pool for given parameters. + */ + function lookup( + IERC20 _tokenA, + IERC20 _tokenB, + uint256 startIndex, + uint256 endIndex + ) external view returns (IMaverickV2Pool[] memory pools); + + /** + * @notice Lookup a pool for given parameters. + */ + function lookup(uint256 startIndex, uint256 endIndex) + external + view + returns (IMaverickV2Pool[] memory pools); + + /** + * @notice Count of permissionless pools. + */ + function poolCount() external view returns (uint256 _poolCount); + + /** + * @notice Count of pools for a given accessor and token pair. For + * permissionless pools, pass `accessor = address(0)`. + */ + function poolByTokenCount( + IERC20 _tokenA, + IERC20 _tokenB, + address accessor + ) external view returns (uint256 _poolCount); + + /** + * @notice Get the current factory owner. + */ + function owner() external view returns (address); +} diff --git a/contracts/contracts/interfaces/plume/IMaverickV2LiquidityManager.sol b/contracts/contracts/interfaces/plume/IMaverickV2LiquidityManager.sol new file mode 100644 index 0000000000..5f6b40854d --- /dev/null +++ b/contracts/contracts/interfaces/plume/IMaverickV2LiquidityManager.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.25; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IMaverickV2Pool } from "./IMaverickV2Pool.sol"; + +import { IMaverickV2Position } from "./IMaverickV2Position.sol"; +import { IMaverickV2PoolLens } from "./IMaverickV2PoolLens.sol"; + +interface IMaverickV2LiquidityManager { + error LiquidityManagerNotFactoryPool(); + error LiquidityManagerNotTokenIdOwner(); + + /** + * @notice Maverick V2 NFT position contract that tracks NFT-based + * liquditiy positions. + */ + function position() external view returns (IMaverickV2Position); + + /** + * @notice Create Maverick V2 pool. Function is a pass through to the pool + * factory and is provided here so that is can be assembled as part of a + * multicall transaction. + */ + function createPool( + uint64 fee, + uint16 tickSpacing, + uint32 lookback, + IERC20 tokenA, + IERC20 tokenB, + int32 activeTick, + uint8 kinds + ) external payable returns (IMaverickV2Pool pool); + + /** + * @notice Add Liquidity position NFT for msg.sender by specifying + * msg.sender's token index. + * @dev Token index is different from tokenId. + * On the Position NFT contract a user can own multiple NFT tokenIds and + * these are indexes by an enumeration index which is the `index` input + * here. + * + * See addLiquidity for a description of the add params. + */ + function addPositionLiquidityToSenderByTokenIndex( + IMaverickV2Pool pool, + uint256 index, + bytes memory packedSqrtPriceBreaks, + bytes[] memory packedArgs + ) + external + payable + returns ( + uint256 tokenAAmount, + uint256 tokenBAmount, + uint32[] memory binIds + ); + + /** + * @notice Mint new tokenId in the Position NFt contract to msg.sender. + * Both mints an NFT and adds liquidity to the pool that is held by the + * NFT. + */ + function mintPositionNftToSender( + IMaverickV2Pool pool, + bytes calldata packedSqrtPriceBreaks, + bytes[] calldata packedArgs + ) + external + payable + returns ( + uint256 tokenAAmount, + uint256 tokenBAmount, + uint32[] memory binIds, + uint256 tokenId + ); + + /** + * @notice Donates liqudity to a pool that is held by the position contract + * and will never be retrievable. Can be used to start a pool and ensure + * there will always be a base level of liquditiy in the pool. + */ + function donateLiquidity( + IMaverickV2Pool pool, + IMaverickV2Pool.AddLiquidityParams memory args + ) external payable; + + /** + * @notice Packs sqrtPrice breaks array with this format: [length, + * array[0], array[1],..., array[length-1]] where length is 1 byte. + */ + function packUint88Array(uint88[] memory fullArray) + external + pure + returns (bytes memory packedArray); + + /** + * @notice Packs addLiquidity paramters array element-wise. + */ + function packAddLiquidityArgsArray( + IMaverickV2Pool.AddLiquidityParams[] memory args + ) external pure returns (bytes[] memory argsPacked); +} diff --git a/contracts/contracts/interfaces/plume/IMaverickV2Pool.sol b/contracts/contracts/interfaces/plume/IMaverickV2Pool.sol new file mode 100644 index 0000000000..3f9d4eafbd --- /dev/null +++ b/contracts/contracts/interfaces/plume/IMaverickV2Pool.sol @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IMaverickV2Factory } from "./IMaverickV2Factory.sol"; + +interface IMaverickV2Pool { + error PoolZeroLiquidityAdded(); + error PoolMinimumLiquidityNotMet(); + error PoolLocked(); + error PoolInvalidFee(); + error PoolTicksNotSorted(uint256 index, int256 previousTick, int256 tick); + error PoolTicksAmountsLengthMismatch( + uint256 ticksLength, + uint256 amountsLength + ); + error PoolBinIdsAmountsLengthMismatch( + uint256 binIdsLength, + uint256 amountsLength + ); + error PoolKindNotSupported(uint256 kinds, uint256 kind); + error PoolInsufficientBalance( + uint256 deltaLpAmount, + uint256 accountBalance + ); + error PoolReservesExceedMaximum(uint256 amount); + error PoolValueExceedsBits(uint256 amount, uint256 bits); + error PoolTickMaxExceeded(uint256 tick); + error PoolMigrateBinFirst(); + error PoolCurrentTickBeyondSwapLimit(int32 startingTick); + error PoolSenderNotAccessor(address sender_, address accessor); + error PoolSenderNotFactory(address sender_, address accessor); + error PoolFunctionNotImplemented(); + error PoolTokenNotSolvent( + uint256 internalReserve, + uint256 tokenBalance, + IERC20 token + ); + error PoolNoProtocolFeeReceiverSet(); + + event PoolSwap( + address sender, + address recipient, + SwapParams params, + uint256 amountIn, + uint256 amountOut + ); + event PoolFlashLoan( + address sender, + address recipient, + uint256 amountA, + uint256 amountB + ); + event PoolProtocolFeeCollected(IERC20 token, uint256 protocolFee); + + event PoolAddLiquidity( + address sender, + address recipient, + uint256 subaccount, + AddLiquidityParams params, + uint256 tokenAAmount, + uint256 tokenBAmount, + uint32[] binIds + ); + + event PoolMigrateBinsUpStack( + address sender, + uint32 binId, + uint32 maxRecursion + ); + + event PoolRemoveLiquidity( + address sender, + address recipient, + uint256 subaccount, + RemoveLiquidityParams params, + uint256 tokenAOut, + uint256 tokenBOut + ); + + event PoolTickState(int32 tick, uint256 reserveA, uint256 reserveB); + event PoolTickBinUpdate(int32 tick, uint8 kind, uint32 binId); + event PoolSqrtPrice(uint256 sqrtPrice); + + /** + * @notice Tick state parameters. + */ + struct TickState { + uint128 reserveA; + uint128 reserveB; + uint128 totalSupply; + uint32[4] binIdsByTick; + } + + /** + * @notice Tick data parameters. + * @param currentReserveA Current reserve of token A. + * @param currentReserveB Current reserve of token B. + * @param currentLiquidity Current liquidity amount. + */ + struct TickData { + uint256 currentReserveA; + uint256 currentReserveB; + uint256 currentLiquidity; + } + + /** + * @notice Bin state parameters. + * @param mergeBinBalance LP token balance that this bin possesses of the merge bin. + * @param mergeId Bin ID of the bin that this bin has merged into. + * @param totalSupply Total amount of LP tokens in this bin. + * @param kind One of the 4 kinds (0=static, 1=right, 2=left, 3=both). + * @param tick The lower price tick of the bin in its current state. + * @param tickBalance Balance of the tick. + */ + struct BinState { + uint128 mergeBinBalance; + uint128 tickBalance; + uint128 totalSupply; + uint8 kind; + int32 tick; + uint32 mergeId; + } + + /** + * @notice Parameters for swap. + * @param amount Amount of the token that is either the input if exactOutput is false + * or the output if exactOutput is true. + * @param tokenAIn Boolean indicating whether tokenA is the input. + * @param exactOutput Boolean indicating whether the amount specified is + * the exact output amount (true). + * @param tickLimit The furthest tick a swap will execute in. If no limit + * is desired, value should be set to type(int32).max for a tokenAIn swap + * and type(int32).min for a swap where tokenB is the input. + */ + struct SwapParams { + uint256 amount; + bool tokenAIn; + bool exactOutput; + int32 tickLimit; + } + + /** + * @notice Parameters associated with adding liquidity. + * @param kind One of the 4 kinds (0=static, 1=right, 2=left, 3=both). + * @param ticks Array of ticks to add liquidity to. + * @param amounts Array of bin LP amounts to add. + */ + struct AddLiquidityParams { + uint8 kind; + int32[] ticks; + uint128[] amounts; + } + + /** + * @notice Parameters for each bin that will have liquidity removed. + * @param binIds Index array of the bins losing liquidity. + * @param amounts Array of bin LP amounts to remove. + */ + struct RemoveLiquidityParams { + uint32[] binIds; + uint128[] amounts; + } + + /** + * @notice State of the pool. + * @param reserveA Pool tokenA balanceOf at end of last operation + * @param reserveB Pool tokenB balanceOf at end of last operation + * @param lastTwaD8 Value of log time weighted average price at last block. + * Value is 8-decimal scale and is in the fractional tick domain. E.g. a + * value of 12.3e8 indicates the TWAP was 3/10ths of the way into the 12th + * tick. + * @param lastLogPriceD8 Value of log price at last block. Value is + * 8-decimal scale and is in the fractional tick domain. E.g. a value of + * 12.3e8 indicates the price was 3/10ths of the way into the 12th tick. + * @param lastTimestamp Last block.timestamp value in seconds for latest + * swap transaction. + * @param activeTick Current tick position that contains the active bins. + * @param isLocked Pool isLocked, E.g., locked or unlocked; isLocked values + * defined in Pool.sol. + * @param binCounter Index of the last bin created. + * @param protocolFeeRatioD3 Ratio of the swap fee that is kept for the + * protocol. + */ + struct State { + uint128 reserveA; + uint128 reserveB; + int64 lastTwaD8; + int64 lastLogPriceD8; + uint40 lastTimestamp; + int32 activeTick; + bool isLocked; + uint32 binCounter; + uint8 protocolFeeRatioD3; + } + + /** + * @notice Internal data used for data passing between Pool and Bin code. + */ + struct BinDelta { + uint128 deltaA; + uint128 deltaB; + } + + /** + * @notice 1-15 number to represent the active kinds. + * @notice 0b0001 = static; + * @notice 0b0010 = right; + * @notice 0b0100 = left; + * @notice 0b1000 = both; + * + * E.g. a pool with all 4 modes will have kinds = b1111 = 15 + */ + function kinds() external view returns (uint8 _kinds); + + /** + * @notice Pool swap fee for the given direction (A-in or B-in swap) in + * 18-decimal format. E.g. 0.01e18 is a 1% swap fee. + */ + function fee(bool tokenAIn) external view returns (uint256); + + /** + * @notice TickSpacing of pool where 1.0001^tickSpacing is the bin width. + */ + function tickSpacing() external view returns (uint256); + + /** + * @notice Lookback period of pool in seconds. + */ + function lookback() external view returns (uint256); + + /** + * @notice Address of Pool accessor. This is Zero address for + * permissionless pools. + */ + function accessor() external view returns (address); + + /** + * @notice Pool tokenA. Address of tokenA is such that tokenA < tokenB. + */ + function tokenA() external view returns (IERC20); + + /** + * @notice Pool tokenB. + */ + function tokenB() external view returns (IERC20); + + /** + * @notice Deploying factory of the pool and also contract that has ability + * to set and collect protocol fees for the pool. + */ + function factory() external view returns (IMaverickV2Factory); + + /** + * @notice Most significant bit of scale value is a flag to indicate whether + * tokenA has more or less than 18 decimals. Scale is used in conjuction + * with Math.toScale/Math.fromScale functions to convert from token amounts + * to D18 scale internal pool accounting. + */ + function tokenAScale() external view returns (uint256); + + /** + * @notice Most significant bit of scale value is a flag to indicate whether + * tokenA has more or less than 18 decimals. Scale is used in conjuction + * with Math.toScale/Math.fromScale functions to convert from token amounts + * to D18 scale internal pool accounting. + */ + function tokenBScale() external view returns (uint256); + + /** + * @notice ID of bin at input tick position and kind. + */ + function binIdByTickKind(int32 tick, uint256 kind) + external + view + returns (uint32); + + /** + * @notice Accumulated tokenA protocol fee. + */ + function protocolFeeA() external view returns (uint128); + + /** + * @notice Accumulated tokenB protocol fee. + */ + function protocolFeeB() external view returns (uint128); + + /** + * @notice Lending fee rate on flash loans. + */ + function lendingFeeRateD18() external view returns (uint256); + + /** + * @notice External function to get the current time-weighted average price. + */ + function getCurrentTwa() external view returns (int256); + + /** + * @notice External function to get the state of the pool. + */ + function getState() external view returns (State memory); + + /** + * @notice Return state of Bin at input binId. + */ + function getBin(uint32 binId) external view returns (BinState memory bin); + + /** + * @notice Return state of Tick at input tick position. + */ + function getTick(int32 tick) + external + view + returns (TickState memory tickState); + + /** + * @notice Retrieves the balance of a user within a bin. + * @param user The user's address. + * @param subaccount The subaccount for the user. + * @param binId The ID of the bin. + */ + function balanceOf( + address user, + uint256 subaccount, + uint32 binId + ) external view returns (uint128 lpToken); + + /** + * @notice Add liquidity to a pool. This function allows users to deposit + * tokens into a liquidity pool. + * @dev This function will call `maverickV2AddLiquidityCallback` on the + * calling contract to collect the tokenA/tokenB payment. + * @param recipient The account that will receive credit for the added liquidity. + * @param subaccount The account that will receive credit for the added liquidity. + * @param params Parameters containing the details for adding liquidity, + * such as token types and amounts. + * @param data Bytes information that gets passed to the callback. + * @return tokenAAmount The amount of token A added to the pool. + * @return tokenBAmount The amount of token B added to the pool. + * @return binIds An array of bin IDs where the liquidity is stored. + */ + function addLiquidity( + address recipient, + uint256 subaccount, + AddLiquidityParams calldata params, + bytes calldata data + ) + external + returns ( + uint256 tokenAAmount, + uint256 tokenBAmount, + uint32[] memory binIds + ); + + /** + * @notice Removes liquidity from the pool. + * @dev Liquidy can only be removed from a bin that is either unmerged or + * has a mergeId of an unmerged bin. If a bin is merged more than one + * level deep, it must be migrated up the merge stack to the root bin + * before liquidity removal. + * @param recipient The address to receive the tokens. + * @param subaccount The subaccount for the recipient. + * @param params The parameters for removing liquidity. + * @return tokenAOut The amount of token A received. + * @return tokenBOut The amount of token B received. + */ + function removeLiquidity( + address recipient, + uint256 subaccount, + RemoveLiquidityParams calldata params + ) external returns (uint256 tokenAOut, uint256 tokenBOut); + + /** + * @notice Migrate bins up the linked list of merged bins so that its + * mergeId is the currrent active bin. + * @dev Liquidy can only be removed from a bin that is either unmerged or + * has a mergeId of an unmerged bin. If a bin is merged more than one + * level deep, it must be migrated up the merge stack to the root bin + * before liquidity removal. + * @param binId The ID of the bin to migrate. + * @param maxRecursion The maximum recursion depth for the migration. + */ + function migrateBinUpStack(uint32 binId, uint32 maxRecursion) external; + + /** + * @notice Swap tokenA/tokenB assets in the pool. The swap user has two + * options for funding their swap. + * - The user can push the input token amount to the pool before calling + * the swap function. In order to avoid having the pool call the callback, + * the user should pass a zero-length `data` bytes object with the swap + * call. + * - The user can send the input token amount to the pool when the pool + * calls the `maverickV2SwapCallback` function on the calling contract. + * That callback has input parameters that specify the token address of the + * input token, the input and output amounts, and the bytes data sent to + * the swap function. + * @dev If the users elects to do a callback-based swap, the output + * assets will be sent before the callback is called, allowing the user to + * execute flash swaps. However, the pool does have reentrancy protection, + * so a swapper will not be able to interact with the same pool again + * while they are in the callback function. + * @param recipient The address to receive the output tokens. + * @param params Parameters containing the details of the swap + * @param data Bytes information that gets passed to the callback. + */ + function swap( + address recipient, + SwapParams memory params, + bytes calldata data + ) external returns (uint256 amountIn, uint256 amountOut); + + /** + * @notice Loan tokenA/tokenB assets from the pool to recipient. The fee + * rate of a loan is determined by `lendingFeeRateD18`, which is set at the + * protocol level by the factory. This function calls + * `maverickV2FlashLoanCallback` on the calling contract. At the end of + * the callback, the caller must pay back the loan with fee (if there is a + * fee). + * @param recipient The address to receive the loaned tokens. + * @param amountB Loan amount of tokenA sent to recipient. + * @param amountB Loan amount of tokenB sent to recipient. + * @param data Bytes information that gets passed to the callback. + */ + function flashLoan( + address recipient, + uint256 amountA, + uint256 amountB, + bytes calldata data + ) external; + + /** + * @notice Distributes accumulated protocol fee to factory protocolFeeReceiver + */ + function distributeFees(bool isTokenA) + external + returns (uint256 protocolFee, IERC20 token); +} diff --git a/contracts/contracts/interfaces/plume/IMaverickV2PoolLens.sol b/contracts/contracts/interfaces/plume/IMaverickV2PoolLens.sol new file mode 100644 index 0000000000..2cca0c7d78 --- /dev/null +++ b/contracts/contracts/interfaces/plume/IMaverickV2PoolLens.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.25; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IMaverickV2Factory } from "./IMaverickV2Factory.sol"; +import { IMaverickV2Pool } from "./IMaverickV2Pool.sol"; + +interface IMaverickV2PoolLens { + error LensTargetPriceOutOfBounds( + uint256 targetSqrtPrice, + uint256 sqrtLowerTickPrice, + uint256 sqrtUpperTickPrice + ); + error LensTooLittleLiquidity( + uint256 relativeLiquidityAmount, + uint256 deltaA, + uint256 deltaB + ); + error LensTargetingTokenWithNoDelta( + bool targetIsA, + uint256 deltaA, + uint256 deltaB + ); + + /** + * @notice Add liquidity slippage parameters for a distribution of liquidity. + * @param pool Pool where liquidity is being added. + * @param kind Bin kind; all bins must have the same kind in a given call + * to addLiquidity. + * @param ticks Array of tick values to add liquidity to. + * @param relativeLiquidityAmounts Relative liquidity amounts for the + * specified ticks. Liquidity in this case is not bin LP balance, it is + * the bin liquidity as defined by liquidity = deltaA / (sqrt(upper) - + * sqrt(lower)) or deltaB = liquidity / sqrt(lower) - liquidity / + * sqrt(upper). + * @param addSpec Slippage specification. + */ + struct AddParamsViewInputs { + IMaverickV2Pool pool; + uint8 kind; + int32[] ticks; + uint128[] relativeLiquidityAmounts; + AddParamsSpecification addSpec; + } + + /** + * @notice Multi-price add param specification. + * @param slippageFactorD18 Max slippage allowed as a percent in D18 scale. e.g. 1% slippage is 0.01e18 + * @param numberOfPriceBreaksPerSide Number of price break values on either + * side of current price. + * @param targetAmount Target token contribution amount in tokenA if + * targetIsA is true, otherwise this is the target amount for tokenB. + * @param targetIsA Indicates if the target amount is for tokenA or tokenB + */ + struct AddParamsSpecification { + uint256 slippageFactorD18; + uint256 numberOfPriceBreaksPerSide; + uint256 targetAmount; + bool targetIsA; + } + + /** + * @notice Specification for deriving create pool parameters. Creating a + * pool in the liquidity manager has several steps: + * + * - Deploy pool + * - Donate a small amount of initial liquidity in the activeTick + * - Execute a small swap to set the pool price to the desired value + * - Add liquidity + * + * In order to execute these steps, the caller must specify the parameters + * of each step. The PoolLens has helper function to derive the values + * used by the LiquidityManager, but this struct is the input to that + * helper function and represents the core intent of the pool creator. + * + * @param fee Fraction of the pool swap amount that is retained as an LP in + * D18 scale. + * @param tickSpacing Tick spacing of pool where 1.0001^tickSpacing is the + * bin width. + * @param lookback Pool lookback in seconds. + * @param tokenA Address of tokenA. + * @param tokenB Address of tokenB. + * @param activeTick Tick position that contains the active bins. + * @param kinds 1-15 number to represent the active kinds + * 0b0001 = static; + * 0b0010 = right; + * 0b0100 = left; + * 0b1000 = both. + * e.g. a pool with all 4 modes will have kinds = b1111 = 15 + * @param initialTargetB Amount of B to be donated to the pool after pool + * create. This amount needs to be big enough to meet the minimum bin + * liquidity. + * @param sqrtPrice Target sqrt price of the pool. + * @param kind Bin kind; all bins must have the same kind in a given call + * to addLiquidity. + * @param ticks Array of tick values to add liquidity to. + * @param relativeLiquidityAmounts Relative liquidity amounts for the + * specified ticks. Liquidity in this case is not bin LP balance, it is + * the bin liquidity as defined by liquidity = deltaA / (sqrt(upper) - + * sqrt(lower)) or deltaB = liquidity / sqrt(lower) - liquidity / + * sqrt(upper). + * @param targetAmount Target token contribution amount in tokenA if + * targetIsA is true, otherwise this is the target amount for tokenB. + * @param targetIsA Indicates if the target amount is for tokenA or tokenB + */ + struct CreateAndAddParamsViewInputs { + uint64 feeAIn; + uint64 feeBIn; + uint16 tickSpacing; + uint32 lookback; + IERC20 tokenA; + IERC20 tokenB; + int32 activeTick; + uint8 kinds; + // donate params + uint256 initialTargetB; + uint256 sqrtPrice; + // add target + uint8 kind; + int32[] ticks; + uint128[] relativeLiquidityAmounts; + uint256 targetAmount; + bool targetIsA; + } + + struct Output { + uint256 deltaAOut; + uint256 deltaBOut; + uint256[] deltaAs; + uint256[] deltaBs; + uint128[] deltaLpBalances; + } + + struct Reserves { + uint256 amountA; + uint256 amountB; + } + + struct BinPositionKinds { + uint128[4] values; + } + + struct PoolState { + IMaverickV2Pool.TickState[] tickStateMapping; + IMaverickV2Pool.BinState[] binStateMapping; + BinPositionKinds[] binIdByTickKindMapping; + IMaverickV2Pool.State state; + Reserves protocolFees; + } + + struct BoostedPositionSpecification { + IMaverickV2Pool pool; + uint32[] binIds; + uint128[] ratios; + uint8 kind; + } + + struct CreateAndAddParamsInputs { + uint64 feeAIn; + uint64 feeBIn; + uint16 tickSpacing; + uint32 lookback; + IERC20 tokenA; + IERC20 tokenB; + int32 activeTick; + uint8 kinds; + // donate params + IMaverickV2Pool.AddLiquidityParams donateParams; + // swap params + uint256 swapAmount; + // add params + IMaverickV2Pool.AddLiquidityParams addParams; + bytes[] packedAddParams; + uint256 deltaAOut; + uint256 deltaBOut; + uint256 preAddReserveA; + uint256 preAddReserveB; + } + + struct TickDeltas { + uint256 deltaAOut; + uint256 deltaBOut; + uint256[] deltaAs; + uint256[] deltaBs; + } + + /** + * @notice Converts add parameter slippage specification into add + * parameters. The return values are given in both raw format and as packed + * values that can be used in the LiquidityManager contract. + */ + function getAddLiquidityParams(AddParamsViewInputs memory params) + external + view + returns ( + bytes memory packedSqrtPriceBreaks, + bytes[] memory packedArgs, + uint88[] memory sqrtPriceBreaks, + IMaverickV2Pool.AddLiquidityParams[] memory addParams, + IMaverickV2PoolLens.TickDeltas[] memory tickDeltas + ); + + /** + * @notice Converts add parameter slippage specification and new pool + * specification into CreateAndAddParamsInputs parameters that can be used in the + * LiquidityManager contract. + */ + function getCreatePoolAtPriceAndAddLiquidityParams( + CreateAndAddParamsViewInputs memory params + ) external view returns (CreateAndAddParamsInputs memory output); + + /** + * @notice View function that provides information about pool ticks within + * a tick radius from the activeTick. Ticks with no reserves are not + * included in part o f the return array. + */ + function getTicksAroundActive(IMaverickV2Pool pool, int32 tickRadius) + external + view + returns ( + int32[] memory ticks, + IMaverickV2Pool.TickState[] memory tickStates + ); + + /** + * @notice View function that provides information about pool ticks within + * a range. Ticks with no reserves are not included in part o f the return + * array. + */ + function getTicks( + IMaverickV2Pool pool, + int32 tickStart, + int32 tickEnd + ) + external + view + returns ( + int32[] memory ticks, + IMaverickV2Pool.TickState[] memory tickStates + ); + + /** + * @notice View function that provides information about pool ticks within + * a range. Information returned includes all pool state needed to emulate + * a swap off chain. Ticks with no reserves are not included in part o f + * the return array. + */ + function getTicksAroundActiveWLiquidity( + IMaverickV2Pool pool, + int32 tickRadius + ) + external + view + returns ( + int32[] memory ticks, + IMaverickV2Pool.TickState[] memory tickStates, + uint256[] memory liquidities, + uint256[] memory sqrtLowerTickPrices, + uint256[] memory sqrtUpperTickPrices, + IMaverickV2Pool.State memory poolState, + uint256 sqrtPrice, + uint256 feeAIn, + uint256 feeBIn + ); + + /** + * @notice View function that provides pool state information. + */ + function getFullPoolState( + IMaverickV2Pool pool, + uint32 binStart, + uint32 binEnd + ) external view returns (PoolState memory poolState); + + /** + * @notice View function that provides price and liquidity of a given tick. + */ + function getTickSqrtPriceAndL(IMaverickV2Pool pool, int32 tick) + external + view + returns (uint256 sqrtPrice, uint256 liquidity); + + /** + * @notice Pool sqrt price. + */ + function getPoolSqrtPrice(IMaverickV2Pool pool) + external + view + returns (uint256 sqrtPrice); + + /** + * @notice Pool price. + */ + function getPoolPrice(IMaverickV2Pool pool) + external + view + returns (uint256 price); + + /** + * @notice Token scale of two tokens in a pool. + */ + function tokenScales(IMaverickV2Pool pool) + external + view + returns (uint256 tokenAScale, uint256 tokenBScale); +} diff --git a/contracts/contracts/interfaces/plume/IMaverickV2Position.sol b/contracts/contracts/interfaces/plume/IMaverickV2Position.sol new file mode 100644 index 0000000000..7e751b438b --- /dev/null +++ b/contracts/contracts/interfaces/plume/IMaverickV2Position.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.25; + +import { IMaverickV2Factory } from "./IMaverickV2Factory.sol"; +import { IMaverickV2Pool } from "./IMaverickV2Pool.sol"; +import { ILiquidityRegistry } from "./ILiquidityRegistry.sol"; + +interface IMaverickV2Position { + event PositionClearData(uint256 indexed tokenId); + event PositionSetData( + uint256 indexed tokenId, + uint256 index, + PositionPoolBinIds newData + ); + event SetLpReward(ILiquidityRegistry lpReward); + + error PositionDuplicatePool(uint256 index, IMaverickV2Pool pool); + error PositionNotFactoryPool(); + error PositionPermissionedLiquidityPool(); + + struct PositionPoolBinIds { + IMaverickV2Pool pool; + uint32[] binIds; + } + + struct PositionFullInformation { + PositionPoolBinIds poolBinIds; + uint256 amountA; + uint256 amountB; + uint256[] binAAmounts; + uint256[] binBAmounts; + int32[] ticks; + uint256[] liquidities; + } + + /** + * @notice Factory that tracks lp rewards. + */ + function lpReward() external view returns (ILiquidityRegistry); + + /** + * @notice Pool factory. + */ + function factory() external view returns (IMaverickV2Factory); + + /** + * @notice Mint NFT that holds liquidity in a Maverick V2 Pool. To mint + * liquidity to an NFT, add liquidity to bins in a pool where the + * add liquidity recipient is this contract and the subaccount is the + * tokenId. LiquidityManager can be used to simplify minting Position NFTs. + */ + function mint( + address recipient, + IMaverickV2Pool pool, + uint32[] memory binIds + ) external returns (uint256 tokenId); + + /** + * @notice Overwrites tokenId pool/binId information for a given data index. + */ + function setTokenIdData( + uint256 tokenId, + uint256 index, + IMaverickV2Pool pool, + uint32[] memory binIds + ) external; + + /** + * @notice Overwrites entire pool/binId data set for a given tokenId. + */ + function setTokenIdData(uint256 tokenId, PositionPoolBinIds[] memory data) + external; + + /** + * @notice Append new pool/binIds data array to tokenId. + */ + function appendTokenIdData( + uint256 tokenId, + IMaverickV2Pool pool, + uint32[] memory binIds + ) external; + + /** + * @notice Get array pool/binIds data for a given tokenId. + */ + function getTokenIdData(uint256 tokenId) + external + view + returns (PositionPoolBinIds[] memory); + + /** + * @notice Get value from array of pool/binIds data for a given tokenId. + */ + function getTokenIdData(uint256 tokenId, uint256 index) + external + view + returns (PositionPoolBinIds memory); + + /** + * @notice Length of array of pool/binIds data for a given tokenId. + */ + function tokenIdDataLength(uint256 tokenId) + external + view + returns (uint256 length); + + /** + * @notice Remove liquidity from tokenId for a given pool. User can + * specify arbitrary bins to remove from for their subaccount in the pool + * even if those bins are not in the tokenIdData set. + */ + function removeLiquidity( + uint256 tokenId, + address recipient, + IMaverickV2Pool pool, + IMaverickV2Pool.RemoveLiquidityParams memory params + ) external returns (uint256 tokenAAmount, uint256 tokenBAmount); + + /** + * @notice Remove liquidity from tokenId for a given pool to sender. User + * can specify arbitrary bins to remove from for their subaccount in the + * pool even if those bins are not in the tokenIdData set. + */ + function removeLiquidityToSender( + uint256 tokenId, + IMaverickV2Pool pool, + IMaverickV2Pool.RemoveLiquidityParams memory params + ) external returns (uint256 tokenAAmount, uint256 tokenBAmount); + + /** + * @notice NFT asset information for a given range of pool/binIds indexes. + * This function only returns the liquidity in the pools/binIds stored as + * part of the tokenIdData, but it is possible that the NFT has additional + * liquidity in pools/binIds that have not been recorded. + */ + function tokenIdPositionInformation( + uint256 tokenId, + uint256 startIndex, + uint256 stopIndex + ) external view returns (PositionFullInformation[] memory output); + + /** + * @notice NFT asset information for a given pool/binIds index. This + * function only returns the liquidity in the pools/binIds stored as part + * of the tokenIdData, but it is possible that the NFT has additional + * liquidity in pools/binIds that have not been recorded. + */ + function tokenIdPositionInformation(uint256 tokenId, uint256 index) + external + view + returns (PositionFullInformation memory output); + + /** + * @notice Get remove parameters for removing a fractional part of the + * liquidity owned by a given tokenId. The fractional factor to remove is + * given by proporationD18 in 18-decimal scale. + */ + function getRemoveParams( + uint256 tokenId, + uint256 index, + uint256 proportionD18 + ) + external + view + returns (IMaverickV2Pool.RemoveLiquidityParams memory params); + + /** + * @notice Register the bin balances in the nft with the LpReward contract. + */ + function checkpointBinLpBalance( + uint256 tokenId, + IMaverickV2Pool pool, + uint32[] memory binIds + ) external; +} diff --git a/contracts/contracts/interfaces/plume/IMaverickV2Quoter.sol b/contracts/contracts/interfaces/plume/IMaverickV2Quoter.sol new file mode 100644 index 0000000000..8b18506e01 --- /dev/null +++ b/contracts/contracts/interfaces/plume/IMaverickV2Quoter.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.25; + +import { IMaverickV2Pool } from "./IMaverickV2Pool.sol"; + +interface IMaverickV2Quoter { + error QuoterInvalidSwap(); + error QuoterInvalidAddLiquidity(); + + /** + * @notice Calculates a swap on a MaverickV2Pool and returns the resulting + * amount and estimated gas. The gas estimate is only a rough estimate and + * may not match a swap's gas. + * @param pool The MaverickV2Pool to swap on. + * @param amount The input amount. + * @param tokenAIn Indicates if token A is the input token. + * @param exactOutput Indicates if the amount is the output amount (true) + * or input amount (false). If the tickLimit is reached, the full value of + * the exactOutput may not be returned because the pool will stop swapping + * before the whole order is filled. + * @param tickLimit The tick limit for the swap. Once the swap lands in + * this tick, it will stop and return the output amount swapped up to that + * tick. + */ + function calculateSwap( + IMaverickV2Pool pool, + uint128 amount, + bool tokenAIn, + bool exactOutput, + int32 tickLimit + ) + external + returns ( + uint256 amountIn, + uint256 amountOut, + uint256 gasEstimate + ); + + /** + * @notice Calculates a multihop swap and returns the resulting amount and + * estimated gas. The gas estimate is only a rough estimate and + * may not match a swap's gas. + * @param path The path of pools to swap through. Path is given by an + * packed array of (pool, tokenAIn) tuples. So each step in the path is 160 + * + 8 = 168 bits of data. e.g. path = abi.encodePacked(pool1, true, pool2, false); + * @param amount The input amount. + * @param exactOutput A boolean indicating if exact output is required. + */ + function calculateMultiHopSwap( + bytes memory path, + uint256 amount, + bool exactOutput + ) external returns (uint256 returnAmount, uint256 gasEstimate); + + /** + * @notice Computes the token amounts required for a given set of + * addLiquidity parameters. The gas estimate is only a rough estimate and + * may not match a add's gas. + */ + function calculateAddLiquidity( + IMaverickV2Pool pool, + IMaverickV2Pool.AddLiquidityParams calldata params + ) + external + returns ( + uint256 amountA, + uint256 amountB, + uint256 gasEstimate + ); + + /** + * @notice Pool's sqrt price. + */ + function poolSqrtPrice(IMaverickV2Pool pool) + external + view + returns (uint256 sqrtPrice); +} diff --git a/contracts/contracts/interfaces/plume/IPoolDistributor.sol b/contracts/contracts/interfaces/plume/IPoolDistributor.sol new file mode 100644 index 0000000000..432816a5bc --- /dev/null +++ b/contracts/contracts/interfaces/plume/IPoolDistributor.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import { IMaverickV2Pool } from "./IMaverickV2Pool.sol"; + +interface IPoolDistributor { + function rewardToken() external view returns (address); + + function claimLp( + address recipient, + uint256 tokenId, + IMaverickV2Pool pool, + uint32[] memory binIds, + uint256 epoch + ) external returns (uint256 amount); +} diff --git a/contracts/contracts/interfaces/plume/IVotingDistributor.sol b/contracts/contracts/interfaces/plume/IVotingDistributor.sol new file mode 100644 index 0000000000..e3060b9f0c --- /dev/null +++ b/contracts/contracts/interfaces/plume/IVotingDistributor.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IVotingDistributor { + function lastEpoch() external view returns (uint256); +} diff --git a/contracts/contracts/mocks/MockMaverickDistributor.sol b/contracts/contracts/mocks/MockMaverickDistributor.sol new file mode 100644 index 0000000000..e2d5f2df37 --- /dev/null +++ b/contracts/contracts/mocks/MockMaverickDistributor.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IMaverickV2Pool } from "../interfaces/plume/IMaverickV2Pool.sol"; + +contract MockMaverickDistributor { + IERC20 public immutable rewardToken; + + uint256 public lastEpoch; + uint256 public rewardAmount; + + constructor(address _rewardToken) { + rewardToken = IERC20(_rewardToken); + } + + function setLastEpoch(uint256 _epoch) external { + lastEpoch = _epoch; + } + + function setRewardTokenAmount(uint256 _amount) external { + rewardAmount = _amount; + } + + function claimLp( + address recipient, + uint256, + IMaverickV2Pool, + uint32[] memory, + uint256 + ) external returns (uint256 amount) { + rewardToken.transfer(recipient, rewardAmount); + return rewardAmount; + } +} diff --git a/contracts/contracts/mocks/MockRoosterAMOStrategy.sol b/contracts/contracts/mocks/MockRoosterAMOStrategy.sol new file mode 100644 index 0000000000..46627a7342 --- /dev/null +++ b/contracts/contracts/mocks/MockRoosterAMOStrategy.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Rooster AMO strategy exposing extra functionality + * @author Origin Protocol Inc + */ + +import { RoosterAMOStrategy } from "../strategies/plume/RoosterAMOStrategy.sol"; +import { IMaverickV2Pool } from "../interfaces/plume/IMaverickV2Pool.sol"; + +contract MockRoosterAMOStrategy is RoosterAMOStrategy { + constructor( + BaseStrategyConfig memory _stratConfig, + address _wethAddress, + address _oethpAddress, + address _liquidityManager, + address _poolLens, + address _maverickPosition, + address _maverickQuoter, + address _mPool, + bool _upperTickAtParity, + address _votingDistributor, + address _poolDistributor + ) + RoosterAMOStrategy( + _stratConfig, + _wethAddress, + _oethpAddress, + _liquidityManager, + _poolLens, + _maverickPosition, + _maverickQuoter, + _mPool, + _upperTickAtParity, + _votingDistributor, + _poolDistributor + ) + {} + + function getCurrentWethShare() external view returns (uint256) { + uint256 _currentPrice = getPoolSqrtPrice(); + + return _getWethShare(_currentPrice); + } +} diff --git a/contracts/contracts/proxies/PlumeProxies.sol b/contracts/contracts/proxies/PlumeProxies.sol index f8a77b0dcc..49ec1f599e 100644 --- a/contracts/contracts/proxies/PlumeProxies.sol +++ b/contracts/contracts/proxies/PlumeProxies.sol @@ -23,3 +23,10 @@ contract OETHPlumeProxy is InitializeGovernedUpgradeabilityProxy { contract WOETHPlumeProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice RoosterAMOStrategyProxy delegates calls to a RoosterAMOStrategy implementation + */ +contract RoosterAMOStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/contracts/strategies/BridgedWOETHStrategy.sol b/contracts/contracts/strategies/BridgedWOETHStrategy.sol index f17c0c3d97..1f602318d0 100644 --- a/contracts/contracts/strategies/BridgedWOETHStrategy.sol +++ b/contracts/contracts/strategies/BridgedWOETHStrategy.sol @@ -27,17 +27,6 @@ contract BridgedWOETHStrategy is InitializableAbstractStrategy { uint128 public lastOraclePrice; uint128 public maxPriceDiffBps; - /** - * @dev Verifies that the caller is the Governor or Strategist. - */ - modifier onlyGovernorOrStrategist() { - require( - isGovernor() || msg.sender == IVault(vaultAddress).strategistAddr(), - "Caller is not the Strategist or Governor" - ); - _; - } - constructor( BaseStrategyConfig memory _stratConfig, address _weth, diff --git a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol index 48d1a69b08..15472dc243 100644 --- a/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol +++ b/contracts/contracts/strategies/aerodrome/AerodromeAMOStrategy.sol @@ -137,18 +137,6 @@ contract AerodromeAMOStrategy is InitializableAbstractStrategy { event UnderlyingAssetsUpdated(uint256 underlyingAssets); - /** - * @dev Verifies that the caller is the Governor, or Strategist. - */ - modifier onlyGovernorOrStrategist() { - require( - msg.sender == IVault(vaultAddress).strategistAddr() || - msg.sender == governor(), - "Not the Governor or Strategist" - ); - _; - } - /** * @dev Un-stakes the token from the gauge for the execution duration of * the function and after that re-stakes it back in. diff --git a/contracts/contracts/strategies/plume/RoosterAMOStrategy.sol b/contracts/contracts/strategies/plume/RoosterAMOStrategy.sol new file mode 100644 index 0000000000..2f4190b96e --- /dev/null +++ b/contracts/contracts/strategies/plume/RoosterAMOStrategy.sol @@ -0,0 +1,1204 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Rooster AMO strategy + * @author Origin Protocol Inc + * @custom:security-contact security@originprotocol.com + */ +import { Math as MathRooster } from "../../../lib/rooster/v2-common/libraries/Math.sol"; +import { Math as Math_v5 } from "../../../lib/rooster/openzeppelin-custom/contracts/utils/math/Math.sol"; +import { StableMath } from "../../utils/StableMath.sol"; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { IVault } from "../../interfaces/IVault.sol"; +import { IMaverickV2Pool } from "../../interfaces/plume/IMaverickV2Pool.sol"; +import { IMaverickV2Quoter } from "../../interfaces/plume/IMaverickV2Quoter.sol"; +import { IMaverickV2LiquidityManager } from "../../interfaces/plume/IMaverickV2LiquidityManager.sol"; +import { IMaverickV2PoolLens } from "../../interfaces/plume/IMaverickV2PoolLens.sol"; +import { IMaverickV2Position } from "../../interfaces/plume/IMaverickV2Position.sol"; +import { IVotingDistributor } from "../../interfaces/plume/IVotingDistributor.sol"; +import { IPoolDistributor } from "../../interfaces/plume/IPoolDistributor.sol"; +// importing custom version of rooster TickMath because of dependency collision. Maverick uses +// a newer OpenZepplin Math library with functionality that is not present in 4.4.2 (the one we use) +import { TickMath } from "../../../lib/rooster/v2-common/libraries/TickMath.sol"; + +contract RoosterAMOStrategy is InitializableAbstractStrategy { + using StableMath for uint256; + using SafeERC20 for IERC20; + using SafeCast for uint256; + + /*************************************** + Storage slot members + ****************************************/ + + /// @notice NFT tokenId of the liquidity position + /// + /// @dev starts with value of 1 and can not be 0 + // solhint-disable-next-line max-line-length + /// https://github.com/rooster-protocol/rooster-contracts/blob/fbfecbc519e4495b12598024a42630b4a8ea4489/v2-common/contracts/base/Nft.sol#L14 + uint256 public tokenId; + /// @dev Minimum amount of tokens the strategy would be able to withdraw from the pool. + /// minimum amount of tokens are withdrawn at a 1:1 price + /// Important: Underlying assets contains only assets that are deposited in the underlying Rooster pool. + /// WETH or OETH held by this contract is not accounted for in underlying assets + uint256 public underlyingAssets; + /// @notice Marks the start of the interval that defines the allowed range of WETH share in + /// the pre-configured pool's liquidity ticker + uint256 public allowedWethShareStart; + /// @notice Marks the end of the interval that defines the allowed range of WETH share in + /// the pre-configured pool's liquidity ticker + uint256 public allowedWethShareEnd; + /// @dev reserved for inheritance + int256[46] private __reserved; + + /*************************************** + Constants, structs and events + ****************************************/ + + /// @notice The address of the Wrapped ETH (WETH) token contract + address public immutable WETH; + /// @notice The address of the OETH token contract + address public immutable OETH; + /// @notice the underlying AMO Maverick (Rooster) pool + IMaverickV2Pool public immutable mPool; + /// @notice the Liquidity manager used to add liquidity to the pool + IMaverickV2LiquidityManager public immutable liquidityManager; + /// @notice the Maverick V2 poolLens + /// + /// @dev only used to provide the pool's current sqrtPrice + IMaverickV2PoolLens public immutable poolLens; + /// @notice the Maverick V2 position + /// + /// @dev provides details of the NFT LP position and offers functions to + /// remove the liquidity. + IMaverickV2Position public immutable maverickPosition; + /// @notice the Maverick Quoter + IMaverickV2Quoter public immutable quoter; + /// @notice the Maverick Voting Distributor + IVotingDistributor public immutable votingDistributor; + /// @notice the Maverick Pool Distributor + IPoolDistributor public immutable poolDistributor; + + /// @notice sqrtPriceTickLower + /// @dev tick lower represents the lower price of OETH priced in WETH. Meaning the pool + /// offers more than 1 OETH for 1 WETH. In other terms to get 1 OETH the swap needs to offer 0.9999 WETH + /// this is where purchasing OETH with WETH within the liquidity position is the cheapest. + /// + /// _____________________ + /// | | | + /// | WETH | OETH | + /// | | | + /// | | | + /// --------- * ---- * ---------- * --------- + /// currentPrice + /// sqrtPriceHigher-(1:1 parity) + /// sqrtPriceLower + /// + /// + /// Price is defined as price of token1 in terms of token0. (token1 / token0) + /// @notice sqrtPriceTickLower - OETH is priced 0.9999 WETH + uint256 public immutable sqrtPriceTickLower; + /// @notice sqrtPriceTickHigher + /// @dev tick higher represents 1:1 price parity of WETH to OETH + uint256 public immutable sqrtPriceTickHigher; + /// @dev price at parity (in OETH this is equal to sqrtPriceTickHigher) + uint256 public immutable sqrtPriceAtParity; + /// @notice The tick where the strategy deploys the liquidity to + int32 public constant TICK_NUMBER = -1; + /// @notice Minimum liquidity that must be exceeded to continue with the action + /// e.g. deposit, add liquidity + uint256 public constant ACTION_THRESHOLD = 1e12; + /// @notice Maverick pool static liquidity bin type + uint8 public constant MAV_STATIC_BIN_KIND = 0; + /// @dev a threshold under which the contract no longer allows for the protocol to rebalance. Guarding + /// against a strategist / guardian being taken over and with multiple transactions draining the + /// protocol funds. + uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; + /// @notice Emitted when the allowed interval within which the strategy contract is allowed to deposit + /// liquidity to the underlying pool is updated. + /// @param allowedWethShareStart The start of the interval + /// @param allowedWethShareEnd The end of the interval + event PoolWethShareIntervalUpdated( + uint256 allowedWethShareStart, + uint256 allowedWethShareEnd + ); + /// @notice Emitted when liquidity is removed from the underlying pool + /// @param withdrawLiquidityShare Share of strategy's liquidity that has been removed + /// @param removedWETHAmount The amount of WETH removed + /// @param removedOETHAmount The amount of OETH removed + /// @param underlyingAssets Updated amount of strategy's underlying assets + event LiquidityRemoved( + uint256 withdrawLiquidityShare, + uint256 removedWETHAmount, + uint256 removedOETHAmount, + uint256 underlyingAssets + ); + + /// @notice Emitted when the underlying pool is rebalanced + /// @param currentPoolWethShare The resulting share of strategy's liquidity + /// in the TICK_NUMBER + event PoolRebalanced(uint256 currentPoolWethShare); + + /// @notice Emitted when the amount of underlying assets the strategy hold as + /// liquidity in the pool is updated. + /// @param underlyingAssets Updated amount of strategy's underlying assets + event UnderlyingAssetsUpdated(uint256 underlyingAssets); + + /// @notice Emitted when liquidity is added to the underlying pool + /// @param wethAmountDesired Amount of WETH desired to be deposited + /// @param oethAmountDesired Amount of OETH desired to be deposited + /// @param wethAmountSupplied Amount of WETH deposited + /// @param oethAmountSupplied Amount of OETH deposited + /// @param tokenId NFT liquidity token id + /// @param underlyingAssets Updated amount of underlying assets + event LiquidityAdded( + uint256 wethAmountDesired, + uint256 oethAmountDesired, + uint256 wethAmountSupplied, + uint256 oethAmountSupplied, + uint256 tokenId, + uint256 underlyingAssets + ); // 0x1530ec74 + + error PoolRebalanceOutOfBounds( + uint256 currentPoolWethShare, + uint256 allowedWethShareStart, + uint256 allowedWethShareEnd + ); // 0x3681e8e0 + + error NotEnoughWethForSwap(uint256 wethBalance, uint256 requiredWeth); // 0x989e5ca8 + error NotEnoughWethLiquidity(uint256 wethBalance, uint256 requiredWeth); // 0xa6737d87 + error OutsideExpectedTickRange(); // 0xa6e1bad2 + error SlippageCheck(uint256 tokenReceived); // 0x355cdb78 + + /// @notice the constructor + /// @dev This contract is intended to be used as a proxy. To prevent the + /// potential confusion of having a functional implementation contract + /// the constructor has the `initializer` modifier. This way the + /// `initialize` function can not be called on the implementation contract. + /// For the same reason the implementation contract also has the governor + /// set to a zero address. + /// @param _stratConfig the basic strategy configuration + /// @param _wethAddress Address of the Erc20 WETH Token contract + /// @param _oethAddress Address of the Erc20 OETH Token contract + /// @param _liquidityManager Address of liquidity manager to add + /// the liquidity + /// @param _poolLens Address of the pool lens contract + /// @param _maverickPosition Address of the Maverick's position contract + /// @param _maverickQuoter Address of the Maverick's Quoter contract + /// @param _mPool Address of the Rooster concentrated liquidity pool + /// @param _upperTickAtParity Bool when true upperTick is the one where the + /// price of OETH and WETH are at parity + constructor( + BaseStrategyConfig memory _stratConfig, + address _wethAddress, + address _oethAddress, + address _liquidityManager, + address _poolLens, + address _maverickPosition, + address _maverickQuoter, + address _mPool, + bool _upperTickAtParity, + address _votingDistributor, + address _poolDistributor + ) initializer InitializableAbstractStrategy(_stratConfig) { + require( + address(IMaverickV2Pool(_mPool).tokenA()) == _wethAddress, + "WETH not TokenA" + ); + require( + address(IMaverickV2Pool(_mPool).tokenB()) == _oethAddress, + "OETH not TokenB" + ); + require( + _liquidityManager != address(0), + "LiquidityManager zero address not allowed" + ); + require( + _maverickQuoter != address(0), + "Quoter zero address not allowed" + ); + require(_poolLens != address(0), "PoolLens zero address not allowed"); + require( + _maverickPosition != address(0), + "Position zero address not allowed" + ); + require( + _votingDistributor != address(0), + "Voting distributor zero address not allowed" + ); + require( + _poolDistributor != address(0), + "Pool distributor zero address not allowed" + ); + + uint256 _tickSpacing = IMaverickV2Pool(_mPool).tickSpacing(); + require(_tickSpacing == 1, "Unsupported tickSpacing"); + + // tickSpacing == 1 + (sqrtPriceTickLower, sqrtPriceTickHigher) = TickMath.tickSqrtPrices( + _tickSpacing, + TICK_NUMBER + ); + sqrtPriceAtParity = _upperTickAtParity + ? sqrtPriceTickHigher + : sqrtPriceTickLower; + + WETH = _wethAddress; + OETH = _oethAddress; + liquidityManager = IMaverickV2LiquidityManager(_liquidityManager); + poolLens = IMaverickV2PoolLens(_poolLens); + maverickPosition = IMaverickV2Position(_maverickPosition); + quoter = IMaverickV2Quoter(_maverickQuoter); + mPool = IMaverickV2Pool(_mPool); + votingDistributor = IVotingDistributor(_votingDistributor); + poolDistributor = IPoolDistributor(_poolDistributor); + + // prevent implementation contract to be governed + _setGovernor(address(0)); + } + + /** + * @notice initialize function, to set up initial internal state + */ + function initialize() external onlyGovernor initializer { + // Read reward + address[] memory _rewardTokens = new address[](1); + _rewardTokens[0] = poolDistributor.rewardToken(); + + require(_rewardTokens[0] != address(0), "No reward token configured"); + + InitializableAbstractStrategy._initialize( + _rewardTokens, + new address[](0), + new address[](0) + ); + } + + /*************************************** + Configuration + ****************************************/ + + /** + * @notice Set allowed pool weth share interval. After the rebalance happens + * the share of WETH token in the ticker needs to be within the specifications + * of the interval. + * + * @param _allowedWethShareStart Start of WETH share interval expressed as 18 decimal amount + * @param _allowedWethShareEnd End of WETH share interval expressed as 18 decimal amount + */ + function setAllowedPoolWethShareInterval( + uint256 _allowedWethShareStart, + uint256 _allowedWethShareEnd + ) external onlyGovernor { + require( + _allowedWethShareStart < _allowedWethShareEnd, + "Invalid interval" + ); + // can not go below 1% weth share + require(_allowedWethShareStart > 0.01 ether, "Invalid interval start"); + // can not go above 95% weth share + require(_allowedWethShareEnd < 0.95 ether, "Invalid interval end"); + + allowedWethShareStart = _allowedWethShareStart; + allowedWethShareEnd = _allowedWethShareEnd; + emit PoolWethShareIntervalUpdated( + _allowedWethShareStart, + _allowedWethShareEnd + ); + } + + /*************************************** + Strategy overrides + ****************************************/ + + /** + * @notice Deposits funds to the strategy which deposits them to the + * underlying Rooster pool if the pool price is within the expected interval. + * @param _asset Address for the asset + * @param _amount Units of asset to deposit + */ + function deposit(address _asset, uint256 _amount) + external + override + onlyVault + nonReentrant + { + _deposit(_asset, _amount); + } + + /** + * @notice Deposits all the funds to the strategy which deposits them to the + * underlying Rooster pool if the pool price is within the expected interval. + */ + function depositAll() external override onlyVault nonReentrant { + uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); + _deposit(WETH, _wethBalance); + } + + /** + * @dev Deposits funds to the strategy which deposits them to the + * underlying Rooster pool if the pool price is within the expected interval. + * Before this function can be called the initial pool position needs to already + * be minted. + * @param _asset Address of the asset to deposit + * @param _amount Amount of assets to deposit + */ + function _deposit(address _asset, uint256 _amount) internal { + require(_asset == WETH, "Unsupported asset"); + require(_amount > 0, "Must deposit something"); + require(tokenId > 0, "Initial position not minted"); + emit Deposit(_asset, address(0), _amount); + + // if the pool price is not within the expected interval leave the WETH on the contract + // as to not break the mints - in case it would be configured as a default asset strategy + (bool _isExpectedRange, ) = _checkForExpectedPoolPrice(false); + if (_isExpectedRange) { + // deposit funds into the underlying pool. Because no swap is performed there is no + // need to remove any of the liquidity beforehand. + _rebalance(0, false, 0, 0); + } + } + + /** + * @notice Withdraw an `amount` of WETH from the platform and + * send to the `_recipient`. + * @param _recipient Address to which the asset should be sent + * @param _asset WETH address + * @param _amount Amount of WETH to withdraw + */ + function withdraw( + address _recipient, + address _asset, + uint256 _amount + ) external override onlyVault nonReentrant { + require(_asset == WETH, "Unsupported asset"); + require(_amount > 0, "Must withdraw something"); + require(_recipient == vaultAddress, "Only withdraw to vault allowed"); + + _ensureWETHBalance(_amount); + + _withdraw(_recipient, _amount); + } + + /** + * @notice Withdraw WETH and sends it to the Vault. + */ + function withdrawAll() external override onlyVault nonReentrant { + if (tokenId != 0) { + _removeLiquidity(1e18); + } + + uint256 _balance = IERC20(WETH).balanceOf(address(this)); + if (_balance > 0) { + _withdraw(vaultAddress, _balance); + } + } + + function _withdraw(address _recipient, uint256 _amount) internal { + IERC20(WETH).safeTransfer(_recipient, _amount); + emit Withdrawal(WETH, address(0), _amount); + } + + /** + * @dev Retuns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + * @return bool True when the _asset is WETH + */ + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == WETH; + } + + /** + * @dev Approve the spending amounts for the assets + */ + function _approveTokenAmounts( + uint256 _wethAllowance, + uint256 _oethAllowance + ) internal { + IERC20(WETH).approve(address(liquidityManager), _wethAllowance); + IERC20(OETH).approve(address(liquidityManager), _oethAllowance); + } + + /*************************************** + Liquidity management + ****************************************/ + /** + * @dev Add liquidity into the pool in the pre-configured WETH to OETH share ratios + * defined by the allowedPoolWethShareStart|End interval. + * + * Normally a PoolLens contract is used to prepare the parameters to add liquidity to the + * Rooster pools. It has some errors when doing those calculation and for that reason a + * much more accurate Quoter contract is used. This is possible due to our requirement of + * adding liquidity only to one tick - PoolLens supports adding liquidity into multiple ticks + * using different distribution ratios. + */ + function _addLiquidity() internal { + uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); + uint256 _oethBalance = IERC20(OETH).balanceOf(address(this)); + // don't deposit small liquidity amounts + if (_wethBalance <= ACTION_THRESHOLD) { + return; + } + + ( + bytes memory packedSqrtPriceBreaks, + bytes[] memory packedArgs, + uint256 WETHRequired, + uint256 OETHRequired + ) = _getAddLiquidityParams(_wethBalance, 1e30); + + if (OETHRequired > _oethBalance) { + IVault(vaultAddress).mintForStrategy(OETHRequired - _oethBalance); + } + + _approveTokenAmounts(WETHRequired, OETHRequired); + + ( + uint256 _wethAmount, + uint256 _oethAmount, + uint32[] memory binIds + ) = liquidityManager.addPositionLiquidityToSenderByTokenIndex( + mPool, + 0, // NFT token index + packedSqrtPriceBreaks, + packedArgs + ); + + require(binIds.length == 1, "Unexpected binIds length"); + + // burn remaining OETH + _burnOethOnTheContract(); + _updateUnderlyingAssets(); + + // needs to be called after _updateUnderlyingAssets so the updated amount + // is reflected in the event + emit LiquidityAdded( + _wethBalance, // wethAmountDesired + OETHRequired, // oethAmountDesired + _wethAmount, // wethAmountSupplied + _oethAmount, // oethAmountSupplied + tokenId, // tokenId + underlyingAssets + ); + } + + /** + * @dev The function creates liquidity parameters required to be able to add liquidity to the pool. + * The function needs to handle the 3 different cases of the way liquidity is added: + * - only WETH present in the tick + * - only OETH present in the tick + * - both tokens present in the tick + * + */ + function _getAddLiquidityParams(uint256 _maxWETH, uint256 _maxOETH) + internal + returns ( + bytes memory packedSqrtPriceBreaks, + bytes[] memory packedArgs, + uint256 WETHRequired, + uint256 OETHRequired + ) + { + IMaverickV2Pool.AddLiquidityParams[] + memory addParams = new IMaverickV2Pool.AddLiquidityParams[](1); + int32[] memory ticks = new int32[](1); + uint128[] memory amounts = new uint128[](1); + ticks[0] = TICK_NUMBER; + // arbitrary LP amount + amounts[0] = 1e24; + + // construct value for Quoter with arbitrary LP amount + IMaverickV2Pool.AddLiquidityParams memory addParam = IMaverickV2Pool + .AddLiquidityParams({ + kind: MAV_STATIC_BIN_KIND, + ticks: ticks, + amounts: amounts + }); + + // get the WETH and OETH required to get the proportion of tokens required + // given the arbitrary liquidity + (WETHRequired, OETHRequired, ) = quoter.calculateAddLiquidity( + mPool, + addParam + ); + + /** + * If either token required is 0 then the tick consists only of the other token. In that + * case the liquidity calculations need to be done using the non 0 token. By setting the + * tokenRequired from 0 to 1 the `min` in next step will ignore that (the bigger) value. + */ + WETHRequired = WETHRequired == 0 ? 1 : WETHRequired; + OETHRequired = OETHRequired == 0 ? 1 : OETHRequired; + + addParam.amounts[0] = Math_v5 + .min( + ((_maxWETH - 1) * 1e24) / WETHRequired, + ((_maxOETH - 1) * 1e24) / OETHRequired + ) + .toUint128(); + + // update the quotes with the actual amounts + (WETHRequired, OETHRequired, ) = quoter.calculateAddLiquidity( + mPool, + addParam + ); + + require(_maxWETH >= WETHRequired, "More WETH required than specified"); + require(_maxOETH >= OETHRequired, "More OETH required than specified"); + + // organize values to be used by manager + addParams[0] = addParam; + packedArgs = liquidityManager.packAddLiquidityArgsArray(addParams); + // price can stay 0 if array only has one element + packedSqrtPriceBreaks = liquidityManager.packUint88Array( + new uint88[](1) + ); + } + + /** + * @dev Check that the Rooster pool price is within the expected + * parameters. + * This function works whether the strategy contract has liquidity + * position in the pool or not. The function returns _wethSharePct + * as a gas optimization measure. + * @param _throwException when set to true the function throws an exception + * when pool's price is not within expected range. + * @return _isExpectedRange Bool expressing price is within expected range + * @return _wethSharePct Share of WETH owned by this strategy contract in the + * configured ticker. + */ + function _checkForExpectedPoolPrice(bool _throwException) + internal + view + returns (bool _isExpectedRange, uint256 _wethSharePct) + { + require( + allowedWethShareStart != 0 && allowedWethShareEnd != 0, + "Weth share interval not set" + ); + + uint256 _currentPrice = getPoolSqrtPrice(); + + /** + * First check pool price is in expected tick range + * + * A revert is issued even though price being equal to the lower bound as that can not + * be within the approved tick range. + */ + if ( + _currentPrice <= sqrtPriceTickLower || + _currentPrice >= sqrtPriceTickHigher + ) { + if (_throwException) { + revert OutsideExpectedTickRange(); + } + + return (false, _currentPrice <= sqrtPriceTickLower ? 0 : 1e18); + } + + // 18 decimal number expressed WETH tick share + _wethSharePct = _getWethShare(_currentPrice); + + if ( + _wethSharePct < allowedWethShareStart || + _wethSharePct > allowedWethShareEnd + ) { + if (_throwException) { + revert PoolRebalanceOutOfBounds( + _wethSharePct, + allowedWethShareStart, + allowedWethShareEnd + ); + } + return (false, _wethSharePct); + } + + return (true, _wethSharePct); + } + + /** + * @notice Rebalance the pool to the desired token split and Deposit any WETH on the contract to the + * underlying rooster pool. Print the required amount of corresponding OETH. After the rebalancing is + * done burn any potentially remaining OETH tokens still on the strategy contract. + * + * This function has a slightly different behaviour depending on the status of the underlying Rooster + * pool. The function consists of the following 3 steps: + * 1. withdrawLiquidityOption -> this is a configurable option where either only part of the liquidity + * necessary for the swap is removed, or all of it. This way the rebalance + * is able to optimize for volume, for efficiency or anything in between + * 2. swapToDesiredPosition -> move active trading price in the pool to be able to deposit WETH & OETH + * tokens with the desired pre-configured ratios + * 3. addLiquidity -> add liquidity into the pool respecting ratio split configuration + * + * + * Exact _amountToSwap, _swapWeth & _minTokenReceived parameters shall be determined by simulating the + * transaction off-chain. The strategy checks that after the swap the share of the tokens is in the + * expected ranges. + * + * @param _amountToSwap The amount of the token to swap + * @param _swapWeth Swap using WETH when true, use OETH when false + * @param _minTokenReceived Slippage check -> minimum amount of token expected in return + * @param _liquidityToRemovePct Percentage of liquidity to remove -> the percentage amount of liquidity to + * remove before performing the swap. 1e18 denominated + */ + function rebalance( + uint256 _amountToSwap, + bool _swapWeth, + uint256 _minTokenReceived, + uint256 _liquidityToRemovePct + ) external nonReentrant onlyGovernorOrStrategist { + _rebalance( + _amountToSwap, + _swapWeth, + _minTokenReceived, + _liquidityToRemovePct + ); + } + + // slither-disable-start reentrancy-no-eth + function _rebalance( + uint256 _amountToSwap, + bool _swapWeth, + uint256 _minTokenReceived, + uint256 _liquidityToRemovePct + ) internal { + // Remove the required amount of liquidity + if (_liquidityToRemovePct > 0) { + _removeLiquidity(_liquidityToRemovePct); + } + + // in some cases (e.g. deposits) we will just want to add liquidity and not + // issue a swap to move the active trading position within the pool. Before or after a + // deposit or as a standalone call the strategist might issue a rebalance to move the + // active trading price to a more desired position. + if (_amountToSwap > 0) { + // In case liquidity has been removed and there is still not enough WETH owned by the + // strategy contract remove additional required amount of WETH. + if (_swapWeth) _ensureWETHBalance(_amountToSwap); + + _swapToDesiredPosition(_amountToSwap, _swapWeth, _minTokenReceived); + } + + // calling check liquidity early so we don't get unexpected errors when adding liquidity + // in the later stages of this function + _checkForExpectedPoolPrice(true); + + _addLiquidity(); + + // this call shouldn't be necessary, since adding liquidity shouldn't affect the active + // trading price. It is a defensive programming measure. + (, uint256 _wethSharePct) = _checkForExpectedPoolPrice(true); + + // revert if protocol insolvent + _solvencyAssert(); + + emit PoolRebalanced(_wethSharePct); + } + + // slither-disable-end reentrancy-no-eth + + /** + * @dev Perform a swap so that after the swap the tick has the desired WETH to OETH token share. + */ + function _swapToDesiredPosition( + uint256 _amountToSwap, + bool _swapWeth, + uint256 _minTokenReceived + ) internal { + IERC20 _tokenToSwap = IERC20(_swapWeth ? WETH : OETH); + uint256 _balance = _tokenToSwap.balanceOf(address(this)); + + if (_balance < _amountToSwap) { + // This should never trigger since _ensureWETHBalance will already + // throw an error if there is not enough WETH + if (_swapWeth) { + revert NotEnoughWethForSwap(_balance, _amountToSwap); + } + // if swapping OETH + uint256 mintForSwap = _amountToSwap - _balance; + IVault(vaultAddress).mintForStrategy(mintForSwap); + } + + // SafeERC20 is used for IERC20 transfers. Not sure why slither complains + // slither-disable-next-line unchecked-transfer + _tokenToSwap.transfer(address(mPool), _amountToSwap); + + // tickLimit: the furthest tick a swap will execute in. If no limit is desired, + // value should be set to type(int32).max for a tokenAIn (WETH) swap + // and type(int32).min for a swap where tokenB (OETH) is the input + + IMaverickV2Pool.SwapParams memory swapParams = IMaverickV2Pool + // exactOutput defines whether the amount specified is the output + // or the input amount of the swap + .SwapParams({ + amount: _amountToSwap, + tokenAIn: _swapWeth, + exactOutput: false, + tickLimit: TICK_NUMBER + }); + + // swaps without a callback as the assets are already sent to the pool + (, uint256 amountOut) = mPool.swap( + address(this), + swapParams, + bytes("") + ); + + /** + * There could be additional checks here for validating minTokenReceived is within the + * expected range (e.g. 99% - 101% of the token sent in). Though that doesn't provide + * any additional security. After the swap the `_checkForExpectedPoolPrice` validates + * that the swap has moved the price into the expected tick (# -1). + * + * If the guardian forgets to set a `_minTokenReceived` and a sandwich attack bends + * the pool before the swap the `_checkForExpectedPoolPrice` will fail the transaction. + * + * A check would not prevent a compromised guardian from stealing funds as multiple + * transactions each loosing smaller amount of funds are still possible. + */ + if (amountOut < _minTokenReceived) { + revert SlippageCheck(amountOut); + } + + /** + * In the interest of each function in `_rebalance` to leave the contract state as + * clean as possible the OETH tokens here are burned. This decreases the + * dependence where `_swapToDesiredPosition` function relies on later functions + * (`addLiquidity`) to burn the OETH. Reducing the risk of error introduction. + */ + _burnOethOnTheContract(); + } + + /** + * @dev This function removes the appropriate amount of liquidity to ensure that the required + * amount of WETH is available on the contract + * + * @param _amount WETH balance required on the contract + */ + function _ensureWETHBalance(uint256 _amount) internal { + uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); + if (_wethBalance >= _amount) { + return; + } + + require(tokenId != 0, "No liquidity available"); + uint256 _additionalWethRequired = _amount - _wethBalance; + (uint256 _wethInThePool, ) = getPositionPrincipal(); + + if (_wethInThePool < _additionalWethRequired) { + revert NotEnoughWethLiquidity( + _wethInThePool, + _additionalWethRequired + ); + } + + uint256 shareOfWethToRemove = _wethInThePool <= 1 + ? 1e18 + : Math_v5.min( + /** + * When dealing with shares of liquidity to remove there is always some + * rounding involved. After extensive fuzz testing the below approach + * yielded the best results where the strategy overdraws the least and + * never removes insufficient amount of WETH. + */ + (_additionalWethRequired + 2).divPrecisely(_wethInThePool - 1) + 2, + 1e18 + ); + + _removeLiquidity(shareOfWethToRemove); + } + + /** + * @dev Decrease partial or all liquidity from the pool. + * @param _liquidityToDecrease The amount of liquidity to remove denominated in 1e18 + */ + function _removeLiquidity(uint256 _liquidityToDecrease) internal { + require(_liquidityToDecrease > 0, "Must remove some liquidity"); + require( + _liquidityToDecrease <= 1e18, + "Can not remove more than 100% of liquidity" + ); + + // 0 indicates the first (and only) bin in the NFT LP position. + IMaverickV2Pool.RemoveLiquidityParams memory params = maverickPosition + .getRemoveParams(tokenId, 0, _liquidityToDecrease); + (uint256 _amountWeth, uint256 _amountOeth) = maverickPosition + .removeLiquidityToSender(tokenId, mPool, params); + + _burnOethOnTheContract(); + _updateUnderlyingAssets(); + + // needs to be called after the _updateUnderlyingAssets so the updated amount is reflected + // in the event + emit LiquidityRemoved( + _liquidityToDecrease, + _amountWeth, + _amountOeth, + underlyingAssets + ); + } + + /** + * @dev Burns any OETH tokens remaining on the strategy contract if the balance is + * above the action threshold. + */ + function _burnOethOnTheContract() internal { + uint256 _oethBalance = IERC20(OETH).balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(_oethBalance); + } + + /** + * @notice Returns the percentage of WETH liquidity in the configured ticker + * owned by this strategy contract. + * @return uint256 1e18 denominated percentage expressing the share + */ + function getWETHShare() external view returns (uint256) { + uint256 _currentPrice = getPoolSqrtPrice(); + return _getWethShare(_currentPrice); + } + + /** + * @dev Returns the share of WETH in tick denominated in 1e18 + */ + function _getWethShare(uint256 _currentPrice) + internal + view + returns (uint256) + { + ( + uint256 wethAmount, + uint256 oethAmount + ) = _reservesInTickForGivenPriceAndLiquidity( + sqrtPriceTickLower, + sqrtPriceTickHigher, + _currentPrice, + 1e24 + ); + + return wethAmount.divPrecisely(wethAmount + oethAmount); + } + + /** + * @notice Returns the current pool price in square root + * @return Square root of the pool price + */ + function getPoolSqrtPrice() public view returns (uint256) { + return poolLens.getPoolSqrtPrice(mPool); + } + + /** + * @notice Returns the current active trading tick of the underlying pool + * @return _currentTick Current pool trading tick + */ + function getCurrentTradingTick() public view returns (int32 _currentTick) { + _currentTick = mPool.getState().activeTick; + } + + /** + * @notice Mint the initial NFT position + * + * @dev This amount is "gifted" to the strategy contract and will count as a yield + * surplus. + */ + // slither-disable-start reentrancy-no-eth + function mintInitialPosition() external onlyGovernor nonReentrant { + require(tokenId == 0, "Initial position already minted"); + ( + bytes memory packedSqrtPriceBreaks, + bytes[] memory packedArgs, + uint256 WETHRequired, + uint256 OETHRequired + ) = _getAddLiquidityParams(1e16, 1e16); + + // Mint rounded up OETH amount + if (OETHRequired > 0) { + IVault(vaultAddress).mintForStrategy(OETHRequired); + } + + _approveTokenAmounts(WETHRequired, OETHRequired); + + // Store the tokenId before calling updateUnderlyingAssets as it relies on the tokenId + // not being 0 + (, , , tokenId) = liquidityManager.mintPositionNftToSender( + mPool, + packedSqrtPriceBreaks, + packedArgs + ); + + // burn remaining OETH + _burnOethOnTheContract(); + _updateUnderlyingAssets(); + } + + // slither-disable-end reentrancy-no-eth + + /** + * @notice Returns the balance of tokens the strategy holds in the LP position + * @return _amountWeth Amount of WETH in position + * @return _amountOeth Amount of OETH in position + */ + function getPositionPrincipal() + public + view + returns (uint256 _amountWeth, uint256 _amountOeth) + { + if (tokenId == 0) { + return (0, 0); + } + + (_amountWeth, _amountOeth, ) = _getPositionInformation(); + } + + /** + * @dev Returns the balance of tokens the strategy holds in the LP position + * @return _amountWeth Amount of WETH in position + * @return _amountOeth Amount of OETH in position + * @return liquidity Amount of liquidity in the position + */ + function _getPositionInformation() + internal + view + returns ( + uint256 _amountWeth, + uint256 _amountOeth, + uint256 liquidity + ) + { + IMaverickV2Position.PositionFullInformation + memory positionInfo = maverickPosition.tokenIdPositionInformation( + tokenId, + 0 + ); + + require( + positionInfo.liquidities.length == 1, + "Unexpected liquidities length" + ); + require(positionInfo.ticks.length == 1, "Unexpected ticks length"); + + _amountWeth = positionInfo.amountA; + _amountOeth = positionInfo.amountB; + liquidity = positionInfo.liquidities[0]; + } + + /** + * Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can + * keep rebalancing the pool in both directions making the protocol lose a tiny amount of + * funds each time. + * + * Protocol must be at least SOLVENCY_THRESHOLD (99.8%) backed in order for the rebalances to + * function. + */ + function _solvencyAssert() internal view { + uint256 _totalVaultValue = IVault(vaultAddress).totalValue(); + uint256 _totalOethSupply = IERC20(OETH).totalSupply(); + + if ( + _totalVaultValue.divPrecisely(_totalOethSupply) < SOLVENCY_THRESHOLD + ) { + revert("Protocol insolvent"); + } + } + + /** + * @dev Collect Rooster reward token, and send it to the harvesterAddress + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // Do nothing if there's no position minted + if (tokenId > 0) { + uint32[] memory binIds = new uint32[](1); + IMaverickV2Pool.TickState memory tickState = mPool.getTick( + TICK_NUMBER + ); + // get the binId for the MAV_STATIC_BIN_KIND in tick TICK_NUMBER (-1) + binIds[0] = tickState.binIdsByTick[0]; + + uint256 lastEpoch = votingDistributor.lastEpoch(); + + poolDistributor.claimLp( + address(this), + tokenId, + mPool, + binIds, + lastEpoch + ); + } + + // Run the internal inherited function + _collectRewardTokens(); + } + + /*************************************** + Balances and Fees + ****************************************/ + + /** + * @dev Get the total asset value held in the platform + * @param _asset Address of the asset + * @return balance Total value of the asset in the platform + */ + function checkBalance(address _asset) + external + view + override + returns (uint256) + { + require(_asset == WETH, "Only WETH supported"); + + // because of PoolLens inaccuracy there is usually some dust WETH left on the contract + uint256 _wethBalance = IERC20(WETH).balanceOf(address(this)); + // just paranoia check, in case there is OETH in the strategy that for some reason hasn't + // been burned yet. This should always be 0. + uint256 _oethBalance = IERC20(OETH).balanceOf(address(this)); + return underlyingAssets + _wethBalance + _oethBalance; + } + + /// @dev This function updates the amount of underlying assets with the approach of the least possible + /// total tokens extracted for the current liquidity in the pool. + function _updateUnderlyingAssets() internal { + /** + * Our net value represent the smallest amount of tokens we are able to extract from the position + * given our liquidity. + * + * The least amount of tokens ex-tractable from the position is where the active trading price is + * at the edge between tick -1 & tick 0. There the pool is offering 1:1 trades between WETH & OETH. + * At that moment the pool consists completely of WETH and no OETH. + * + * The more swaps from OETH -> WETH happen on the pool the more the price starts to move away from the tick 0 + * towards the middle of tick -1 making OETH (priced in WETH) cheaper. + */ + + uint256 _wethAmount = tokenId == 0 ? 0 : _balanceInPosition(); + + underlyingAssets = _wethAmount; + emit UnderlyingAssetsUpdated(_wethAmount); + } + + /** + * @dev Strategy reserves (which consist only of WETH in case of Rooster - Plume pool) + * when the tick price is closest to parity - assuring the lowest amount of tokens + * returned for the current position liquidity. + */ + function _balanceInPosition() internal view returns (uint256 _wethBalance) { + (, , uint256 liquidity) = _getPositionInformation(); + + uint256 _oethBalance; + + (_wethBalance, _oethBalance) = _reservesInTickForGivenPriceAndLiquidity( + sqrtPriceTickLower, + sqrtPriceTickHigher, + sqrtPriceAtParity, + liquidity + ); + + require(_oethBalance == 0, "Non zero oethBalance"); + } + + /** + * @notice Tick dominance denominated in 1e18 + * @return _tickDominance The share of liquidity in TICK_NUMBER tick owned + * by the strategy contract denominated in 1e18 + */ + function tickDominance() public view returns (uint256 _tickDominance) { + IMaverickV2Pool.TickState memory tickState = mPool.getTick(TICK_NUMBER); + + uint256 wethReserve = tickState.reserveA; + uint256 oethReserve = tickState.reserveB; + + // prettier-ignore + (uint256 _amountWeth, uint256 _amountOeth, ) = _getPositionInformation(); + + if (wethReserve + oethReserve == 0) { + return 0; + } + + _tickDominance = (_amountWeth + _amountOeth).divPrecisely( + wethReserve + oethReserve + ); + } + + /*************************************** + Hidden functions + ****************************************/ + /** + * @dev Unsupported + */ + function setPTokenAddress(address, address) external pure override { + // The pool tokens can never change. + revert("Unsupported method"); + } + + /** + * @dev Unsupported + */ + function removePToken(uint256) external pure override { + // The pool tokens can never change. + revert("Unsupported method"); + } + + /** + * @dev Unsupported + */ + function _abstractSetPToken(address, address) internal pure override { + revert("Unsupported method"); + } + + /** + * @dev Unsupported + */ + function safeApproveAllTokens() external pure override { + // all the amounts are approved at the time required + revert("Unsupported method"); + } + + /*************************************** + Maverick liquidity utilities + ****************************************/ + + /// @notice Calculates deltaA = liquidity * (sqrt(upper) - sqrt(lower)) + /// Calculates deltaB = liquidity / sqrt(lower) - liquidity / sqrt(upper), + /// i.e. liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower)) + /// + /// @dev refactored from here: + // solhint-disable-next-line max-line-length + /// https://github.com/rooster-protocol/rooster-contracts/blob/main/v2-supplemental/contracts/libraries/LiquidityUtilities.sol#L665-L695 + function _reservesInTickForGivenPriceAndLiquidity( + uint256 _lowerSqrtPrice, + uint256 _upperSqrtPrice, + uint256 _newSqrtPrice, + uint256 _liquidity + ) internal pure returns (uint128 reserveA, uint128 reserveB) { + if (_liquidity == 0) { + (reserveA, reserveB) = (0, 0); + } else { + uint256 lowerEdge = MathRooster.max(_lowerSqrtPrice, _newSqrtPrice); + + reserveA = MathRooster + .mulCeil( + _liquidity, + MathRooster.clip( + MathRooster.min(_upperSqrtPrice, _newSqrtPrice), + _lowerSqrtPrice + ) + ) + .toUint128(); + reserveB = MathRooster + .mulDivCeil( + _liquidity, + 1e18 * MathRooster.clip(_upperSqrtPrice, lowerEdge), + _upperSqrtPrice * lowerEdge + ) + .toUint128(); + } + } +} diff --git a/contracts/contracts/utils/InitializableAbstractStrategy.sol b/contracts/contracts/utils/InitializableAbstractStrategy.sol index 28ef3033f2..890da82e57 100644 --- a/contracts/contracts/utils/InitializableAbstractStrategy.sol +++ b/contracts/contracts/utils/InitializableAbstractStrategy.sol @@ -78,6 +78,17 @@ abstract contract InitializableAbstractStrategy is Initializable, Governable { address vaultAddress; // Address of the OToken's Vault } + /** + * @dev Verifies that the caller is the Governor or Strategist. + */ + modifier onlyGovernorOrStrategist() { + require( + isGovernor() || msg.sender == IVault(vaultAddress).strategistAddr(), + "Caller is not the Strategist or Governor" + ); + _; + } + /** * @param _config The platform and OToken vault addresses */ diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 4c2ffdaf18..8d1015e623 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -9,6 +9,8 @@ const { isHoleskyOrFork, isSonicOrFork, isTest, + isFork, + isPlume, } = require("../test/helpers.js"); const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); const { metapoolLPCRVPid } = require("../utils/constants"); @@ -1347,6 +1349,74 @@ const deployBaseAerodromeAMOStrategyImplementation = async () => { return await ethers.getContract("AerodromeAMOStrategy"); }; +const deployPlumeRoosterAMOStrategyImplementation = async (poolAddress) => { + return _deployPlumeRoosterAMOImplementationConfigurable( + poolAddress, + "RoosterAMOStrategy" + ); +}; + +const deployPlumeMockRoosterAMOStrategyImplementation = async (poolAddress) => { + return _deployPlumeRoosterAMOImplementationConfigurable( + poolAddress, + "MockRoosterAMOStrategy" + ); +}; + +const _deployPlumeRoosterAMOImplementationConfigurable = async ( + poolAddress, + stratContractName +) => { + const cOETHpProxy = await ethers.getContract("OETHPlumeProxy"); + const cOETHpVaultProxy = await ethers.getContract("OETHPlumeVaultProxy"); + + if (!isFork && isPlume) { + throw new Error("You cannot deploy this strategy yet"); + } + + const cMockMaverickDistributor = await ethers.getContract( + "MockMaverickDistributor" + ); + + await deployWithConfirmation(stratContractName, [ + /* Used first by the 002_rooster_amo_ deploy file + */ + [addresses.zero, cOETHpVaultProxy.address], // platformAddress, VaultAddress + addresses.plume.WETH, // weth address + cOETHpProxy.address, // OETHp address + addresses.plume.MaverickV2LiquidityManager, // liquidity mananger + addresses.plume.MaverickV2PoolLens, // pool lens + addresses.plume.MaverickV2Position, // position + addresses.plume.MaverickV2Quoter, // quoter + poolAddress, // superOETHp/WPLUME pool + true, // uppperTickAtParity + // TODO: change these to actual addresses + cMockMaverickDistributor.address, // votingDistributor + cMockMaverickDistributor.address, // poolDistributor + ]); + + return await ethers.getContract(stratContractName); +}; + +const getPlumeContracts = async () => { + const maverickV2LiquidityManager = await ethers.getContractAt( + "IMaverickV2LiquidityManager", + addresses.plume.MaverickV2LiquidityManager + ); + const maverickV2PoolLens = await ethers.getContractAt( + "IMaverickV2PoolLens", + addresses.plume.MaverickV2PoolLens + ); + const cOETHpProxy = await ethers.getContract("OETHPlumeProxy"); + const cOETHp = await ethers.getContractAt("OETHPlume", cOETHpProxy.address); + + return { + maverickV2LiquidityManager, + maverickV2PoolLens, + cOETHp, + }; +}; + const deploySonicSwapXAMOStrategyImplementation = async () => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1421,5 +1491,8 @@ module.exports = { upgradeNativeStakingSSVStrategy, upgradeNativeStakingFeeAccumulator, deployBaseAerodromeAMOStrategyImplementation, + deployPlumeRoosterAMOStrategyImplementation, + deployPlumeMockRoosterAMOStrategyImplementation, + getPlumeContracts, deploySonicSwapXAMOStrategyImplementation, }; diff --git a/contracts/deploy/plume/005_rooster_amo.js b/contracts/deploy/plume/005_rooster_amo.js new file mode 100644 index 0000000000..c865b8d1c8 --- /dev/null +++ b/contracts/deploy/plume/005_rooster_amo.js @@ -0,0 +1,145 @@ +const hre = require("hardhat"); +const { deployOnPlume } = require("../../utils/deploy-l2"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); +const { + deployPlumeRoosterAMOStrategyImplementation, +} = require("../deployActions"); +const { isFork, oethUnits } = require("../../test/helpers"); +const { setERC20TokenBalance } = require("../../test/_fund"); +const { utils } = require("ethers"); + +module.exports = deployOnPlume( + { + deployName: "005_rooster_amo", + }, + async () => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.getSigner(deployerAddr); + const cOETHpVaultProxy = await ethers.getContract("OETHPlumeVaultProxy"); + const weth = await ethers.getContractAt("IWETH9", addresses.plume.WETH); + const deployerWethBalance = await weth + .connect(sDeployer) + .balanceOf(sDeployer.address); + + console.log("Deployer WETH balance", deployerWethBalance.toString()); + if (!isFork) { + if (deployerWethBalance.lt(oethUnits("1"))) { + throw new Error( + "Deployer needs at least 1e18 of WETH to mint the initial balance" + ); + } + } + + const cOETHpVault = await ethers.getContractAt( + "IVault", + cOETHpVaultProxy.address + ); + + await deployWithConfirmation("RoosterAMOStrategyProxy"); + const cAMOStrategyProxy = await ethers.getContract( + "RoosterAMOStrategyProxy" + ); + + if (isFork) { + // Just pretend wPlume is the reward token for testing + await deployWithConfirmation("MockMaverickDistributor", [ + addresses.plume.WPLUME, + ]); + const cMockMaverickDistributor = await ethers.getContract( + "MockMaverickDistributor" + ); + + await withConfirmation( + cMockMaverickDistributor + .connect(sDeployer) + .setRewardTokenAmount(oethUnits("1")) + ); + + // Fund the mock contract + const wPlume = await ethers.getContractAt( + "IWETH9", + addresses.plume.WPLUME + ); + await withConfirmation( + wPlume.connect(sDeployer).deposit({ value: oethUnits("10") }) + ); + await withConfirmation( + wPlume + .connect(sDeployer) + .transfer(cMockMaverickDistributor.address, oethUnits("10")) + ); + } + + const cAMOStrategyImpl = await deployPlumeRoosterAMOStrategyImplementation( + addresses.plume.OethpWETHRoosterPool + ); + const strategyImplInitData = cAMOStrategyImpl.interface.encodeFunctionData( + "initialize()", + [] + ); + + // prettier-ignore + await withConfirmation( + cAMOStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + cAMOStrategyImpl.address, + addresses.plume.timelock, + strategyImplInitData + ) + ); + + const cAMOStrategy = await ethers.getContractAt( + "RoosterAMOStrategy", + cAMOStrategyProxy.address + ); + + if (isFork) { + // 1 WETH + await setERC20TokenBalance(sDeployer.address, weth, "1", hre); + } + + // transfer 1e16 of WETH to the strategy to mint the initial position + await weth + .connect(sDeployer) + .transfer(cAMOStrategy.address, oethUnits("0.01")); + + return { + actions: [ + { + // Approve the AMO strategy on the Vault + contract: cOETHpVault, + signature: "approveStrategy(address)", + args: [cAMOStrategy.address], + }, + { + // Set strategy as whitelisted one to mint OETHp tokens + contract: cOETHpVault, + signature: "addStrategyToMintWhitelist(address)", + args: [cAMOStrategy.address], + }, + { + // Safe approve tokens + contract: cAMOStrategy, + signature: "mintInitialPosition()", + args: [], + }, + { + // Safe approve tokens + contract: cAMOStrategy, + signature: "setAllowedPoolWethShareInterval(uint256,uint256)", + args: [utils.parseUnits("0.10", 18), utils.parseUnits("0.25", 18)], + }, + { + // Set Harvester address to the multisig + contract: cAMOStrategy, + signature: "setHarvesterAddress(address)", + args: [addresses.multichainStrategist], + }, + ], + }; + } +); diff --git a/contracts/lib/rooster/openzeppelin-custom/contracts/utils/Panic.sol b/contracts/lib/rooster/openzeppelin-custom/contracts/utils/Panic.sol new file mode 100644 index 0000000000..1c66c7c67e --- /dev/null +++ b/contracts/lib/rooster/openzeppelin-custom/contracts/utils/Panic.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Helper library for emitting standardized panic codes. + * + * ```solidity + * contract Example { + * using Panic for uint256; + * + * // Use any of the declared internal constants + * function foo() { Panic.GENERIC.panic(); } + * + * // Alternatively + * function foo() { Panic.panic(Panic.GENERIC); } + * } + * ``` + * + * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil]. + * + * _Available since v5.1._ + */ +// slither-disable-next-line unused-state +library Panic { + /// @dev generic / unspecified error + uint256 internal constant GENERIC = 0x00; + /// @dev used by the assert() builtin + uint256 internal constant ASSERT = 0x01; + /// @dev arithmetic underflow or overflow + uint256 internal constant UNDER_OVERFLOW = 0x11; + /// @dev division or modulo by zero + uint256 internal constant DIVISION_BY_ZERO = 0x12; + /// @dev enum conversion error + uint256 internal constant ENUM_CONVERSION_ERROR = 0x21; + /// @dev invalid encoding in storage + uint256 internal constant STORAGE_ENCODING_ERROR = 0x22; + /// @dev empty array pop + uint256 internal constant EMPTY_ARRAY_POP = 0x31; + /// @dev array out of bounds access + uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32; + /// @dev resource error (too large allocation or too large array) + uint256 internal constant RESOURCE_ERROR = 0x41; + /// @dev calling invalid internal function + uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51; + + /// @dev Reverts with a panic code. Recommended to use with + /// the internal constants with predefined codes. + function panic(uint256 code) internal pure { + assembly ("memory-safe") { + mstore(0x00, 0x4e487b71) + mstore(0x20, code) + revert(0x1c, 0x24) + } + } +} \ No newline at end of file diff --git a/contracts/lib/rooster/openzeppelin-custom/contracts/utils/math/Math.sol b/contracts/lib/rooster/openzeppelin-custom/contracts/utils/math/Math.sol new file mode 100644 index 0000000000..0ff9234cff --- /dev/null +++ b/contracts/lib/rooster/openzeppelin-custom/contracts/utils/math/Math.sol @@ -0,0 +1,749 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/Math.sol) + +pragma solidity ^0.8.20; + +import {Panic} from "../Panic.sol"; +import {SafeCast} from "./SafeCast.sol"; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + enum Rounding { + Floor, // Toward negative infinity + Ceil, // Toward positive infinity + Trunc, // Toward zero + Expand // Away from zero + } + + /** + * @dev Return the 512-bit addition of two uint256. + * + * The result is stored in two 256 variables such that sum = high * 2²⁵⁶ + low. + */ + function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) { + assembly ("memory-safe") { + low := add(a, b) + high := lt(low, a) + } + } + + /** + * @dev Return the 512-bit multiplication of two uint256. + * + * The result is stored in two 256 variables such that product = high * 2²⁵⁶ + low. + */ + function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) { + // 512-bit multiply [high low] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use + // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = high * 2²⁵⁶ + low. + assembly ("memory-safe") { + let mm := mulmod(a, b, not(0)) + low := mul(a, b) + high := sub(sub(mm, low), lt(mm, low)) + } + } + + /** + * @dev Returns the addition of two unsigned integers, with a success flag (no overflow). + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + uint256 c = a + b; + success = c >= a; + result = c * SafeCast.toUint(success); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with a success flag (no overflow). + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + uint256 c = a - b; + success = c <= a; + result = c * SafeCast.toUint(success); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with a success flag (no overflow). + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + uint256 c = a * b; + assembly ("memory-safe") { + // Only true when the multiplication doesn't overflow + // (c / a == b) || (a == 0) + success := or(eq(div(c, a), b), iszero(a)) + } + // equivalent to: success ? c : 0 + result = c * SafeCast.toUint(success); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a success flag (no division by zero). + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + success = b > 0; + assembly ("memory-safe") { + // The `DIV` opcode returns zero when the denominator is 0. + result := div(a, b) + } + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero). + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + success = b > 0; + assembly ("memory-safe") { + // The `MOD` opcode returns zero when the denominator is 0. + result := mod(a, b) + } + } + } + + /** + * @dev Unsigned saturating addition, bounds to `2²⁵⁶ - 1` instead of overflowing. + */ + function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) { + (bool success, uint256 result) = tryAdd(a, b); + return ternary(success, result, type(uint256).max); + } + + /** + * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing. + */ + function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) { + (, uint256 result) = trySub(a, b); + return result; + } + + /** + * @dev Unsigned saturating multiplication, bounds to `2²⁵⁶ - 1` instead of overflowing. + */ + function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) { + (bool success, uint256 result) = tryMul(a, b); + return ternary(success, result, type(uint256).max); + } + + /** + * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant. + * + * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone. + * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute + * one branch when needed, making this function more expensive. + */ + function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) { + unchecked { + // branchless ternary works because: + // b ^ (a ^ b) == a + // b ^ 0 == b + return b ^ ((a ^ b) * SafeCast.toUint(condition)); + } + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return ternary(a > b, a, b); + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return ternary(a < b, a, b); + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds towards infinity instead + * of rounding towards zero. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + Panic.panic(Panic.DIVISION_BY_ZERO); + } + + // The following calculation ensures accurate ceiling division without overflow. + // Since a is non-zero, (a - 1) / b will not overflow. + // The largest possible result occurs when (a - 1) / b is type(uint256).max, + // but the largest value we can obtain is type(uint256).max - 1, which happens + // when a = type(uint256).max and b = 1. + unchecked { + return SafeCast.toUint(a > 0) * ((a - 1) / b + 1); + } + } + + /** + * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or + * denominator == 0. + * + * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by + * Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + (uint256 high, uint256 low) = mul512(x, y); + + // Handle non-overflow cases, 256 by 256 division. + if (high == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return low / denominator; + } + + // Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0. + if (denominator <= high) { + Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW)); + } + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [high low]. + uint256 remainder; + assembly ("memory-safe") { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + high := sub(high, gt(remainder, low)) + low := sub(low, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. + // Always >= 1. See https://cs.stackexchange.com/q/138556/92363. + + uint256 twos = denominator & (0 - denominator); + assembly ("memory-safe") { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [high low] by twos. + low := div(low, twos) + + // Flip twos such that it is 2²⁵⁶ / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from high into low. + low |= high * twos; + + // Invert denominator mod 2²⁵⁶. Now that denominator is an odd number, it has an inverse modulo 2²⁵⁶ such + // that denominator * inv ≡ 1 mod 2²⁵⁶. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv ≡ 1 mod 2⁴. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also + // works in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2¹⁶ + inverse *= 2 - denominator * inverse; // inverse mod 2³² + inverse *= 2 - denominator * inverse; // inverse mod 2⁶⁴ + inverse *= 2 - denominator * inverse; // inverse mod 2¹²⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2²⁵⁶ + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2²⁵⁶. Since the preconditions guarantee that the outcome is + // less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and high + // is no longer required. + result = low * inverse; + return result; + } + } + + /** + * @dev Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0); + } + + /** + * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256. + */ + function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) { + unchecked { + (uint256 high, uint256 low) = mul512(x, y); + if (high >= 1 << n) { + Panic.panic(Panic.UNDER_OVERFLOW); + } + return (high << (256 - n)) | (low >> n); + } + } + + /** + * @dev Calculates x * y >> n with full precision, following the selected rounding direction. + */ + function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) { + return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0); + } + + /** + * @dev Calculate the modular multiplicative inverse of a number in Z/nZ. + * + * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0. + * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible. + * + * If the input value is not inversible, 0 is returned. + * + * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the + * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}. + */ + function invMod(uint256 a, uint256 n) internal pure returns (uint256) { + unchecked { + if (n == 0) return 0; + + // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version) + // Used to compute integers x and y such that: ax + ny = gcd(a, n). + // When the gcd is 1, then the inverse of a modulo n exists and it's x. + // ax + ny = 1 + // ax = 1 + (-y)n + // ax ≡ 1 (mod n) # x is the inverse of a modulo n + + // If the remainder is 0 the gcd is n right away. + uint256 remainder = a % n; + uint256 gcd = n; + + // Therefore the initial coefficients are: + // ax + ny = gcd(a, n) = n + // 0a + 1n = n + int256 x = 0; + int256 y = 1; + + while (remainder != 0) { + uint256 quotient = gcd / remainder; + + (gcd, remainder) = ( + // The old remainder is the next gcd to try. + remainder, + // Compute the next remainder. + // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd + // where gcd is at most n (capped to type(uint256).max) + gcd - remainder * quotient + ); + + (x, y) = ( + // Increment the coefficient of a. + y, + // Decrement the coefficient of n. + // Can overflow, but the result is casted to uint256 so that the + // next value of y is "wrapped around" to a value between 0 and n - 1. + x - y * int256(quotient) + ); + } + + if (gcd != 1) return 0; // No inverse exists. + return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative. + } + } + + /** + * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`. + * + * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is + * prime, then `a**(p-1) ≡ 1 mod p`. As a consequence, we have `a * a**(p-2) ≡ 1 mod p`, which means that + * `a**(p-2)` is the modular multiplicative inverse of a in Fp. + * + * NOTE: this function does NOT check that `p` is a prime greater than `2`. + */ + function invModPrime(uint256 a, uint256 p) internal view returns (uint256) { + unchecked { + return Math.modExp(a, p - 2, p); + } + } + + /** + * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m) + * + * Requirements: + * - modulus can't be zero + * - underlying staticcall to precompile must succeed + * + * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make + * sure the chain you're using it on supports the precompiled contract for modular exponentiation + * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, + * the underlying function will succeed given the lack of a revert, but the result may be incorrectly + * interpreted as 0. + */ + function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) { + (bool success, uint256 result) = tryModExp(b, e, m); + if (!success) { + Panic.panic(Panic.DIVISION_BY_ZERO); + } + return result; + } + + /** + * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m). + * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying + * to operate modulo 0 or if the underlying precompile reverted. + * + * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain + * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in + * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack + * of a revert, but the result may be incorrectly interpreted as 0. + */ + function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) { + if (m == 0) return (false, 0); + assembly ("memory-safe") { + let ptr := mload(0x40) + // | Offset | Content | Content (Hex) | + // |-----------|------------|--------------------------------------------------------------------| + // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 | + // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 | + // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 | + // | 0x60:0x7f | value of b | 0x<.............................................................b> | + // | 0x80:0x9f | value of e | 0x<.............................................................e> | + // | 0xa0:0xbf | value of m | 0x<.............................................................m> | + mstore(ptr, 0x20) + mstore(add(ptr, 0x20), 0x20) + mstore(add(ptr, 0x40), 0x20) + mstore(add(ptr, 0x60), b) + mstore(add(ptr, 0x80), e) + mstore(add(ptr, 0xa0), m) + + // Given the result < m, it's guaranteed to fit in 32 bytes, + // so we can use the memory scratch space located at offset 0. + success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20) + result := mload(0x00) + } + } + + /** + * @dev Variant of {modExp} that supports inputs of arbitrary length. + */ + function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) { + (bool success, bytes memory result) = tryModExp(b, e, m); + if (!success) { + Panic.panic(Panic.DIVISION_BY_ZERO); + } + return result; + } + + /** + * @dev Variant of {tryModExp} that supports inputs of arbitrary length. + */ + function tryModExp( + bytes memory b, + bytes memory e, + bytes memory m + ) internal view returns (bool success, bytes memory result) { + if (_zeroBytes(m)) return (false, new bytes(0)); + + uint256 mLen = m.length; + + // Encode call args in result and move the free memory pointer + result = abi.encodePacked(b.length, e.length, mLen, b, e, m); + + assembly ("memory-safe") { + let dataPtr := add(result, 0x20) + // Write result on top of args to avoid allocating extra memory. + success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen) + // Overwrite the length. + // result.length > returndatasize() is guaranteed because returndatasize() == m.length + mstore(result, mLen) + // Set the memory pointer after the returned data. + mstore(0x40, add(dataPtr, mLen)) + } + } + + /** + * @dev Returns whether the provided byte array is zero. + */ + function _zeroBytes(bytes memory byteArray) private pure returns (bool) { + for (uint256 i = 0; i < byteArray.length; ++i) { + if (byteArray[i] != 0) { + return false; + } + } + return true; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded + * towards zero. + * + * This method is based on Newton's method for computing square roots; the algorithm is restricted to only + * using integer operations. + */ + function sqrt(uint256 a) internal pure returns (uint256) { + unchecked { + // Take care of easy edge cases when a == 0 or a == 1 + if (a <= 1) { + return a; + } + + // In this function, we use Newton's method to get a root of `f(x) := x² - a`. It involves building a + // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between + // the current value as `ε_n = | x_n - sqrt(a) |`. + // + // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root + // of the target. (i.e. `2**(e-1) ≤ sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is + // bigger than any uint256. + // + // By noticing that + // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ a < 2**(2*e)` + // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar + // to the msb function. + uint256 aa = a; + uint256 xn = 1; + + if (aa >= (1 << 128)) { + aa >>= 128; + xn <<= 64; + } + if (aa >= (1 << 64)) { + aa >>= 64; + xn <<= 32; + } + if (aa >= (1 << 32)) { + aa >>= 32; + xn <<= 16; + } + if (aa >= (1 << 16)) { + aa >>= 16; + xn <<= 8; + } + if (aa >= (1 << 8)) { + aa >>= 8; + xn <<= 4; + } + if (aa >= (1 << 4)) { + aa >>= 4; + xn <<= 2; + } + if (aa >= (1 << 2)) { + xn <<= 1; + } + + // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * x_n`. This implies ε_n ≤ 2**(e-1). + // + // We can refine our estimation by noticing that the middle of that interval minimizes the error. + // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to ε_n ≤ 2**(e-2). + // This is going to be our x_0 (and ε_0) + xn = (3 * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2) + + // From here, Newton's method give us: + // x_{n+1} = (x_n + a / x_n) / 2 + // + // One should note that: + // x_{n+1}² - a = ((x_n + a / x_n) / 2)² - a + // = ((x_n² + a) / (2 * x_n))² - a + // = (x_n⁴ + 2 * a * x_n² + a²) / (4 * x_n²) - a + // = (x_n⁴ + 2 * a * x_n² + a² - 4 * a * x_n²) / (4 * x_n²) + // = (x_n⁴ - 2 * a * x_n² + a²) / (4 * x_n²) + // = (x_n² - a)² / (2 * x_n)² + // = ((x_n² - a) / (2 * x_n))² + // ≥ 0 + // Which proves that for all n ≥ 1, sqrt(a) ≤ x_n + // + // This gives us the proof of quadratic convergence of the sequence: + // ε_{n+1} = | x_{n+1} - sqrt(a) | + // = | (x_n + a / x_n) / 2 - sqrt(a) | + // = | (x_n² + a - 2*x_n*sqrt(a)) / (2 * x_n) | + // = | (x_n - sqrt(a))² / (2 * x_n) | + // = | ε_n² / (2 * x_n) | + // = ε_n² / | (2 * x_n) | + // + // For the first iteration, we have a special case where x_0 is known: + // ε_1 = ε_0² / | (2 * x_0) | + // ≤ (2**(e-2))² / (2 * (2**(e-1) + 2**(e-2))) + // ≤ 2**(2*e-4) / (3 * 2**(e-1)) + // ≤ 2**(e-3) / 3 + // ≤ 2**(e-3-log2(3)) + // ≤ 2**(e-4.5) + // + // For the following iterations, we use the fact that, 2**(e-1) ≤ sqrt(a) ≤ x_n: + // ε_{n+1} = ε_n² / | (2 * x_n) | + // ≤ (2**(e-k))² / (2 * 2**(e-1)) + // ≤ 2**(2*e-2*k) / 2**e + // ≤ 2**(e-2*k) + xn = (xn + a / xn) >> 1; // ε_1 := | x_1 - sqrt(a) | ≤ 2**(e-4.5) -- special case, see above + xn = (xn + a / xn) >> 1; // ε_2 := | x_2 - sqrt(a) | ≤ 2**(e-9) -- general case with k = 4.5 + xn = (xn + a / xn) >> 1; // ε_3 := | x_3 - sqrt(a) | ≤ 2**(e-18) -- general case with k = 9 + xn = (xn + a / xn) >> 1; // ε_4 := | x_4 - sqrt(a) | ≤ 2**(e-36) -- general case with k = 18 + xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72) -- general case with k = 36 + xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144) -- general case with k = 72 + + // Because e ≤ 128 (as discussed during the first estimation phase), we know have reached a precision + // ε_6 ≤ 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either + // sqrt(a) or sqrt(a) + 1. + return xn - SafeCast.toUint(xn > a / xn); + } + } + + /** + * @dev Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a); + } + } + + /** + * @dev Return the log in base 2 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log2(uint256 x) internal pure returns (uint256 r) { + // If value has upper 128 bits set, log2 result is at least 128 + r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7; + // If upper 64 bits of 128-bit half set, add 64 to result + r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6; + // If upper 32 bits of 64-bit half set, add 32 to result + r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5; + // If upper 16 bits of 32-bit half set, add 16 to result + r |= SafeCast.toUint((x >> r) > 0xffff) << 4; + // If upper 8 bits of 16-bit half set, add 8 to result + r |= SafeCast.toUint((x >> r) > 0xff) << 3; + // If upper 4 bits of 8-bit half set, add 4 to result + r |= SafeCast.toUint((x >> r) > 0xf) << 2; + + // Shifts value right by the current result and use it as an index into this lookup table: + // + // | x (4 bits) | index | table[index] = MSB position | + // |------------|---------|-----------------------------| + // | 0000 | 0 | table[0] = 0 | + // | 0001 | 1 | table[1] = 0 | + // | 0010 | 2 | table[2] = 1 | + // | 0011 | 3 | table[3] = 1 | + // | 0100 | 4 | table[4] = 2 | + // | 0101 | 5 | table[5] = 2 | + // | 0110 | 6 | table[6] = 2 | + // | 0111 | 7 | table[7] = 2 | + // | 1000 | 8 | table[8] = 3 | + // | 1001 | 9 | table[9] = 3 | + // | 1010 | 10 | table[10] = 3 | + // | 1011 | 11 | table[11] = 3 | + // | 1100 | 12 | table[12] = 3 | + // | 1101 | 13 | table[13] = 3 | + // | 1110 | 14 | table[14] = 3 | + // | 1111 | 15 | table[15] = 3 | + // + // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes. + assembly ("memory-safe") { + r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000)) + } + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value); + } + } + + /** + * @dev Return the log in base 10 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10 ** 64) { + value /= 10 ** 64; + result += 64; + } + if (value >= 10 ** 32) { + value /= 10 ** 32; + result += 32; + } + if (value >= 10 ** 16) { + value /= 10 ** 16; + result += 16; + } + if (value >= 10 ** 8) { + value /= 10 ** 8; + result += 8; + } + if (value >= 10 ** 4) { + value /= 10 ** 4; + result += 4; + } + if (value >= 10 ** 2) { + value /= 10 ** 2; + result += 2; + } + if (value >= 10 ** 1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value); + } + } + + /** + * @dev Return the log in base 256 of a positive value rounded towards zero. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 x) internal pure returns (uint256 r) { + // If value has upper 128 bits set, log2 result is at least 128 + r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7; + // If upper 64 bits of 128-bit half set, add 64 to result + r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6; + // If upper 32 bits of 64-bit half set, add 32 to result + r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5; + // If upper 16 bits of 32-bit half set, add 16 to result + r |= SafeCast.toUint((x >> r) > 0xffff) << 4; + // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8 + return (r >> 3) | SafeCast.toUint((x >> r) > 0xff); + } + + /** + * @dev Return the log in base 256, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value); + } + } + + /** + * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. + */ + function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) { + return uint8(rounding) % 2 == 1; + } +} \ No newline at end of file diff --git a/contracts/lib/rooster/openzeppelin-custom/contracts/utils/math/SafeCast.sol b/contracts/lib/rooster/openzeppelin-custom/contracts/utils/math/SafeCast.sol new file mode 100644 index 0000000000..85cc552aa3 --- /dev/null +++ b/contracts/lib/rooster/openzeppelin-custom/contracts/utils/math/SafeCast.sol @@ -0,0 +1,1162 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol) +// This file was procedurally generated from scripts/generate/templates/SafeCast.js. + +pragma solidity ^0.8.20; + +/** + * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow + * checks. + * + * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can + * easily result in undesired exploitation or bugs, since developers usually + * assume that overflows raise errors. `SafeCast` restores this intuition by + * reverting the transaction when such an operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeCast { + /** + * @dev Value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); + + /** + * @dev Returns the downcasted uint248 from uint256, reverting on + * overflow (when the input is greater than largest uint248). + * + * Counterpart to Solidity's `uint248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toUint248(uint256 value) internal pure returns (uint248) { + if (value > type(uint248).max) { + revert SafeCastOverflowedUintDowncast(248, value); + } + return uint248(value); + } + + /** + * @dev Returns the downcasted uint240 from uint256, reverting on + * overflow (when the input is greater than largest uint240). + * + * Counterpart to Solidity's `uint240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toUint240(uint256 value) internal pure returns (uint240) { + if (value > type(uint240).max) { + revert SafeCastOverflowedUintDowncast(240, value); + } + return uint240(value); + } + + /** + * @dev Returns the downcasted uint232 from uint256, reverting on + * overflow (when the input is greater than largest uint232). + * + * Counterpart to Solidity's `uint232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toUint232(uint256 value) internal pure returns (uint232) { + if (value > type(uint232).max) { + revert SafeCastOverflowedUintDowncast(232, value); + } + return uint232(value); + } + + /** + * @dev Returns the downcasted uint224 from uint256, reverting on + * overflow (when the input is greater than largest uint224). + * + * Counterpart to Solidity's `uint224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toUint224(uint256 value) internal pure returns (uint224) { + if (value > type(uint224).max) { + revert SafeCastOverflowedUintDowncast(224, value); + } + return uint224(value); + } + + /** + * @dev Returns the downcasted uint216 from uint256, reverting on + * overflow (when the input is greater than largest uint216). + * + * Counterpart to Solidity's `uint216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toUint216(uint256 value) internal pure returns (uint216) { + if (value > type(uint216).max) { + revert SafeCastOverflowedUintDowncast(216, value); + } + return uint216(value); + } + + /** + * @dev Returns the downcasted uint208 from uint256, reverting on + * overflow (when the input is greater than largest uint208). + * + * Counterpart to Solidity's `uint208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toUint208(uint256 value) internal pure returns (uint208) { + if (value > type(uint208).max) { + revert SafeCastOverflowedUintDowncast(208, value); + } + return uint208(value); + } + + /** + * @dev Returns the downcasted uint200 from uint256, reverting on + * overflow (when the input is greater than largest uint200). + * + * Counterpart to Solidity's `uint200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toUint200(uint256 value) internal pure returns (uint200) { + if (value > type(uint200).max) { + revert SafeCastOverflowedUintDowncast(200, value); + } + return uint200(value); + } + + /** + * @dev Returns the downcasted uint192 from uint256, reverting on + * overflow (when the input is greater than largest uint192). + * + * Counterpart to Solidity's `uint192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toUint192(uint256 value) internal pure returns (uint192) { + if (value > type(uint192).max) { + revert SafeCastOverflowedUintDowncast(192, value); + } + return uint192(value); + } + + /** + * @dev Returns the downcasted uint184 from uint256, reverting on + * overflow (when the input is greater than largest uint184). + * + * Counterpart to Solidity's `uint184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toUint184(uint256 value) internal pure returns (uint184) { + if (value > type(uint184).max) { + revert SafeCastOverflowedUintDowncast(184, value); + } + return uint184(value); + } + + /** + * @dev Returns the downcasted uint176 from uint256, reverting on + * overflow (when the input is greater than largest uint176). + * + * Counterpart to Solidity's `uint176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toUint176(uint256 value) internal pure returns (uint176) { + if (value > type(uint176).max) { + revert SafeCastOverflowedUintDowncast(176, value); + } + return uint176(value); + } + + /** + * @dev Returns the downcasted uint168 from uint256, reverting on + * overflow (when the input is greater than largest uint168). + * + * Counterpart to Solidity's `uint168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toUint168(uint256 value) internal pure returns (uint168) { + if (value > type(uint168).max) { + revert SafeCastOverflowedUintDowncast(168, value); + } + return uint168(value); + } + + /** + * @dev Returns the downcasted uint160 from uint256, reverting on + * overflow (when the input is greater than largest uint160). + * + * Counterpart to Solidity's `uint160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toUint160(uint256 value) internal pure returns (uint160) { + if (value > type(uint160).max) { + revert SafeCastOverflowedUintDowncast(160, value); + } + return uint160(value); + } + + /** + * @dev Returns the downcasted uint152 from uint256, reverting on + * overflow (when the input is greater than largest uint152). + * + * Counterpart to Solidity's `uint152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toUint152(uint256 value) internal pure returns (uint152) { + if (value > type(uint152).max) { + revert SafeCastOverflowedUintDowncast(152, value); + } + return uint152(value); + } + + /** + * @dev Returns the downcasted uint144 from uint256, reverting on + * overflow (when the input is greater than largest uint144). + * + * Counterpart to Solidity's `uint144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toUint144(uint256 value) internal pure returns (uint144) { + if (value > type(uint144).max) { + revert SafeCastOverflowedUintDowncast(144, value); + } + return uint144(value); + } + + /** + * @dev Returns the downcasted uint136 from uint256, reverting on + * overflow (when the input is greater than largest uint136). + * + * Counterpart to Solidity's `uint136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toUint136(uint256 value) internal pure returns (uint136) { + if (value > type(uint136).max) { + revert SafeCastOverflowedUintDowncast(136, value); + } + return uint136(value); + } + + /** + * @dev Returns the downcasted uint128 from uint256, reverting on + * overflow (when the input is greater than largest uint128). + * + * Counterpart to Solidity's `uint128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toUint128(uint256 value) internal pure returns (uint128) { + if (value > type(uint128).max) { + revert SafeCastOverflowedUintDowncast(128, value); + } + return uint128(value); + } + + /** + * @dev Returns the downcasted uint120 from uint256, reverting on + * overflow (when the input is greater than largest uint120). + * + * Counterpart to Solidity's `uint120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toUint120(uint256 value) internal pure returns (uint120) { + if (value > type(uint120).max) { + revert SafeCastOverflowedUintDowncast(120, value); + } + return uint120(value); + } + + /** + * @dev Returns the downcasted uint112 from uint256, reverting on + * overflow (when the input is greater than largest uint112). + * + * Counterpart to Solidity's `uint112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toUint112(uint256 value) internal pure returns (uint112) { + if (value > type(uint112).max) { + revert SafeCastOverflowedUintDowncast(112, value); + } + return uint112(value); + } + + /** + * @dev Returns the downcasted uint104 from uint256, reverting on + * overflow (when the input is greater than largest uint104). + * + * Counterpart to Solidity's `uint104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toUint104(uint256 value) internal pure returns (uint104) { + if (value > type(uint104).max) { + revert SafeCastOverflowedUintDowncast(104, value); + } + return uint104(value); + } + + /** + * @dev Returns the downcasted uint96 from uint256, reverting on + * overflow (when the input is greater than largest uint96). + * + * Counterpart to Solidity's `uint96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toUint96(uint256 value) internal pure returns (uint96) { + if (value > type(uint96).max) { + revert SafeCastOverflowedUintDowncast(96, value); + } + return uint96(value); + } + + /** + * @dev Returns the downcasted uint88 from uint256, reverting on + * overflow (when the input is greater than largest uint88). + * + * Counterpart to Solidity's `uint88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toUint88(uint256 value) internal pure returns (uint88) { + if (value > type(uint88).max) { + revert SafeCastOverflowedUintDowncast(88, value); + } + return uint88(value); + } + + /** + * @dev Returns the downcasted uint80 from uint256, reverting on + * overflow (when the input is greater than largest uint80). + * + * Counterpart to Solidity's `uint80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toUint80(uint256 value) internal pure returns (uint80) { + if (value > type(uint80).max) { + revert SafeCastOverflowedUintDowncast(80, value); + } + return uint80(value); + } + + /** + * @dev Returns the downcasted uint72 from uint256, reverting on + * overflow (when the input is greater than largest uint72). + * + * Counterpart to Solidity's `uint72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toUint72(uint256 value) internal pure returns (uint72) { + if (value > type(uint72).max) { + revert SafeCastOverflowedUintDowncast(72, value); + } + return uint72(value); + } + + /** + * @dev Returns the downcasted uint64 from uint256, reverting on + * overflow (when the input is greater than largest uint64). + * + * Counterpart to Solidity's `uint64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toUint64(uint256 value) internal pure returns (uint64) { + if (value > type(uint64).max) { + revert SafeCastOverflowedUintDowncast(64, value); + } + return uint64(value); + } + + /** + * @dev Returns the downcasted uint56 from uint256, reverting on + * overflow (when the input is greater than largest uint56). + * + * Counterpart to Solidity's `uint56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toUint56(uint256 value) internal pure returns (uint56) { + if (value > type(uint56).max) { + revert SafeCastOverflowedUintDowncast(56, value); + } + return uint56(value); + } + + /** + * @dev Returns the downcasted uint48 from uint256, reverting on + * overflow (when the input is greater than largest uint48). + * + * Counterpart to Solidity's `uint48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toUint48(uint256 value) internal pure returns (uint48) { + if (value > type(uint48).max) { + revert SafeCastOverflowedUintDowncast(48, value); + } + return uint48(value); + } + + /** + * @dev Returns the downcasted uint40 from uint256, reverting on + * overflow (when the input is greater than largest uint40). + * + * Counterpart to Solidity's `uint40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toUint40(uint256 value) internal pure returns (uint40) { + if (value > type(uint40).max) { + revert SafeCastOverflowedUintDowncast(40, value); + } + return uint40(value); + } + + /** + * @dev Returns the downcasted uint32 from uint256, reverting on + * overflow (when the input is greater than largest uint32). + * + * Counterpart to Solidity's `uint32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toUint32(uint256 value) internal pure returns (uint32) { + if (value > type(uint32).max) { + revert SafeCastOverflowedUintDowncast(32, value); + } + return uint32(value); + } + + /** + * @dev Returns the downcasted uint24 from uint256, reverting on + * overflow (when the input is greater than largest uint24). + * + * Counterpart to Solidity's `uint24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toUint24(uint256 value) internal pure returns (uint24) { + if (value > type(uint24).max) { + revert SafeCastOverflowedUintDowncast(24, value); + } + return uint24(value); + } + + /** + * @dev Returns the downcasted uint16 from uint256, reverting on + * overflow (when the input is greater than largest uint16). + * + * Counterpart to Solidity's `uint16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toUint16(uint256 value) internal pure returns (uint16) { + if (value > type(uint16).max) { + revert SafeCastOverflowedUintDowncast(16, value); + } + return uint16(value); + } + + /** + * @dev Returns the downcasted uint8 from uint256, reverting on + * overflow (when the input is greater than largest uint8). + * + * Counterpart to Solidity's `uint8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toUint8(uint256 value) internal pure returns (uint8) { + if (value > type(uint8).max) { + revert SafeCastOverflowedUintDowncast(8, value); + } + return uint8(value); + } + + /** + * @dev Converts a signed int256 into an unsigned uint256. + * + * Requirements: + * + * - input must be greater than or equal to 0. + */ + function toUint256(int256 value) internal pure returns (uint256) { + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } + return uint256(value); + } + + /** + * @dev Returns the downcasted int248 from int256, reverting on + * overflow (when the input is less than smallest int248 or + * greater than largest int248). + * + * Counterpart to Solidity's `int248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toInt248(int256 value) internal pure returns (int248 downcasted) { + downcasted = int248(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(248, value); + } + } + + /** + * @dev Returns the downcasted int240 from int256, reverting on + * overflow (when the input is less than smallest int240 or + * greater than largest int240). + * + * Counterpart to Solidity's `int240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toInt240(int256 value) internal pure returns (int240 downcasted) { + downcasted = int240(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(240, value); + } + } + + /** + * @dev Returns the downcasted int232 from int256, reverting on + * overflow (when the input is less than smallest int232 or + * greater than largest int232). + * + * Counterpart to Solidity's `int232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toInt232(int256 value) internal pure returns (int232 downcasted) { + downcasted = int232(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(232, value); + } + } + + /** + * @dev Returns the downcasted int224 from int256, reverting on + * overflow (when the input is less than smallest int224 or + * greater than largest int224). + * + * Counterpart to Solidity's `int224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toInt224(int256 value) internal pure returns (int224 downcasted) { + downcasted = int224(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(224, value); + } + } + + /** + * @dev Returns the downcasted int216 from int256, reverting on + * overflow (when the input is less than smallest int216 or + * greater than largest int216). + * + * Counterpart to Solidity's `int216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toInt216(int256 value) internal pure returns (int216 downcasted) { + downcasted = int216(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(216, value); + } + } + + /** + * @dev Returns the downcasted int208 from int256, reverting on + * overflow (when the input is less than smallest int208 or + * greater than largest int208). + * + * Counterpart to Solidity's `int208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toInt208(int256 value) internal pure returns (int208 downcasted) { + downcasted = int208(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(208, value); + } + } + + /** + * @dev Returns the downcasted int200 from int256, reverting on + * overflow (when the input is less than smallest int200 or + * greater than largest int200). + * + * Counterpart to Solidity's `int200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toInt200(int256 value) internal pure returns (int200 downcasted) { + downcasted = int200(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(200, value); + } + } + + /** + * @dev Returns the downcasted int192 from int256, reverting on + * overflow (when the input is less than smallest int192 or + * greater than largest int192). + * + * Counterpart to Solidity's `int192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toInt192(int256 value) internal pure returns (int192 downcasted) { + downcasted = int192(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(192, value); + } + } + + /** + * @dev Returns the downcasted int184 from int256, reverting on + * overflow (when the input is less than smallest int184 or + * greater than largest int184). + * + * Counterpart to Solidity's `int184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toInt184(int256 value) internal pure returns (int184 downcasted) { + downcasted = int184(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(184, value); + } + } + + /** + * @dev Returns the downcasted int176 from int256, reverting on + * overflow (when the input is less than smallest int176 or + * greater than largest int176). + * + * Counterpart to Solidity's `int176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toInt176(int256 value) internal pure returns (int176 downcasted) { + downcasted = int176(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(176, value); + } + } + + /** + * @dev Returns the downcasted int168 from int256, reverting on + * overflow (when the input is less than smallest int168 or + * greater than largest int168). + * + * Counterpart to Solidity's `int168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toInt168(int256 value) internal pure returns (int168 downcasted) { + downcasted = int168(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(168, value); + } + } + + /** + * @dev Returns the downcasted int160 from int256, reverting on + * overflow (when the input is less than smallest int160 or + * greater than largest int160). + * + * Counterpart to Solidity's `int160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toInt160(int256 value) internal pure returns (int160 downcasted) { + downcasted = int160(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(160, value); + } + } + + /** + * @dev Returns the downcasted int152 from int256, reverting on + * overflow (when the input is less than smallest int152 or + * greater than largest int152). + * + * Counterpart to Solidity's `int152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toInt152(int256 value) internal pure returns (int152 downcasted) { + downcasted = int152(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(152, value); + } + } + + /** + * @dev Returns the downcasted int144 from int256, reverting on + * overflow (when the input is less than smallest int144 or + * greater than largest int144). + * + * Counterpart to Solidity's `int144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toInt144(int256 value) internal pure returns (int144 downcasted) { + downcasted = int144(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(144, value); + } + } + + /** + * @dev Returns the downcasted int136 from int256, reverting on + * overflow (when the input is less than smallest int136 or + * greater than largest int136). + * + * Counterpart to Solidity's `int136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toInt136(int256 value) internal pure returns (int136 downcasted) { + downcasted = int136(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(136, value); + } + } + + /** + * @dev Returns the downcasted int128 from int256, reverting on + * overflow (when the input is less than smallest int128 or + * greater than largest int128). + * + * Counterpart to Solidity's `int128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toInt128(int256 value) internal pure returns (int128 downcasted) { + downcasted = int128(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(128, value); + } + } + + /** + * @dev Returns the downcasted int120 from int256, reverting on + * overflow (when the input is less than smallest int120 or + * greater than largest int120). + * + * Counterpart to Solidity's `int120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toInt120(int256 value) internal pure returns (int120 downcasted) { + downcasted = int120(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(120, value); + } + } + + /** + * @dev Returns the downcasted int112 from int256, reverting on + * overflow (when the input is less than smallest int112 or + * greater than largest int112). + * + * Counterpart to Solidity's `int112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toInt112(int256 value) internal pure returns (int112 downcasted) { + downcasted = int112(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(112, value); + } + } + + /** + * @dev Returns the downcasted int104 from int256, reverting on + * overflow (when the input is less than smallest int104 or + * greater than largest int104). + * + * Counterpart to Solidity's `int104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toInt104(int256 value) internal pure returns (int104 downcasted) { + downcasted = int104(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(104, value); + } + } + + /** + * @dev Returns the downcasted int96 from int256, reverting on + * overflow (when the input is less than smallest int96 or + * greater than largest int96). + * + * Counterpart to Solidity's `int96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toInt96(int256 value) internal pure returns (int96 downcasted) { + downcasted = int96(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(96, value); + } + } + + /** + * @dev Returns the downcasted int88 from int256, reverting on + * overflow (when the input is less than smallest int88 or + * greater than largest int88). + * + * Counterpart to Solidity's `int88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toInt88(int256 value) internal pure returns (int88 downcasted) { + downcasted = int88(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(88, value); + } + } + + /** + * @dev Returns the downcasted int80 from int256, reverting on + * overflow (when the input is less than smallest int80 or + * greater than largest int80). + * + * Counterpart to Solidity's `int80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toInt80(int256 value) internal pure returns (int80 downcasted) { + downcasted = int80(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(80, value); + } + } + + /** + * @dev Returns the downcasted int72 from int256, reverting on + * overflow (when the input is less than smallest int72 or + * greater than largest int72). + * + * Counterpart to Solidity's `int72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toInt72(int256 value) internal pure returns (int72 downcasted) { + downcasted = int72(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(72, value); + } + } + + /** + * @dev Returns the downcasted int64 from int256, reverting on + * overflow (when the input is less than smallest int64 or + * greater than largest int64). + * + * Counterpart to Solidity's `int64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toInt64(int256 value) internal pure returns (int64 downcasted) { + downcasted = int64(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(64, value); + } + } + + /** + * @dev Returns the downcasted int56 from int256, reverting on + * overflow (when the input is less than smallest int56 or + * greater than largest int56). + * + * Counterpart to Solidity's `int56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toInt56(int256 value) internal pure returns (int56 downcasted) { + downcasted = int56(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(56, value); + } + } + + /** + * @dev Returns the downcasted int48 from int256, reverting on + * overflow (when the input is less than smallest int48 or + * greater than largest int48). + * + * Counterpart to Solidity's `int48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toInt48(int256 value) internal pure returns (int48 downcasted) { + downcasted = int48(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(48, value); + } + } + + /** + * @dev Returns the downcasted int40 from int256, reverting on + * overflow (when the input is less than smallest int40 or + * greater than largest int40). + * + * Counterpart to Solidity's `int40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toInt40(int256 value) internal pure returns (int40 downcasted) { + downcasted = int40(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(40, value); + } + } + + /** + * @dev Returns the downcasted int32 from int256, reverting on + * overflow (when the input is less than smallest int32 or + * greater than largest int32). + * + * Counterpart to Solidity's `int32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toInt32(int256 value) internal pure returns (int32 downcasted) { + downcasted = int32(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(32, value); + } + } + + /** + * @dev Returns the downcasted int24 from int256, reverting on + * overflow (when the input is less than smallest int24 or + * greater than largest int24). + * + * Counterpart to Solidity's `int24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toInt24(int256 value) internal pure returns (int24 downcasted) { + downcasted = int24(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(24, value); + } + } + + /** + * @dev Returns the downcasted int16 from int256, reverting on + * overflow (when the input is less than smallest int16 or + * greater than largest int16). + * + * Counterpart to Solidity's `int16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toInt16(int256 value) internal pure returns (int16 downcasted) { + downcasted = int16(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(16, value); + } + } + + /** + * @dev Returns the downcasted int8 from int256, reverting on + * overflow (when the input is less than smallest int8 or + * greater than largest int8). + * + * Counterpart to Solidity's `int8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toInt8(int256 value) internal pure returns (int8 downcasted) { + downcasted = int8(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(8, value); + } + } + + /** + * @dev Converts an unsigned uint256 into a signed int256. + * + * Requirements: + * + * - input must be less than or equal to maxInt256. + */ + function toInt256(uint256 value) internal pure returns (int256) { + // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive + if (value > uint256(type(int256).max)) { + revert SafeCastOverflowedUintToInt(value); + } + return int256(value); + } + + /** + * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump. + */ + function toUint(bool b) internal pure returns (uint256 u) { + assembly ("memory-safe") { + u := iszero(iszero(b)) + } + } +} \ No newline at end of file diff --git a/contracts/lib/rooster/v2-common/libraries/ArrayOperations.sol b/contracts/lib/rooster/v2-common/libraries/ArrayOperations.sol new file mode 100644 index 0000000000..e2629b77a9 --- /dev/null +++ b/contracts/lib/rooster/v2-common/libraries/ArrayOperations.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +library ArrayOperations { + error ArrayElementsNotUnique(uint256 index, uint256 duplicateEntry); + + /** + * @notice Checks that array of numbers are unique. + * @param array Array of numbers to check. + * @param maxArrayElementValue Maximum value possible in Array. + */ + function checkUnique(uint32[] memory array, uint256 maxArrayElementValue) internal pure { + // for pool with few bins (less than ~100k), the bitmap approach is + // more gas efficient than exhaustive search of a 10-bin list. As the + // bin count in a pool grows beyond this, the gas cost of the bitmap + // memory allocation quickly overtakes the exhaustive search cost. + if (maxArrayElementValue < 100_000) { + // allocate bitmap and set indexes to check uniqueness + checkUniqueViaBitMap(array, maxArrayElementValue); + } else { + // search to check uniqueness + checkUniqueViaSearch(array); + } + } + + /** + * @notice Search all pair-wise combinations; low memory, but quadratic + * comparison cost. + */ + function checkUniqueViaSearch(uint32[] memory array) internal pure { + uint256 length = array.length; + if (length <= 1) return; + for (uint256 i = 0; i < length - 1; i++) { + for (uint256 j = i + 1; j < length; j++) { + if (array[i] == array[j]) revert ArrayElementsNotUnique(j, array[j]); + } + } + } + + /** + * @notice Fill bitmap with values and revert on collision; memory is + * proportional to pool bin count while comparison costs are linear in + * array length. + */ + function checkUniqueViaBitMap(uint32[] memory array, uint256 maxArrayElementValue) internal pure { + uint256 length = array.length; + if (length <= 1) return; + uint256[] memory bitMap = new uint256[]((maxArrayElementValue >> 8) + 1); + for (uint256 i; i < length; i++) { + if (get(bitMap, array[i])) revert ArrayElementsNotUnique(i, array[i]); + set(bitMap, array[i]); + } + } + + /** + * @notice Gets the bit at `index`. + */ + function get(uint256[] memory bitmap, uint256 index) private pure returns (bool) { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + return bitmap[bucket] & mask != 0; + } + + /** + * @notice Sets the bit at `index`. + */ + function set(uint256[] memory bitmap, uint256 index) private pure { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap[bucket] |= mask; + } +} \ No newline at end of file diff --git a/contracts/lib/rooster/v2-common/libraries/Constants.sol b/contracts/lib/rooster/v2-common/libraries/Constants.sol new file mode 100644 index 0000000000..0e69b50a5a --- /dev/null +++ b/contracts/lib/rooster/v2-common/libraries/Constants.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +// factory contraints on pools +uint8 constant MAX_PROTOCOL_FEE_RATIO_D3 = 0.25e3; // 25% +uint256 constant MAX_PROTOCOL_LENDING_FEE_RATE_D18 = 0.02e18; // 2% +uint64 constant MAX_POOL_FEE_D18 = 0.9e18; // 90% +uint64 constant MIN_LOOKBACK = 1 seconds; + +// pool constraints +uint8 constant NUMBER_OF_KINDS = 4; +int32 constant NUMBER_OF_KINDS_32 = int32(int8(NUMBER_OF_KINDS)); +uint256 constant MAX_TICK = 322_378; // max price 1e14 in D18 scale +int32 constant MAX_TICK_32 = int32(int256(MAX_TICK)); +int32 constant MIN_TICK_32 = int32(-int256(MAX_TICK)); +uint256 constant MAX_BINS_TO_MERGE = 3; +uint128 constant MINIMUM_LIQUIDITY = 1e8; + +// accessor named constants +uint8 constant ALL_KINDS_MASK = 0xF; // 0b1111 +uint8 constant PERMISSIONED_LIQUIDITY_MASK = 0x10; // 0b010000 +uint8 constant PERMISSIONED_SWAP_MASK = 0x20; // 0b100000 +uint8 constant OPTIONS_MASK = ALL_KINDS_MASK | PERMISSIONED_LIQUIDITY_MASK | PERMISSIONED_SWAP_MASK; // 0b111111 + +// named values +address constant MERGED_LP_BALANCE_ADDRESS = address(0); +uint256 constant MERGED_LP_BALANCE_SUBACCOUNT = 0; +uint128 constant ONE = 1e18; +uint128 constant ONE_SQUARED = 1e36; +int256 constant INT256_ONE = 1e18; +uint256 constant ONE_D8 = 1e8; +uint256 constant ONE_D3 = 1e3; +int40 constant INT_ONE_D8 = 1e8; +int40 constant HALF_TICK_D8 = 0.5e8; +uint8 constant DEFAULT_DECIMALS = 18; +uint256 constant DEFAULT_SCALE = 1; +bytes constant EMPTY_PRICE_BREAKS = hex"010000000000000000000000"; \ No newline at end of file diff --git a/contracts/lib/rooster/v2-common/libraries/Math.sol b/contracts/lib/rooster/v2-common/libraries/Math.sol new file mode 100644 index 0000000000..f6240553ad --- /dev/null +++ b/contracts/lib/rooster/v2-common/libraries/Math.sol @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +import {Math as OzMath} from "../../openzeppelin-custom/contracts/utils/math/Math.sol"; + +import {ONE, DEFAULT_SCALE, DEFAULT_DECIMALS, INT_ONE_D8, ONE_SQUARED} from "./Constants.sol"; + +/** + * @notice Math functions. + */ +library Math { + /** + * @notice Returns the lesser of two values. + * @param x First uint256 value. + * @param y Second uint256 value. + */ + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly ("memory-safe") { + z := xor(x, mul(xor(x, y), lt(y, x))) + } + } + + /** + * @notice Returns the lesser of two uint128 values. + * @param x First uint128 value. + * @param y Second uint128 value. + */ + function min128(uint128 x, uint128 y) internal pure returns (uint128 z) { + assembly ("memory-safe") { + z := xor(x, mul(xor(x, y), lt(y, x))) + } + } + + /** + * @notice Returns the lesser of two int256 values. + * @param x First int256 value. + * @param y Second int256 value. + */ + function min(int256 x, int256 y) internal pure returns (int256 z) { + assembly ("memory-safe") { + z := xor(x, mul(xor(x, y), slt(y, x))) + } + } + + /** + * @notice Returns the greater of two uint256 values. + * @param x First uint256 value. + * @param y Second uint256 value. + */ + function max(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly ("memory-safe") { + z := xor(x, mul(xor(x, y), gt(y, x))) + } + } + + /** + * @notice Returns the greater of two int256 values. + * @param x First int256 value. + * @param y Second int256 value. + */ + function max(int256 x, int256 y) internal pure returns (int256 z) { + assembly ("memory-safe") { + z := xor(x, mul(xor(x, y), sgt(y, x))) + } + } + + /** + * @notice Returns the greater of two uint128 values. + * @param x First uint128 value. + * @param y Second uint128 value. + */ + function max128(uint128 x, uint128 y) internal pure returns (uint128 z) { + assembly ("memory-safe") { + z := xor(x, mul(xor(x, y), gt(y, x))) + } + } + + /** + * @notice Thresholds a value to be within the specified bounds. + * @param value The value to bound. + * @param lowerLimit The minimum allowable value. + * @param upperLimit The maximum allowable value. + */ + function boundValue( + uint256 value, + uint256 lowerLimit, + uint256 upperLimit + ) internal pure returns (uint256 outputValue) { + outputValue = min(max(value, lowerLimit), upperLimit); + } + + /** + * @notice Returns the difference between two uint128 values or zero if the result would be negative. + * @param x The minuend. + * @param y The subtrahend. + */ + function clip128(uint128 x, uint128 y) internal pure returns (uint128) { + unchecked { + return x < y ? 0 : x - y; + } + } + + /** + * @notice Returns the difference between two uint256 values or zero if the result would be negative. + * @param x The minuend. + * @param y The subtrahend. + */ + function clip(uint256 x, uint256 y) internal pure returns (uint256) { + unchecked { + return x < y ? 0 : x - y; + } + } + + /** + * @notice Divides one uint256 by another, rounding down to the nearest + * integer. + * @param x The dividend. + * @param y The divisor. + */ + function divFloor(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivFloor(x, ONE, y); + } + + /** + * @notice Divides one uint256 by another, rounding up to the nearest integer. + * @param x The dividend. + * @param y The divisor. + */ + function divCeil(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivCeil(x, ONE, y); + } + + /** + * @notice Multiplies two uint256 values and then divides by ONE, rounding down. + * @param x The multiplicand. + * @param y The multiplier. + */ + function mulFloor(uint256 x, uint256 y) internal pure returns (uint256) { + return OzMath.mulDiv(x, y, ONE); + } + + /** + * @notice Multiplies two uint256 values and then divides by ONE, rounding up. + * @param x The multiplicand. + * @param y The multiplier. + */ + function mulCeil(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivCeil(x, y, ONE); + } + + /** + * @notice Calculates the multiplicative inverse of a uint256, rounding down. + * @param x The value to invert. + */ + function invFloor(uint256 x) internal pure returns (uint256) { + unchecked { + return ONE_SQUARED / x; + } + } + + /** + * @notice Calculates the multiplicative inverse of a uint256, rounding up. + * @param denominator The value to invert. + */ + function invCeil(uint256 denominator) internal pure returns (uint256 z) { + assembly ("memory-safe") { + // divide z - 1 by the denominator and add 1. + z := add(div(sub(ONE_SQUARED, 1), denominator), 1) + } + } + + /** + * @notice Multiplies two uint256 values and divides by a third, rounding down. + * @param x The multiplicand. + * @param y The multiplier. + * @param k The divisor. + */ + function mulDivFloor(uint256 x, uint256 y, uint256 k) internal pure returns (uint256 result) { + result = OzMath.mulDiv(x, y, max(1, k)); + } + + /** + * @notice Multiplies two uint256 values and divides by a third, rounding up if there's a remainder. + * @param x The multiplicand. + * @param y The multiplier. + * @param k The divisor. + */ + function mulDivCeil(uint256 x, uint256 y, uint256 k) internal pure returns (uint256 result) { + result = mulDivFloor(x, y, k); + if (mulmod(x, y, max(1, k)) != 0) result = result + 1; + } + + /** + * @notice Multiplies two uint256 values and divides by a third, rounding + * down. Will revert if `x * y` is larger than `type(uint256).max`. + * @param x The first operand for multiplication. + * @param y The second operand for multiplication. + * @param denominator The divisor after multiplication. + */ + function mulDivDown(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 z) { + assembly ("memory-safe") { + // Store x * y in z for now. + z := mul(x, y) + if iszero(denominator) { + denominator := 1 + } + + if iszero(or(iszero(x), eq(div(z, x), y))) { + revert(0, 0) + } + + // Divide z by the denominator. + z := div(z, denominator) + } + } + + /** + * @notice Multiplies two uint256 values and divides by a third, rounding + * up. Will revert if `x * y` is larger than `type(uint256).max`. + * @param x The first operand for multiplication. + * @param y The second operand for multiplication. + * @param denominator The divisor after multiplication. + */ + function mulDivUp(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 z) { + assembly ("memory-safe") { + // Store x * y in z for now. + z := mul(x, y) + if iszero(denominator) { + denominator := 1 + } + + if iszero(or(iszero(x), eq(div(z, x), y))) { + revert(0, 0) + } + + // First, divide z - 1 by the denominator and add 1. + // We allow z - 1 to underflow if z is 0, because we multiply the + // end result by 0 if z is zero, ensuring we return 0 if z is zero. + z := mul(iszero(iszero(z)), add(div(sub(z, 1), denominator), 1)) + } + } + + /** + * @notice Multiplies a uint256 by another and divides by a constant, + * rounding down. Will revert if `x * y` is larger than + * `type(uint256).max`. + * @param x The multiplicand. + * @param y The multiplier. + */ + function mulDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, y, ONE); + } + + /** + * @notice Divides a uint256 by another, rounding down the result. Will + * revert if `x * 1e18` is larger than `type(uint256).max`. + * @param x The dividend. + * @param y The divisor. + */ + function divDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, ONE, y); + } + + /** + * @notice Divides a uint256 by another, rounding up the result. Will + * revert if `x * 1e18` is larger than `type(uint256).max`. + * @param x The dividend. + * @param y The divisor. + */ + function divUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, ONE, y); + } + + /** + * @notice Scales a number based on a difference in decimals from a default. + * @param decimals The new decimal precision. + */ + function scale(uint8 decimals) internal pure returns (uint256) { + unchecked { + if (decimals == DEFAULT_DECIMALS) { + return DEFAULT_SCALE; + } else { + return 10 ** (DEFAULT_DECIMALS - decimals); + } + } + } + + /** + * @notice Adjusts a scaled amount to the token decimal scale. + * @param amount The scaled amount. + * @param scaleFactor The scaling factor to adjust by. + * @param ceil Whether to round up (true) or down (false). + */ + function ammScaleToTokenScale(uint256 amount, uint256 scaleFactor, bool ceil) internal pure returns (uint256 z) { + unchecked { + if (scaleFactor == DEFAULT_SCALE || amount == 0) { + return amount; + } else { + if (!ceil) return amount / scaleFactor; + assembly ("memory-safe") { + z := add(div(sub(amount, 1), scaleFactor), 1) + } + } + } + } + + /** + * @notice Adjusts a token amount to the D18 AMM scale. + * @param amount The amount in token scale. + * @param scaleFactor The scale factor for adjustment. + */ + function tokenScaleToAmmScale(uint256 amount, uint256 scaleFactor) internal pure returns (uint256) { + if (scaleFactor == DEFAULT_SCALE) { + return amount; + } else { + return amount * scaleFactor; + } + } + + /** + * @notice Returns the absolute value of a signed 32-bit integer. + * @param x The integer to take the absolute value of. + */ + function abs32(int32 x) internal pure returns (uint32) { + unchecked { + return uint32(x < 0 ? -x : x); + } + } + + /** + * @notice Returns the absolute value of a signed 256-bit integer. + * @param x The integer to take the absolute value of. + */ + function abs(int256 x) internal pure returns (uint256) { + unchecked { + return uint256(x < 0 ? -x : x); + } + } + + /** + * @notice Calculates the integer square root of a uint256 rounded down. + * @param x The number to take the square root of. + */ + function sqrt(uint256 x) internal pure returns (uint256 z) { + // from https://github.com/transmissions11/solmate/blob/e8f96f25d48fe702117ce76c79228ca4f20206cb/src/utils/FixedPointMathLib.sol + assembly ("memory-safe") { + let y := x + z := 181 + + if iszero(lt(y, 0x10000000000000000000000000000000000)) { + y := shr(128, y) + z := shl(64, z) + } + if iszero(lt(y, 0x1000000000000000000)) { + y := shr(64, y) + z := shl(32, z) + } + if iszero(lt(y, 0x10000000000)) { + y := shr(32, y) + z := shl(16, z) + } + if iszero(lt(y, 0x1000000)) { + y := shr(16, y) + z := shl(8, z) + } + + z := shr(18, mul(z, add(y, 65536))) + + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + z := sub(z, lt(div(x, z), z)) + } + } + + /** + * @notice Computes the floor of a D8-scaled number as an int32, ignoring + * potential overflow in the cast. + * @param val The D8-scaled number. + */ + function floorD8Unchecked(int256 val) internal pure returns (int32) { + int32 val32; + bool check; + unchecked { + val32 = int32(val / INT_ONE_D8); + check = (val < 0 && val % INT_ONE_D8 != 0); + } + return check ? val32 - 1 : val32; + } +} \ No newline at end of file diff --git a/contracts/lib/rooster/v2-common/libraries/PoolLib.sol b/contracts/lib/rooster/v2-common/libraries/PoolLib.sol new file mode 100644 index 0000000000..1cd973af06 --- /dev/null +++ b/contracts/lib/rooster/v2-common/libraries/PoolLib.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +import {SafeCast as Cast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IMaverickV2Pool} from "../interfaces/IMaverickV2Pool.sol"; +import {TickMath} from "./TickMath.sol"; +import {Math} from "./Math.sol"; + +/** + * @notice Library of pool functions. + */ +library PoolLib { + using Cast for uint256; + + struct AddLiquidityInfo { + uint256 deltaA; + uint256 deltaB; + bool tickLtActive; + uint256 tickSpacing; + int32 tick; + } + + /** + * @notice Check to ensure that the ticks are in ascending order and amount + * array is same length as tick array. + * @param ticks An array of int32 values representing ticks to be checked. + * @param amountsLength Amount array length. + */ + function uniqueOrderedTicksCheck(int32[] memory ticks, uint256 amountsLength) internal pure { + unchecked { + if (ticks.length != amountsLength) + revert IMaverickV2Pool.PoolTicksAmountsLengthMismatch(ticks.length, amountsLength); + int32 lastTick = type(int32).min; + for (uint256 i; i < ticks.length; ) { + if (ticks[i] <= lastTick) revert IMaverickV2Pool.PoolTicksNotSorted(i, lastTick, ticks[i]); + lastTick = ticks[i]; + i = i + 1; + } + } + } + + /** + * @notice Compute bin reserves assuming the bin is not merged; not accurate + * reflection of reserves for merged bins. + * @param bin The storage reference to the state for this bin. + * @param tick The memory reference to the state for this tick. + * @return reserveA The reserve amount for token A. + * @return reserveB The reserve amount for token B. + */ + function binReserves( + IMaverickV2Pool.BinState storage bin, + IMaverickV2Pool.TickState memory tick + ) internal view returns (uint128 reserveA, uint128 reserveB) { + return binReserves(bin.tickBalance, tick.reserveA, tick.reserveB, tick.totalSupply); + } + + /** + * @notice Compute bin reserves assuming the bin is not merged; not accurate + * reflection of reserves for merged bins. + * @param tickBalance Bin's balance in the tick. + * @param tickReserveA Tick's tokenA reserves. + * @param tickReserveB Tick's tokenB reserves. + * @param tickTotalSupply Tick total supply of bin balances. + */ + function binReserves( + uint128 tickBalance, + uint128 tickReserveA, + uint128 tickReserveB, + uint128 tickTotalSupply + ) internal pure returns (uint128 reserveA, uint128 reserveB) { + if (tickTotalSupply != 0) { + reserveA = reserveValue(tickReserveA, tickBalance, tickTotalSupply); + reserveB = reserveValue(tickReserveB, tickBalance, tickTotalSupply); + } + } + + /** + * @notice Reserves of a bin in a tick. + * @param tickReserve Tick reserve amount in a given token. + * @param tickBalance Bin's balance in the tick. + * @param tickTotalSupply Tick total supply of bin balances. + */ + function reserveValue( + uint128 tickReserve, + uint128 tickBalance, + uint128 tickTotalSupply + ) internal pure returns (uint128 reserve) { + reserve = Math.mulDivFloor(tickReserve, tickBalance, tickTotalSupply).toUint128(); + reserve = Math.min128(tickReserve, reserve); + } + + /** + * @notice Calculate delta A, delta B, and delta Tick Balance based on delta + * LP balance and the Tick/Bin state. + */ + function deltaTickBalanceFromDeltaLpBalance( + uint256 binTickBalance, + uint256 binTotalSupply, + IMaverickV2Pool.TickState memory tickState, + uint128 deltaLpBalance, + AddLiquidityInfo memory addLiquidityInfo + ) internal pure returns (uint256 deltaTickBalance) { + unchecked { + if (tickState.reserveA != 0 || tickState.reserveB != 0) { + // if there are already reserves, then we just contribute pro rata + // deltaLiquidity = deltaBinLP / binTS * binTickBalance / tickTS * tickL + uint256 numerator = Math.max(1, binTickBalance) * uint256(deltaLpBalance); + uint256 denominator = Math.max(1, tickState.totalSupply) * Math.max(1, binTotalSupply); + addLiquidityInfo.deltaA = Math.mulDivCeil(tickState.reserveA, numerator, denominator); + addLiquidityInfo.deltaB = Math.mulDivCeil(tickState.reserveB, numerator, denominator); + } else { + _setRequiredDeltaReservesForEmptyTick(deltaLpBalance, addLiquidityInfo); + } + + // round down the amount credited to the tick; this could lead to a + // small add amount getting zero reserves credit. + deltaTickBalance = tickState.totalSupply == 0 + ? deltaLpBalance + : Math.mulDivDown(deltaLpBalance, Math.max(1, binTickBalance), binTotalSupply); + } + } + + /** + * @notice Calculates deltaA = liquidity * (sqrt(upper) - sqrt(lower)) + * @notice Calculates deltaB = liquidity / sqrt(lower) - liquidity / sqrt(upper), + * @notice i.e. liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower)) + * @notice we set liquidity = deltaLpBalance / (1.0001^(tick * tickspacing) - 1) + * @notice which simplifies the A/B amounts to: + * @notice deltaA = deltaLpBalance * sqrt(lower) + * @notice deltaB = deltaLpBalance / sqrt(upper) + */ + function _setRequiredDeltaReservesForEmptyTick( + uint128 deltaLpBalance, + AddLiquidityInfo memory addLiquidityInfo + ) internal pure { + // No reserves, so we will use deltaLpBalance as liquidity to be added. + // In this logic branch, the tick is empty, so we know the tick will be + // a one-asset add. + (uint256 sqrtLowerTickPrice, uint256 sqrtUpperTickPrice) = TickMath.tickSqrtPrices( + addLiquidityInfo.tickSpacing, + addLiquidityInfo.tick + ); + + addLiquidityInfo.deltaA = addLiquidityInfo.tickLtActive ? Math.mulCeil(deltaLpBalance, sqrtLowerTickPrice) : 0; + addLiquidityInfo.deltaB = addLiquidityInfo.tickLtActive ? 0 : Math.divCeil(deltaLpBalance, sqrtUpperTickPrice); + } +} \ No newline at end of file diff --git a/contracts/lib/rooster/v2-common/libraries/TickMath.sol b/contracts/lib/rooster/v2-common/libraries/TickMath.sol new file mode 100644 index 0000000000..86b7d40f40 --- /dev/null +++ b/contracts/lib/rooster/v2-common/libraries/TickMath.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +import {Math as OzMath} from "../../openzeppelin-custom/contracts/utils/math/Math.sol"; +import {Math} from "./Math.sol"; +import {MAX_TICK, ONE} from "./Constants.sol"; + +/** + * @notice Math functions related to tick operations. + */ +// slither-disable-start divide-before-multiply +library TickMath { + using Math for uint256; + + error TickMaxExceeded(int256 tick); + + /** + * @notice Compute the lower and upper sqrtPrice of a tick. + * @param tickSpacing The tick spacing used for calculations. + * @param _tick The input tick value. + */ + function tickSqrtPrices( + uint256 tickSpacing, + int32 _tick + ) internal pure returns (uint256 sqrtLowerPrice, uint256 sqrtUpperPrice) { + unchecked { + sqrtLowerPrice = tickSqrtPrice(tickSpacing, _tick); + sqrtUpperPrice = tickSqrtPrice(tickSpacing, _tick + 1); + } + } + + /** + * @notice Compute the base tick value from the pool tick and the + * tickSpacing. Revert if base tick is beyond the max tick boundary. + * @param tickSpacing The tick spacing used for calculations. + * @param _tick The input tick value. + */ + function subTickIndex(uint256 tickSpacing, int32 _tick) internal pure returns (uint32 subTick) { + subTick = Math.abs32(_tick); + subTick *= uint32(tickSpacing); + if (subTick > MAX_TICK) { + revert TickMaxExceeded(_tick); + } + } + + /** + * @notice Calculate the square root price for a given tick and tick spacing. + * @param tickSpacing The tick spacing used for calculations. + * @param _tick The input tick value. + * @return _result The square root price. + */ + function tickSqrtPrice(uint256 tickSpacing, int32 _tick) internal pure returns (uint256 _result) { + unchecked { + uint256 tick = subTickIndex(tickSpacing, _tick); + + uint256 ratio = tick & 0x1 != 0 ? 0xfffcb933bd6fad9d3af5f0b9f25db4d6 : 0x100000000000000000000000000000000; + if (tick & 0x2 != 0) ratio = (ratio * 0xfff97272373d41fd789c8cb37ffcaa1c) >> 128; + if (tick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656ac9229c67059486f389) >> 128; + if (tick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e81259b3cddc7a064941) >> 128; + if (tick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f67b19e8887e0bd251eb7) >> 128; + if (tick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98cd2e57b660be99eb2c4a) >> 128; + if (tick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c9838804e327cb417cafcb) >> 128; + if (tick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99d51e2cc356c2f617dbe0) >> 128; + if (tick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900aecf64236ab31f1f9dcb5) >> 128; + if (tick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac4d9194200696907cf2e37) >> 128; + if (tick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b88206f8abe8a3b44dd9be) >> 128; + if (tick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c578ef4f1d17b2b235d480) >> 128; + if (tick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd254ee83bdd3f248e7e785e) >> 128; + if (tick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d8f7dd10e744d913d033333) >> 128; + if (tick & 0x4000 != 0) ratio = (ratio * 0x70d869a156ddd32a39e257bc3f50aa9b) >> 128; + if (tick & 0x8000 != 0) ratio = (ratio * 0x31be135f97da6e09a19dc367e3b6da40) >> 128; + if (tick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7e5a9780b0cc4e25d61a56) >> 128; + if (tick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedbcb3a6ccb7ce618d14225) >> 128; + if (tick & 0x40000 != 0) ratio = (ratio * 0x2216e584f630389b2052b8db590e) >> 128; + if (_tick > 0) ratio = type(uint256).max / ratio; + _result = (ratio * ONE) >> 128; + } + } + + /** + * @notice Calculate liquidity of a tick. + * @param reserveA Tick reserve of token A. + * @param reserveB Tick reserve of token B. + * @param sqrtLowerTickPrice The square root price of the lower tick edge. + * @param sqrtUpperTickPrice The square root price of the upper tick edge. + */ + function getTickL( + uint256 reserveA, + uint256 reserveB, + uint256 sqrtLowerTickPrice, + uint256 sqrtUpperTickPrice + ) internal pure returns (uint256 liquidity) { + // known: + // - sqrt price values are different + // - reserveA and reserveB fit in 128 bit + // - sqrt price is in (1e-7, 1e7) + // - D18 max for uint256 is 1.15e59 + // - D18 min is 1e-18 + + unchecked { + // diff is in (5e-12, 4e6); max tick spacing is 10_000 + uint256 diff = sqrtUpperTickPrice - sqrtLowerTickPrice; + + // Need to maximize precision by shifting small values A and B up so + // that they use more of the available bit range. Two constraints to + // consider: we need A * B * diff / sqrtPrice to be bigger than 1e-18 + // when the bump is not in play. This constrains the threshold for + // bumping to be at least 77 bit; ie, either a or b needs 2^77 which + // means that term A * B * diff / sqrtPrice > 1e-18. + // + // At the other end, the second constraint is that b^2 needs to fit in + // a 256-bit number, so, post bump, the max reserve value needs to be + // less than 6e22. With a 78-bit threshold and a 57-bit bump, we have A + // and B are in (1.4e-1, 4.4e22 (2^(78+57))) with bump, and one of A or + // B is at least 2^78 without the bump, but the other reserve value may + // be as small as 1 wei. + uint256 precisionBump = 0; + if ((reserveA >> 78) == 0 && (reserveB >> 78) == 0) { + precisionBump = 57; + reserveA <<= precisionBump; + reserveB <<= precisionBump; + } + + if (reserveB == 0) return Math.divDown(reserveA, diff) >> precisionBump; + if (reserveA == 0) + return Math.mulDivDown(reserveB.mulDown(sqrtLowerTickPrice), sqrtUpperTickPrice, diff) >> precisionBump; + + // b is in (7.2e-9 (2^57 / 1e7 / 2), 2.8e29 (2^(78+57) * 1e7 / 2)) with bump + // b is in a subset of the same range without bump + uint256 b = (reserveA.divDown(sqrtUpperTickPrice) + reserveB.mulDown(sqrtLowerTickPrice)) >> 1; + + // b^2 is in (5.1e-17, 4.8e58); and will not overflow on either end; + // A*B is in (3e-13 (2^78 / 1e18 * 1e-18), 1.9e45) without bump and is in a subset range with bump + // A*B*diff/sqrtUpper is in (1.5e-17 (3e-13 * 5e-12 * 1e7), 7.6e58); + + // Since b^2 is at the upper edge of the precision range, we are not + // able to multiply the argument of the sqrt by 1e18, instead, we move + // this factor outside of the sqrt. The resulting loss of precision + // means that this liquidity value is a lower bound on the tick + // liquidity + return + OzMath.mulDiv( + b + + Math.sqrt( + (OzMath.mulDiv(b, b, ONE) + + OzMath.mulDiv(reserveB.mulFloor(reserveA), diff, sqrtUpperTickPrice)) + ) * + 1e9, + sqrtUpperTickPrice, + diff + ) >> precisionBump; + } + } + + /** + * @notice Calculate square root price of a tick. Returns left edge of the + * tick if the tick has no reserves. + * @param reserveA Tick reserve of token A. + * @param reserveB Tick reserve of token B. + * @param sqrtLowerTickPrice The square root price of the lower tick edge. + * @param sqrtUpperTickPrice The square root price of the upper tick edge. + * @return sqrtPrice The calculated square root price. + */ + function getSqrtPrice( + uint256 reserveA, + uint256 reserveB, + uint256 sqrtLowerTickPrice, + uint256 sqrtUpperTickPrice, + uint256 liquidity + ) internal pure returns (uint256 sqrtPrice) { + unchecked { + if (reserveA == 0) { + return sqrtLowerTickPrice; + } + if (reserveB == 0) { + return sqrtUpperTickPrice; + } + sqrtPrice = Math.sqrt( + ONE * + (reserveA + liquidity.mulDown(sqrtLowerTickPrice)).divDown( + reserveB + liquidity.divDown(sqrtUpperTickPrice) + ) + ); + sqrtPrice = Math.boundValue(sqrtPrice, sqrtLowerTickPrice, sqrtUpperTickPrice); + } + } + + /** + * @notice Calculate square root price of a tick. Returns left edge of the + * tick if the tick has no reserves. + * @param reserveA Tick reserve of token A. + * @param reserveB Tick reserve of token B. + * @param sqrtLowerTickPrice The square root price of the lower tick edge. + * @param sqrtUpperTickPrice The square root price of the upper tick edge. + * @return sqrtPrice The calculated square root price. + * @return liquidity The calculated liquidity. + */ + function getTickSqrtPriceAndL( + uint256 reserveA, + uint256 reserveB, + uint256 sqrtLowerTickPrice, + uint256 sqrtUpperTickPrice + ) internal pure returns (uint256 sqrtPrice, uint256 liquidity) { + liquidity = getTickL(reserveA, reserveB, sqrtLowerTickPrice, sqrtUpperTickPrice); + sqrtPrice = getSqrtPrice(reserveA, reserveB, sqrtLowerTickPrice, sqrtUpperTickPrice, liquidity); + } +} +// slither-disable-end divide-before-multiply \ No newline at end of file diff --git a/contracts/lib/rooster/v2-common/libraries/TransferLib.sol b/contracts/lib/rooster/v2-common/libraries/TransferLib.sol new file mode 100644 index 0000000000..c56f0a181d --- /dev/null +++ b/contracts/lib/rooster/v2-common/libraries/TransferLib.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// As the copyright holder of this work, Ubiquity Labs retains +// the right to distribute, use, and modify this code under any license of +// their choosing, in addition to the terms of the GPL-v2 or later. +pragma solidity ^0.8.25; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @notice Low-gas transfer functions. + */ +library TransferLib { + error TransferFailed(IERC20 token, address to, uint256 amount); + error TransferFromFailed(IERC20 token, address from, address to, uint256 amount); + + // implementation adapted from + // https://github.com/transmissions11/solmate/blob/e8f96f25d48fe702117ce76c79228ca4f20206cb/src/utils/SafeTransferLib.sol + + /** + * @notice Transfer token amount. Amount is sent from caller address to `to` address. + */ + function transfer(IERC20 token, address to, uint256 amount) internal { + bool success; + assembly ("memory-safe") { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(memPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) + // Append arguments. Addresses are assumed clean. Transfer will fail otherwise. + mstore(add(memPointer, 0x4), to) + mstore(add(memPointer, 0x24), amount) // Append the "amount" argument. + // 68 bytes total + + // fail if reverted; only allocate 32 bytes for return to ensure we + // only use mem slot 0 which is scatch space and memory safe to use. + success := call(gas(), token, 0, memPointer, 68, 0, 32) + // handle transfers that return 1/true and ensure the value is from + // the return and not dirty bits left in the scratch space. + let returnedOne := and(eq(mload(0), 1), gt(returndatasize(), 31)) + // handle transfers that return nothing + let noReturn := iszero(returndatasize()) + // good if didn't revert and the return is either empty or true + success := and(success, or(returnedOne, noReturn)) + } + + if (!success) revert TransferFailed(token, to, amount); + } + + /** + * @notice Transfer token amount. Amount is sent from `from` address to `to` address. + */ + function transferFrom(IERC20 token, address from, address to, uint256 amount) internal { + bool success; + + assembly ("memory-safe") { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(memPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) + // Append arguments. Addresses are assumed clean. Transfer will fail otherwise. + mstore(add(memPointer, 0x4), from) // Append the "from" argument. + mstore(add(memPointer, 0x24), to) // Append the "to" argument. + mstore(add(memPointer, 0x44), amount) // Append the "amount" argument. + // 100 bytes total + + // fail if reverted; only allocate 32 bytes for return to ensure we + // only use mem slot 0 which is scatch space and memory safe to use. + success := call(gas(), token, 0, memPointer, 100, 0, 32) + // handle transfers that return 1/true and ensure the value is from + // the return and not dirty bits left in the scratch space. + let returnedOne := and(eq(mload(0), 1), gt(returndatasize(), 31)) + // handle transfers that return nothing + let noReturn := iszero(returndatasize()) + // good if didn't revert and the return is either empty or true + success := and(success, or(returnedOne, noReturn)) + } + + if (!success) revert TransferFromFailed(token, from, to, amount); + } +} \ No newline at end of file diff --git a/contracts/test/_fixture-plume.js b/contracts/test/_fixture-plume.js index 02b0129a0f..57fec1bd5c 100644 --- a/contracts/test/_fixture-plume.js +++ b/contracts/test/_fixture-plume.js @@ -4,6 +4,10 @@ const mocha = require("mocha"); const { isFork, isPlumeFork, oethUnits } = require("./helpers"); const { impersonateAndFund } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); +const { deployWithConfirmation } = require("../utils/deploy"); +const { + deployPlumeMockRoosterAMOStrategyImplementation, +} = require("../deploy/deployActions.js"); const addresses = require("../utils/addresses"); const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); const log = require("../utils/logger")("test:fixtures-plume"); @@ -14,7 +18,49 @@ const BURNER_ROLE = "0x3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848"; let snapshotId; -const defaultPlumeFixture = deployments.createFixture(async () => { + +const baseFixtureWithMockedVaultAdminConfig = async () => { + const fixture = await defaultFixture(); + + const cOETHVaultProxy = await ethers.getContract("OETHPlumeVaultProxy"); + const cOETHVaultAdmin = await ethers.getContractAt( + "IVault", + cOETHVaultProxy.address + ); + await deployWithConfirmation("MockOETHVaultAdmin", [fixture.weth.address]); + + const mockVaultAdmin = await ethers.getContract("MockOETHVaultAdmin"); + await cOETHVaultAdmin + .connect(fixture.governor) + .setAdminImpl(mockVaultAdmin.address); + + fixture.oethpVault = await ethers.getContractAt( + "IMockVault", + fixture.oethpVault.address + ); + + const mockImplementation = + await deployPlumeMockRoosterAMOStrategyImplementation( + addresses.plume.OethpWETHRoosterPool + ); + + const roosterAmoStrategyProxy = await ethers.getContract( + "RoosterAMOStrategyProxy" + ); + + await roosterAmoStrategyProxy + .connect(fixture.governor) + .upgradeTo(mockImplementation.address); + + fixture.roosterAmoStrategy = await ethers.getContractAt( + "MockRoosterAMOStrategy", + roosterAmoStrategyProxy.address + ); + + return fixture; +}; + +const defaultFixture = async () => { if (!snapshotId && !isFork) { snapshotId = await nodeSnapshot(); } @@ -62,7 +108,6 @@ const defaultPlumeFixture = deployments.createFixture(async () => { const governor = isFork ? timelock : await ethers.getSigner(governorAddr); const strategist = await ethers.getSigner(strategistAddr); - // WETH const weth = await ethers.getContractAt("MockWETH", addresses.plume.WETH); // OETHp @@ -130,11 +175,26 @@ const defaultPlumeFixture = deployments.createFixture(async () => { } }; + let roosterAmoStrategy, roosterOETHpWETHpool; if (isFork) { // Allow governor to mint WETH const wethOwner = "0xb8ce2bE5c3c13712b4da61722EAd9d64bB57AbC9"; const ownerSigner = await impersonateAndFund(wethOwner); await wethMintableContract.connect(ownerSigner).addMinter(governor.address); + + // Aerodrome AMO Strategy + const roosterAmoStrategyProxy = await ethers.getContract( + "RoosterAMOStrategyProxy" + ); + roosterAmoStrategy = await ethers.getContractAt( + "RoosterAMOStrategy", + roosterAmoStrategyProxy.address + ); + + roosterOETHpWETHpool = await ethers.getContractAt( + "IMaverickV2Pool", + addresses.plume.OethpWETHRoosterPool + ); } for (const signer of [rafael, daniel, nick, domen, clement]) { @@ -142,7 +202,7 @@ const defaultPlumeFixture = deployments.createFixture(async () => { await hhHelpers.setBalance(signer.address, oethUnits("100000000")); // And WETH - _mintWETH(signer, oethUnits("10000000")); + await _mintWETH(signer, oethUnits("10000000")); // Set allowance on the vault await weth @@ -167,7 +227,8 @@ const defaultPlumeFixture = deployments.createFixture(async () => { oethp, wOETHp, oethpVault, - + roosterAmoStrategy, + roosterOETHpWETHpool, // Bridged wOETH woeth, woethProxy, @@ -177,7 +238,12 @@ const defaultPlumeFixture = deployments.createFixture(async () => { // Helpers _mintWETH, }; -}); +}; + +const defaultPlumeFixture = deployments.createFixture(defaultFixture); +const plumeFixtureWithMockedVaultAdmin = deployments.createFixture( + baseFixtureWithMockedVaultAdminConfig +); mocha.after(async () => { if (snapshotId) { @@ -187,4 +253,5 @@ mocha.after(async () => { module.exports = { defaultPlumeFixture, + plumeFixtureWithMockedVaultAdmin, }; diff --git a/contracts/test/strategies/plume/rooster-amo.plume.fork-test.js b/contracts/test/strategies/plume/rooster-amo.plume.fork-test.js new file mode 100644 index 0000000000..61d0e908a1 --- /dev/null +++ b/contracts/test/strategies/plume/rooster-amo.plume.fork-test.js @@ -0,0 +1,1136 @@ +const hre = require("hardhat"); +const { createFixtureLoader } = require("../../_fixture"); + +const addresses = require("../../../utils/addresses"); +const { plumeFixtureWithMockedVaultAdmin } = require("../../_fixture-plume"); +const { expect } = require("chai"); +const { oethUnits } = require("../../helpers"); +const ethers = hre.ethers; +const { impersonateAndFund } = require("../../../utils/signers"); +const { BigNumber } = ethers; + +const plumeFixtureWithMockedVault = createFixtureLoader( + plumeFixtureWithMockedVaultAdmin +); + +const { setERC20TokenBalance } = require("../../_fund"); + +describe("ForkTest: Rooster AMO Strategy (Plume)", async function () { + let fixture, + oethpVault, + oethVaultSigner, + oethp, + weth, + roosterAmoStrategy, + roosterOETHpWETHpool, + governor, + strategist, + rafael; + + beforeEach(async () => { + fixture = await plumeFixtureWithMockedVault(); + weth = fixture.weth; + oethp = fixture.oethp; + oethpVault = fixture.oethpVault; + roosterAmoStrategy = fixture.roosterAmoStrategy; + roosterOETHpWETHpool = fixture.roosterOETHpWETHpool; + governor = fixture.governor; + strategist = fixture.strategist; + rafael = fixture.rafael; + oethVaultSigner = await impersonateAndFund(oethpVault.address); + + await setup(); + }); + + const configureAutomaticDepositOnMint = async (vaultBuffer) => { + await oethpVault.connect(governor).setVaultBuffer(vaultBuffer); + + const totalValue = await oethpVault.totalValue(); + + // min mint to trigger deposits + return totalValue.mul(vaultBuffer).div(oethUnits("1")); + }; + + describe("ForkTest: Initial state (Plume)", function () { + it("Should have the correct initial state", async function () { + // correct pool weth share interval + expect(await roosterAmoStrategy.allowedWethShareStart()).to.equal( + oethUnits("0.10") + ); + + expect(await roosterAmoStrategy.allowedWethShareEnd()).to.equal( + oethUnits("0.25") + ); + + expect(await roosterAmoStrategy.tickDominance()).to.gt(0); + expect(await roosterAmoStrategy.tickDominance()).to.lte(oethUnits("1")); + + await verifyEndConditions(); + }); + + it("Should revert setting ptoken address", async function () { + await expect( + roosterAmoStrategy + .connect(governor) + .setPTokenAddress(weth.address, weth.address) + ).to.be.revertedWith("Unsupported method"); + }); + + it("Should revert setting ptoken address", async function () { + await expect( + roosterAmoStrategy.connect(governor).removePToken(weth.address) + ).to.be.revertedWith("Unsupported method"); + }); + + it("Should revert calling safe approve all tokens", async function () { + await expect( + roosterAmoStrategy.connect(governor).safeApproveAllTokens() + ).to.be.revertedWith("Unsupported method"); + }); + + it("Should allow calling getWethShare", async function () { + await expect( + await roosterAmoStrategy.connect(governor).getWETHShare() + ).to.be.lte(oethUnits("1")); + }); + + it("Should support WETH", async function () { + await expect( + await roosterAmoStrategy.supportsAsset(weth.address) + ).to.equal(true); + + await expect( + await roosterAmoStrategy.supportsAsset(oethp.address) + ).to.equal(false); + }); + + it("Should not revert calling public views", async function () { + await roosterAmoStrategy.getPoolSqrtPrice(); + await roosterAmoStrategy.getCurrentTradingTick(); + await roosterAmoStrategy.getPositionPrincipal(); + await roosterAmoStrategy.tickDominance(); + }); + }); + + describe("Configuration", function () { + it("Governor can set the allowed pool weth share interval", async () => { + const { roosterAmoStrategy } = fixture; + const gov = await roosterAmoStrategy.governor(); + + await roosterAmoStrategy + .connect(await impersonateAndFund(gov)) + .setAllowedPoolWethShareInterval(oethUnits("0.19"), oethUnits("0.23")); + + expect(await roosterAmoStrategy.allowedWethShareStart()).to.equal( + oethUnits("0.19") + ); + + expect(await roosterAmoStrategy.allowedWethShareEnd()).to.equal( + oethUnits("0.23") + ); + }); + + it("Only the governor can set the pool weth share", async () => { + const { rafael, roosterAmoStrategy } = fixture; + + await expect( + roosterAmoStrategy + .connect(rafael) + .setAllowedPoolWethShareInterval(oethUnits("0.19"), oethUnits("0.23")) + ).to.be.revertedWith("Caller is not the Governor"); + }); + + it("Can not mint initial position twice", async () => { + const { governor, roosterAmoStrategy } = fixture; + + await expect( + roosterAmoStrategy.connect(governor).mintInitialPosition() + ).to.be.revertedWith("Initial position already minted"); + }); + + it("Only the governor can mint the initial position", async () => { + const { rafael, roosterAmoStrategy } = fixture; + + await expect( + roosterAmoStrategy.connect(rafael).mintInitialPosition() + ).to.be.revertedWith("Caller is not the Governor"); + }); + + it("Can not set incorrect pool WETH share intervals", async () => { + const { roosterAmoStrategy } = fixture; + const gov = await roosterAmoStrategy.governor(); + + await expect( + roosterAmoStrategy + .connect(await impersonateAndFund(gov)) + .setAllowedPoolWethShareInterval(oethUnits("0.5"), oethUnits("0.4")) + ).to.be.revertedWith("Invalid interval"); + + await expect( + roosterAmoStrategy + .connect(await impersonateAndFund(gov)) + .setAllowedPoolWethShareInterval( + oethUnits("0.0001"), + oethUnits("0.5") + ) + ).to.be.revertedWith("Invalid interval start"); + + await expect( + roosterAmoStrategy + .connect(await impersonateAndFund(gov)) + .setAllowedPoolWethShareInterval(oethUnits("0.2"), oethUnits("0.96")) + ).to.be.revertedWith("Invalid interval end"); + }); + }); + + describe("Withdraw", function () { + it("Should allow withdraw when the pool is 80:20 balanced", async () => { + const { oethpVault, roosterAmoStrategy, weth } = fixture; + + const impersonatedVaultSigner = await impersonateAndFund( + oethpVault.address + ); + + const balanceBefore = await weth.balanceOf(oethpVault.address); + + const poolPrice = await roosterAmoStrategy.getPoolSqrtPrice(); + + // setup() moves the pool closer to 80:20 + const [amountWETH, amountOETHb] = + await roosterAmoStrategy.getPositionPrincipal(); + + // Try withdrawing an amount + await roosterAmoStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethpVault.address, weth.address, oethUnits("1")); + + // Make sure that 1 WETH and 4 OETHb were burned + const [amountWETHAfter, amountOETHbAfter] = + await roosterAmoStrategy.getPositionPrincipal(); + + expect(amountWETHAfter).to.approxEqualTolerance( + amountWETH.sub(oethUnits("1")) + ); + + expect(amountOETHbAfter).to.approxEqualTolerance( + amountOETHb.sub(oethUnits("4")), + 3 + ); + + // Make sure there's no price movement + expect(await roosterAmoStrategy.getPoolSqrtPrice()).to.eq(poolPrice); + + // And recipient has got it + expect(await weth.balanceOf(oethpVault.address)).to.eq( + balanceBefore.add(oethUnits("1")) + ); + + // There may remain some WETH left on the strategy contract because of the rounding when + // removing the liquidity + expect(await weth.balanceOf(roosterAmoStrategy.address)).to.lte( + BigNumber.from("1000") + ); + + await verifyEndConditions(); + }); + + it("Should allow withdrawAll when the pool is 80:20 balanced", async () => { + const { oethpVault, roosterAmoStrategy, weth, oethp } = fixture; + + const impersonatedVaultSigner = await impersonateAndFund( + oethpVault.address + ); + + const balanceBefore = await weth.balanceOf(oethpVault.address); + const supplyBefore = await oethp.totalSupply(); + + // setup() moves the pool closer to 80:20 + const [amountWETHBefore, amountOETHpBefore] = + await roosterAmoStrategy.getPositionPrincipal(); + + // Try withdrawing an amount + await roosterAmoStrategy.connect(impersonatedVaultSigner).withdrawAll(); + + // // Make sure pool is empty + const [amountWETH, amountOETHb] = + await roosterAmoStrategy.getPositionPrincipal(); + expect(amountOETHb).to.eq(0); + expect(amountWETH).to.eq(0); + + // And recipient has got it + expect(await weth.balanceOf(oethpVault.address)).to.approxEqualTolerance( + balanceBefore.add(amountWETHBefore) + ); + + // And supply has gone down + expect(await oethp.totalSupply()).to.eq( + supplyBefore.sub(amountOETHpBefore) + ); + + // There should be no WETH on the strategy contract + expect(await weth.balanceOf(roosterAmoStrategy.address)).to.eq( + oethUnits("0") + ); + }); + + it("Should allow double withdrawAll", async () => { + const { oethpVault, roosterAmoStrategy } = fixture; + + const impersonatedVaultSigner = await impersonateAndFund( + oethpVault.address + ); + + // Try withdrawing an amount + await roosterAmoStrategy.connect(impersonatedVaultSigner).withdrawAll(); + await roosterAmoStrategy.connect(impersonatedVaultSigner).withdrawAll(); + }); + + it("Should withdraw when there's little WETH in the pool", async () => { + const { oethpVault, roosterAmoStrategy, weth } = fixture; + + const impersonatedVaultSigner = await impersonateAndFund( + oethpVault.address + ); + + // Drain out most of WETH + await rebalanceThePoolToWETHRatio("0.01"); + + const balanceBefore = await weth.balanceOf(oethpVault.address); + + const [amountWETH, amountOETHb] = + await roosterAmoStrategy.getPositionPrincipal(); + + // Try withdrawing an amount + await roosterAmoStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethpVault.address, weth.address, oethUnits("0.01")); + + // Make sure that 1 WETH was burned and pool composition remains the same + const [amountWETHAfter, amountOETHbAfter] = + await roosterAmoStrategy.getPositionPrincipal(); + expect(amountWETHAfter).to.approxEqualTolerance( + amountWETH.sub(oethUnits("0.01")) + ); + expect(amountOETHbAfter.div(amountWETHAfter)).to.approxEqualTolerance( + amountOETHb.div(amountWETH) + ); + + // And recipient has got it + expect(await weth.balanceOf(oethpVault.address)).to.approxEqualTolerance( + balanceBefore.add(oethUnits("0.01")) + ); + + // There may remain some WETH left on the strategy contract because of the rounding + // when removing the liquidity + expect(await weth.balanceOf(roosterAmoStrategy.address)).to.lte( + BigNumber.from("1000") + ); + + await verifyEndConditions(); + }); + + it("Should withdrawAll when there's little WETH in the pool", async () => { + const { oethpVault, roosterAmoStrategy, weth } = fixture; + + const impersonatedVaultSigner = await impersonateAndFund( + oethpVault.address + ); + + // setup() moves the pool closer to 80:20 + + // Drain out most of WETH + await rebalanceThePoolToWETHRatio("0.01"); + + const balanceBefore = await weth.balanceOf(oethpVault.address); + + const [amountWETH] = await roosterAmoStrategy.getPositionPrincipal(); + + // Try withdrawing an amount + await roosterAmoStrategy.connect(impersonatedVaultSigner).withdrawAll(); + + // And recipient has got it + expect(await weth.balanceOf(oethpVault.address)).to.approxEqualTolerance( + balanceBefore.add(amountWETH) + ); + + // There should be no WETH on the strategy contract + expect(await weth.balanceOf(roosterAmoStrategy.address)).to.eq( + oethUnits("0") + ); + + await verifyEndConditions(); + }); + + it("Should withdraw when there's little OETHp in the pool", async () => { + const { oethpVault, roosterAmoStrategy, weth } = fixture; + + const impersonatedVaultSigner = await impersonateAndFund( + oethpVault.address + ); + + // setup() moves the pool closer to 80:20 + await rebalanceThePoolToWETHRatio("0.97"); + + const balanceBefore = await weth.balanceOf(oethpVault.address); + + const [amountWETH, amountOETHb] = + await roosterAmoStrategy.getPositionPrincipal(); + + // Try withdrawing an amount + await roosterAmoStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethpVault.address, weth.address, oethUnits("1")); + + // Make sure that 1 WETH was burned and pool composition remains the same + const [amountWETHAfter, amountOETHbAfter] = + await roosterAmoStrategy.getPositionPrincipal(); + expect(amountWETHAfter).to.approxEqualTolerance( + amountWETH.sub(oethUnits("1")) + ); + expect(amountOETHbAfter.div(amountWETHAfter)).to.approxEqualTolerance( + amountOETHb.div(amountWETH) + ); + + // And recipient has got it + expect(await weth.balanceOf(oethpVault.address)).to.approxEqualTolerance( + balanceBefore.add(oethUnits("1")) + ); + + // There may remain some WETH left on the strategy contract because of the rounding + // when removing the liquidity + expect(await weth.balanceOf(roosterAmoStrategy.address)).to.lte( + BigNumber.from("10000") + ); + + await verifyEndConditions(); + }); + + it("Should withdrawAll when there's little OETHp in the pool", async () => { + const { oethpVault, roosterAmoStrategy, weth } = fixture; + + const impersonatedVaultSigner = await impersonateAndFund( + oethpVault.address + ); + + // setup() moves the pool closer to 80:20 + await rebalanceThePoolToWETHRatio("0.97"); + + const balanceBefore = await weth.balanceOf(oethpVault.address); + + const [amountWETH] = await roosterAmoStrategy.getPositionPrincipal(); + + // Try withdrawing an amount + await roosterAmoStrategy.connect(impersonatedVaultSigner).withdrawAll(); + + // And recipient has got it + expect(await weth.balanceOf(oethpVault.address)).to.approxEqualTolerance( + balanceBefore.add(amountWETH) + ); + + // There should be no WETH on the strategy contract + expect(await weth.balanceOf(roosterAmoStrategy.address)).to.eq( + oethUnits("0") + ); + + await verifyEndConditions(); + }); + }); + + describe("Deposit and rebalance", function () { + it("Should be able to deposit to the strategy", async () => { + await mintAndDepositToStrategy(); + + await verifyEndConditions(); + }); + + it("Should revert when not depositing WETH or amount is 0, or withdrawing WETH", async () => { + await expect( + roosterAmoStrategy + .connect(oethVaultSigner) + .deposit(oethp.address, BigNumber.from("1")) + ).to.be.revertedWith("Unsupported asset"); + + await expect( + roosterAmoStrategy + .connect(oethVaultSigner) + .withdraw(oethpVault.address, oethp.address, oethUnits("1")) + ).to.be.revertedWith("Unsupported asset"); + + await expect( + roosterAmoStrategy + .connect(oethVaultSigner) + .withdraw(oethpVault.address, weth.address, 0) + ).to.be.revertedWith("Must withdraw something"); + + await expect( + roosterAmoStrategy + .connect(oethVaultSigner) + .withdraw(weth.address, weth.address, oethUnits("1")) + ).to.be.revertedWith("Only withdraw to vault allowed"); + + await expect( + roosterAmoStrategy + .connect(oethVaultSigner) + .deposit(weth.address, BigNumber.from("0")) + ).to.be.revertedWith("Must deposit something"); + }); + + it("Should check that add liquidity in difference cases leaves little weth on the contract", async () => { + const amount = oethUnits("5"); + + await weth.connect(rafael).approve(oethpVault.address, amount); + await oethpVault.connect(rafael).mint(weth.address, amount, amount); + + const gov = await roosterAmoStrategy.governor(); + await oethpVault + .connect(await impersonateAndFund(gov)) + .depositToStrategy( + roosterAmoStrategy.address, + [weth.address], + [amount] + ); + + // Rooster LENS contracts have issues where they over-calculate the amount of tokens that need to be + // deposited + expect(await weth.balanceOf(roosterAmoStrategy.address)).to.lte( + oethUnits("10000000000") + ); + + await verifyEndConditions(); + }); + + it("Should revert when there is not enough WETH to perform a swap", async () => { + await rebalanceThePoolToWETHRatio("0.02"); + await expect( + rebalance( + oethUnits("1000000000"), + true, // _swapWETH + oethUnits("0.009") + ) + ).to.be.revertedWithCustomError( + "NotEnoughWethLiquidity(uint256,uint256)" + ); + }); + + it("Should have the correct balance within some tolerance", async () => { + const balance = await roosterAmoStrategy.checkBalance(weth.address); + const amountToDeposit = oethUnits("6"); + await mintAndDepositToStrategy({ amount: amountToDeposit }); + + // just add liquidity don't move the active trading position + await rebalance(BigNumber.from("0"), true, BigNumber.from("0")); + + const wethShare = await roosterAmoStrategy.getCurrentWethShare(); + + await expect( + await roosterAmoStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance( + balance.add(amountToDeposit.div(wethShare).mul(oethUnits("1"))), + 1.5 + ); + + await verifyEndConditions(); + }); + + it("Current trading price shouldn't affect checkBalance", async () => { + const amountToDeposit = oethUnits("6"); + await mintAndDepositToStrategy({ amount: amountToDeposit }); + const balanceBefore = await roosterAmoStrategy.checkBalance(weth.address); + + // perform a swap + await swap({ + amount: oethUnits("1"), + swapWeth: false, + }); + + expect(await roosterAmoStrategy.checkBalance(weth.address)).to.equal( + balanceBefore + ); + + await verifyEndConditions(); + }); + + it("Should be able to rebalance removing half of liquidity", async () => { + const balance = await roosterAmoStrategy.checkBalance(weth.address); + const amountToDeposit = oethUnits("6"); + await mintAndDepositToStrategy({ amount: amountToDeposit }); + + // just add liquidity don't move the active trading position + await rebalance(BigNumber.from("0"), true, BigNumber.from("0"), "0.5"); + + const wethShare = await roosterAmoStrategy.getCurrentWethShare(); + + await expect( + await roosterAmoStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance( + balance.add(amountToDeposit.div(wethShare).mul(oethUnits("1"))), + 1.5 + ); + + await verifyEndConditions(); + }); + + it("Should be able to rebalance removing all of liquidity", async () => { + const balance = await roosterAmoStrategy.checkBalance(weth.address); + const amountToDeposit = oethUnits("6"); + await mintAndDepositToStrategy({ amount: amountToDeposit }); + + // just add liquidity don't move the active trading position + await rebalance(BigNumber.from("0"), true, BigNumber.from("0"), "1"); + + const wethShare = await roosterAmoStrategy.getCurrentWethShare(); + + await expect( + await roosterAmoStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance( + balance.add(amountToDeposit.div(wethShare).mul(oethUnits("1"))), + 1.5 + ); + + await verifyEndConditions(); + }); + + it("Should revert when it fails the slippage check", async () => { + const amountToDeposit = oethUnits("6"); + await mintAndDepositToStrategy({ amount: amountToDeposit }); + + await expect( + rebalance(oethUnits("1"), true, oethUnits("1.1")) + ).to.be.revertedWithCustomError("SlippageCheck(uint256)"); + + await verifyEndConditions(); + }); + + it("Should revert on non WETH balance", async () => { + await expect( + roosterAmoStrategy.checkBalance(oethp.address) + ).to.be.revertedWith("Only WETH supported"); + }); + + it("Should throw an exception if not enough WETH on rebalance to perform a swap", async () => { + // swap out most of the weth + await swap({ + // Pool has 5 WETH + amount: oethUnits("4.99"), + swapWeth: false, + }); + + await expect( + rebalance( + (await weth.balanceOf(await roosterAmoStrategy.mPool())).mul("2"), + true, + oethUnits("4") + ) + ).to.be.revertedWithCustomError( + "NotEnoughWethLiquidity(uint256,uint256)" + ); + + await verifyEndConditions(); + }); + + it("Should not be able to rebalance when protocol is insolvent", async () => { + await mintAndDepositToStrategy({ amount: oethUnits("1000") }); + await roosterAmoStrategy.connect(oethVaultSigner).withdrawAll(); + + // ensure there is a LP position + await mintAndDepositToStrategy({ amount: oethUnits("1") }); + + // transfer WETH out making the protocol insolvent + const swapBal = oethUnits("0.00001"); + const addLiquidityBal = oethUnits("1"); + const balRemaining = (await weth.balanceOf(oethpVault.address)) + .sub(swapBal) + .sub(addLiquidityBal); + + await weth + .connect(oethVaultSigner) + .transfer(roosterAmoStrategy.address, swapBal.add(addLiquidityBal)); + await weth + .connect(oethVaultSigner) + .transfer(addresses.dead, balRemaining); + + await expect( + rebalance( + swapBal, + true, // _swapWETHs + oethUnits("0.000009"), + "0" + ) + ).to.be.revertedWith("Protocol insolvent"); + }); + }); + + describe("Perform multiple actions", function () { + it("LP token should stay staked with multiple deposit/withdraw actions", async () => { + // deposit into pool once + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + // prettier-ignore + const tx = await rebalance( + oethUnits("0.00001"), + true, // _swapWETHs + oethUnits("0.000009") + ); + await expect(tx).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + + // deposit into pool again + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + // prettier-ignore + const tx1 = await rebalance( + oethUnits("0"), + true, // _swapWETHs + oethUnits("0") + ); + await expect(tx1).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + + // Withdraw from the pool + await roosterAmoStrategy + .connect(oethVaultSigner) + .withdraw(oethpVault.address, weth.address, oethUnits("1")); + await verifyEndConditions(); + + // deposit into pool again + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + // prettier-ignore + const tx2 = await rebalance( + oethUnits("0"), + true, // _swapWETHs + oethUnits("0") + ); + await expect(tx2).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + + // Withdraw from the pool + await roosterAmoStrategy + .connect(oethVaultSigner) + .withdraw(oethpVault.address, weth.address, oethUnits("1")); + await verifyEndConditions(); + + // Withdraw from the pool + await roosterAmoStrategy.connect(oethVaultSigner).withdrawAll(); + + // deposit into pool again + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + // prettier-ignore + const tx3 = await rebalance( + oethUnits("0"), + true, // _swapWETHs + oethUnits("0") + ); + await expect(tx3).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + }); + }); + + describe("Deposit and rebalance with mocked Vault", async () => { + beforeEach(async () => { + fixture = await plumeFixtureWithMockedVault(); + weth = fixture.weth; + oethpVault = fixture.oethpVault; + roosterAmoStrategy = fixture.roosterAmoStrategy; + governor = fixture.governor; + strategist = fixture.strategist; + + await setup(); + }); + + const depositAllVaultWeth = async ({ returnTransaction } = {}) => { + const wethAvailable = await oethpVault.wethAvailable(); + const gov = await oethpVault.governor(); + const tx = await oethpVault + .connect(await impersonateAndFund(gov)) + .depositToStrategy( + roosterAmoStrategy.address, + [weth.address], + [wethAvailable] + ); + + if (returnTransaction) { + return tx; + } + + await expect(tx).to.emit(roosterAmoStrategy, "PoolRebalanced"); + }; + + const depositAllWethAndConfigure1pct = async () => { + // configure to leave no WETH on the vault + await configureAutomaticDepositOnMint(oethUnits("0")); + const outstandingWeth = await oethpVault.outstandingWithdrawalsAmount(); + + // send WETH to the vault that is outstanding to be claimed + await weth + .connect(fixture.clement) + .transfer(oethpVault.address, outstandingWeth); + + await depositAllVaultWeth(); + + // configure to only keep 1bp of the Vault's totalValue in the Vault; + const minAmountReserved = await configureAutomaticDepositOnMint( + oethUnits("0.01") + ); + + return minAmountReserved; + }; + + it("Should not automatically deposit to strategy when below vault buffer threshold", async () => { + const minAmountReserved = await depositAllWethAndConfigure1pct(); + + const amountBelowThreshold = minAmountReserved.div(BigNumber.from("2")); + + await mint({ amount: amountBelowThreshold }); + // There is some WETH usually on the strategy contract because liquidity manager doesn't consume all. + // Also the strategy contract adjusts WETH supplied to pool down, to mitigate the PoolLens liquidity + // calculation. + await expect(await weth.balanceOf(roosterAmoStrategy.address)).to.lte( + BigNumber.from("1000") + ); + + await expect(await oethpVault.wethAvailable()).to.approxEqualTolerance( + amountBelowThreshold + ); + + await verifyEndConditions(); + }); + + it("Should revert when pool rebalance is off target", async () => { + const { amount, swapWeth } = await estimateSwapAmountsToReachWethRatio( + oethUnits("0.91") + ); + await swap({ amount, swapWeth }); + await mintAndDepositToStrategy({ amount: oethUnits("1000") }, false); + const { amount: amount2, swapWeth: swapWeth2 } = + await estimateSwapAmountsToReachWethRatio(oethUnits("0.91")); + + await expect( + rebalance(amount2, swapWeth2, 0, "0") + ).to.be.revertedWithCustomError( + "PoolRebalanceOutOfBounds(uint256,uint256,uint256)" + ); + + await expect( + rebalance(amount.add(amount), swapWeth2, 0, "0") + ).to.be.revertedWithCustomError("OutsideExpectedTickRange()"); + }); + + it("Should be able to rebalance the pool when price pushed very close to 1:1", async () => { + const { amount: amountToSwap, swapWeth: swapWethToSwap } = + await estimateSwapAmountsToReachWethRatio(oethUnits("0.99")); + await swap({ + amount: amountToSwap, + swapWeth: swapWethToSwap, + }); + + const { amount, swapWeth } = + await estimateSwapAmountsToGetToConfiguredInterval(); + + await rebalance(amount, swapWeth, 0, "0"); + await verifyEndConditions(); + }); + + it("Should be able to rebalance the pool when price pushed very close to OETHb costing 0.9999 WETH", async () => { + const { amount: amountToSwap, swapWeth: swapWethToSwap } = + await estimateSwapAmountsToReachWethRatio(oethUnits("0.01")); + await swap({ + amount: amountToSwap, + swapWeth: swapWethToSwap, + }); + + const { amount, swapWeth } = + await estimateSwapAmountsToGetToConfiguredInterval(); + await mintAndDepositToStrategy({ amount: oethUnits("1000") }, false); + + await rebalance(amount, swapWeth, 0, "0"); + await verifyEndConditions(); + }); + + it("Should be able to deposit to the pool & rebalance", async () => { + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + let { amount, swapWeth } = + await estimateSwapAmountsToGetToConfiguredInterval(); + + const tx = await rebalance(amount, swapWeth, 0, "0"); + + await expect(tx).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + }); + + it("Should be able to rebalance when small amount of WETH needs to be removed as swap liquidity", async () => { + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + + const { amount, swapWeth } = + await estimateSwapAmountsToGetToConfiguredInterval(); + // rebalance to use up any WETH liquidity on the contract + await rebalance(amount, swapWeth, 0, "0"); + // rebalance requiring small amount of WETH (increasing) + await rebalance(BigNumber.from("100"), true, 0, "0"); + await rebalance(BigNumber.from("10000"), true, 0, "0"); + await rebalance(BigNumber.from("1000000"), true, 0, "0"); + await rebalance(BigNumber.from("100000000"), true, 0, "0"); + await rebalance(BigNumber.from("10000000000"), true, 0, "0"); + await rebalance(BigNumber.from("1000000000000"), true, 0, "0"); + + await verifyEndConditions(); + }); + + it("Should be able to deposit to the pool & rebalance multiple times", async () => { + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + let { amount, swapWeth } = + await estimateSwapAmountsToGetToConfiguredInterval(); + const tx = await rebalance(amount, swapWeth, 0, "0"); + await expect(tx).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + + let { amount: amount1, swapWeth: swapWeth1 } = + await estimateSwapAmountsToGetToConfiguredInterval(); + const tx1 = await rebalance(amount1, swapWeth1, 0, "0"); + await expect(tx1).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + }); + + const depositValues = [ + "0.5", + "2.5", + "3.5", + "5", + "9", + "50", + "250", + "500", + "1500", + "2500", + ]; + for (const depositValue of depositValues) { + it(`Should be able to deposit to the pool & rebalance multiple times with ${depositValue} deposit`, async () => { + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + let { amount, swapWeth } = + await estimateSwapAmountsToGetToConfiguredInterval(); + const tx = await rebalance(amount, swapWeth, 0, "0"); + await expect(tx).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + + let { amount: amount1, swapWeth: swapWeth1 } = + await estimateSwapAmountsToGetToConfiguredInterval(); + const tx1 = await rebalance(amount1, swapWeth1, 0, "0"); + await expect(tx1).to.emit(roosterAmoStrategy, "PoolRebalanced"); + await verifyEndConditions(); + }); + } + }); + + describe("Rewards", function () { + it("Should be able to claim rewards when available", async () => { + // Make sure the strategy has something + await mintAndDepositToStrategy({ amount: oethUnits("5") }); + + const wPlume = await ethers.getContractAt( + "IWETH9", + addresses.plume.WPLUME + ); + const balanceBefore = await wPlume.balanceOf(strategist.address); + await roosterAmoStrategy.connect(strategist).collectRewardTokens(); + const balanceAfter = await wPlume.balanceOf(strategist.address); + + const balanceDiff = balanceAfter.sub(balanceBefore); + expect(balanceDiff).to.eq(oethUnits("1")); + }); + }); + + const setup = async () => { + await mintAndDepositToStrategy({ + amount: oethUnits("100"), + returnTransaction: true, + }); + + await oethpVault.connect(rafael).rebase(); + + let { amount, swapWeth } = + await estimateSwapAmountsToGetToConfiguredInterval(); + + await rebalance(amount, swapWeth, 0, "0"); + }; + + const swap = async ({ amount, swapWeth }) => { + // Check if rafael as enough token to perform swap + // If not, mint some + const balanceOETHp = await oethp.balanceOf(rafael.address); + if (!swapWeth && balanceOETHp.lt(amount)) { + await weth.connect(rafael).approve(oethpVault.address, amount); + await oethpVault.connect(rafael).mint(weth.address, amount, amount); + } + + if (swapWeth) { + await weth.connect(rafael).transfer(roosterOETHpWETHpool.address, amount); + } else { + await oethp + .connect(rafael) + .transfer(roosterOETHpWETHpool.address, amount); + } + + await roosterOETHpWETHpool.connect(rafael).swap( + rafael.address, + { + amount: amount, + tokenAIn: swapWeth, + exactOutput: false, + tickLimit: swapWeth ? 2147483647 : -2147483648, + }, + "0x" + ); + }; + + // get the middle point of configured weth share interval + const getConfiguredWethShare = async () => { + const wethShareStart = await roosterAmoStrategy.allowedWethShareStart(); + const wethShareEnd = await roosterAmoStrategy.allowedWethShareEnd(); + + return wethShareStart.add(wethShareEnd).div(BigNumber.from("2")); + }; + + const rebalanceThePoolToWETHRatio = async (wethRatio) => { + const { amount, swapWeth } = await estimateSwapAmountsToReachWethRatio( + oethUnits(wethRatio) + ); + await swap({ amount, swapWeth }); + }; + + const estimateSwapAmountsToGetToConfiguredInterval = async () => { + const configuredWethShare = await getConfiguredWethShare(); + return await estimateSwapAmountsToReachWethRatio(configuredWethShare); + }; + + // the amount to swap (and token type) to reach desired WETH ratio + // Notice: this only works if the pools is already in the tick -1 where + // all the liquidity is deployed + const estimateSwapAmountsToReachWethRatio = async (wethRatio) => { + let currentTradingTick = parseInt((await roosterAmoStrategy.getCurrentTradingTick()).toString()); + let totalAmount = BigNumber.from("0"); + + while(currentTradingTick < -1) { + totalAmount = totalAmount.add(await _getOETHInTick(currentTradingTick)); + currentTradingTick += 1; + } + + while(currentTradingTick > -1) { + totalAmount = totalAmount.add(await _getWETHInTick(currentTradingTick)); + currentTradingTick -= 1; + } + + let { amount, swapWeth } = await _estimateAmountsWithinTheAMOTick(wethRatio); + amount = amount.add(totalAmount); + return { + amount, + swapWeth + } + }; + + const _getWETHInTick = async (tradingTick) => { + const tickState = await roosterOETHpWETHpool.getTick(tradingTick); + return tickState.reserveA; + } + + const _getOETHInTick = async (tradingTick) => { + const tickState = await roosterOETHpWETHpool.getTick(tradingTick); + return tickState.reserveB; + } + + const _estimateAmountsWithinTheAMOTick = async (wethRatio) => { + const tickState = await roosterOETHpWETHpool.getTick(-1); + + const wethAmount = tickState.reserveA; + const oethAmount = tickState.reserveB; + + const total = wethAmount.add(oethAmount); + // 1e18 denominated + const currentWethRatio = wethAmount.mul(oethUnits("1")).div(total); + + let diff, swapWeth; + if (wethRatio.gt(currentWethRatio)) { + diff = wethRatio.sub(currentWethRatio); + swapWeth = true; + } else { + diff = currentWethRatio.sub(wethRatio); + swapWeth = false; + } + + return { + amount: diff.mul(total).div(oethUnits("1")), + swapWeth, + }; + } + + const rebalance = async ( + amountToSwap, + swapWETH, + minTokenReceived, + liquidityToRemove = "1" + ) => { + return await roosterAmoStrategy + .connect(strategist) + .rebalance( + amountToSwap, + swapWETH, + minTokenReceived, + oethUnits(liquidityToRemove) + ); + }; + + const mint = async ({ userOverride, amount } = {}) => { + const user = userOverride || rafael; + amount = amount || oethUnits("5"); + + const balance = weth.balanceOf(user.address); + if (balance < amount) { + await setERC20TokenBalance(user.address, weth, amount + balance, hre); + } + await weth.connect(user).approve(oethpVault.address, amount); + const tx = await oethpVault + .connect(user) + .mint(weth.address, amount, amount); + return tx; + }; + + const mintAndDepositToStrategy = async ( + { userOverride, amount, returnTransaction } = {}, + expectPoolRebalanced = true + ) => { + const user = userOverride || rafael; + amount = amount || oethUnits("5"); + + const balance = weth.balanceOf(user.address); + if (balance < amount) { + await setERC20TokenBalance(user.address, weth, amount + balance, hre); + } + await weth.connect(user).approve(oethpVault.address, amount); + await oethpVault.connect(user).mint(weth.address, amount, amount); + + const gov = await oethpVault.governor(); + const tx = await oethpVault + .connect(await impersonateAndFund(gov)) + .depositToStrategy(roosterAmoStrategy.address, [weth.address], [amount]); + + if (returnTransaction) { + return tx; + } + + if (expectPoolRebalanced) { + await expect(tx).to.emit(roosterAmoStrategy, "PoolRebalanced"); + } + }; + + /** When tests finish: + * - there should be no substantial amount of WETH / OETHp left on the strategy contract + */ + const verifyEndConditions = async () => { + await expect(await weth.balanceOf(roosterAmoStrategy.address)).to.lte( + oethUnits("0.00001") + ); + await expect(await oethp.balanceOf(roosterAmoStrategy.address)).to.lte( + oethUnits("0.000001") + ); + }; +}); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 113eca5174..f7c812e38a 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -599,8 +599,20 @@ addresses.plume.LayerZeroEndpointV2 = "0xC1b15d3B262bEeC0e3565C11C9e0F6134BdaCB36"; addresses.plume.WOETHOmnichainAdapter = "0x592CB6A596E7919930bF49a27AdAeCA7C055e4DB"; - addresses.plume.timelock = "0x6C6f8F839A7648949873D3D2beEa936FC2932e5c"; +addresses.plume.WPLUME = "0xEa237441c92CAe6FC17Caaf9a7acB3f953be4bd1"; +addresses.plume.MaverickV2Factory = + "0x056A588AfdC0cdaa4Cab50d8a4D2940C5D04172E"; +addresses.plume.MaverickV2PoolLens = + "0x15B4a8cc116313b50C19BCfcE4e5fc6EC8C65793"; +addresses.plume.MaverickV2Quoter = "0xf245948e9cf892C351361d298cc7c5b217C36D82"; +addresses.plume.MaverickV2Router = "0x35e44dc4702Fd51744001E248B49CBf9fcc51f0C"; +addresses.plume.MaverickV2Position = + "0x0b452E8378B65FD16C0281cfe48Ed9723b8A1950"; +addresses.plume.MaverickV2LiquidityManager = + "0x28d79eddBF5B215cAccBD809B967032C1E753af7"; +addresses.plume.OethpWETHRoosterPool = + "0x3F86B564A9B530207876d2752948268b9Bf04F71"; addresses.plume.strategist = addresses.multichainStrategist; addresses.plume.admin = "0x92A19381444A001d62cE67BaFF066fA1111d7202";