From e797ed6494f028331d93a513ae4535e90b1326df Mon Sep 17 00:00:00 2001 From: LHerskind Date: Tue, 11 Apr 2023 11:15:09 +0000 Subject: [PATCH 1/4] chore: forge fmt --- src/deployment/AggregateDeployment.s.sol | 4 ++-- src/deployment/dataprovider/DataProviderDeployment.s.sol | 4 ++-- src/gas/angle/AngleSLPGas.s.sol | 4 ++-- src/gas/erc4626/ERC4626Gas.s.sol | 6 +++--- src/test/bridges/element/Element.t.sol | 2 +- src/test/bridges/element/aztecmocks/RollupProcessor.sol | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/deployment/AggregateDeployment.s.sol b/src/deployment/AggregateDeployment.s.sol index a504dc51a..3cbba9734 100644 --- a/src/deployment/AggregateDeployment.s.sol +++ b/src/deployment/AggregateDeployment.s.sol @@ -234,7 +234,7 @@ contract AggregateDeployment is BaseDeployment { Strings.toString(gas) ) ) - ); + ); } uint256 bridgeCount = bridgesLength(); @@ -245,7 +245,7 @@ contract AggregateDeployment is BaseDeployment { emit log_named_string( string(abi.encodePacked(" Bridge ", Strings.toString(i))), string(abi.encodePacked(Strings.toHexString(bridge), ", ", Strings.toString(gas))) - ); + ); } } } diff --git a/src/deployment/dataprovider/DataProviderDeployment.s.sol b/src/deployment/dataprovider/DataProviderDeployment.s.sol index d3c70aa8f..2d249a556 100644 --- a/src/deployment/dataprovider/DataProviderDeployment.s.sol +++ b/src/deployment/dataprovider/DataProviderDeployment.s.sol @@ -53,7 +53,7 @@ contract DataProviderDeployment is BaseDeployment { " gas)" ) ) - ); + ); } } emit log(" - Bridges"); @@ -76,7 +76,7 @@ contract DataProviderDeployment is BaseDeployment { " gas)" ) ) - ); + ); } } } diff --git a/src/gas/angle/AngleSLPGas.s.sol b/src/gas/angle/AngleSLPGas.s.sol index 042e33964..bcc87ac8b 100644 --- a/src/gas/angle/AngleSLPGas.s.sol +++ b/src/gas/angle/AngleSLPGas.s.sol @@ -100,7 +100,7 @@ contract AngleMeasure is AngleSLPDeployment { ); emit log_named_uint( "sanWeth balance of gasBase", IERC20(sanWethAsset.erc20Address).balanceOf(address(gasBase)) - ); + ); } uint256 claimableSubsidyAfterWithdrawal = SUBSIDY.claimableAmount(BENEFICIARY); @@ -151,7 +151,7 @@ contract AngleMeasure is AngleSLPDeployment { ); emit log_named_uint( "sanWeth balance of gasBase", IERC20(sanWethAsset.erc20Address).balanceOf(address(gasBase)) - ); + ); } uint256 claimableSubsidyAfterWithdrawal = SUBSIDY.claimableAmount(BENEFICIARY); diff --git a/src/gas/erc4626/ERC4626Gas.s.sol b/src/gas/erc4626/ERC4626Gas.s.sol index e41a3f33b..e7dd05c82 100644 --- a/src/gas/erc4626/ERC4626Gas.s.sol +++ b/src/gas/erc4626/ERC4626Gas.s.sol @@ -144,7 +144,7 @@ contract ERC4626Measure is ERC4626Deployment { ); emit log_named_uint( "weweth balance of gasBase", IERC20(wewethAsset.erc20Address).balanceOf(address(gasBase)) - ); + ); } uint256 claimableSubsidyAfterWithdrawal = SUBSIDY.claimableAmount(BENEFICIARY); @@ -195,7 +195,7 @@ contract ERC4626Measure is ERC4626Deployment { ); emit log_named_uint( "weweth balance of gasBase", IERC20(wewethAsset.erc20Address).balanceOf(address(gasBase)) - ); + ); } uint256 claimableSubsidyAfterWithdrawal = SUBSIDY.claimableAmount(BENEFICIARY); @@ -255,7 +255,7 @@ contract ERC4626Measure is ERC4626Deployment { ); emit log_named_uint( "wewstethBalance balance of gasBase", IERC20(wewstethAsset.erc20Address).balanceOf(address(gasBase)) - ); + ); } uint256 claimableSubsidyAfterWithdrawal = SUBSIDY.claimableAmount(BENEFICIARY); diff --git a/src/test/bridges/element/Element.t.sol b/src/test/bridges/element/Element.t.sol index ba69d0a28..0f96f6266 100644 --- a/src/test/bridges/element/Element.t.sol +++ b/src/test/bridges/element/Element.t.sol @@ -257,7 +257,7 @@ contract ElementTest is Test { vm.expectEmit(false, false, false, true); emit LogPoolAdded( trancheConfigs["USDC"][0].poolAddress, wrappedPositions["USDC"], trancheConfigs["USDC"][0].expiry - ); + ); elementBridge.registerConvergentPoolAddress( trancheConfigs["USDC"][0].poolAddress, wrappedPositions["USDC"], trancheConfigs["USDC"][0].expiry diff --git a/src/test/bridges/element/aztecmocks/RollupProcessor.sol b/src/test/bridges/element/aztecmocks/RollupProcessor.sol index e91deed27..58c01a2e8 100644 --- a/src/test/bridges/element/aztecmocks/RollupProcessor.sol +++ b/src/test/bridges/element/aztecmocks/RollupProcessor.sol @@ -153,7 +153,7 @@ contract RollupProcessor { emit DefiBridgeProcessed( 0, interaction.interactionNonce, interaction.totalInputValue, outputValueA, outputValueB, true - ); + ); interaction.finalised = true; interaction.outputValueA = outputValueA; interaction.outputValueB = outputValueB; @@ -217,7 +217,7 @@ contract RollupProcessor { if (!isAsync) { emit DefiBridgeProcessed( 0, convertArgs.interactionNonce, convertArgs.totalInputValue, outputValueA, outputValueB, true - ); + ); } else { emit AsyncDefiBridgeProcessed(0, convertArgs.interactionNonce, convertArgs.totalInputValue); } From d2c2c7feec1c8ea324adc4d04fb713bba6447f37 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Wed, 12 Apr 2023 11:34:19 +0000 Subject: [PATCH 2/4] feat: Add euler redemption bridge --- .gitmodules | 6 + foundry.toml | 4 +- lib/euler-4626-redemption | 1 + lib/solmate | 1 + package.json | 7 +- .../EulerRedemptionBridge.sol | 319 ++++++++++++++++++ .../euler-redemption/EulerRedE2E.t.sol | 148 ++++++++ .../euler-redemption/EulerRedUnit.t.sol | 261 ++++++++++++++ 8 files changed, 742 insertions(+), 5 deletions(-) create mode 160000 lib/euler-4626-redemption create mode 160000 lib/solmate create mode 100644 src/bridges/euler-redemption/EulerRedemptionBridge.sol create mode 100644 src/test/bridges/euler-redemption/EulerRedE2E.t.sol create mode 100644 src/test/bridges/euler-redemption/EulerRedUnit.t.sol diff --git a/.gitmodules b/.gitmodules index 5412639bd..b3ec2e86f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,9 @@ path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts branch = v4.8.0 +[submodule "lib/euler-4626-redemption"] + path = lib/euler-4626-redemption + url = https://github.com/lherskind/euler-4626-redemption +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/foundry.toml b/foundry.toml index 08f69bbd7..85baeaa89 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,11 +2,11 @@ src = 'src' out = 'out' libs = ['lib'] -remappings = ['@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'forge-std/=lib/forge-std/src', 'rollup-encoder/=lib/rollup-encoder/src'] +remappings = ['@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', 'forge-std/=lib/forge-std/src', 'rollup-encoder/=lib/rollup-encoder/src', 'euler-redemption/=lib/euler-4626-redemption/src', 'solmate/=lib/solmate/src'] fuzz_runs = 256 gas_reports = ["*"] eth-rpc-url = 'https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c' -solc_version = '0.8.10' +solc_version = '0.8.18' ffi = true optimizer = true optimizer_runs = 100000 diff --git a/lib/euler-4626-redemption b/lib/euler-4626-redemption new file mode 160000 index 000000000..58dc29fa2 --- /dev/null +++ b/lib/euler-4626-redemption @@ -0,0 +1 @@ +Subproject commit 58dc29fa285bfbdefbfbd503ec4d25df57ca4e82 diff --git a/lib/solmate b/lib/solmate new file mode 160000 index 000000000..2001af43a --- /dev/null +++ b/lib/solmate @@ -0,0 +1 @@ +Subproject commit 2001af43aedb46fdc2335d2a7714fb2dae7cfcd1 diff --git a/package.json b/package.json index 52cc1e2a3..9600d9daa 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,14 @@ "clean": "rm -rf ./cache ./dest ./out ./typechain-types", "build": "forge build", "compile:typechain": "yarn clean && forge build --skip test --skip script && typechain --target ethers-v5 --out-dir ./typechain-types './out/?(DataProvider|RollupProcessor|*Bridge|I*).sol/*.json'", - "test:pinned:14000000": "forge test --fork-block-number 14000000 --match-contract 'Element' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", + "test:pinned:14000000": "forge test --fork-block-number 14000000 --match-contract 'Element' -vvv --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14950000": "forge test --fork-block-number 14950000 --match-contract 'BiDCA' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14970000": "forge test --fork-block-number 14970000 -m 'testRedistributionSuccessfulSwap|testRedistributionExitWhenICREqualsMCR' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14972000": "forge test --fork-block-number 14972000 -m 'testRedistributionFailingSwap' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:16000000": "forge test --fork-block-number 16000000 --match-contract 'Yearn' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", - "test:pinned": "yarn test:pinned:14000000 && yarn test:pinned:16000000 && yarn test:pinned:14950000 && yarn test:pinned:14970000 && yarn test:pinned:14972000", - "test": "forge test --fork-block-number 16000000 --no-match-contract 'Element|BiDCA|Yearn' --no-match-test 'testRedistribution' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c && yarn test:pinned", + "test:pinned:17000000": "forge test --fork-block-number 17000000 --match-contract 'Euler' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", + "test:pinned": "yarn test:pinned:14000000 && yarn test:pinned:16000000 && yarn test:pinned:14950000 && yarn test:pinned:14970000 && yarn test:pinned:14972000 && yarn test:pinned:17000000", + "test": "forge test --fork-block-number 16000000 --no-match-contract 'Element|BiDCA|Yearn|Euler' --no-match-test 'testRedistribution' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c && yarn test:pinned", "formatting": "forge fmt", "formatting:check": "forge fmt --check", "lint": "solhint --config ./.solhint.json --fix \"src/**/*.sol\"" diff --git a/src/bridges/euler-redemption/EulerRedemptionBridge.sol b/src/bridges/euler-redemption/EulerRedemptionBridge.sol new file mode 100644 index 000000000..3d1c4cfc7 --- /dev/null +++ b/src/bridges/euler-redemption/EulerRedemptionBridge.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IRollupProcessor} from "rollup-encoder/interfaces/IRollupProcessor.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; +import {BridgeBase} from "../base/BridgeBase.sol"; +import {ErrorLib} from "../base/ErrorLib.sol"; +import {IWETH} from "../../interfaces/IWETH.sol"; +import {ISwapRouter} from "../../interfaces/uniswapv3/ISwapRouter.sol"; +import {IVault, IAsset, PoolSpecialization} from "../../interfaces/element/IVault.sol"; + +interface IMigrator { + function migrate(uint256 _amount, bytes32 _acceptanceToken) external returns (uint256, uint256, uint256); + + function ERC4626Token() external view returns (address); +} + +contract EulerRedemptionBridge is BridgeBase { + using SafeERC20 for IERC20; + using SafeERC20 for IERC4626; + + error SlippageExceeded(); + + IWETH public constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 public constant WSTETH = IERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); + + IERC4626 public constant WEWETH = IERC4626(0x3c66B18F67CA6C1A71F829E2F6a0c987f97462d0); + IERC4626 public constant WEDAI = IERC4626(0x4169Df1B7820702f566cc10938DA51F6F597d264); + IERC4626 public constant WEWSTETH = IERC4626(0x60897720AA966452e8706e74296B018990aEc527); + + bytes32 public constant TERMS_AND_CONDITIONS_HASH = + 0x427a506ff6e15bd1b7e4e93da52c8ec95f6af1279618a2f076946e83d8294996; + + ISwapRouter public constant ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + IVault public constant BALANCER = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + bytes32 public constant BALANCER_WSTETH_POOLID = 0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080; + + IMigrator public immutable WETH_MIGRATOR; + IMigrator public immutable DAI_MIGRATOR; + IMigrator public immutable WSTETH_MIGRATOR; + + constructor(address _rollupProcessor, address _wethMigrator, address _daiMigrator, address _wstethMigrator) + BridgeBase(_rollupProcessor) + { + WETH_MIGRATOR = IMigrator(_wethMigrator); + DAI_MIGRATOR = IMigrator(_daiMigrator); + WSTETH_MIGRATOR = IMigrator(_wstethMigrator); + + IERC20(WETH_MIGRATOR.ERC4626Token()).approve(address(WETH_MIGRATOR), type(uint256).max); + IERC20(DAI_MIGRATOR.ERC4626Token()).approve(address(DAI_MIGRATOR), type(uint256).max); + IERC20(WSTETH_MIGRATOR.ERC4626Token()).approve(address(WSTETH_MIGRATOR), type(uint256).max); + + WETH.approve(address(BALANCER), type(uint256).max); + + WETH.approve(address(ROUTER), type(uint256).max); + DAI.approve(address(ROUTER), type(uint256).max); + USDC.approve(address(ROUTER), type(uint256).max); + + DAI.approve(address(ROLLUP_PROCESSOR), type(uint256).max); + WSTETH.approve(address(ROLLUP_PROCESSOR), type(uint256).max); + } + + receive() external payable {} + + /** + * @notice Redeems shares of Euler ERC4626 vaults for underlying assets + * following the redemption scheme. Will take the assets received and swap it into + * the expected underlying. + * @param _inputAssetA - The input asset to redeem + * @param _outputAssetA - The output asset to receive + * @param _totalInputValue - The total amount of input asset to redeem + * @param _interactionNonce - The nonce of the interaction + * @param _auxData - The aux data of the interaction (minAmountPerFullShare) + * @return outputValueA - The amount of output asset received + */ + function convert( + AztecTypes.AztecAsset calldata _inputAssetA, + AztecTypes.AztecAsset calldata, + AztecTypes.AztecAsset calldata _outputAssetA, + AztecTypes.AztecAsset calldata, + uint256 _totalInputValue, + uint256 _interactionNonce, + uint64 _auxData, + address + ) external payable override(BridgeBase) onlyRollup returns (uint256 outputValueA, uint256, bool) { + if (_inputAssetA.erc20Address == address(WEWETH)) { + return _exitWeweth(_totalInputValue, _outputAssetA.erc20Address, _interactionNonce, _auxData); + } else if (_inputAssetA.erc20Address == address(WEDAI)) { + return _exitDai(_totalInputValue, _outputAssetA.erc20Address, _auxData); + } else if (_inputAssetA.erc20Address == address(WEWSTETH)) { + return _exitWsteth(_totalInputValue, _outputAssetA.erc20Address, _auxData); + } else { + revert ErrorLib.InvalidInputA(); + } + } + + /** + * @notice Redeems shares of WEWETH vault for weth, dai, usdc + * swaps assets to Eth + * @param _totalInputValue - The total amount of input asset to redeem + * @param _outputAssetA - The output asset to receive + * @param _interactionNonce - The nonce of the interaction + * @param _auxData - The aux data of the interaction (minAmountPerFullShare) + * @return outputValueA - The amount of output asset received + */ + function _exitWeweth(uint256 _totalInputValue, address _outputAssetA, uint256 _interactionNonce, uint64 _auxData) + internal + returns (uint256 outputValueA, uint256, bool) + { + if (_outputAssetA != address(0)) { + revert ErrorLib.InvalidOutputA(); + } + + // Migrate the asset. + (uint256 wethAmount, uint256 daiAmount, uint256 usdcAmount) = + WETH_MIGRATOR.migrate(_totalInputValue, _acceptanceToken()); + + // Swap dai for usdc on uniswap + { + if (daiAmount > 0) { + bytes memory path = abi.encodePacked(address(DAI), uint24(100), address(USDC)); + usdcAmount += ROUTER.exactInput( + ISwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp, + amountIn: daiAmount, + amountOutMinimum: 0 + }) + ); + } + } + + // Swap usdc to weth + { + if (usdcAmount > 0) { + bytes memory path = abi.encodePacked(address(USDC), uint24(500), address(WETH)); + wethAmount += ROUTER.exactInput( + ISwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp, + amountIn: usdcAmount, + amountOutMinimum: 0 + }) + ); + } + } + + // @todo slippage aux could be 1e16 precision if there are very high interest amounts. + uint256 minExpected = _totalInputValue * _auxData / 1e18; + if (wethAmount < minExpected) { + revert SlippageExceeded(); + } + + IWETH(WETH).withdraw(wethAmount); + IRollupProcessor(ROLLUP_PROCESSOR).receiveEthFromBridge{value: wethAmount}(_interactionNonce); + return (wethAmount, 0, false); + } + + /** + * @notice Redeems shares of WEDAI vault for weth, dai, usdc + * swaps assets to Dai + * @param _totalInputValue - The total amount of input asset to redeem + * @param _outputAssetA - The output asset to receive + * @param _auxData - The aux data of the interaction (minAmountPerFullShare) + * @return outputValueA - The amount of output asset received + */ + function _exitDai(uint256 _totalInputValue, address _outputAssetA, uint64 _auxData) + internal + returns (uint256 outputValueA, uint256, bool) + { + if (_outputAssetA != address(DAI)) { + revert ErrorLib.InvalidOutputA(); + } + + // Migrate the asset. + (uint256 wethAmount, uint256 daiAmount, uint256 usdcAmount) = + DAI_MIGRATOR.migrate(_totalInputValue, _acceptanceToken()); + + // Swap weth to usdc + { + if (wethAmount > 0) { + bytes memory path = abi.encodePacked(address(WETH), uint24(500), address(USDC)); + usdcAmount += ROUTER.exactInput( + ISwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp, + amountIn: wethAmount, + amountOutMinimum: 0 + }) + ); + } + } + + // Swap usdc for dai on uniswap + { + if (usdcAmount > 0) { + bytes memory path = abi.encodePacked(address(USDC), uint24(100), address(DAI)); + daiAmount += ROUTER.exactInput( + ISwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp, + amountIn: usdcAmount, + amountOutMinimum: 0 + }) + ); + } + } + + // @todo slippage aux could be 1e16 precision if there are very high interest amounts. + uint256 minExpected = _totalInputValue * _auxData / 1e18; + if (daiAmount < minExpected) { + revert SlippageExceeded(); + } + + return (daiAmount, 0, false); + } + + /** + * @notice Redeems shares of WEWESTETH vault for weth, dai, usdc + * swaps assets to Wsteth + * @param _totalInputValue - The total amount of input asset to redeem + * @param _outputAssetA - The output asset to receive + * @param _auxData - The aux data of the interaction (minAmountPerFullShare) + * @return outputValueA - The amount of output asset received + */ + function _exitWsteth(uint256 _totalInputValue, address _outputAssetA, uint64 _auxData) + internal + returns (uint256 outputValueA, uint256, bool) + { + if (_outputAssetA != address(WSTETH)) { + revert ErrorLib.InvalidOutputA(); + } + + // Migrate the asset. + (uint256 wethAmount, uint256 daiAmount, uint256 usdcAmount) = + WSTETH_MIGRATOR.migrate(_totalInputValue, _acceptanceToken()); + // Swap dai for usdc on uniswap + { + if (daiAmount > 0) { + bytes memory path = abi.encodePacked(address(DAI), uint24(100), address(USDC)); + usdcAmount += ROUTER.exactInput( + ISwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp, + amountIn: daiAmount, + amountOutMinimum: 0 + }) + ); + } + } + + // Swap usdc to weth + { + if (usdcAmount > 0) { + bytes memory path = abi.encodePacked(address(USDC), uint24(500), address(WETH)); + wethAmount += ROUTER.exactInput( + ISwapRouter.ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp, + amountIn: usdcAmount, + amountOutMinimum: 0 + }) + ); + } + } + + // Swap weth to wsteth + uint256 wstethBal; + { + if (wethAmount > 0) { + IVault.SingleSwap memory singleSwap = IVault.SingleSwap({ + poolId: BALANCER_WSTETH_POOLID, + kind: IVault.SwapKind.GIVEN_IN, + assetIn: IAsset(address(WETH)), + assetOut: IAsset(address(WSTETH)), + amount: wethAmount, + userData: "0x00" + }); + IVault.FundManagement memory fundManagement = IVault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + + wstethBal = BALANCER.swap(singleSwap, fundManagement, 0, block.timestamp); + } + } + + // @todo slippage aux could be 1e16 precision if there are very high interest amounts. + uint256 minExpected = _totalInputValue * _auxData / 1e18; + if (wstethBal < minExpected) { + revert SlippageExceeded(); + } + + return (wstethBal, 0, false); + } + + /** + * @notice Computes the acceptance token for the migration. + * @return The acceptance token. + */ + function _acceptanceToken() internal view returns (bytes32) { + return keccak256(abi.encodePacked(address(this), TERMS_AND_CONDITIONS_HASH)); + } +} diff --git a/src/test/bridges/euler-redemption/EulerRedE2E.t.sol b/src/test/bridges/euler-redemption/EulerRedE2E.t.sol new file mode 100644 index 000000000..20a56facf --- /dev/null +++ b/src/test/bridges/euler-redemption/EulerRedE2E.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +import {IWETH} from "../../../interfaces/IWETH.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {EulerRedemptionBridge} from "../../../bridges/euler-redemption/EulerRedemptionBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + +import {ERC4626Migrator} from "euler-redemption/ERC4626Migrator.sol"; + +/** + * @notice The purpose of this test is to test the bridge in an environment that is as close to the final deployment + * as possible without spinning up all the rollup infrastructure (sequencer, proof generator etc.). + */ +contract EulerE2ETest is BridgeTestBase { + IWETH public constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 public constant DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + ERC20 public constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 public constant WSTETH = ERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); + + IERC4626 public constant WEDAI = IERC4626(0x4169Df1B7820702f566cc10938DA51F6F597d264); + IERC4626 public constant WEWETH = IERC4626(0x3c66B18F67CA6C1A71F829E2F6a0c987f97462d0); + IERC4626 public constant WEWSTETH = IERC4626(0x60897720AA966452e8706e74296B018990aEc527); + + ERC20[3] public assets = [ERC20(address(WETH)), DAI, WSTETH]; + IERC4626[3] public weAssets = [WEWETH, WEDAI, WEWSTETH]; + ERC4626Migrator[] public migrators; + + EulerRedemptionBridge private bridge; + uint256 private id; + + function setUp() public { + // Deploy migrators + migrators.push(new ERC4626Migrator(ERC20(address(WEWETH)))); + migrators.push(new ERC4626Migrator(ERC20(address(WEDAI)))); + migrators.push(new ERC4626Migrator(ERC20(address(WEWSTETH)))); + + // Deploy bridge + bridge = new EulerRedemptionBridge( + address(ROLLUP_PROCESSOR), + address(migrators[0]), + address(migrators[1]), + address(migrators[2]) + ); + + deal(address(WETH), address(this), 0); + deal(address(DAI), address(this), 0); + deal(address(WSTETH), address(this), 0); + + vm.label(address(bridge), "EulerRedemptionBridge"); + vm.label(address(WEWETH), "WEWETH"); + vm.label(address(WEDAI), "WEDAI"); + vm.label(address(WEWSTETH), "WEWSTETH"); + vm.label(address(migrators[0]), "WETH_MIGRATOR"); + vm.label(address(migrators[1]), "DAI_MIGRATOR"); + vm.label(address(migrators[2]), "WSTETH_MIGRATOR"); + vm.label(0xE592427A0AEce92De3Edee1F18E0157C05861564, "UNISWAP_ROUTER"); + vm.label(0xBA12222222228d8Ba445958a75a0704d566BF2C8, "BALANCER_VAULT"); + vm.label(0xA1BBa894a6D39D79C0D1ef9c68a2139c84B81487, "Defi bridge proxy"); + + vm.prank(MULTI_SIG); + ROLLUP_PROCESSOR.setSupportedBridge(address(bridge), 2_000_000); // Very gas hungry for tests. + id = ROLLUP_PROCESSOR.getSupportedBridgesLength(); + } + + function testFuzzExitE2E( + uint256 _asset, + uint256 _wethRedeemable, + uint256 _daiRedeemable, + uint256 _usdcRedeemable, + uint256 _exitAmount + ) public { + uint256 assetIndex = bound(_asset, 0, 2); + ERC20 asset = assets[assetIndex]; + IERC4626 weAsset = weAssets[assetIndex]; + + uint256 wethRedeemable = bound(_wethRedeemable, 1e18, 1e22); + uint256 daiRedeemable = bound(_daiRedeemable, 1e18, 1e24); + uint256 usdcRedeemable = bound(_usdcRedeemable, 1e6, 1e12); + uint256 exitAmount = bound(_exitAmount, 1e6, weAsset.balanceOf(address(ROLLUP_PROCESSOR))); + + _migratorAmounts(migrators[assetIndex], wethRedeemable, daiRedeemable, usdcRedeemable); + AztecTypes.AztecAsset memory aztecInputAsset = ROLLUP_ENCODER.getRealAztecAsset(address(weAsset)); + AztecTypes.AztecAsset memory aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + uint256 balanceBefore; + if (assetIndex > 0) { + aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(asset)); + balanceBefore = asset.balanceOf(address(ROLLUP_PROCESSOR)); + } else { + balanceBefore = address(ROLLUP_PROCESSOR).balance; + } + + ROLLUP_ENCODER.defiInteractionL2(id, aztecInputAsset, emptyAsset, aztecExitAsset, emptyAsset, 0, exitAmount); + + // Execute the rollup with the bridge interaction. Ensure that event as seen above is emitted. + (uint256 a, uint256 outputValueB, bool isAsync) = ROLLUP_ENCODER.processRollupAndGetBridgeResult(); + assertEq(outputValueB, 0, "Invalid output value B"); + assertFalse(isAsync, "Invalid isAsync"); + + assertGt(a, 0, "Invalid amount received"); + + if (assetIndex == 0) { + assertEq(a, address(ROLLUP_PROCESSOR).balance - balanceBefore, "Invalid eth amount received"); + } else { + assertEq(a, asset.balanceOf(address(ROLLUP_PROCESSOR)) - balanceBefore, "Invalid asset amount received"); + } + + assertEq(WETH.balanceOf(address(bridge)), 0, "Bridge should have no eth"); + assertEq(DAI.balanceOf(address(bridge)), 0, "Bridge should have no dai"); + assertEq(USDC.balanceOf(address(bridge)), 0, "Bridge should have no usdc"); + + // Then we want to ensure that we are receiving more of the planned asset, than we could get directly (e.g., that something was swapped) + // We need there to a be a swap of >1$ or so before check really makes sense. Otherwise we could easily just be rounding down. + if (assetIndex == 0) { + uint256 stableValueExpected = (daiRedeemable * exitAmount) / weAsset.totalSupply() + + (usdcRedeemable * exitAmount * 1e12) / weAsset.totalSupply(); + if (stableValueExpected > 1e18) { + assertGt(a, (wethRedeemable * exitAmount) / weAsset.totalSupply(), "No extra eth from swaps"); + } + } else if (assetIndex == 1) { + if ( + (wethRedeemable * exitAmount) / weAsset.totalSupply() > 1e15 + || (usdcRedeemable * exitAmount * 1e12) / weAsset.totalSupply() > 1e18 + ) { + assertGt(a, (daiRedeemable * exitAmount) / weAsset.totalSupply(), "No extra dai from swaps"); + } + } + } + + function _migratorAmounts(ERC4626Migrator _migrator, uint256 _wethAmount, uint256 _daiAmount, uint256 _usdcAmount) + internal + { + // Mint assets to self, then transfer to the migrator + deal(address(WETH), address(this), _wethAmount); + deal(address(DAI), address(this), _daiAmount); + deal(address(USDC), address(this), _usdcAmount); + + WETH.transfer(address(_migrator), _wethAmount); + DAI.transfer(address(_migrator), _daiAmount); + USDC.transfer(address(_migrator), _usdcAmount); + } +} diff --git a/src/test/bridges/euler-redemption/EulerRedUnit.t.sol b/src/test/bridges/euler-redemption/EulerRedUnit.t.sol new file mode 100644 index 000000000..3d957a935 --- /dev/null +++ b/src/test/bridges/euler-redemption/EulerRedUnit.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec. +pragma solidity >=0.8.4; + +import {BridgeTestBase} from "./../../aztec/base/BridgeTestBase.sol"; +import {AztecTypes} from "rollup-encoder/libraries/AztecTypes.sol"; + +import {IWETH} from "../../../interfaces/IWETH.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {EulerRedemptionBridge} from "../../../bridges/euler-redemption/EulerRedemptionBridge.sol"; +import {ErrorLib} from "../../../bridges/base/ErrorLib.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + +import {ERC4626Migrator} from "euler-redemption/ERC4626Migrator.sol"; + +// @notice The purpose of this test is to directly test convert functionality of the bridge. +contract EulerUnitTest is BridgeTestBase { + address private rollupProcessor; + EulerRedemptionBridge private bridge; + + IWETH public constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 public constant DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + ERC20 public constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 public constant WSTETH = ERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); + + IERC4626 public constant WEDAI = IERC4626(0x4169Df1B7820702f566cc10938DA51F6F597d264); + IERC4626 public constant WEWETH = IERC4626(0x3c66B18F67CA6C1A71F829E2F6a0c987f97462d0); + IERC4626 public constant WEWSTETH = IERC4626(0x60897720AA966452e8706e74296B018990aEc527); + + ERC20[3] public assets = [ERC20(address(WETH)), DAI, WSTETH]; + IERC4626[3] public weAssets = [WEWETH, WEDAI, WEWSTETH]; + ERC4626Migrator[] public migrators; + + function receiveEthFromBridge(uint256 _interactionNonce) external payable {} + + function setUp() public { + rollupProcessor = address(this); + + // Deploy migrators + migrators.push(new ERC4626Migrator(ERC20(address(WEWETH)))); + migrators.push(new ERC4626Migrator(ERC20(address(WEDAI)))); + migrators.push(new ERC4626Migrator(ERC20(address(WEWSTETH)))); + + // Deploy bridge + bridge = new EulerRedemptionBridge( + rollupProcessor, + address(migrators[0]), + address(migrators[1]), + address(migrators[2]) + ); + + deal(address(WETH), address(this), 0); + deal(address(DAI), address(this), 0); + deal(address(WSTETH), address(this), 0); + + vm.label(address(bridge), "EulerRedemptionBridge"); + vm.label(address(WEWETH), "WEWETH"); + vm.label(address(WEDAI), "WEDAI"); + vm.label(address(WEWSTETH), "WEWSTETH"); + vm.label(address(migrators[0]), "WETH_MIGRATOR"); + vm.label(address(migrators[1]), "DAI_MIGRATOR"); + vm.label(address(migrators[2]), "WSTETH_MIGRATOR"); + vm.label(0xE592427A0AEce92De3Edee1F18E0157C05861564, "UNISWAP_ROUTER"); + vm.label(0xBA12222222228d8Ba445958a75a0704d566BF2C8, "BALANCER_VAULT"); + } + + function testFuzzExit( + uint256 _asset, + uint256 _wethRedeemable, + uint256 _daiRedeemable, + uint256 _usdcRedeemable, + uint256 _exitAmount + ) public { + uint256 assetIndex = bound(_asset, 0, 2); + ERC20 asset = assets[assetIndex]; + IERC4626 weAsset = weAssets[assetIndex]; + + uint256 wethRedeemable = bound(_wethRedeemable, 1e18, 1e22); + uint256 daiRedeemable = bound(_daiRedeemable, 1e18, 1e24); + uint256 usdcRedeemable = bound(_usdcRedeemable, 1e6, 1e12); + uint256 exitAmount = bound(_exitAmount, 1e6, weAsset.balanceOf(address(ROLLUP_PROCESSOR))); + + // Impersonate being the actual rollup for funds here. To avoid having to deal with updating totalSupply ourselves.AztecTypes + vm.prank(address(ROLLUP_PROCESSOR)); + weAsset.transfer(address(bridge), exitAmount); + + _migratorAmounts(migrators[assetIndex], wethRedeemable, daiRedeemable, usdcRedeemable); + AztecTypes.AztecAsset memory aztecInputAsset = ROLLUP_ENCODER.getRealAztecAsset(address(weAsset)); + AztecTypes.AztecAsset memory aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + if (assetIndex > 0) { + aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(asset)); + } + + uint256 ethBefore = address(this).balance; + + (uint256 a,,) = + bridge.convert(aztecInputAsset, emptyAsset, aztecExitAsset, emptyAsset, exitAmount, 0, 0, address(0)); + + // We need to actually know that something should be returned. The correct could very well be to return 0. + assertGt(a, 0, "Invalid amount received"); + + if (assetIndex == 0) { + assertEq(a, address(this).balance - ethBefore, "Invalid eth amount received"); + } else { + asset.transferFrom(address(bridge), address(this), a); + assertEq(a, asset.balanceOf(address(this)), "Invalid asset amount received"); + } + + assertEq(WETH.balanceOf(address(bridge)), 0, "Bridge should have no eth"); + assertEq(DAI.balanceOf(address(bridge)), 0, "Bridge should have no dai"); + assertEq(USDC.balanceOf(address(bridge)), 0, "Bridge should have no usdc"); + + // Then we want to ensure that we are receiving more of the planned asset, than we could get directly (e.g., that something was swapped) + // We need there to a be a swap of >1$ or so before check really makes sense. Otherwise we could easily just be rounding down. + if (assetIndex == 0) { + uint256 stableValueExpected = (daiRedeemable * exitAmount) / weAsset.totalSupply() + + (usdcRedeemable * exitAmount * 1e12) / weAsset.totalSupply(); + if (stableValueExpected > 1e18) { + assertGt(a, (wethRedeemable * exitAmount) / weAsset.totalSupply(), "No extra eth from swaps"); + } + } else if (assetIndex == 1) { + if ( + (wethRedeemable * exitAmount) / weAsset.totalSupply() > 1e15 + || (usdcRedeemable * exitAmount * 1e12) / weAsset.totalSupply() > 1e18 + ) { + assertGt(a, (daiRedeemable * exitAmount) / weAsset.totalSupply(), "No extra dai from swaps"); + } + } + } + + function testOnlyOneRedeemableAsset() public { + for (uint256 i = 0; i < 3; i++) { + for (uint256 j = 0; j < 3; j++) { + ERC20 asset = assets[i]; + IERC4626 weAsset = weAssets[i]; + + vm.deal(address(this), 0); + deal(address(WSTETH), address(this), 0); + deal(address(DAI), address(this), 0); + + deal(address(WETH), address(migrators[i]), 0); + deal(address(DAI), address(migrators[i]), 0); + deal(address(USDC), address(migrators[i]), 0); + + assertEq(WETH.balanceOf(address(migrators[i])), 0, "migrator should have no eth"); + assertEq(DAI.balanceOf(address(migrators[i])), 0, "migrator should have no dai"); + assertEq(USDC.balanceOf(address(migrators[i])), 0, "migrator should have no usdc"); + + uint256 wethRedeemable = j == 0 ? 100e18 : 0; + uint256 daiRedeemable = j == 1 ? 100e18 : 0; + uint256 usdcRedeemable = j == 2 ? 100e6 : 0; + uint256 exitAmount = bound(1e18, 1e6, weAsset.balanceOf(address(ROLLUP_PROCESSOR))); + + vm.prank(address(ROLLUP_PROCESSOR)); + weAsset.transfer(address(bridge), exitAmount); + + _migratorAmounts(migrators[i], wethRedeemable, daiRedeemable, usdcRedeemable); + assertEq( + WETH.balanceOf(address(migrators[i])), j == 0 ? wethRedeemable : 0, "migrator should have no eth" + ); + assertEq( + DAI.balanceOf(address(migrators[i])), j == 1 ? daiRedeemable : 0, "migrator should have no dai" + ); + assertEq( + USDC.balanceOf(address(migrators[i])), j == 2 ? usdcRedeemable : 0, "migrator should have no usdc" + ); + + AztecTypes.AztecAsset memory aztecInputAsset = ROLLUP_ENCODER.getRealAztecAsset(address(weAsset)); + AztecTypes.AztecAsset memory aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + if (i > 0) { + aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(asset)); + } + + (uint256 a,,) = bridge.convert( + aztecInputAsset, emptyAsset, aztecExitAsset, emptyAsset, exitAmount, 0, 0, address(0) + ); + assertGt(a, 0, "Invalid amount received"); + + // For non eth output, need to pull it. + if (aztecExitAsset.erc20Address != address(0)) { + asset.transferFrom(address(bridge), address(this), a); + } + + assertEq(address(this).balance, i == 0 ? a : 0, "Self should have no eth"); + assertEq(DAI.balanceOf(address(this)), i == 1 ? a : 0, "Self should have no dai"); + assertEq(WSTETH.balanceOf(address(this)), i == 2 ? a : 0, "Self should have no wsteth"); + + assertEq(WETH.balanceOf(address(bridge)), 0, "Bridge should have no eth"); + assertEq(DAI.balanceOf(address(bridge)), 0, "Bridge should have no dai"); + assertEq(USDC.balanceOf(address(bridge)), 0, "Bridge should have no usdc"); + } + } + } + + function testFuzzSlippageExceeded( + uint256 _asset, + uint256 _wethRedeemable, + uint256 _daiRedeemable, + uint256 _usdcRedeemable, + uint256 _exitAmount + ) public { + // Set slippage to expect very large profit from swap such that it intentionally fails. + + uint256 assetIndex = bound(_asset, 0, 2); + ERC20 asset = assets[assetIndex]; + IERC4626 weAsset = weAssets[assetIndex]; + + uint256 wethRedeemable = bound(_wethRedeemable, 1e18, 1e20); + uint256 daiRedeemable = bound(_daiRedeemable, 1e18, 1e20); + uint256 usdcRedeemable = bound(_usdcRedeemable, 1e6, 1e8); + uint256 exitAmount = bound(_exitAmount, 1e6, weAsset.balanceOf(address(ROLLUP_PROCESSOR))); + + // Impersonate being the actual rollup for funds here. To avoid having to deal with updating totalSupply ourselves.AztecTypes + vm.prank(address(ROLLUP_PROCESSOR)); + weAsset.transfer(address(bridge), exitAmount); + + _migratorAmounts(migrators[assetIndex], wethRedeemable, daiRedeemable, usdcRedeemable); + AztecTypes.AztecAsset memory aztecInputAsset = ROLLUP_ENCODER.getRealAztecAsset(address(weAsset)); + AztecTypes.AztecAsset memory aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + if (assetIndex > 0) { + aztecExitAsset = ROLLUP_ENCODER.getRealAztecAsset(address(asset)); + } + + vm.expectRevert(EulerRedemptionBridge.SlippageExceeded.selector); + bridge.convert( + aztecInputAsset, emptyAsset, aztecExitAsset, emptyAsset, exitAmount, 0, type(uint64).max, address(0) + ); + } + + function testWrongInputAsset() public { + AztecTypes.AztecAsset memory aztecEthAsset = ROLLUP_ENCODER.getRealAztecAsset(address(0)); + vm.expectRevert(ErrorLib.InvalidInputA.selector); + bridge.convert(aztecEthAsset, emptyAsset, aztecEthAsset, emptyAsset, 0, 0, type(uint64).max, address(0)); + } + + function testWrongOutputAsset() public { + for (uint256 i = 0; i < 3; i++) { + AztecTypes.AztecAsset memory aztecInputAsset = ROLLUP_ENCODER.getRealAztecAsset(address(weAssets[i])); + + AztecTypes.AztecAsset memory aztecOutputAsset = + ROLLUP_ENCODER.getRealAztecAsset(ROLLUP_PROCESSOR.getSupportedAsset(10)); + + vm.expectRevert(ErrorLib.InvalidOutputA.selector); + bridge.convert(aztecInputAsset, emptyAsset, aztecOutputAsset, emptyAsset, 1, 0, 0, address(0)); + } + } + + function _migratorAmounts(ERC4626Migrator _migrator, uint256 _wethAmount, uint256 _daiAmount, uint256 _usdcAmount) + internal + { + // Mint assets to self, then transfer to the migrator + deal(address(WETH), address(this), _wethAmount); + deal(address(DAI), address(this), _daiAmount); + deal(address(USDC), address(this), _usdcAmount); + + WETH.transfer(address(_migrator), _wethAmount); + DAI.transfer(address(_migrator), _daiAmount); + USDC.transfer(address(_migrator), _usdcAmount); + } +} From a59fcf9bf09dab16551e7ee026f1cfbe212f625d Mon Sep 17 00:00:00 2001 From: LHerskind Date: Wed, 12 Apr 2023 11:35:07 +0000 Subject: [PATCH 3/4] fix: remove -vvv --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9600d9daa..49eb0c8b6 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "clean": "rm -rf ./cache ./dest ./out ./typechain-types", "build": "forge build", "compile:typechain": "yarn clean && forge build --skip test --skip script && typechain --target ethers-v5 --out-dir ./typechain-types './out/?(DataProvider|RollupProcessor|*Bridge|I*).sol/*.json'", - "test:pinned:14000000": "forge test --fork-block-number 14000000 --match-contract 'Element' -vvv --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", + "test:pinned:14000000": "forge test --fork-block-number 14000000 --match-contract 'Element' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14950000": "forge test --fork-block-number 14950000 --match-contract 'BiDCA' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14970000": "forge test --fork-block-number 14970000 -m 'testRedistributionSuccessfulSwap|testRedistributionExitWhenICREqualsMCR' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", "test:pinned:14972000": "forge test --fork-block-number 14972000 -m 'testRedistributionFailingSwap' --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c", From 70a0b5a58bb5a7b464b833e46d988b706be36dd0 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Thu, 13 Apr 2023 12:59:01 +0000 Subject: [PATCH 4/4] fix: replace euler-4626-redemption lib --- .gitmodules | 6 +++--- lib/euler-4626-redemption | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index b3ec2e86f..2ee11a142 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,9 +8,9 @@ path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts branch = v4.8.0 -[submodule "lib/euler-4626-redemption"] - path = lib/euler-4626-redemption - url = https://github.com/lherskind/euler-4626-redemption [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate +[submodule "lib/euler-4626-redemption"] + path = lib/euler-4626-redemption + url = https://github.com/kasperpawlowski/euler-4626-redemption diff --git a/lib/euler-4626-redemption b/lib/euler-4626-redemption index 58dc29fa2..e900cc74e 160000 --- a/lib/euler-4626-redemption +++ b/lib/euler-4626-redemption @@ -1 +1 @@ -Subproject commit 58dc29fa285bfbdefbfbd503ec4d25df57ca4e82 +Subproject commit e900cc74e78f8aacd637d07c9ed16006b5900b4a