diff --git a/foundry.toml b/foundry.toml index fc855a1..4c9b590 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,5 +3,8 @@ src = "src" out = "out" libs = ["lib"] solc = "0.8.27" +optimizer = true +optimizer_runs = 10000 +gas_reports = ["*"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/src/EulerSwap.sol b/src/EulerSwap.sol index 698faf1..837001d 100644 --- a/src/EulerSwap.sol +++ b/src/EulerSwap.sol @@ -9,22 +9,6 @@ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol" import {EVCUtil} from "evc/utils/EVCUtil.sol"; contract EulerSwap is IEulerSwap, EVCUtil { - struct Params { - address vault0; - address vault1; - address myAccount; - uint112 debtLimit0; - uint112 debtLimit1; - uint256 fee; - } - - struct CurveParams { - uint256 priceX; - uint256 priceY; - uint256 concentrationX; - uint256 concentrationY; - } - bytes32 public constant curve = keccak256("EulerSwap v1"); address public immutable vault0; diff --git a/src/EulerSwapFactory.sol b/src/EulerSwapFactory.sol new file mode 100644 index 0000000..7249472 --- /dev/null +++ b/src/EulerSwapFactory.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IEulerSwapFactory} from "./interfaces/IEulerSwapFactory.sol"; +import {IEulerSwap, EulerSwap} from "./EulerSwap.sol"; +import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; + +/// @title EulerSwapFactory contract +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +contract EulerSwapFactory is IEulerSwapFactory, Ownable { + /// @dev An array to store all pools addresses. + address[] public allPools; + /// @dev Mapping to store pool addresses + mapping(bytes32 poolKey => address pool) public getPool; + + event PoolDeployed( + address indexed asset0, + address indexed asset1, + uint256 indexed feeMultiplier, + address swapAccount, + uint256 priceX, + uint256 priceY, + uint256 concentrationX, + uint256 concentrationY, + address pool + ); + + error InvalidQuery(); + + constructor() Ownable(msg.sender) {} + + /// @notice Deploy EulerSwap pool. + function deployPool(DeployParams memory params) external returns (address) { + EulerSwap pool = new EulerSwap( + IEulerSwap.Params({ + vault0: params.vault0, + vault1: params.vault1, + myAccount: params.holder, + debtLimit0: params.debtLimit0, + debtLimit1: params.debtLimit1, + fee: params.fee + }), + IEulerSwap.CurveParams({ + priceX: params.priceX, + priceY: params.priceY, + concentrationX: params.concentrationX, + concentrationY: params.concentrationY + }) + ); + + address poolAsset0 = pool.asset0(); + address poolAsset1 = pool.asset1(); + uint256 feeMultiplier = pool.feeMultiplier(); + + bytes32 poolKey = keccak256( + abi.encode( + poolAsset0, + poolAsset1, + feeMultiplier, + params.holder, + params.priceX, + params.priceY, + params.concentrationX, + params.concentrationY + ) + ); + + getPool[poolKey] = address(pool); + allPools.push(address(pool)); + + emit PoolDeployed( + poolAsset0, + poolAsset1, + feeMultiplier, + params.holder, + params.priceX, + params.priceY, + params.concentrationX, + params.concentrationY, + address(pool) + ); + + return address(pool); + } + + /// @notice Get the length of `allPools` array. + /// @return `allPools` length. + function allPoolsLength() external view returns (uint256) { + return allPools.length; + } + + /// @notice Get a slice of the deployed pools array. + /// @param _start Start index of the slice. + /// @param _end End index of the slice. + /// @return An array containing the slice of the deployed pools. + function getAllPoolsListSlice(uint256 _start, uint256 _end) external view returns (address[] memory) { + uint256 length = allPools.length; + if (_end == type(uint256).max) _end = length; + if (_end < _start || _end > length) revert InvalidQuery(); + + address[] memory allPoolsList = new address[](_end - _start); + for (uint256 i; i < _end - _start; ++i) { + allPoolsList[i] = allPools[_start + i]; + } + + return allPoolsList; + } +} diff --git a/src/interfaces/IEulerSwap.sol b/src/interfaces/IEulerSwap.sol index c9a9865..b939815 100644 --- a/src/interfaces/IEulerSwap.sol +++ b/src/interfaces/IEulerSwap.sol @@ -2,6 +2,22 @@ pragma solidity >=0.8.0; interface IEulerSwap { + struct Params { + address vault0; + address vault1; + address myAccount; + uint112 debtLimit0; + uint112 debtLimit1; + uint256 fee; + } + + struct CurveParams { + uint256 priceX; + uint256 priceY; + uint256 concentrationX; + uint256 concentrationY; + } + /// @notice Optimistically sends the requested amounts of tokens to the `to` /// address, invokes `uniswapV2Call` callback on `to` (if `data` was provided), /// and then verifies that a sufficient amount of tokens were transferred to diff --git a/src/interfaces/IEulerSwapFactory.sol b/src/interfaces/IEulerSwapFactory.sol new file mode 100644 index 0000000..87746c7 --- /dev/null +++ b/src/interfaces/IEulerSwapFactory.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +interface IEulerSwapFactory { + struct DeployParams { + address vault0; + address vault1; + address holder; + uint256 fee; + uint256 priceX; + uint256 priceY; + uint256 concentrationX; + uint256 concentrationY; + uint112 debtLimit0; + uint112 debtLimit1; + } + + function deployPool(DeployParams memory params) external returns (address); + + function allPools(uint256 index) external view returns (address); + function getPool(bytes32 poolKey) external view returns (address); + function allPoolsLength() external view returns (uint256); + function getAllPoolsListSlice(uint256 start, uint256 end) external view returns (address[] memory); +} diff --git a/test/EulerSwapFactoryTest.t.sol b/test/EulerSwapFactoryTest.t.sol new file mode 100644 index 0000000..e939056 --- /dev/null +++ b/test/EulerSwapFactoryTest.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {EulerSwapTestBase, EulerSwap} from "./EulerSwapTestBase.t.sol"; +import {EulerSwapFactory, IEulerSwapFactory} from "../src/EulerSwapFactory.sol"; + +contract EulerSwapFactoryTest is EulerSwapTestBase { + EulerSwapFactory public eulerSwapFactory; + + uint256 minFee = 0.0000000000001e18; + + function setUp() public virtual override { + super.setUp(); + + vm.prank(creator); + eulerSwapFactory = new EulerSwapFactory(); + } + + function testDeployPool() public { + uint256 allPoolsLengthBefore = eulerSwapFactory.allPoolsLength(); + + vm.prank(creator); + EulerSwap eulerSwap = EulerSwap( + eulerSwapFactory.deployPool( + IEulerSwapFactory.DeployParams( + address(eTST), address(eTST2), holder, 0, 1e18, 1e18, 0.4e18, 0.85e18, 50e18, 50e18 + ) + ) + ); + + uint256 allPoolsLengthAfter = eulerSwapFactory.allPoolsLength(); + bytes32 poolKey = keccak256( + abi.encode( + eulerSwap.asset0(), + eulerSwap.asset1(), + eulerSwap.feeMultiplier(), + eulerSwap.myAccount(), + eulerSwap.priceX(), + eulerSwap.priceY(), + eulerSwap.concentrationX(), + eulerSwap.concentrationY() + ) + ); + + assertEq(allPoolsLengthAfter - allPoolsLengthBefore, 1); + assertEq(eulerSwapFactory.getPool(poolKey), address(eulerSwap)); + assertEq(eulerSwapFactory.getPool(poolKey), address(eulerSwap)); + + address[] memory poolsList = eulerSwapFactory.getAllPoolsListSlice(0, type(uint256).max); + assertEq(poolsList.length, 1); + assertEq(poolsList[0], address(eulerSwap)); + assertEq(eulerSwapFactory.allPools(0), address(eulerSwap)); + } + + function testInvalidGetAllPoolsListSliceQuery() public { + vm.expectRevert(EulerSwapFactory.InvalidQuery.selector); + eulerSwapFactory.getAllPoolsListSlice(1, 0); + } + + function testDeployWithAssetsOutOfOrderOrEqual() public { + vm.prank(creator); + vm.expectRevert(EulerSwap.AssetsOutOfOrderOrEqual.selector); + eulerSwapFactory.deployPool( + IEulerSwapFactory.DeployParams( + address(eTST), address(eTST), holder, 0, 1e18, 1e18, 0.4e18, 0.85e18, 50e18, 50e18 + ) + ); + } + + function testDeployWithBadFee() public { + vm.prank(creator); + vm.expectRevert(EulerSwap.BadFee.selector); + eulerSwapFactory.deployPool( + IEulerSwapFactory.DeployParams( + address(eTST), address(eTST2), holder, 1e18, 1e18, 1e18, 0.4e18, 0.85e18, 50e18, 50e18 + ) + ); + } +} diff --git a/test/EulerSwapTest.t.sol b/test/EulerSwapTest.t.sol index a690af5..8e0c72d 100644 --- a/test/EulerSwapTest.t.sol +++ b/test/EulerSwapTest.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.24; -import {IEVault, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; +import {IEVault, IEulerSwap, EulerSwapTestBase, EulerSwap, TestERC20} from "./EulerSwapTestBase.t.sol"; contract EulerSwapTest is EulerSwapTestBase { EulerSwap public eulerSwap; diff --git a/test/EulerSwapTestBase.t.sol b/test/EulerSwapTestBase.t.sol index 3deb004..dce25c4 100644 --- a/test/EulerSwapTestBase.t.sol +++ b/test/EulerSwapTestBase.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; import {Test, console} from "forge-std/Test.sol"; import {EVaultTestBase, TestERC20} from "evk-test/unit/evault/EVaultTestBase.t.sol"; import {IEVault} from "evk/EVault/IEVault.sol"; -import {EulerSwap} from "../src/EulerSwap.sol"; +import {IEulerSwap, EulerSwap} from "../src/EulerSwap.sol"; import {EulerSwapPeriphery} from "../src/EulerSwapPeriphery.sol"; contract EulerSwapTestBase is EVaultTestBase { @@ -87,7 +87,7 @@ contract EulerSwapTestBase is EVaultTestBase { vm.prank(creator); EulerSwap eulerSwap = new EulerSwap( getEulerSwapParams(debtLimitA, debtLimitB, fee), - EulerSwap.CurveParams({priceX: px, priceY: py, concentrationX: cx, concentrationY: cy}) + IEulerSwap.CurveParams({priceX: px, priceY: py, concentrationX: cx, concentrationY: cy}) ); vm.prank(holder); @@ -146,7 +146,7 @@ contract EulerSwapTestBase is EVaultTestBase { view returns (EulerSwap.Params memory) { - return EulerSwap.Params({ + return IEulerSwap.Params({ vault0: address(eTST), vault1: address(eTST2), myAccount: holder,