diff --git a/docs/architecture.md b/docs/architecture.md index fcbad7c..2415cde 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -43,7 +43,7 @@ Virtual reserves control the maximum debt that the EulerSwap contract will attem ### Reserve synchronisation -The EulerSwap contract tracks what it believes the reserves to be by caching their values in storage. These reserves are updated on each swap. However, since the balance is not actually held by the EulerSwap contract (it is simply an operator), the actual underlying balances may get out of sync. This can happen gradually as interest is accrued, or suddenly if the holder moves funds or the position is liquidated. When this occurs, the `syncVirtualReserves()` should be invoked. This determines the actual balances (and debts) of the holder and adjusts them by the configured virtual reserve levels. +The EulerSwap contract tracks what it believes the reserves to be by caching their values in storage. These reserves are updated on each swap. However, since the balance is not actually held by the EulerSwap contract (it is simply an operator), the actual underlying balances may get out of sync. This can happen gradually as interest is accrued, or suddenly if the holder moves funds or the position is liquidated. When this occurs, the EulerSwap operator should be uninstalled and a new, updated one installed instead. ## Components diff --git a/script/DeployProtocol.s.sol b/script/DeployProtocol.s.sol index 7628f60..b0db2bd 100644 --- a/script/DeployProtocol.s.sol +++ b/script/DeployProtocol.s.sol @@ -19,10 +19,11 @@ contract DeployProtocol is ScriptUtil { address evc = vm.parseJsonAddress(json, ".evc"); address poolManager = vm.parseJsonAddress(json, ".poolManager"); + address factory = vm.parseJsonAddress(json, ".factory"); vm.startBroadcast(deployerAddress); - new EulerSwapFactory(IPoolManager(poolManager), evc); + new EulerSwapFactory(IPoolManager(poolManager), evc, factory); new EulerSwapPeriphery(); vm.stopBroadcast(); diff --git a/script/json/DeployProtocol_input.json b/script/json/DeployProtocol_input.json index fc8cb76..80096a8 100644 --- a/script/json/DeployProtocol_input.json +++ b/script/json/DeployProtocol_input.json @@ -1,4 +1,5 @@ { "evc": "0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383", - "poolManager": "0x000000000004444c5dc75cB358380D2e3dE08A90" -} \ No newline at end of file + "poolManager": "0x000000000004444c5dc75cB358380D2e3dE08A90", + "factory": "0xF75548aF02f1928CbE9015985D4Fcbf96d728544" +} diff --git a/src/EulerSwap.sol b/src/EulerSwap.sol index de30f0a..d3e8cb4 100644 --- a/src/EulerSwap.sol +++ b/src/EulerSwap.sol @@ -50,7 +50,6 @@ contract EulerSwap is IEulerSwap, EVCUtil { error Overflow(); error BadParam(); error AmountTooBig(); - error DifferentEVC(); error AssetsOutOfOrderOrEqual(); error CurveViolation(); error DepositFailure(bytes reason); @@ -70,7 +69,6 @@ contract EulerSwap is IEulerSwap, EVCUtil { require(curveParams.priceX > 0 && curveParams.priceY > 0, BadParam()); require(curveParams.priceX <= 1e36 && curveParams.priceY <= 1e36, BadParam()); require(curveParams.concentrationX <= 1e18 && curveParams.concentrationY <= 1e18, BadParam()); - require(IEVault(params.vault0).EVC() == IEVault(params.vault1).EVC(), DifferentEVC()); address asset0Addr = IEVault(params.vault0).asset(); address asset1Addr = IEVault(params.vault1).asset(); @@ -96,9 +94,9 @@ contract EulerSwap is IEulerSwap, EVCUtil { // Validate reserves - require(verify(equilibriumReserve0, equilibriumReserve1), CurveViolation()); require(verify(reserve0, reserve1), CurveViolation()); - require(!verify(reserve0 > 0 ? reserve0 - 1 : 0, reserve1 > 0 ? reserve1 - 1 : 0), CurveViolation()); + require(!verify(reserve0 > 0 ? reserve0 - 1 : 0, reserve1), CurveViolation()); + require(!verify(reserve0, reserve1 > 0 ? reserve1 - 1 : 0), CurveViolation()); emit EulerSwapCreated(asset0Addr, asset1Addr); } @@ -128,7 +126,7 @@ contract EulerSwap is IEulerSwap, EVCUtil { uint256 amount1In = IERC20(asset1).balanceOf(address(this)); if (amount1In > 0) amount1In = depositAssets(vault1, amount1In) * feeMultiplier / 1e18; - // Verify curve invariant is satisified + // Verify curve invariant is satisfied { uint256 newReserve0 = reserve0 + amount0In - amount0Out; @@ -154,6 +152,7 @@ contract EulerSwap is IEulerSwap, EVCUtil { /// @inheritdoc IEulerSwap function getReserves() external view returns (uint112, uint112, uint32) { + require(status != 2, Locked()); return (reserve0, reserve1, status); } @@ -220,23 +219,32 @@ contract EulerSwap is IEulerSwap, EVCUtil { /// @dev After successful deposit, if the user has any outstanding controller-enabled debt, it attempts to repay it. /// @dev If all debt is repaid, the controller is automatically disabled to reduce gas costs in future operations. function depositAssets(address vault, uint256 amount) internal returns (uint256) { - try IEVault(vault).deposit(amount, eulerAccount) {} - catch (bytes memory reason) { - require(bytes4(reason) == EVKErrors.E_ZeroShares.selector, DepositFailure(reason)); - return 0; - } + uint256 deposited; if (IEVC(evc).isControllerEnabled(eulerAccount, vault)) { - IEVC(evc).call( - vault, eulerAccount, 0, abi.encodeCall(IBorrowing.repayWithShares, (type(uint256).max, eulerAccount)) - ); + uint256 debt = myDebt(vault); + uint256 repaid = IEVault(vault).repay(amount > debt ? debt : amount, eulerAccount); - if (myDebt(vault) == 0) { + amount -= repaid; + debt -= repaid; + deposited += repaid; + + if (debt == 0) { IEVC(evc).call(vault, eulerAccount, 0, abi.encodeCall(IRiskManager.disableController, ())); } } - return amount; + if (amount > 0) { + try IEVault(vault).deposit(amount, eulerAccount) {} + catch (bytes memory reason) { + require(bytes4(reason) == EVKErrors.E_ZeroShares.selector, DepositFailure(reason)); + return deposited; + } + + deposited += amount; + } + + return deposited; } /// @notice Approves tokens for a given vault, supporting both standard approvals and permit2 diff --git a/src/EulerSwapFactory.sol b/src/EulerSwapFactory.sol index 538a715..b3ac0ab 100644 --- a/src/EulerSwapFactory.sol +++ b/src/EulerSwapFactory.sol @@ -5,6 +5,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IEulerSwapFactory, IEulerSwap} from "./interfaces/IEulerSwapFactory.sol"; import {EulerSwapHook} from "./EulerSwapHook.sol"; import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; +import {GenericFactory} from "evk/GenericFactory/GenericFactory.sol"; /// @title EulerSwapFactory contract /// @custom:security-contact security@euler.xyz @@ -14,6 +15,8 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil { address[] public allPools; /// @dev Mapping between euler account and deployed pool that is currently set as operator mapping(address eulerAccount => address operator) public eulerAccountToPool; + /// @dev Vaults must be deployed by this factory + address public immutable evkFactory; IPoolManager immutable poolManager; @@ -37,9 +40,11 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil { error Unauthorized(); error OldOperatorStillInstalled(); error OperatorNotInstalled(); + error InvalidVaultImplementation(); - constructor(IPoolManager _manager, address evc) EVCUtil(evc) { + constructor(IPoolManager _manager, address evc, address evkFactory_) EVCUtil(evc) { poolManager = _manager; + evkFactory = evkFactory_; } /// @inheritdoc IEulerSwapFactory @@ -48,6 +53,10 @@ contract EulerSwapFactory is IEulerSwapFactory, EVCUtil { returns (address) { require(_msgSender() == params.eulerAccount, Unauthorized()); + require( + GenericFactory(evkFactory).isProxy(params.vault0) && GenericFactory(evkFactory).isProxy(params.vault1), + InvalidVaultImplementation() + ); EulerSwapHook pool = new EulerSwapHook{salt: keccak256(abi.encode(params.eulerAccount, salt))}(poolManager, params, curveParams); diff --git a/src/EulerSwapPeriphery.sol b/src/EulerSwapPeriphery.sol index 0c2bd74..2983e7e 100644 --- a/src/EulerSwapPeriphery.sol +++ b/src/EulerSwapPeriphery.sol @@ -24,7 +24,7 @@ contract EulerSwapPeriphery is IEulerSwapPeriphery { require(amountOut >= amountOutMin, AmountOutLessThanMin()); - swap(eulerSwap, tokenIn, tokenOut, amountIn, amountOut); + swap(IEulerSwap(eulerSwap), tokenIn, tokenOut, amountIn, amountOut); } /// @inheritdoc IEulerSwapPeriphery @@ -35,7 +35,7 @@ contract EulerSwapPeriphery is IEulerSwapPeriphery { require(amountIn <= amountInMax, AmountInMoreThanMax()); - swap(eulerSwap, tokenIn, tokenOut, amountIn, amountOut); + swap(IEulerSwap(eulerSwap), tokenIn, tokenOut, amountIn, amountOut); } /// @inheritdoc IEulerSwapPeriphery @@ -73,13 +73,13 @@ contract EulerSwapPeriphery is IEulerSwapPeriphery { /// @param tokenOut The address of the output token being received /// @param amountIn The amount of input tokens to swap /// @param amountOut The amount of output tokens to receive - function swap(address eulerSwap, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut) internal { - IERC20(tokenIn).safeTransferFrom(msg.sender, eulerSwap, amountIn); + function swap(IEulerSwap eulerSwap, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut) + internal + { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(eulerSwap), amountIn); bool isAsset0In = tokenIn < tokenOut; - (isAsset0In) - ? IEulerSwap(eulerSwap).swap(0, amountOut, msg.sender, "") - : IEulerSwap(eulerSwap).swap(amountOut, 0, msg.sender, ""); + (isAsset0In) ? eulerSwap.swap(0, amountOut, msg.sender, "") : eulerSwap.swap(amountOut, 0, msg.sender, ""); } /// @dev Computes the quote for a swap by applying fees and validating state conditions diff --git a/test/DepositFailures.t.sol b/test/DepositFailures.t.sol index a0f93ed..a3f6290 100644 --- a/test/DepositFailures.t.sol +++ b/test/DepositFailures.t.sol @@ -85,4 +85,26 @@ contract DepositFailuresTest is EulerSwapTestBase { assertEq(assetTST2.balanceOf(address(eulerSwap)), 1); // griefing transfer was untouched } + + function test_manualEnableController() public monotonicHolderNAV { + vm.prank(holder); + evc.enableController(holder, address(eTST)); + + uint256 amountIn = 50e18; + uint256 amountOut = + periphery.quoteExactInput(address(eulerSwap), address(assetTST), address(assetTST2), amountIn); + + assetTST.mint(address(this), amountIn); + assetTST.transfer(address(eulerSwap), amountIn); + eulerSwap.swap(0, amountOut, address(this), ""); + + // Swap the other way to measure gas impact + + amountIn = 100e18; + amountOut = periphery.quoteExactInput(address(eulerSwap), address(assetTST2), address(assetTST), amountIn); + + assetTST2.mint(address(this), amountIn); + assetTST2.transfer(address(eulerSwap), amountIn); + eulerSwap.swap(amountOut, 0, address(this), ""); + } } diff --git a/test/EulerSwapFactoryTest.t.sol b/test/EulerSwapFactoryTest.t.sol index 895609e..9588806 100644 --- a/test/EulerSwapFactoryTest.t.sol +++ b/test/EulerSwapFactoryTest.t.sol @@ -20,7 +20,7 @@ contract EulerSwapFactoryTest is EulerSwapTestBase { vm.startPrank(creator); poolManager = PoolManagerDeployer.deploy(creator); - eulerSwapFactory = new EulerSwapFactory(poolManager, address(evc)); + eulerSwapFactory = new EulerSwapFactory(poolManager, address(evc), address(factory)); vm.stopPrank(); }