diff --git a/src/ConstantSum/ConstantSum.sol b/src/ConstantSum/ConstantSum.sol index 08a1c27d..ef0ef9ee 100644 --- a/src/ConstantSum/ConstantSum.sol +++ b/src/ConstantSum/ConstantSum.sol @@ -2,7 +2,10 @@ pragma solidity 0.8.22; import { - FixedPointMathLib, computeTradingFunction + FixedPointMathLib, + computeTradingFunction, + computeSwapDeltaLiquidity, + computeDeltaLiquidity } from "./ConstantSumMath.sol"; import { decodePriceUpdate, @@ -10,6 +13,7 @@ import { decodeControllerUpdate } from "./ConstantSumUtils.sol"; import { PairStrategy, IStrategy, Pool } from "src/PairStrategy.sol"; +import { EPSILON } from "src/lib/StrategyLib.sol"; struct InternalParams { uint256 price; @@ -33,6 +37,9 @@ enum UpdateCode { contract ConstantSum is PairStrategy { using FixedPointMathLib for uint256; + /// @notice Thrown when the expected liquidity is not met. + error InvalidDeltaLiquidity(); + /// @inheritdoc IStrategy string public constant name = "ConstantSum"; @@ -45,7 +52,7 @@ contract ConstantSum is PairStrategy { function init( address, uint256 poolId, - Pool calldata, + Pool calldata pool, bytes calldata data ) public @@ -58,21 +65,106 @@ contract ConstantSum is PairStrategy { ) { ConstantSumParams memory params; - (reserves, totalLiquidity, params) = - abi.decode(data, (uint256[], uint256, ConstantSumParams)); + + (reserves, params) = abi.decode(data, (uint256[], ConstantSumParams)); + totalLiquidity = + computeDeltaLiquidity(reserves[0], reserves[1], params.price); + + if (pool.reserves.length != 2 || reserves.length != 2) { + revert InvalidReservesLength(); + } internalParams[poolId].price = params.price; internalParams[poolId].swapFee = params.swapFee; + internalParams[poolId].controller = params.controller; // Get the trading function and check this is valid invariant = tradingFunction(reserves, totalLiquidity, abi.encode(params)); - valid = invariant >= 0; + valid = invariant >= 0 && invariant <= EPSILON; return (valid, invariant, reserves, totalLiquidity); } + function validateAllocate( + address, + uint256 poolId, + Pool memory pool, + bytes calldata data + ) + external + view + override + returns ( + bool valid, + int256 invariant, + uint256[] memory deltas, + uint256 deltaLiquidity + ) + { + (uint256 deltaX, uint256 deltaY, uint256 minDeltaL) = + abi.decode(data, (uint256, uint256, uint256)); + + deltaLiquidity = + computeDeltaLiquidity(deltaX, deltaY, internalParams[poolId].price); + if (deltaLiquidity < minDeltaL) revert InvalidDeltaLiquidity(); + + deltas = new uint256[](2); + deltas[0] = deltaX; + deltas[1] = deltaY; + + pool.reserves[0] += deltaX; + pool.reserves[1] += deltaY; + + invariant = tradingFunction( + pool.reserves, + pool.totalLiquidity + deltaLiquidity, + getPoolParams(poolId) + ); + + valid = invariant >= 0; + } + + function validateDeallocate( + address, + uint256 poolId, + Pool memory pool, + bytes calldata data + ) + external + view + override + returns ( + bool valid, + int256 invariant, + uint256[] memory deltas, + uint256 deltaLiquidity + ) + { + (uint256 deltaX, uint256 deltaY, uint256 maxDeltaL) = + abi.decode(data, (uint256, uint256, uint256)); + + deltaLiquidity = + computeDeltaLiquidity(deltaX, deltaY, internalParams[poolId].price); + if (maxDeltaL > deltaLiquidity) revert InvalidDeltaLiquidity(); + + deltas = new uint256[](2); + deltas[0] = deltaX; + deltas[1] = deltaY; + + pool.reserves[0] -= deltaX; + pool.reserves[1] -= deltaY; + + invariant = tradingFunction( + pool.reserves, + pool.totalLiquidity - deltaLiquidity, + getPoolParams(poolId) + ); + + valid = invariant >= 0; + } + /// @inheritdoc IStrategy function update( address sender, @@ -105,6 +197,7 @@ contract ConstantSum is PairStrategy { params.price = internalParams[poolId].price; params.swapFee = internalParams[poolId].swapFee; + params.controller = internalParams[poolId].controller; return abi.encode(params); } @@ -128,7 +221,7 @@ contract ConstantSum is PairStrategy { Pool memory, bytes memory ) internal pure override returns (uint256[] memory) { - return new uint256[](0); + return new uint256[](2); } /// @inheritdoc PairStrategy @@ -137,6 +230,20 @@ contract ConstantSum is PairStrategy { Pool memory, bytes memory ) internal pure override returns (uint256[] memory) { - return new uint256[](0); + return new uint256[](2); + } + + /// @inheritdoc PairStrategy + function _computeSwapDeltaLiquidity( + Pool memory, + bytes memory params, + uint256 tokenInIndex, + uint256, + uint256 amountIn, + uint256 + ) internal pure override returns (uint256) { + return computeSwapDeltaLiquidity( + amountIn, abi.decode(params, (ConstantSumParams)), tokenInIndex == 0 + ); } } diff --git a/src/ConstantSum/ConstantSumMath.sol b/src/ConstantSum/ConstantSumMath.sol index 5e58f9e2..0f26c7e0 100644 --- a/src/ConstantSum/ConstantSumMath.sol +++ b/src/ConstantSum/ConstantSumMath.sol @@ -6,15 +6,16 @@ import { ConstantSumParams } from "src/ConstantSum/ConstantSum.sol"; import { ONE } from "src/lib/StrategyLib.sol"; using FixedPointMathLib for uint256; -using FixedPointMathLib for int256; function computeTradingFunction( uint256[] memory reserves, uint256 totalLiquidity, uint256 price ) pure returns (int256) { - return int256(reserves[0].divWadUp(totalLiquidity)) - + int256(reserves[1].divWadUp(totalLiquidity.mulWadUp(price))) - int256(ONE); + return int256( + price.mulWadUp(reserves[0].divWadUp(totalLiquidity)) + + reserves[1].divWadUp(totalLiquidity) + ) - int256(ONE); } function computeInitialPoolData( @@ -24,77 +25,28 @@ function computeInitialPoolData( ) pure returns (bytes memory) { // The pool can be initialized with any non-negative amount of rx, and ry. // so we have to allow a user to pass an amount of both even if one is zero. - uint256 L = rx + ry.divWadUp(params.price); uint256[] memory reserves = new uint256[](2); reserves[0] = rx; reserves[1] = ry; - return abi.encode(reserves, L, params); + return abi.encode(reserves, params); } -function computeDeallocateGivenDeltaX( +function computeDeltaLiquidity( uint256 deltaX, - uint256 rX, - uint256 rY, - uint256 totalLiquidity -) pure returns (uint256 deltaY, uint256 deltaL) { - uint256 a = deltaX.divWadDown(rX); - if (rY > 0) { - deltaY = a.mulWadDown(rY); - } - deltaL = a.mulWadDown(totalLiquidity); -} - -function computeDeallocateGivenDeltaY( - uint256 deltaY, - uint256 rX, - uint256 rY, - uint256 totalLiquidity -) pure returns (uint256 deltaX, uint256 deltaL) { - uint256 a = deltaY.divWadDown(rY); - if (rX > 0) { - deltaX = a.mulWadDown(rX); - } - deltaL = a.mulWadDown(totalLiquidity); -} - -function computeAllocateGivenDeltaX( - uint256 deltaX, - uint256 rX, - uint256 rY, - uint256 totalLiquidity -) pure returns (uint256 deltaY, uint256 deltaL) { - uint256 a = deltaX.divWadUp(rX); - if (rY > 0) { - deltaY = a.mulWadUp(rY); - } - deltaL = a.mulWadUp(totalLiquidity); -} - -function computeAllocateGivenDeltaY( uint256 deltaY, - uint256 rX, - uint256 rY, - uint256 totalLiquidity -) pure returns (uint256 deltaX, uint256 deltaL) { - uint256 a = deltaY.divWadUp(rY); - if (rX > 0) { - deltaX = a.mulWadUp(rX); - } - deltaL = a.mulWadUp(totalLiquidity); -} - -function computeDeltaGivenDeltaLRoundUp( - uint256 reserve, - uint256 deltaLiquidity, - uint256 totalLiquidity + uint256 price ) pure returns (uint256) { - return reserve.mulWadUp(deltaLiquidity.divWadUp(totalLiquidity)); + return price.mulWadUp(deltaX) + deltaY; } -function computeDeltaGivenDeltaLRoundDown( - uint256 reserve, - uint256 deltaLiquidity, - uint256 totalLiquidity +function computeSwapDeltaLiquidity( + uint256 delta, + ConstantSumParams memory params, + bool isSwapXForY ) pure returns (uint256) { - return reserve.mulWadDown(deltaLiquidity.divWadDown(totalLiquidity)); + if (isSwapXForY) { + return (params.swapFee).mulWadUp(delta); + } else { + return (params.swapFee).mulDivUp(delta, params.price); + } } diff --git a/src/ConstantSum/ConstantSumSolver.sol b/src/ConstantSum/ConstantSumSolver.sol index 1144d382..644a2e77 100644 --- a/src/ConstantSum/ConstantSumSolver.sol +++ b/src/ConstantSum/ConstantSumSolver.sol @@ -12,14 +12,14 @@ import { import { ONE, computeInitialPoolData, - FixedPointMathLib + FixedPointMathLib, + computeSwapDeltaLiquidity } from "./ConstantSumMath.sol"; contract ConstantSumSolver { error NotEnoughLiquidity(); using FixedPointMathLib for uint256; - using FixedPointMathLib for int256; struct Reserves { uint256 rx; @@ -59,7 +59,7 @@ contract ConstantSumSolver { SimulateSwapState memory state; if (swapXIn) { - state.deltaLiquidity = amountIn.mulWadUp(poolParams.swapFee); + computeSwapDeltaLiquidity(amountIn, poolParams, true); state.amountOut = amountIn.mulWadDown(poolParams.price).mulWadDown( ONE - poolParams.swapFee ); @@ -68,8 +68,7 @@ contract ConstantSumSolver { revert NotEnoughLiquidity(); } } else { - state.deltaLiquidity = - amountIn.mulWadUp(poolParams.swapFee).divWadUp(poolParams.price); + computeSwapDeltaLiquidity(amountIn, poolParams, false); state.amountOut = (ONE - poolParams.swapFee).mulWadDown(amountIn) .divWadDown(poolParams.price); @@ -81,13 +80,9 @@ contract ConstantSumSolver { bytes memory swapData; if (swapXIn) { - swapData = abi.encode( - 0, 1, amountIn, state.amountOut, state.deltaLiquidity - ); + swapData = abi.encode(0, 1, amountIn, state.amountOut); } else { - swapData = abi.encode( - 1, 0, amountIn, state.amountOut, state.deltaLiquidity - ); + swapData = abi.encode(1, 0, amountIn, state.amountOut); } (bool valid,,,,,,) = IStrategy(strategy).validateSwap( diff --git a/src/ConstantSum/ConstantSumUtils.sol b/src/ConstantSum/ConstantSumUtils.sol index 12064357..71b98d25 100644 --- a/src/ConstantSum/ConstantSumUtils.sol +++ b/src/ConstantSum/ConstantSumUtils.sol @@ -20,7 +20,6 @@ function encodeControllerUpdate(address controller) function decodeFeeUpdate(bytes memory data) pure returns (uint256 swapFee) { (, swapFee) = abi.decode(data, (UpdateCode, uint256)); - return swapFee; } function decodePriceUpdate(bytes memory data) diff --git a/src/DFMM.sol b/src/DFMM.sol index ea727b3e..aea038c7 100644 --- a/src/DFMM.sol +++ b/src/DFMM.sol @@ -92,6 +92,8 @@ contract DFMM is IDFMM { msg.sender, _pools.length, pool, params.data ); + if (reserves.length != params.tokens.length) revert InvalidReserves(); + if (!valid) revert InvalidInvariant(invariant); liquidityToken.initialize(params.name, params.symbol); @@ -109,8 +111,8 @@ contract DFMM is IDFMM { for (uint256 i = 0; i < tokensLength; i++) { address token = params.tokens[i]; - for (uint256 j = 0; j < tokensLength; j++) { - if (i != j && token == params.tokens[j]) { + for (uint256 j = i + 1; j < tokensLength; j++) { + if (token == params.tokens[j]) { revert InvalidDuplicateTokens(); } } @@ -122,10 +124,10 @@ contract DFMM is IDFMM { if (decimals > 18 || decimals < 6) { revert InvalidTokenDecimals(); } - - _transferFrom(params.tokens[i], reserves[i]); } + _transferFrom(params.tokens, reserves); + emit Init( msg.sender, pool.strategy, @@ -136,7 +138,7 @@ contract DFMM is IDFMM { pool.totalLiquidity ); - return (poolId, reserves, totalLiquidity - BURNT_LIQUIDITY); + return (poolId, reserves, totalLiquidity); } /// @inheritdoc IDFMM @@ -161,12 +163,10 @@ contract DFMM is IDFMM { _pools[poolId].reserves[i] += deltas[i]; } - _pools[poolId].totalLiquidity += deltaLiquidity; _manageTokens(msg.sender, poolId, true, deltaLiquidity); + _pools[poolId].totalLiquidity += deltaLiquidity; - for (uint256 i = 0; i < length; i++) { - _transferFrom(_pools[poolId].tokens[i], deltas[i]); - } + _transferFrom(_pools[poolId].tokens, deltas); emit Allocate(msg.sender, poolId, deltas, deltaLiquidity); return deltas; @@ -237,13 +237,14 @@ contract DFMM is IDFMM { if (!state.valid) revert InvalidInvariant(state.invariant); - _pools[poolId].totalLiquidity += state.deltaLiquidity; - - if (_pools[poolId].feeCollector != address(0)) { - uint256 fees = state.deltaLiquidity * _pools[poolId].controllerFee - / FixedPointMathLib.WAD; - if (fees == 0) ++fees; + if (_pools[poolId].controllerFee > 0) { + uint256 fees = + state.deltaLiquidity.mulWadUp(_pools[poolId].controllerFee); + _pools[poolId].totalLiquidity += state.deltaLiquidity - fees; _manageTokens(_pools[poolId].feeCollector, poolId, true, fees); + _pools[poolId].totalLiquidity += fees; + } else { + _pools[poolId].totalLiquidity += state.deltaLiquidity; } _pools[poolId].reserves[state.tokenInIndex] += state.amountIn; @@ -252,7 +253,12 @@ contract DFMM is IDFMM { address tokenIn = _pools[poolId].tokens[state.tokenInIndex]; address tokenOut = _pools[poolId].tokens[state.tokenOutIndex]; - _transferFrom(tokenIn, state.amountIn); + address[] memory tokens = new address[](1); + tokens[0] = tokenIn; + uint256[] memory amounts = new uint256[](1); + amounts[0] = state.amountIn; + _transferFrom(tokens, amounts); + _transfer(tokenOut, recipient, state.amountOut); emit Swap( @@ -278,33 +284,43 @@ contract DFMM is IDFMM { // Internals /** - * @dev Transfers `amount` of `token` from the sender to the contract. Note - * that if ETH is present in the contract, it will be wrapped to WETH. Any - * excess of ETH will be sent back to the sender. - * @param token Address of the token to transfer. - * @param amount Amount to transfer expressed in WAD. + * @dev Transfers `amounts` of `tokens` from the sender to the contract. Note + * that if any ETH is present in the contract, it will be wrapped to WETH and + * used if sufficient. Any excess of ETH will be sent back to the sender. + * @param tokens An array of token addresses to transfer. + * @param amounts An array of amounts to transfer expressed in WAD. */ - function _transferFrom(address token, uint256 amount) internal { - if (address(this).balance >= amount) { - WETH(payable(weth)).deposit{ value: amount }(); + function _transferFrom( + address[] memory tokens, + uint256[] memory amounts + ) internal { + uint256 length = tokens.length; + + for (uint256 i = 0; i < length; i++) { + address token = tokens[i]; + uint256 amount = amounts[i]; - if (address(this).balance > 0) { - SafeTransferLib.safeTransferETH( - msg.sender, address(this).balance - ); - } - } else { uint256 downscaledAmount = downscaleUp(amount, computeScalingFactor(token)); uint256 preBalance = ERC20(token).balanceOf(address(this)); - SafeTransferLib.safeTransferFrom( - ERC20(token), msg.sender, address(this), downscaledAmount - ); + + if (token == weth && address(this).balance >= amount) { + WETH(payable(weth)).deposit{ value: amount }(); + } else { + SafeTransferLib.safeTransferFrom( + ERC20(token), msg.sender, address(this), downscaledAmount + ); + } + uint256 postBalance = ERC20(token).balanceOf(address(this)); if (postBalance < preBalance + downscaledAmount) { revert InvalidTransfer(); } } + + if (address(this).balance > 0) { + SafeTransferLib.safeTransferETH(msg.sender, address(this).balance); + } } /** @@ -362,7 +378,7 @@ contract DFMM is IDFMM { * @dev Deploys and returns the address of a clone contract that mimics * the behaviour of the contract deployed at the address `implementation`. * This function uses the `CREATE` opcode, which should never revert. - * This function was taken from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Clones.sol#L23. + * This function was taken from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7bd2b2aaf68c21277097166a9a51eb72ae239b34/contracts/proxy/Clones.sol#L23-L41. */ function clone(address implementation) internal diff --git a/src/GeometricMean/G3MMath.sol b/src/GeometricMean/G3MMath.sol index 0e409569..836ba8ac 100644 --- a/src/GeometricMean/G3MMath.sol +++ b/src/GeometricMean/G3MMath.sol @@ -15,8 +15,8 @@ function computeTradingFunction( uint256 L, GeometricMeanParams memory params ) pure returns (int256) { - uint256 a = uint256(int256(rX.divWadDown(L)).powWad(int256(params.wX))); - uint256 b = uint256(int256(rY.divWadDown(L)).powWad(int256(params.wY))); + uint256 a = uint256(int256(rX.divWadUp(L)).powWad(int256(params.wX))); + uint256 b = uint256(int256(rY.divWadUp(L)).powWad(int256(params.wY))); return int256(a.mulWadUp(b)) - int256(1 ether); } @@ -26,7 +26,7 @@ function computeDeltaGivenDeltaLRoundUp( uint256 deltaLiquidity, uint256 totalLiquidity ) pure returns (uint256) { - return reserve.mulWadUp(deltaLiquidity.divWadUp(totalLiquidity)); + return reserve.mulDivUp(deltaLiquidity, totalLiquidity); } function computeDeltaGivenDeltaLRoundDown( @@ -34,7 +34,7 @@ function computeDeltaGivenDeltaLRoundDown( uint256 deltaLiquidity, uint256 totalLiquidity ) pure returns (uint256) { - return reserve.mulWadDown(deltaLiquidity.divWadDown(totalLiquidity)); + return reserve.mulDivDown(deltaLiquidity, totalLiquidity); } function computeLGivenX( @@ -251,6 +251,21 @@ function computeInitialPoolData( L = computeNextLiquidity(amountX, rY, invariant, L, params); - return - abi.encode(amountX, rY, L, params.wX, params.swapFee, params.controller); + uint256[] memory reserves = new uint256[](2); + reserves[0] = amountX; + reserves[1] = rY; + + return abi.encode(reserves, L, params.wX, params.swapFee, params.controller); +} + +function computeSwapDeltaLiquidity( + uint256 amountIn, + uint256 reserve, + uint256 totalLiquidity, + uint256 weight, + uint256 swapFee +) pure returns (uint256) { + return weight.mulWadUp(swapFee).mulWadUp(totalLiquidity).mulWadUp( + amountIn.divWadUp(reserve) + ); } diff --git a/src/GeometricMean/GeometricMean.sol b/src/GeometricMean/GeometricMean.sol index 1dcf1942..6585ed1d 100644 --- a/src/GeometricMean/GeometricMean.sol +++ b/src/GeometricMean/GeometricMean.sol @@ -8,9 +8,10 @@ import { Pool } from "src/interfaces/IDFMM.sol"; import { computeTradingFunction, computeDeltaGivenDeltaLRoundUp, - computeDeltaGivenDeltaLRoundDown + computeDeltaGivenDeltaLRoundDown, + computeSwapDeltaLiquidity } from "./G3MMath.sol"; -import { ONE } from "src/lib/StrategyLib.sol"; +import { ONE, EPSILON } from "src/lib/StrategyLib.sol"; /** * @dev Parameterization of the GeometricMean curve. @@ -61,8 +62,6 @@ contract GeometricMean is PairStrategy { struct InitState { bool valid; int256 invariant; - uint256 reserveX; - uint256 reserveY; address controller; uint256 swapFee; uint256 wX; @@ -74,25 +73,24 @@ contract GeometricMean is PairStrategy { function init( address, uint256 poolId, - Pool calldata, + Pool calldata pool, bytes calldata data ) external onlyDFMM returns (bool, int256, uint256[] memory, uint256) { InitState memory state; - state.reserves = new uint256[](2); - ( - state.reserves[0], - state.reserves[1], + state.reserves, state.totalLiquidity, state.wX, state.swapFee, state.controller - ) = abi.decode( - data, (uint256, uint256, uint256, uint256, uint256, address) - ); + ) = abi.decode(data, (uint256[], uint256, uint256, uint256, address)); + + if (pool.reserves.length != 2 || state.reserves.length != 2) { + revert InvalidReservesLength(); + } - if (state.wX >= ONE) { + if (state.wX == 0 || state.wX >= ONE) { revert InvalidWeightX(); } @@ -107,7 +105,7 @@ contract GeometricMean is PairStrategy { abi.decode(getPoolParams(poolId), (GeometricMeanParams)) ); - state.valid = state.invariant >= 0; + state.valid = state.invariant >= 0 && state.invariant <= EPSILON; return (state.valid, state.invariant, state.reserves, state.totalLiquidity); @@ -129,6 +127,9 @@ contract GeometricMean is PairStrategy { } else if (updateCode == UpdateCode.WeightX) { (, uint256 targetWeightX, uint256 targetTimestamp) = abi.decode(data, (UpdateCode, uint256, uint256)); + if (targetWeightX == 0 || targetWeightX >= ONE) { + revert InvalidWeightX(); + } internalParams[poolId].wX.set(targetWeightX, targetTimestamp); } else if (updateCode == UpdateCode.Controller) { (, internalParams[poolId].controller) = @@ -200,4 +201,35 @@ contract GeometricMean is PairStrategy { pool.reserves[1], deltaLiquidity, pool.totalLiquidity ); } + + /// @inheritdoc PairStrategy + function _computeSwapDeltaLiquidity( + Pool memory pool, + bytes memory params, + uint256 tokenInIndex, + uint256, + uint256 amountIn, + uint256 + ) internal pure override returns (uint256) { + GeometricMeanParams memory poolParams = + abi.decode(params, (GeometricMeanParams)); + + if (tokenInIndex == 0) { + return computeSwapDeltaLiquidity( + amountIn, + pool.reserves[0], + pool.totalLiquidity, + poolParams.wX, + poolParams.swapFee + ); + } + + return computeSwapDeltaLiquidity( + amountIn, + pool.reserves[1], + pool.totalLiquidity, + poolParams.wY, + poolParams.swapFee + ); + } } diff --git a/src/GeometricMean/GeometricMeanSolver.sol b/src/GeometricMean/GeometricMeanSolver.sol index 6cf9d359..4e4aeb0d 100644 --- a/src/GeometricMean/GeometricMeanSolver.sol +++ b/src/GeometricMean/GeometricMeanSolver.sol @@ -211,13 +211,8 @@ contract GeometricMeanSolver { state.amountOut = state.outReserve - a; } - bytes memory swapData = abi.encode( - tokenInIndex, - tokenOutIndex, - amountIn, - state.amountOut, - state.deltaLiquidity - ); + bytes memory swapData = + abi.encode(tokenInIndex, tokenOutIndex, amountIn, state.amountOut); (bool valid,,,,,,) = IStrategy(strategy).validateSwap( address(this), poolId, pool, swapData diff --git a/src/LogNormal/LogNormal.sol b/src/LogNormal/LogNormal.sol index 1bcd5513..d3fb8587 100644 --- a/src/LogNormal/LogNormal.sol +++ b/src/LogNormal/LogNormal.sol @@ -7,7 +7,9 @@ import { DynamicParamLib, DynamicParam } from "src/lib/DynamicParamLib.sol"; import { computeTradingFunction, computeDeltaGivenDeltaLRoundUp, - computeDeltaGivenDeltaLRoundDown + computeDeltaGivenDeltaLRoundDown, + computeDeltaLXIn, + computeDeltaLYIn } from "src/LogNormal/LogNormalMath.sol"; import { decodeFeeUpdate, @@ -15,6 +17,7 @@ import { decodeWidthUpdate, decodeControllerUpdate } from "src/LogNormal/LogNormalUtils.sol"; +import { EPSILON } from "src/lib/StrategyLib.sol"; enum UpdateCode { Invalid, @@ -39,6 +42,17 @@ struct LogNormalParams { address controller; } +/// @dev Thrown when the mean parameter is not within the allowed bounds. +error InvalidMean(); + +/// @dev Thrown when the width parameter is not within the allowed bounds. +error InvalidWidth(); + +uint256 constant MIN_WIDTH = 1; +uint256 constant MAX_WIDTH = uint256(type(int256).max); +uint256 constant MIN_MEAN = 1; +uint256 constant MAX_MEAN = uint256(type(int256).max); + /** * @title LogNormal Strategy for DFMM. * @author Primitive @@ -58,7 +72,7 @@ contract LogNormal is PairStrategy { function init( address, uint256 poolId, - Pool calldata, + Pool calldata pool, bytes calldata data ) public @@ -74,6 +88,18 @@ contract LogNormal is PairStrategy { (reserves, totalLiquidity, params) = abi.decode(data, (uint256[], uint256, LogNormalParams)); + if (params.mean < MIN_WIDTH || params.mean > MAX_MEAN) { + revert InvalidMean(); + } + + if (params.width < MIN_WIDTH || params.width > MAX_WIDTH) { + revert InvalidWidth(); + } + + if (pool.reserves.length != 2 || reserves.length != 2) { + revert InvalidReservesLength(); + } + internalParams[poolId].mean.lastComputedValue = params.mean; internalParams[poolId].width.lastComputedValue = params.width; internalParams[poolId].swapFee = params.swapFee; @@ -81,7 +107,7 @@ contract LogNormal is PairStrategy { invariant = tradingFunction(reserves, totalLiquidity, getPoolParams(poolId)); - valid = invariant >= 0; + valid = invariant >= 0 && invariant <= EPSILON; } /// @inheritdoc IStrategy @@ -99,10 +125,16 @@ contract LogNormal is PairStrategy { } else if (updateCode == UpdateCode.Width) { (uint256 targetWidth, uint256 targetTimestamp) = decodeWidthUpdate(data); + if (targetWidth < MIN_WIDTH || targetWidth > MAX_WIDTH) { + revert InvalidWidth(); + } internalParams[poolId].width.set(targetWidth, targetTimestamp); } else if (updateCode == UpdateCode.Mean) { (uint256 targetMean, uint256 targetTimestamp) = decodeMeanUpdate(data); + if (targetMean < MIN_MEAN || targetMean > MAX_MEAN) { + revert InvalidMean(); + } internalParams[poolId].mean.set(targetMean, targetTimestamp); } else if (updateCode == UpdateCode.Controller) { internalParams[poolId].controller = decodeControllerUpdate(data); @@ -177,4 +209,34 @@ contract LogNormal is PairStrategy { ); return deltas; } + + function _computeSwapDeltaLiquidity( + Pool memory pool, + bytes memory params, + uint256 tokenInIndex, + uint256, + uint256 amountIn, + uint256 + ) internal pure override returns (uint256) { + LogNormalParams memory poolParams = + abi.decode(params, (LogNormalParams)); + + if (tokenInIndex == 0) { + return computeDeltaLXIn( + amountIn, + pool.reserves[0], + pool.reserves[1], + pool.totalLiquidity, + poolParams + ); + } + + return computeDeltaLYIn( + amountIn, + pool.reserves[0], + pool.reserves[1], + pool.totalLiquidity, + poolParams + ); + } } diff --git a/src/LogNormal/LogNormalMath.sol b/src/LogNormal/LogNormalMath.sol index 5a5c9a08..801e6c33 100644 --- a/src/LogNormal/LogNormalMath.sol +++ b/src/LogNormal/LogNormalMath.sol @@ -31,7 +31,7 @@ function computeDeltaGivenDeltaLRoundUp( uint256 deltaLiquidity, uint256 totalLiquidity ) pure returns (uint256) { - return reserve.mulWadUp(deltaLiquidity.divWadUp(totalLiquidity)); + return reserve.mulDivUp(deltaLiquidity, totalLiquidity); } function computeDeltaGivenDeltaLRoundDown( @@ -39,11 +39,14 @@ function computeDeltaGivenDeltaLRoundDown( uint256 deltaLiquidity, uint256 totalLiquidity ) pure returns (uint256) { - return reserve.mulWadDown(deltaLiquidity.divWadDown(totalLiquidity)); + return reserve.mulDivDown(deltaLiquidity, totalLiquidity); } -function computeLnSDivK(uint256 S, uint256 K) pure returns (int256 lnSDivK) { - lnSDivK = int256(S.divWadUp(K)).lnWad(); +function computeLnSDivMean( + uint256 S, + uint256 mean +) pure returns (int256 lnSDivK) { + lnSDivK = int256(S.divWadUp(mean)).lnWad(); } /** @@ -110,9 +113,9 @@ function computeD1( uint256 S, LogNormalParams memory params ) pure returns (int256 d1) { - int256 lnSDivK = computeLnSDivK(S, params.mean); - uint256 halfSigmaPowTwoTau = computeHalfSigmaSquared(params.width); - d1 = (lnSDivK + int256(halfSigmaPowTwoTau)).wadDiv(int256(params.width)); + int256 lnSDivMean = computeLnSDivMean(S, params.mean); + uint256 halfSigmaPowTwo = computeHalfSigmaSquared(params.width); + d1 = (lnSDivMean + int256(halfSigmaPowTwo)).wadDiv(int256(params.width)); } /// @dev Computes the d2 parameter for the Black-Scholes formula. @@ -124,9 +127,9 @@ function computeD2( uint256 S, LogNormalParams memory params ) pure returns (int256 d2) { - int256 lnSDivK = computeLnSDivK(S, params.mean); + int256 lnSDivMean = computeLnSDivMean(S, params.mean); uint256 halfSigmaPowTwo = computeHalfSigmaSquared(params.width); - d2 = (lnSDivK - int256(halfSigmaPowTwo)).wadDiv(int256(params.width)); + d2 = (lnSDivMean - int256(halfSigmaPowTwo)).wadDiv(int256(params.width)); } /** diff --git a/src/LogNormal/LogNormalSolver.sol b/src/LogNormal/LogNormalSolver.sol index 49f4d408..9266fc85 100644 --- a/src/LogNormal/LogNormalSolver.sol +++ b/src/LogNormal/LogNormalSolver.sol @@ -310,13 +310,9 @@ contract LogNormalSolver { bytes memory swapData; if (swapXIn) { - swapData = abi.encode( - 0, 1, amountIn, state.amountOut, state.deltaLiquidity - ); + swapData = abi.encode(0, 1, amountIn, state.amountOut); } else { - swapData = abi.encode( - 1, 0, amountIn, state.amountOut, state.deltaLiquidity - ); + swapData = abi.encode(1, 0, amountIn, state.amountOut); } uint256 poolId = poolId; diff --git a/src/LogNormal/LogNormalUtils.sol b/src/LogNormal/LogNormalUtils.sol index 55e39843..e7a59a8f 100644 --- a/src/LogNormal/LogNormalUtils.sol +++ b/src/LogNormal/LogNormalUtils.sol @@ -79,8 +79,6 @@ function computeInitialPoolData( /// @dev Casts a positived signed integer to an unsigned integer, reverting if `x` is negative. function toUint(int256 x) pure returns (uint256) { - unchecked { - require(x >= 0, "toUint: negative"); - return uint256(x); - } + require(x >= 0, "toUint: negative"); + return uint256(x); } diff --git a/src/NTokenGeometricMean/NTokenGeometricMean.sol b/src/NTokenGeometricMean/NTokenGeometricMean.sol index be7e88d4..e885a9e4 100644 --- a/src/NTokenGeometricMean/NTokenGeometricMean.sol +++ b/src/NTokenGeometricMean/NTokenGeometricMean.sol @@ -12,9 +12,10 @@ import { import { computeTradingFunction, computeDeltaGivenDeltaLRoundUp, - computeDeltaGivenDeltaLRoundDown + computeDeltaGivenDeltaLRoundDown, + computeSwapDeltaLiquidity } from "src/NTokenGeometricMean/NTokenGeometricMeanMath.sol"; -import { ONE } from "src/lib/StrategyLib.sol"; +import { ONE, EPSILON } from "src/lib/StrategyLib.sol"; /** * @dev Parameterization of the GeometricMean curve. @@ -69,7 +70,6 @@ contract NTokenGeometricMean is NTokenStrategy { int256 invariant; address controller; uint256 swapFee; - uint256 wX; uint256 totalLiquidity; uint256[] reserves; uint256[] weights; @@ -81,7 +81,7 @@ contract NTokenGeometricMean is NTokenStrategy { uint256 poolId, Pool calldata, bytes calldata data - ) external returns (bool, int256, uint256[] memory, uint256) { + ) external onlyDFMM returns (bool, int256, uint256[] memory, uint256) { InitState memory state; ( @@ -122,7 +122,7 @@ contract NTokenGeometricMean is NTokenStrategy { state.reserves, state.totalLiquidity, getPoolParams(poolId) ); - bool valid = invariant >= 0; + bool valid = invariant >= 0 && invariant <= EPSILON; return (valid, invariant, state.reserves, state.totalLiquidity); } @@ -133,7 +133,7 @@ contract NTokenGeometricMean is NTokenStrategy { uint256 poolId, Pool calldata, bytes calldata data - ) external { + ) external onlyDFMM { if (sender != internalParams[poolId].controller) revert InvalidSender(); UpdateCode updateCode = abi.decode(data, (UpdateCode)); @@ -250,4 +250,25 @@ contract NTokenGeometricMean is NTokenStrategy { nextReserves[i] = reserveT - deltas[i]; } } + + /// @inheritdoc NTokenStrategy + function _computeSwapDeltaLiquidity( + Pool memory pool, + bytes memory params, + uint256 tokenInIndex, + uint256, + uint256 amountIn, + uint256 + ) internal pure override returns (uint256) { + NTokenGeometricMeanParams memory poolParams = + abi.decode(params, (NTokenGeometricMeanParams)); + + return computeSwapDeltaLiquidity( + amountIn, + pool.reserves[tokenInIndex], + pool.totalLiquidity, + poolParams.weights[tokenInIndex], + poolParams.swapFee + ); + } } diff --git a/src/NTokenGeometricMean/NTokenGeometricMeanMath.sol b/src/NTokenGeometricMean/NTokenGeometricMeanMath.sol index cc69b4a3..d46ecdbf 100644 --- a/src/NTokenGeometricMean/NTokenGeometricMeanMath.sol +++ b/src/NTokenGeometricMean/NTokenGeometricMeanMath.sol @@ -30,7 +30,7 @@ function computeDeltaGivenDeltaLRoundUp( uint256 deltaLiquidity, uint256 totalLiquidity ) pure returns (uint256) { - return reserve.mulWadUp(deltaLiquidity.divWadUp(totalLiquidity)); + return reserve.mulDivUp(deltaLiquidity, totalLiquidity); } function computeDeltaGivenDeltaLRoundDown( @@ -38,7 +38,7 @@ function computeDeltaGivenDeltaLRoundDown( uint256 deltaLiquidity, uint256 totalLiquidity ) pure returns (uint256) { - return reserve.mulWadDown(deltaLiquidity.divWadDown(totalLiquidity)); + return reserve.mulDivDown(deltaLiquidity, totalLiquidity); } function computeL( @@ -126,3 +126,15 @@ function computeNextLiquidity( } return accumulator; } + +function computeSwapDeltaLiquidity( + uint256 amountIn, + uint256 reserve, + uint256 totalLiquidity, + uint256 weight, + uint256 swapFee +) pure returns (uint256) { + return weight.mulWadUp(ONE - swapFee).mulWadUp(totalLiquidity).mulWadUp( + amountIn.divWadUp(reserve) + ); +} diff --git a/src/NTokenGeometricMean/NTokenGeometricMeanSolver.sol b/src/NTokenGeometricMean/NTokenGeometricMeanSolver.sol index e254d7a4..ba4de9f7 100644 --- a/src/NTokenGeometricMean/NTokenGeometricMeanSolver.sol +++ b/src/NTokenGeometricMean/NTokenGeometricMeanSolver.sol @@ -111,13 +111,8 @@ contract NTokenGeometricMeanSolver { state.amountOut = state.outReserve - a; } - bytes memory swapData = abi.encode( - tokenInIndex, - tokenOutIndex, - amountIn, - state.amountOut, - state.deltaLiquidity - ); + bytes memory swapData = + abi.encode(tokenInIndex, tokenOutIndex, amountIn, state.amountOut); (bool valid,,,,,,) = IStrategy(strategy).validateSwap( address(this), poolId, pool, swapData diff --git a/src/NTokenGeometricMean/NTokenGeometricMeanUtils.sol b/src/NTokenGeometricMean/NTokenGeometricMeanUtils.sol index bf40e314..7e655bf1 100644 --- a/src/NTokenGeometricMean/NTokenGeometricMeanUtils.sol +++ b/src/NTokenGeometricMean/NTokenGeometricMeanUtils.sol @@ -7,8 +7,6 @@ import { } from "src/NTokenGeometricMean/NTokenGeometricMean.sol"; import { computeReserveFromNumeraire, - computeL, - computeTradingFunction, computeNextLiquidity } from "./NTokenGeometricMeanMath.sol"; diff --git a/src/NTokenStrategy.sol b/src/NTokenStrategy.sol index 8f2f21d4..281dab90 100644 --- a/src/NTokenStrategy.sol +++ b/src/NTokenStrategy.sol @@ -3,6 +3,12 @@ pragma solidity ^0.8.13; import { IStrategy, Pool } from "src/interfaces/IStrategy.sol"; +/** + * @dev Thrown when the length of the deltas array is not the + * same as the length of the reserves array. + */ +error InvalidTokenDeltas(); + /** * @title N-token strategy base contract for DFMM. * @notice This abstract contract defines the basic behavior of @@ -46,6 +52,9 @@ abstract contract NTokenStrategy is IStrategy { // we cannot assign to `deltaLiquidity` directly. (uint256[] memory maxTokenDeltas, uint256 deltaL) = abi.decode(data, (uint256[], uint256)); + if (maxTokenDeltas.length != pool.reserves.length) { + revert InvalidTokenDeltas(); + } deltaLiquidity = deltaL; (uint256[] memory deltas, uint256[] memory nextReserves) = @@ -82,8 +91,11 @@ abstract contract NTokenStrategy is IStrategy { { (uint256[] memory minTokenDeltas, uint256 deltaL) = abi.decode(data, (uint256[], uint256)); - + if (minTokenDeltas.length != pool.reserves.length) { + revert InvalidTokenDeltas(); + } deltaLiquidity = deltaL; + (uint256[] memory deltas, uint256[] memory nextReserves) = _computeDeallocateDeltasAndReservesGivenDeltaL( deltaLiquidity, minTokenDeltas, pool @@ -119,16 +131,20 @@ abstract contract NTokenStrategy is IStrategy { uint256 deltaLiquidity ) { - (tokenInIndex, tokenOutIndex, amountIn, amountOut, deltaLiquidity) = - abi.decode(data, (uint256, uint256, uint256, uint256, uint256)); + bytes memory params = getPoolParams(poolId); + + (tokenInIndex, tokenOutIndex, amountIn, amountOut) = + abi.decode(data, (uint256, uint256, uint256, uint256)); + + deltaLiquidity = _computeSwapDeltaLiquidity( + pool, params, tokenInIndex, tokenOutIndex, amountIn, amountOut + ); pool.reserves[tokenInIndex] += amountIn; pool.reserves[tokenOutIndex] -= amountOut; invariant = tradingFunction( - pool.reserves, - pool.totalLiquidity + deltaLiquidity, - getPoolParams(poolId) + pool.reserves, pool.totalLiquidity + deltaLiquidity, params ); valid = invariant >= 0; @@ -185,4 +201,16 @@ abstract contract NTokenStrategy is IStrategy { view virtual returns (uint256[] memory deltas, uint256[] memory nextReserves); + + /** + * @dev Computes the deltaLiquidity for a swap operation. + */ + function _computeSwapDeltaLiquidity( + Pool memory pool, + bytes memory params, + uint256 tokenInIndex, + uint256 tokenOutIndex, + uint256 amountIn, + uint256 amountOut + ) internal view virtual returns (uint256); } diff --git a/src/PairStrategy.sol b/src/PairStrategy.sol index 5dcff55b..70d0cec9 100644 --- a/src/PairStrategy.sol +++ b/src/PairStrategy.sol @@ -139,8 +139,12 @@ abstract contract PairStrategy is IStrategy { { bytes memory params = getPoolParams(poolId); - (tokenInIndex, tokenOutIndex, amountIn, amountOut, deltaLiquidity) = - abi.decode(data, (uint256, uint256, uint256, uint256, uint256)); + (tokenInIndex, tokenOutIndex, amountIn, amountOut) = + abi.decode(data, (uint256, uint256, uint256, uint256)); + + deltaLiquidity = _computeSwapDeltaLiquidity( + pool, params, tokenInIndex, tokenOutIndex, amountIn, amountOut + ); pool.reserves[tokenInIndex] += amountIn; pool.reserves[tokenOutIndex] -= amountOut; @@ -182,7 +186,7 @@ abstract contract PairStrategy is IStrategy { ) internal view virtual returns (uint256[] memory); /** - * @dev Computes the deltas to de de allocate given a liquidity. + * @dev Computes the deltas to deallocate given a liquidity. * delta. This function is meant to be implemented by the * strategy inheriting from this contract. * @param deltaLiquidity Amount of liquidity to deallocate. @@ -195,4 +199,16 @@ abstract contract PairStrategy is IStrategy { Pool memory pool, bytes memory data ) internal view virtual returns (uint256[] memory); + + /** + * @dev Computes the deltaLiquidity for a swap operation. + */ + function _computeSwapDeltaLiquidity( + Pool memory pool, + bytes memory params, + uint256 tokenInIndex, + uint256 tokenOutIndex, + uint256 amountIn, + uint256 amountOut + ) internal view virtual returns (uint256); } diff --git a/src/interfaces/IDFMM.sol b/src/interfaces/IDFMM.sol index 1adab2cf..7eb9db7e 100644 --- a/src/interfaces/IDFMM.sol +++ b/src/interfaces/IDFMM.sol @@ -8,6 +8,8 @@ pragma solidity ^0.8.13; * @param reserves Array of token reserves in the pool in WAD. * @param totalLiquidity Total liquidity in the pool. * @param liquidityToken Address of the LP token contract. + * @param feeCollector Address receiving controller fees. + * @param controllerFee Fees charged on the swap fee (in WAD). */ struct Pool { address strategy; @@ -26,6 +28,8 @@ struct Pool { * @param strategy Address of the associated strategy contract. * @param tokens Array of token addresses in the pool. * @param data An array of bytes used by the strategy contract. + * @param feeCollector Address receiving controller fees. + * @param controllerFee Fees charged on the swap fee (in WAD). */ struct InitParams { string name; @@ -56,6 +60,9 @@ interface IDFMM { /// @dev Thrown when pool tokens are identical. error InvalidDuplicateTokens(); + /// @dev Thrown when a strategy is returning an invalid amount of reserves. + error InvalidReserves(); + /// @dev Thrown when a pool is being initalized with less than two tokens. error InvalidMinimumTokens(); diff --git a/src/interfaces/IStrategy.sol b/src/interfaces/IStrategy.sol index 40027f55..0b10cd9a 100644 --- a/src/interfaces/IStrategy.sol +++ b/src/interfaces/IStrategy.sol @@ -23,6 +23,9 @@ interface IStrategy { /// @dev Thrown when an expected delta does not match the actual delta. error DeltaError(uint256 expected, uint256 actual); + /// @dev Thrown when the reserves length is not 2. + error InvalidReservesLength(); + // Setters /** diff --git a/src/lib/StrategyLib.sol b/src/lib/StrategyLib.sol index d1d60387..931e0b47 100644 --- a/src/lib/StrategyLib.sol +++ b/src/lib/StrategyLib.sol @@ -3,12 +3,10 @@ pragma solidity ^0.8.13; import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; +int256 constant EPSILON = 30; uint256 constant HALF = 0.5e18; uint256 constant ONE = 1e18; uint256 constant TWO = 2e18; -int256 constant I_ONE = int256(ONE); -int256 constant I_TWO = int256(TWO); -int256 constant I_HALF = int256(HALF); using FixedPointMathLib for uint256; using FixedPointMathLib for int256; @@ -19,8 +17,7 @@ function computeAllocationGivenX( uint256 rx, uint256 L ) pure returns (uint256 nextRx, uint256 nextL) { - uint256 liquidityPerRx = L.divWadUp(rx); - uint256 deltaL = amountX.mulWadUp(liquidityPerRx); + uint256 deltaL = amountX.mulDivDown(L, rx); nextRx = add ? rx + amountX : rx - amountX; nextL = add ? L + deltaL : L - deltaL; } @@ -31,8 +28,7 @@ function computeAllocationGivenY( uint256 ry, uint256 L ) pure returns (uint256 nextRy, uint256 nextL) { - uint256 liquidityPerRy = L.divWadUp(ry); - uint256 deltaL = amountY.mulWadUp(liquidityPerRy); + uint256 deltaL = amountY.mulDivDown(L, ry); nextRy = add ? ry + amountY : ry - amountY; nextL = add ? L + deltaL : L - deltaL; } @@ -42,7 +38,7 @@ function computeDeltaLGivenDeltaX( uint256 liquidity, uint256 reserveX ) pure returns (uint256 deltaL) { - return liquidity.mulWadDown(deltaX.divWadDown(reserveX)); + return liquidity.mulDivDown(deltaX, reserveX); } function computeDeltaLGivenDeltaY( @@ -50,7 +46,7 @@ function computeDeltaLGivenDeltaY( uint256 liquidity, uint256 reserveY ) pure returns (uint256 deltaL) { - return liquidity.mulWadDown(deltaY.divWadDown(reserveY)); + return liquidity.mulDivDown(deltaY, reserveY); } function computeDeltaYGivenDeltaX( @@ -58,7 +54,7 @@ function computeDeltaYGivenDeltaX( uint256 reserveX, uint256 reserveY ) pure returns (uint256 deltaY) { - return reserveY.mulWadDown(deltaX.divWadUp(reserveX)); + return reserveY.mulDivUp(deltaX, reserveX); } function computeDeltaXGivenDeltaL( @@ -66,7 +62,7 @@ function computeDeltaXGivenDeltaL( uint256 liquidity, uint256 reserveX ) pure returns (uint256 deltaX) { - return reserveX.mulWadUp(deltaL.divWadUp(liquidity)); + return reserveX.mulDivUp(deltaL, liquidity); } function computeDeltaYGivenDeltaL( @@ -74,5 +70,5 @@ function computeDeltaYGivenDeltaL( uint256 liquidity, uint256 reserveY ) pure returns (uint256 deltaX) { - return reserveY.mulWadUp(deltaL.divWadUp(liquidity)); + return reserveY.mulDivUp(deltaL, liquidity); } diff --git a/test/ConstantSum/ConstantSumTest.t.sol b/test/ConstantSum/ConstantSumTest.t.sol deleted file mode 100644 index 3d7060c7..00000000 --- a/test/ConstantSum/ConstantSumTest.t.sol +++ /dev/null @@ -1,279 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "solmate/test/utils/mocks/MockERC20.sol"; -import "src/DFMM.sol"; -import "src/ConstantSum/ConstantSum.sol"; -import "src/ConstantSum/ConstantSumSolver.sol"; - -contract ConstantSumTest is Test { - using stdStorage for StdStorage; - - DFMM dfmm; - ConstantSum constantSum; - ConstantSumSolver solver; - MockERC20 tokenX; - MockERC20 tokenY; - - uint256 POOL_ID; - - uint256 public constant TEST_ZERO_FEE = 0; - uint256 public constant TEST_SWAP_FEE = 0.003 ether; - - function setUp() public { - tokenX = new MockERC20("tokenX", "X", 18); - tokenY = new MockERC20("tokenY", "Y", 18); - tokenX.mint(address(this), 100_000_000 ether); - tokenY.mint(address(this), 100_000_000 ether); - - dfmm = new DFMM(address(0)); - constantSum = new ConstantSum(address(dfmm)); - solver = new ConstantSumSolver(address(constantSum)); - tokenX.approve(address(dfmm), type(uint256).max); - tokenY.approve(address(dfmm), type(uint256).max); - } - - modifier basic_feeless() { - vm.warp(0); - - ConstantSumParams memory params = ConstantSumParams({ - price: ONE * 2, - swapFee: 0, - controller: address(0) - }); - - uint256 init_x = ONE * 1; - uint256 init_y = ONE * 1; - - bytes memory initData = - solver.getInitialPoolData(init_x, init_y, params); - - address[] memory tokens = new address[](2); - tokens[0] = address(tokenX); - tokens[1] = address(tokenY); - - InitParams memory initParams = InitParams({ - name: "", - symbol: "", - strategy: address(constantSum), - tokens: tokens, - data: initData, - feeCollector: address(0), - controllerFee: 0 - }); - - (POOL_ID,,) = dfmm.init(initParams); - _; - } - - modifier basic() { - vm.warp(0); - - ConstantSumParams memory params = ConstantSumParams({ - price: ONE * 2, - swapFee: TEST_SWAP_FEE, - controller: address(0) - }); - - uint256 init_x = ONE * 1; - uint256 init_y = ONE * 1; - - bytes memory initData = - solver.getInitialPoolData(init_x, init_y, params); - - address[] memory tokens = new address[](2); - tokens[0] = address(tokenX); - tokens[1] = address(tokenY); - - InitParams memory initParams = InitParams({ - name: "", - symbol: "", - strategy: address(constantSum), - tokens: tokens, - data: initData, - feeCollector: address(0), - controllerFee: 0 - }); - - (POOL_ID,,) = dfmm.init(initParams); - _; - } - - function test_init() public basic { - (ConstantSumParams memory params) = - abi.decode(constantSum.getPoolParams(POOL_ID), (ConstantSumParams)); - assertEq(params.price, 2 ether); - assertEq(params.swapFee, 0.003 ether); - assertEq(params.controller, address(0)); - - Pool memory pool = dfmm.pools(POOL_ID); - - assertEq(pool.reserves[0], 1 ether); - assertEq(pool.reserves[1], 1 ether); - assertEq(pool.totalLiquidity, 1.5 ether); - } - - function test_constant_sum_swap_x_in_no_fee() public basic_feeless { - uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); - uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); - uint256 preUserBalanceX = tokenX.balanceOf(address(this)); - uint256 preUserBalanceY = tokenY.balanceOf(address(this)); - - bool isSwapXForY = true; - uint256 amountIn = 0.1 ether; - - (,, bytes memory swapData) = - solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); - (,, uint256 inputAmount, uint256 outputAmount) = - dfmm.swap(POOL_ID, address(this), swapData); - - assertEq(tokenX.balanceOf(address(dfmm)), preDfmmBalanceX + inputAmount); - assertEq( - tokenY.balanceOf(address(dfmm)), preDfmmBalanceY - outputAmount - ); - assertEq(tokenX.balanceOf(address(this)), preUserBalanceX - inputAmount); - assertEq( - tokenY.balanceOf(address(this)), preUserBalanceY + outputAmount - ); - } - - function test_constant_sum_swap_y_in_no_fee() public basic_feeless { - uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); - uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); - uint256 preUserBalanceX = tokenX.balanceOf(address(this)); - uint256 preUserBalanceY = tokenY.balanceOf(address(this)); - - bool isSwapXForY = false; - uint256 amountIn = 0.1 ether; - (,, bytes memory swapData) = - solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); - (,, uint256 inputAmount, uint256 outputAmount) = - dfmm.swap(POOL_ID, address(this), swapData); - - assertEq( - tokenX.balanceOf(address(dfmm)), preDfmmBalanceX - outputAmount - ); - assertEq(tokenY.balanceOf(address(dfmm)), preDfmmBalanceY + inputAmount); - assertEq( - tokenX.balanceOf(address(this)), preUserBalanceX + outputAmount - ); - assertEq(tokenY.balanceOf(address(this)), preUserBalanceY - inputAmount); - } - - function test_constant_sum_swap_x_in_invalid() public basic_feeless { - bool xIn = true; - uint256 amountIn = 1.1 ether; - vm.expectRevert(ConstantSumSolver.NotEnoughLiquidity.selector); - solver.simulateSwap(POOL_ID, xIn, amountIn); - } - - function test_constant_sum_swap_y_in_invalid() public basic_feeless { - bool xIn = false; - uint256 amountIn = 2.1 ether; - vm.expectRevert(ConstantSumSolver.NotEnoughLiquidity.selector); - solver.simulateSwap(POOL_ID, xIn, amountIn); - } - - function test_constant_sum_swap_x_in_with_fee() public basic { - uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); - uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); - uint256 preUserBalanceX = tokenX.balanceOf(address(this)); - uint256 preUserBalanceY = tokenY.balanceOf(address(this)); - - bool isSwapXForY = true; - uint256 amountIn = 0.1 ether; - (,, bytes memory swapData) = - solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); - (,, uint256 inputAmount, uint256 outputAmount) = - dfmm.swap(POOL_ID, address(this), swapData); - - assertEq(tokenX.balanceOf(address(dfmm)), preDfmmBalanceX + inputAmount); - assertEq( - tokenY.balanceOf(address(dfmm)), preDfmmBalanceY - outputAmount - ); - assertEq(tokenX.balanceOf(address(this)), preUserBalanceX - inputAmount); - assertEq( - tokenY.balanceOf(address(this)), preUserBalanceY + outputAmount - ); - } - - function test_constant_sum_swap_y_in_with_fee() public basic { - uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); - uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); - uint256 preUserBalanceX = tokenX.balanceOf(address(this)); - uint256 preUserBalanceY = tokenY.balanceOf(address(this)); - - bool isSwapXForY = false; - uint256 amountIn = 0.1 ether; - (,, bytes memory swapData) = - solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); - (,, uint256 inputAmount, uint256 outputAmount) = - dfmm.swap(POOL_ID, address(this), swapData); - - assertEq( - tokenX.balanceOf(address(dfmm)), preDfmmBalanceX - outputAmount - ); - assertEq(tokenY.balanceOf(address(dfmm)), preDfmmBalanceY + inputAmount); - assertEq( - tokenX.balanceOf(address(this)), preUserBalanceX + outputAmount - ); - assertEq(tokenY.balanceOf(address(this)), preUserBalanceY - inputAmount); - } - /* - function test_constant_sum_allocate() public basic { - uint256 poolId = dfmm.nonce() - 1; - uint256 amountX = 0.1 ether; - uint256 amountY = 0.1 ether; - (bool valid, bytes memory swapData) = - solver.simulateAllocate(poolId, amountX, amountY); - console2.log("Valid: ", valid); - assert(valid); - - (uint256 endReservesRx, uint256 endReservesRy, uint256 endReservesL) = - abi.decode(swapData, (uint256, uint256, uint256)); - - console2.log("endReservesRx: ", endReservesRx); - assertEq(endReservesRx, 1.1 ether); - - console2.log("endReservesRy: ", endReservesRy); - assertEq(endReservesRy, 1.1 ether); - - console2.log("endReservesL: ", endReservesL); - assertEq(endReservesL, 1.65 ether); - - dfmm.allocate(poolId, swapData); - } - - function test_constant_sum_deallocate() public basic { - uint256 poolId = dfmm.nonce() - 1; - uint256 amountX = 0.1 ether; - uint256 amountY = 0.1 ether; - (, bytes memory swapData) = - solver.simulateDeallocate(poolId, amountX, amountY); - dfmm.deallocate(poolId, swapData); - } - - function test_constant_sum_fail_deallocate() public basic { - uint256 poolId = dfmm.nonce() - 1; - uint256 amountX = 1.2 ether; - uint256 amountY = 1.2 ether; - vm.expectRevert(ConstantSumSolver.NotEnoughLiquidity.selector); - solver.simulateDeallocate(poolId, amountX, amountY); - } - - function test_constant_sum_price_update() public basic { - uint256 poolId = dfmm.nonce() - 1; - uint256 newPrice = 3 ether; - bytes memory data = encodePriceUpdate(newPrice); - - vm.prank(address(0)); - DFMM(dfmm).update(poolId, data); - - (ConstantSumParams memory newParams) = abi.decode( - constantSum.getPoolParams(poolId), (ConstantSumParams) - ); - assertEq(newParams.price, 3 ether); - } - */ -} diff --git a/test/ConstantSum/unit/Allocate.t.sol b/test/ConstantSum/unit/Allocate.t.sol index 94c19cc6..3e21e32b 100644 --- a/test/ConstantSum/unit/Allocate.t.sol +++ b/test/ConstantSum/unit/Allocate.t.sol @@ -1,17 +1,21 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import { console } from "forge-std/console.sol"; import { ConstantSumSetUp } from "./SetUp.sol"; +import { + computeDeltaLiquidity, + ConstantSumParams +} from "src/ConstantSum/ConstantSumMath.sol"; contract ConstantSumAllocateTest is ConstantSumSetUp { -/* function test_ConstantSum_allocate_Works() public defaultPool { uint256 deltaX = 0.1 ether; uint256 deltaY = 0.1 ether; - (bool valid, bytes memory data) = - solver.simulateAllocate(POOL_ID, deltaX, deltaY); - console.log("valid", valid); + + ConstantSumParams memory params = + abi.decode(constantSum.getPoolParams(POOL_ID), (ConstantSumParams)); + + uint256 deltaL = computeDeltaLiquidity(deltaX, deltaY, params.price); + dfmm.allocate(POOL_ID, abi.encode(deltaX, deltaY, deltaL)); } - */ } diff --git a/test/ConstantSum/unit/Constructor.t.sol b/test/ConstantSum/unit/Constructor.t.sol index 136878bb..026e56d3 100644 --- a/test/ConstantSum/unit/Constructor.t.sol +++ b/test/ConstantSum/unit/Constructor.t.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import { ConstantSumSetUp } from "./SetUp.sol"; +import { ConstantSumSetUp, ConstantSum } from "./SetUp.sol"; -contract ConstantSumConstructorTest is ConstantSumSetUp { } +contract ConstantSumConstructorTest is ConstantSumSetUp { + function test_ConstantSum_constructor() public { + ConstantSum constantSum = new ConstantSum(address(dfmm)); + assertEq(constantSum.dfmm(), address(dfmm)); + } +} diff --git a/test/ConstantSum/unit/GetPoolParams.t.sol b/test/ConstantSum/unit/GetPoolParams.t.sol index 8381be67..7213df12 100644 --- a/test/ConstantSum/unit/GetPoolParams.t.sol +++ b/test/ConstantSum/unit/GetPoolParams.t.sol @@ -1,6 +1,43 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import { ConstantSumSetUp } from "./SetUp.sol"; +import { ConstantSumParams } from "src/ConstantSum/ConstantSum.sol"; +import { ConstantSumSetUp, InitParams } from "./SetUp.sol"; -contract ConstantSumGetPoolParamsTest is ConstantSumSetUp { } +contract ConstantSumGetPoolParamsTest is ConstantSumSetUp { + function test_ConstantSum_getPoolParams_ReturnsPoolParams() public { + ConstantSumParams memory initPoolParams = ConstantSumParams({ + price: 2 ether, + swapFee: TEST_SWAP_FEE, + controller: address(this) + }); + + uint256 reserveX = 1 ether; + uint256 reserveY = 1 ether; + + bytes memory initData = + solver.getInitialPoolData(reserveX, reserveY, initPoolParams); + + address[] memory tokens = new address[](2); + tokens[0] = address(tokenX); + tokens[1] = address(tokenY); + + InitParams memory initParams = InitParams({ + name: "", + symbol: "", + strategy: address(constantSum), + tokens: tokens, + data: initData, + feeCollector: address(0), + controllerFee: 0 + }); + + (POOL_ID,,) = dfmm.init(initParams); + + ConstantSumParams memory poolParams = + abi.decode(constantSum.getPoolParams(POOL_ID), (ConstantSumParams)); + assertEq(poolParams.swapFee, initPoolParams.swapFee); + assertEq(poolParams.price, initPoolParams.price); + assertEq(poolParams.controller, initPoolParams.controller); + } +} diff --git a/test/ConstantSum/unit/Init.t.sol b/test/ConstantSum/unit/Init.t.sol index f5792377..ec4b9792 100644 --- a/test/ConstantSum/unit/Init.t.sol +++ b/test/ConstantSum/unit/Init.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import { ConstantSumSetUp } from "./SetUp.sol"; import { ConstantSum, ConstantSumParams } from "src/ConstantSum/ConstantSum.sol"; -import { DFMM, IDFMM, InitParams } from "src/DFMM.sol"; +import { Pool, InitParams } from "src/interfaces/IDFMM.sol"; +import { ConstantSumSetUp } from "./SetUp.sol"; contract ConstantSumInitTest is ConstantSumSetUp { function test_ConstantSum_init_InitializesPool() public { @@ -35,6 +35,98 @@ contract ConstantSumInitTest is ConstantSumSetUp { controllerFee: 0 }); + (POOL_ID,,) = dfmm.init(initParams); + Pool memory pool = dfmm.pools(POOL_ID); + + assertEq(pool.reserves[0], reserveX); + assertEq(pool.reserves[1], reserveY); + } + + // This test doesn't pass because the `controller` param is not stored + function test_ConstantSum_init_StoresPoolParams() public { + skip(); + + uint256 price = 1 ether; + + ConstantSumParams memory params = ConstantSumParams({ + price: price, + swapFee: TEST_SWAP_FEE, + controller: address(this) + }); + + uint256 reserveX = 1 ether; + uint256 reserveY = 1 ether; + + bytes memory initData = + solver.getInitialPoolData(reserveX, reserveY, params); + + address[] memory tokens = new address[](2); + tokens[0] = address(tokenX); + tokens[1] = address(tokenY); + + InitParams memory initParams = InitParams({ + name: "", + symbol: "", + strategy: address(constantSum), + tokens: tokens, + data: initData, + feeCollector: address(0), + controllerFee: 0 + }); + + (POOL_ID,,) = dfmm.init(initParams); + ConstantSumParams memory poolParams = + abi.decode(constantSum.getPoolParams(POOL_ID), (ConstantSumParams)); + + assertEq(poolParams.price, price); + assertEq(poolParams.swapFee, TEST_SWAP_FEE); + assertEq(poolParams.controller, address(this)); + } + + function test_ConstantSum_init_TransfersTokens() public { + uint256 price = 1 ether; + + ConstantSumParams memory params = ConstantSumParams({ + price: price, + swapFee: TEST_SWAP_FEE, + controller: address(this) + }); + + uint256 reserveX = 1 ether; + uint256 reserveY = 1 ether; + + bytes memory initData = + solver.getInitialPoolData(reserveX, reserveY, params); + + address[] memory tokens = new address[](2); + tokens[0] = address(tokenX); + tokens[1] = address(tokenY); + + InitParams memory initParams = InitParams({ + name: "", + symbol: "", + strategy: address(constantSum), + tokens: tokens, + data: initData, + feeCollector: address(0), + controllerFee: 0 + }); + + uint256 dfmmPreTokenXBalance = tokenX.balanceOf(address(dfmm)); + uint256 dfmmPreTokenYBalance = tokenY.balanceOf(address(dfmm)); + uint256 userPreTokenXBalance = tokenX.balanceOf(address(this)); + uint256 userPreTokenYBalance = tokenY.balanceOf(address(this)); + dfmm.init(initParams); + + uint256 dfmmPostTokenXBalance = tokenX.balanceOf(address(dfmm)); + uint256 dfmmPostTokenYBalance = tokenY.balanceOf(address(dfmm)); + uint256 userPostTokenXBalance = tokenX.balanceOf(address(this)); + uint256 userPostTokenYBalance = tokenY.balanceOf(address(this)); + + assertEq(dfmmPreTokenXBalance + reserveX, dfmmPostTokenXBalance); + assertEq(dfmmPreTokenYBalance + reserveY, dfmmPostTokenYBalance); + assertEq(userPreTokenXBalance - reserveX, userPostTokenXBalance); + assertEq(userPreTokenYBalance - reserveY, userPostTokenYBalance); } } diff --git a/test/ConstantSum/unit/SetUp.sol b/test/ConstantSum/unit/SetUp.sol index 4a6acad2..0e7e3100 100644 --- a/test/ConstantSum/unit/SetUp.sol +++ b/test/ConstantSum/unit/SetUp.sol @@ -55,4 +55,30 @@ contract ConstantSumSetUp is SetUp { _; } + + modifier zeroFeePool() { + uint256 reserveX = 1 ether; + uint256 reserveY = 1 ether; + + bytes memory initData = + solver.getInitialPoolData(reserveX, reserveY, zeroFeeParams); + + address[] memory tokens = new address[](2); + tokens[0] = address(tokenX); + tokens[1] = address(tokenY); + + InitParams memory initParams = InitParams({ + name: "", + symbol: "", + strategy: address(constantSum), + tokens: tokens, + data: initData, + feeCollector: address(0), + controllerFee: 0 + }); + + (POOL_ID,,) = dfmm.init(initParams); + + _; + } } diff --git a/test/ConstantSum/unit/Swap.t.sol b/test/ConstantSum/unit/Swap.t.sol index 6c27f322..17384f7c 100644 --- a/test/ConstantSum/unit/Swap.t.sol +++ b/test/ConstantSum/unit/Swap.t.sol @@ -3,4 +3,97 @@ pragma solidity ^0.8.13; import { ConstantSumSetUp } from "./SetUp.sol"; -contract ConstantSumSwapTest is ConstantSumSetUp { } +contract ConstantSumSwapTest is ConstantSumSetUp { + function test_ConstantSum_swap_SwapsXNoFee() public zeroFeePool { + uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); + uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); + uint256 preUserBalanceX = tokenX.balanceOf(address(this)); + uint256 preUserBalanceY = tokenY.balanceOf(address(this)); + + bool isSwapXForY = true; + uint256 amountIn = 0.1 ether; + + (,, bytes memory swapData) = + solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); + (,, uint256 inputAmount, uint256 outputAmount) = + dfmm.swap(POOL_ID, address(this), swapData); + + assertEq(tokenX.balanceOf(address(dfmm)), preDfmmBalanceX + inputAmount); + assertEq( + tokenY.balanceOf(address(dfmm)), preDfmmBalanceY - outputAmount + ); + assertEq(tokenX.balanceOf(address(this)), preUserBalanceX - inputAmount); + assertEq( + tokenY.balanceOf(address(this)), preUserBalanceY + outputAmount + ); + } + + function test_ConstantSum_swap_SwapsX() public defaultPool { + uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); + uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); + uint256 preUserBalanceX = tokenX.balanceOf(address(this)); + uint256 preUserBalanceY = tokenY.balanceOf(address(this)); + + bool isSwapXForY = true; + uint256 amountIn = 0.1 ether; + (,, bytes memory swapData) = + solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); + (,, uint256 inputAmount, uint256 outputAmount) = + dfmm.swap(POOL_ID, address(this), swapData); + + assertEq(tokenX.balanceOf(address(dfmm)), preDfmmBalanceX + inputAmount); + assertEq( + tokenY.balanceOf(address(dfmm)), preDfmmBalanceY - outputAmount + ); + assertEq(tokenX.balanceOf(address(this)), preUserBalanceX - inputAmount); + assertEq( + tokenY.balanceOf(address(this)), preUserBalanceY + outputAmount + ); + } + + function test_ConstantSum_swap_SwapsY() public defaultPool { + uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); + uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); + uint256 preUserBalanceX = tokenX.balanceOf(address(this)); + uint256 preUserBalanceY = tokenY.balanceOf(address(this)); + + bool isSwapXForY = false; + uint256 amountIn = 0.1 ether; + (,, bytes memory swapData) = + solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); + (,, uint256 inputAmount, uint256 outputAmount) = + dfmm.swap(POOL_ID, address(this), swapData); + + assertEq( + tokenX.balanceOf(address(dfmm)), preDfmmBalanceX - outputAmount + ); + assertEq(tokenY.balanceOf(address(dfmm)), preDfmmBalanceY + inputAmount); + assertEq( + tokenX.balanceOf(address(this)), preUserBalanceX + outputAmount + ); + assertEq(tokenY.balanceOf(address(this)), preUserBalanceY - inputAmount); + } + + function test_ConstantSum_swap_SwapsYNoFee() public zeroFeePool { + uint256 preDfmmBalanceX = tokenX.balanceOf(address(dfmm)); + uint256 preDfmmBalanceY = tokenY.balanceOf(address(dfmm)); + uint256 preUserBalanceX = tokenX.balanceOf(address(this)); + uint256 preUserBalanceY = tokenY.balanceOf(address(this)); + + bool isSwapXForY = false; + uint256 amountIn = 0.1 ether; + (,, bytes memory swapData) = + solver.simulateSwap(POOL_ID, isSwapXForY, amountIn); + (,, uint256 inputAmount, uint256 outputAmount) = + dfmm.swap(POOL_ID, address(this), swapData); + + assertEq( + tokenX.balanceOf(address(dfmm)), preDfmmBalanceX - outputAmount + ); + assertEq(tokenY.balanceOf(address(dfmm)), preDfmmBalanceY + inputAmount); + assertEq( + tokenX.balanceOf(address(this)), preUserBalanceX + outputAmount + ); + assertEq(tokenY.balanceOf(address(this)), preUserBalanceY - inputAmount); + } +} diff --git a/test/ConstantSum/unit/Update.t.sol b/test/ConstantSum/unit/Update.t.sol index 8ed75a73..26312886 100644 --- a/test/ConstantSum/unit/Update.t.sol +++ b/test/ConstantSum/unit/Update.t.sol @@ -1,6 +1,38 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import { ConstantSumSetUp } from "./SetUp.sol"; +import { ConstantSumSetUp, ConstantSumParams } from "./SetUp.sol"; +import { + encodeFeeUpdate, + encodePriceUpdate, + encodeControllerUpdate +} from "src/ConstantSum/ConstantSumUtils.sol"; -contract ConstantSumUpdateTest is ConstantSumSetUp { } +contract ConstantSumUpdateTest is ConstantSumSetUp { + function test_ConstantSum_update_SetsSwapFee() public defaultPool { + skip(); + uint256 newSwapFee = 0.004 ether; + dfmm.update(POOL_ID, encodeFeeUpdate(newSwapFee)); + ConstantSumParams memory poolParams = + abi.decode(constantSum.getPoolParams(POOL_ID), (ConstantSumParams)); + assertEq(poolParams.swapFee, newSwapFee); + } + + function test_ConstantSum_update_SetsPrice() public defaultPool { + skip(); + uint256 newPrice = 3 ether; + dfmm.update(POOL_ID, encodePriceUpdate(newPrice)); + ConstantSumParams memory poolParams = + abi.decode(constantSum.getPoolParams(POOL_ID), (ConstantSumParams)); + assertEq(poolParams.price, newPrice); + } + + function test_ConstantSum_update_SetsController() public defaultPool { + skip(); + address newController = address(this); + dfmm.update(POOL_ID, encodeControllerUpdate(newController)); + ConstantSumParams memory poolParams = + abi.decode(constantSum.getPoolParams(POOL_ID), (ConstantSumParams)); + assertEq(poolParams.controller, newController); + } +} diff --git a/test/ConstantSum/unit/ValidateSwap.t.sol b/test/ConstantSum/unit/ValidateSwap.t.sol index 382faf8e..6327d061 100644 --- a/test/ConstantSum/unit/ValidateSwap.t.sol +++ b/test/ConstantSum/unit/ValidateSwap.t.sol @@ -1,6 +1,27 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import { ConstantSumSolver } from "src/ConstantSum/ConstantSumSolver.sol"; import { ConstantSumSetUp } from "./SetUp.sol"; -contract ConstantSumValidateSwapTest is ConstantSumSetUp { } +contract ConstantSumValidateSwapTest is ConstantSumSetUp { + function test_ConstantSum_simulateSwap_RevertsInvalidSwapX() + public + defaultPool + { + bool xIn = true; + uint256 amountIn = 1.1 ether; + vm.expectRevert(ConstantSumSolver.NotEnoughLiquidity.selector); + solver.simulateSwap(POOL_ID, xIn, amountIn); + } + + function test_ConstantSum_simulateSwap_RevertsInvalidSwapY() + public + defaultPool + { + bool xIn = false; + uint256 amountIn = 2.1 ether; + vm.expectRevert(ConstantSumSolver.NotEnoughLiquidity.selector); + solver.simulateSwap(POOL_ID, xIn, amountIn); + } +} diff --git a/test/DFMM/unit/Init.t.sol b/test/DFMM/unit/Init.t.sol index 07625d3f..695d967d 100644 --- a/test/DFMM/unit/Init.t.sol +++ b/test/DFMM/unit/Init.t.sol @@ -31,8 +31,7 @@ contract DFMMInit is DFMMSetUp, Script { function test_DFMM_init_ReturnsStrategyInitialReserves() public { (, uint256[] memory reserves, uint256 totalLiquidity) = dfmm.init(getDefaultPoolParams(defaultData)); - // A bit of the liquidity is burnt - assertEq(initialLiquidity - 1000, totalLiquidity); + assertEq(initialLiquidity, totalLiquidity); assertEq(initialReserveX, reserves[0]); assertEq(initialReserveY, reserves[1]); } diff --git a/test/DFMM/unit/Internal.t.sol b/test/DFMM/unit/Internal.t.sol index 5c35b676..7d0226f4 100644 --- a/test/DFMM/unit/Internal.t.sol +++ b/test/DFMM/unit/Internal.t.sol @@ -8,8 +8,11 @@ import { ERC20WithFees } from "test/utils/ERC20WithFees.sol"; contract DFMMInternal is DFMM { constructor(address weth_) DFMM(weth_) { } - function transferFrom(address token, uint256 amount) external payable { - _transferFrom(token, amount); + function transferFrom( + address[] memory tokens, + uint256[] memory amounts + ) external payable { + _transferFrom(tokens, amounts); } function transfer(address token, address to, uint256 amount) external { @@ -28,21 +31,53 @@ contract DFMMInternalTest is DFMMSetUp { } function test_DFMM_transferFrom_WrapsETH() public { - uint256 amount = 1 ether; - dfmmInternal.transferFrom{ value: amount }(address(0), amount); - assertEq(weth.balanceOf(address(dfmmInternal)), amount); + address[] memory tokens = new address[](1); + tokens[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1 ether; + dfmmInternal.transferFrom{ value: amounts[0] }(tokens, amounts); + assertEq(weth.balanceOf(address(dfmmInternal)), amounts[0]); assertEq(address(weth).balance, 1 ether); assertEq(address(dfmmInternal).balance, 0); } function test_DFMM_transferFrom_RefundsExtraETH() public { - uint256 amount = 1 ether; - dfmmInternal.transferFrom{ value: amount * 2 }(address(0), amount); - assertEq(weth.balanceOf(address(dfmmInternal)), amount); + address[] memory tokens = new address[](1); + tokens[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1 ether; + dfmmInternal.transferFrom{ value: amounts[0] * 2 }(tokens, amounts); + assertEq(weth.balanceOf(address(dfmmInternal)), amounts[0]); assertEq(address(weth).balance, 1 ether); assertEq(address(dfmmInternal).balance, 0); } + function test_DFMM_transferFrom_UsesWETH() public { + address[] memory tokens = new address[](1); + tokens[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1 ether; + weth.deposit{ value: amounts[0] }(); + weth.approve(address(dfmmInternal), amounts[0]); + dfmmInternal.transferFrom(tokens, amounts); + assertEq(weth.balanceOf(address(dfmmInternal)), amounts[0]); + assertEq(address(weth).balance, amounts[0]); + assertEq(address(dfmmInternal).balance, 0); + } + + function test_DFMM_transferFrom_UsesWETHAndRefunds() public { + address[] memory tokens = new address[](1); + tokens[0] = address(weth); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1 ether; + weth.deposit{ value: amounts[0] }(); + weth.approve(address(dfmmInternal), amounts[0]); + dfmmInternal.transferFrom{ value: amounts[0] - 1 }(tokens, amounts); + assertEq(weth.balanceOf(address(dfmmInternal)), amounts[0]); + assertEq(address(weth).balance, amounts[0]); + assertEq(address(dfmmInternal).balance, 0); + } + function testFuzz_DFMM_transferFrom_TransferTokens(uint256 amount) public { vm.assume( amount @@ -50,11 +85,15 @@ contract DFMMInternalTest is DFMMSetUp { 115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_458 ); MockERC20 token = new MockERC20("", "", 18); + address[] memory tokens = new address[](1); + tokens[0] = address(token); + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; token.mint(address(this), amount); token.approve(address(dfmmInternal), amount); uint256 preDFMMBalance = token.balanceOf(address(dfmmInternal)); uint256 preThisBalance = token.balanceOf(address(this)); - dfmmInternal.transferFrom(address(token), amount); + dfmmInternal.transferFrom(tokens, amounts); assertEq( token.balanceOf(address(dfmmInternal)), preDFMMBalance + amount ); @@ -62,14 +101,18 @@ contract DFMMInternalTest is DFMMSetUp { } function test_DFMM_transferFrom_ScalesAmount() public { - uint256 amount = 1_000_000 * 10 ** 18; - uint256 scaledDownAmount = 1_000_000 * 10 ** 6; + address[] memory tokens = new address[](1); MockERC20 token = new MockERC20("", "", 6); + tokens[0] = address(token); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1_000_000 * 10 ** 18; + uint256 scaledDownAmount = 1_000_000 * 10 ** 6; + token.mint(address(this), scaledDownAmount); token.approve(address(dfmmInternal), scaledDownAmount); uint256 preDFMMBalance = token.balanceOf(address(dfmmInternal)); uint256 preThisBalance = token.balanceOf(address(this)); - dfmmInternal.transferFrom(address(token), amount); + dfmmInternal.transferFrom(tokens, amounts); assertEq( token.balanceOf(address(dfmmInternal)), preDFMMBalance + scaledDownAmount @@ -80,12 +123,16 @@ contract DFMMInternalTest is DFMMSetUp { } function test_DFMM_transferFrom_RevertsIfBalanceIsInsufficient() public { + address[] memory tokens = new address[](1); ERC20WithFees token = new ERC20WithFees("", "", 18, 500); - uint256 amount = 1 ether; - token.mint(address(this), amount); - token.approve(address(dfmmInternal), amount); + tokens[0] = address(token); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1 ether; + + token.mint(address(this), amounts[0]); + token.approve(address(dfmmInternal), amounts[0]); vm.expectRevert(IDFMM.InvalidTransfer.selector); - dfmmInternal.transferFrom(address(token), amount); + dfmmInternal.transferFrom(tokens, amounts); } function test_DFMM_transfer_UnwrapsETH() public { diff --git a/test/DFMM/unit/Swap.t.sol b/test/DFMM/unit/Swap.t.sol index 15732e66..0924c941 100644 --- a/test/DFMM/unit/Swap.t.sol +++ b/test/DFMM/unit/Swap.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; import { LPToken, Pool } from "src/DFMM.sol"; import { DFMMSetUp } from "./SetUp.sol"; contract DFMMSwapTest is DFMMSetUp { + using FixedPointMathLib for uint256; + function test_DFMM_swap_IncreasesTotalLiquidity() public { skip(); } @@ -43,9 +46,9 @@ contract DFMMSwapTest is DFMMSetUp { uint256 preBalance = token.balanceOf(address(this)); uint256 deltaLiquidity = 1 ether; - uint256 fees = deltaLiquidity * pool.controllerFee / 1 ether; - uint256 feesInToken = - fees * token.totalSupply() / (pool.totalLiquidity + deltaLiquidity); + uint256 fees = deltaLiquidity.mulWadDown(pool.controllerFee); + uint256 feesInToken = fees * token.totalSupply() + / (pool.totalLiquidity + deltaLiquidity - fees); dfmm.swap( POOL_ID, address(this), diff --git a/test/G3M/unit/Allocate.t.sol b/test/G3M/unit/Allocate.t.sol index a2a4ef2c..c671ebf9 100644 --- a/test/G3M/unit/Allocate.t.sol +++ b/test/G3M/unit/Allocate.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import "./SetUp.sol"; import "solmate/utils/FixedPointMathLib.sol"; +import { LPToken } from "src/LPToken.sol"; contract G3MAllocateTest is G3MSetUp { using FixedPointMathLib for uint256; @@ -107,8 +108,10 @@ contract G3MAllocateTest is G3MSetUp { solver.allocateGivenDeltaY(POOL_ID, maxDeltaY); (uint256[] memory reserves, uint256 liquidity) = getReservesAndLiquidity(POOL_ID); + console2.log("liquidity", liquidity); uint256 preLiquidityBalance = liquidityOf(address(this), POOL_ID); + console2.log(preLiquidityBalance); bytes memory data = abi.encode(maxDeltaX, maxDeltaY, deltaLiquidity); console2.log(maxDeltaX); @@ -124,11 +127,30 @@ contract G3MAllocateTest is G3MSetUp { assertEq(adjustedReserves[1], reserves[1] + deltas[1]); assertEq(adjustedLiquidity, liquidity + deltaLiquidity); - /* assertEq( preLiquidityBalance + deltaLiquidity, liquidityOf(address(this), POOL_ID) ); - */ + } + + function test_G3M_allocate_ReceiveAppropriateLpTokens() public init_100 { + (, uint256 initialL) = getReservesAndLiquidity(POOL_ID); + Pool memory pool = dfmm.pools(POOL_ID); + LPToken liquidityToken = LPToken(pool.liquidityToken); + + uint256 startBalance = liquidityToken.balanceOf(address(this)); + + uint256 dyMax = 100 ether; + (uint256 dxMax, uint256 dL) = solver.allocateGivenDeltaY(POOL_ID, dyMax); + bytes memory data = abi.encode(dxMax, dyMax, dL); + + dfmm.allocate(POOL_ID, data); + + (, uint256 nextL) = getReservesAndLiquidity(POOL_ID); + uint256 endBalance = liquidityToken.balanceOf(address(this)); + + // Add 1_000 wei to account for liquidity that was burnt on init + assertEq(startBalance + 1_000, initialL); + assertEq(endBalance + 1_000, nextL); } } diff --git a/test/G3M/unit/SetUp.sol b/test/G3M/unit/SetUp.sol index 1cd97be1..24069a51 100644 --- a/test/G3M/unit/SetUp.sol +++ b/test/G3M/unit/SetUp.sol @@ -31,6 +31,10 @@ contract G3MSetUp is SetUp { defaultReserveX, defaultStrikePrice, defaultParams ); + bytes default100InitialPoolData = computeInitialPoolData( + defaultReserveX * 100, defaultStrikePrice, defaultParams + ); + function setUp() public override { SetUp.setUp(); g3m = new GeometricMean(address(dfmm)); @@ -58,4 +62,24 @@ contract G3MSetUp is SetUp { _; } + + modifier init_100() { + address[] memory tokens = new address[](2); + tokens[0] = address(tokenX); + tokens[1] = address(tokenY); + + InitParams memory defaultInitParams = InitParams({ + name: "", + symbol: "", + strategy: address(g3m), + tokens: tokens, + data: default100InitialPoolData, + feeCollector: address(0), + controllerFee: 0 + }); + + (POOL_ID,,) = dfmm.init(defaultInitParams); + + _; + } } diff --git a/test/LogNormal/unit/SetUp.sol b/test/LogNormal/unit/SetUp.sol index 2866eb4e..733fcabd 100644 --- a/test/LogNormal/unit/SetUp.sol +++ b/test/LogNormal/unit/SetUp.sol @@ -27,7 +27,7 @@ contract LogNormalSetUp is SetUp { controller: address(this) }); - uint256 defaultReserveX = ONE; + uint256 defaultReserveX = 100 ether; uint256 defaultReserveXDeep = ONE * 10_000_000; uint256 defaultPrice = ONE; diff --git a/test/utils/LogNormalArbitrage.sol b/test/utils/LogNormalArbitrage.sol index f7283127..087de234 100644 --- a/test/utils/LogNormalArbitrage.sol +++ b/test/utils/LogNormalArbitrage.sol @@ -34,6 +34,10 @@ interface SolverLike { returns (LogNormalParams memory); } +int256 constant I_ONE = int256(ONE); +int256 constant I_TWO = int256(TWO); +int256 constant I_HALF = int256(HALF); + contract LogNormalArbitrage { using FixedPointMathLib for uint256; using FixedPointMathLib for int256; @@ -324,8 +328,8 @@ contract LogNormalArbitrage { int256 mean = int256(params.mean); int256 width = int256(params.width); - int256 lnSDivK = computeLnSDivK(uint256(S), params.mean); - int256 a = lnSDivK.wadDiv(width) - width.wadDiv(I_TWO); + int256 lnSDivMean = computeLnSDivMean(uint256(S), params.mean); + int256 a = lnSDivMean.wadDiv(width) - width.wadDiv(I_TWO); int256 cdfA = Gaussian.cdf(a); int256 delta = L.wadMul(mean).wadMul(cdfA); @@ -341,8 +345,8 @@ contract LogNormalArbitrage { int256 gamma = I_ONE - int256(params.swapFee); int256 width = int256(params.width); - int256 lnSDivK = computeLnSDivK(uint256(S), params.mean); - int256 a = Gaussian.cdf(lnSDivK.wadDiv(width) + width.wadDiv(I_TWO)); + int256 lnSDivMean = computeLnSDivMean(uint256(S), params.mean); + int256 a = Gaussian.cdf(lnSDivMean.wadDiv(width) + width.wadDiv(I_TWO)); int256 delta = L.wadMul(I_ONE - a); dx = delta - rX;