diff --git a/.gitignore b/.gitignore index 27c05e8..85c3b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ cache node_modules out +.idea # files *.env diff --git a/.gitmodules b/.gitmodules index 024e485..520b394 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,3 +15,6 @@ [submodule "lib/erc7399"] path = lib/erc7399 url = https://github.com/alcueca/erc7399 +[submodule "lib/registry"] + path = lib/registry + url = https://github.com/alcueca/registry diff --git a/lib/registry b/lib/registry new file mode 160000 index 0000000..8e98080 --- /dev/null +++ b/lib/registry @@ -0,0 +1 @@ +Subproject commit 8e98080c90eae718af721695dc07852d9d43db5a diff --git a/package.json b/package.json index a11e7d7..9dc82e4 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "clean": "rm -rf cache out", "lint": "pnpm lint:sol && pnpm prettier:check", - "lint:sol": "forge fmt --check && pnpm solhint {script,src,test}/**/*.sol", + "lint:sol": "forge fmt --check && pnpm solhint src/**/*.sol", "prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore", "prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore" } diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index beface2..aaed790 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -2,8 +2,44 @@ pragma solidity >=0.8.19 <=0.9.0; import { BaseScript } from "./Base.s.sol"; +import { console2 } from "forge-std/console2.sol"; + +import { Registry } from "lib/registry/src/Registry.sol"; + +import { UniswapV3Wrapper } from "../src/uniswapV3/UniswapV3Wrapper.sol"; +import { AaveWrapper, IPoolAddressesProvider } from "../src/aave/AaveWrapper.sol"; +import { BalancerWrapper, IFlashLoaner } from "../src/balancer/BalancerWrapper.sol"; /// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting contract Deploy is BaseScript { - function run() public broadcast { } + bytes32 public constant SALT = keccak256("ERC7399-wrappers"); + + address internal factory = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + address internal usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address internal usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address internal weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address internal wbtc = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + + IFlashLoaner internal balancer = IFlashLoaner(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + + IPoolAddressesProvider internal provider = IPoolAddressesProvider(0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb); + + function run() public broadcast { + require(block.chainid == 42_161, "Only deploy on Arbitrum"); + + Registry registry = new Registry{salt: SALT}(broadcaster); + + console2.log("Registry deployed at: %s", address(registry)); + + registry.set("UniswapV3Wrapper", abi.encode(factory, weth, usdc, usdt)); + + UniswapV3Wrapper uniswapV3Wrapper = new UniswapV3Wrapper{salt: SALT}(registry); + console2.log("UniswapV3Wrapper deployed at: %s", address(uniswapV3Wrapper)); + + BalancerWrapper balancerWrapper = new BalancerWrapper{salt: SALT}(balancer); + console2.log("BalancerWrapper deployed at: %s", address(balancerWrapper)); + + AaveWrapper aaveWrapper = new AaveWrapper{salt: SALT}(provider); + console2.log("AaveWrapper deployed at: %s", address(aaveWrapper)); + } } diff --git a/src/BaseWrapper.sol b/src/BaseWrapper.sol index ff86328..942341f 100644 --- a/src/BaseWrapper.sol +++ b/src/BaseWrapper.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // Thanks to ultrasecr.eth -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "erc7399/IERC7399.sol"; diff --git a/src/aave/AaveWrapper.sol b/src/aave/AaveWrapper.sol index be4c17c..3902801 100644 --- a/src/aave/AaveWrapper.sol +++ b/src/aave/AaveWrapper.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // Thanks to ultrasecr.eth -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { IPool } from "./interfaces/IPool.sol"; import { DataTypes } from "./interfaces/DataTypes.sol"; @@ -18,7 +18,12 @@ contract AaveWrapper is BaseWrapper, IFlashLoanSimpleReceiver { using FixedPointMathLib for uint256; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + error NotPool(); + error NotInitiator(); + + // solhint-disable-next-line var-name-mixedcase IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; + // solhint-disable-next-line var-name-mixedcase IPool public immutable POOL; constructor(IPoolAddressesProvider provider) { @@ -50,8 +55,8 @@ contract AaveWrapper is BaseWrapper, IFlashLoanSimpleReceiver { override returns (bool) { - require(msg.sender == address(POOL), "AaveFlashLoanProvider: not pool"); - require(initiator == address(this), "AaveFlashLoanProvider: not initiator"); + if (msg.sender != address(POOL)) revert NotPool(); + if (initiator != address(this)) revert NotInitiator(); _bridgeToCallback(asset, amount, fee, params); diff --git a/src/aave/interfaces/DataTypes.sol b/src/aave/interfaces/DataTypes.sol index 20d2f8e..12bd0e8 100644 --- a/src/aave/interfaces/DataTypes.sol +++ b/src/aave/interfaces/DataTypes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; library DataTypes { struct ReserveData { diff --git a/src/aave/interfaces/Errors.sol b/src/aave/interfaces/Errors.sol index c948b15..9ce1e48 100644 --- a/src/aave/interfaces/Errors.sol +++ b/src/aave/interfaces/Errors.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @title Errors library diff --git a/src/aave/interfaces/IFlashLoanSimpleReceiver.sol b/src/aave/interfaces/IFlashLoanSimpleReceiver.sol index 2150092..12a9a3a 100644 --- a/src/aave/interfaces/IFlashLoanSimpleReceiver.sol +++ b/src/aave/interfaces/IFlashLoanSimpleReceiver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { IPoolAddressesProvider } from "./IPoolAddressesProvider.sol"; import { IPool } from "./IPool.sol"; diff --git a/src/aave/interfaces/IPool.sol b/src/aave/interfaces/IPool.sol index 242d464..a4ec807 100644 --- a/src/aave/interfaces/IPool.sol +++ b/src/aave/interfaces/IPool.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { IPoolAddressesProvider } from "./IPoolAddressesProvider.sol"; import { DataTypes } from "./DataTypes.sol"; diff --git a/src/aave/interfaces/IPoolAddressesProvider.sol b/src/aave/interfaces/IPoolAddressesProvider.sol index 3a32733..34ac99b 100644 --- a/src/aave/interfaces/IPoolAddressesProvider.sol +++ b/src/aave/interfaces/IPoolAddressesProvider.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @title IPoolAddressesProvider diff --git a/src/aave/interfaces/ReserveConfiguration.sol b/src/aave/interfaces/ReserveConfiguration.sol index 558e873..a018f60 100644 --- a/src/aave/interfaces/ReserveConfiguration.sol +++ b/src/aave/interfaces/ReserveConfiguration.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { Errors } from "./Errors.sol"; import { DataTypes } from "./DataTypes.sol"; diff --git a/src/balancer/BalancerWrapper.sol b/src/balancer/BalancerWrapper.sol index 125a65e..3721b75 100644 --- a/src/balancer/BalancerWrapper.sol +++ b/src/balancer/BalancerWrapper.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // Thanks to ultrasecr.eth -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { IFlashLoanRecipient } from "./interfaces/IFlashLoanRecipient.sol"; import { IFlashLoaner } from "./interfaces/IFlashLoaner.sol"; @@ -18,6 +18,9 @@ contract BalancerWrapper is BaseWrapper, IFlashLoanRecipient { using Arrays for address; using FixedPointMathLib for uint256; + error NotBalancer(); + error HashMismatch(); + IFlashLoaner public immutable balancer; bytes32 private flashLoanDataHash; @@ -48,8 +51,8 @@ contract BalancerWrapper is BaseWrapper, IFlashLoanRecipient { external override { - require(msg.sender == address(balancer), "BalancerWrapper: not balancer"); - require(keccak256(params) == flashLoanDataHash, "BalancerWrapper: params hash mismatch"); + if (msg.sender != address(balancer)) revert NotBalancer(); + if (keccak256(params) != flashLoanDataHash) revert HashMismatch(); delete flashLoanDataHash; _bridgeToCallback(assets[0], amounts[0], fees[0], params); diff --git a/src/balancer/interfaces/IFlashLoanRecipient.sol b/src/balancer/interfaces/IFlashLoanRecipient.sol index cd76c78..d08fd4d 100644 --- a/src/balancer/interfaces/IFlashLoanRecipient.sol +++ b/src/balancer/interfaces/IFlashLoanRecipient.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; interface IFlashLoanRecipient { /** diff --git a/src/balancer/interfaces/IFlashLoaner.sol b/src/balancer/interfaces/IFlashLoaner.sol index f571c9d..346a633 100644 --- a/src/balancer/interfaces/IFlashLoaner.sol +++ b/src/balancer/interfaces/IFlashLoaner.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { IFlashLoanRecipient } from "./IFlashLoanRecipient.sol"; import { IProtocolFeesCollector } from "./IProtocolFeesCollector.sol"; diff --git a/src/balancer/interfaces/IProtocolFeesCollector.sol b/src/balancer/interfaces/IProtocolFeesCollector.sol index 704a86a..7c99646 100644 --- a/src/balancer/interfaces/IProtocolFeesCollector.sol +++ b/src/balancer/interfaces/IProtocolFeesCollector.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; interface IProtocolFeesCollector { function getFlashLoanFeePercentage() external view returns (uint256); diff --git a/src/erc3156/ERC3156Wrapper.sol b/src/erc3156/ERC3156Wrapper.sol index 5d52796..37cdad6 100644 --- a/src/erc3156/ERC3156Wrapper.sol +++ b/src/erc3156/ERC3156Wrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { IERC3156FlashLender } from "lib/erc3156/contracts/interfaces/IERC3156FlashLender.sol"; import { IERC3156FlashBorrower } from "lib/erc3156/contracts/interfaces/IERC3156FlashBorrower.sol"; @@ -14,7 +14,7 @@ import { BaseWrapper, IERC7399 } from "../BaseWrapper.sol"; contract ERC3156Wrapper is BaseWrapper, IERC3156FlashBorrower { bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); - mapping(address => IERC3156FlashLender) public lenders; + mapping(address asset => IERC3156FlashLender lender) public lenders; /** * @param assets_ Asset contracts supported for flash lending. diff --git a/src/uniswapV3/UniswapV3Wrapper.sol b/src/uniswapV3/UniswapV3Wrapper.sol index 37a93e1..f07f900 100644 --- a/src/uniswapV3/UniswapV3Wrapper.sol +++ b/src/uniswapV3/UniswapV3Wrapper.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT -// Thanks to sunnyRK and yashnaman -pragma solidity ^0.8.0; +// Thanks to sunnyRK, yashnaman & ultrasecr.eth +pragma solidity ^0.8.19; + +import { Registry } from "lib/registry/src/Registry.sol"; import { IUniswapV3FlashCallback } from "./interfaces/callback/IUniswapV3FlashCallback.sol"; import { IUniswapV3Pool } from "./interfaces/IUniswapV3Pool.sol"; @@ -14,23 +16,24 @@ contract UniswapV3Wrapper is BaseWrapper, IUniswapV3FlashCallback { using PoolAddress for address; using { canLoan, balance } for IUniswapV3Pool; + error UnknownPool(); + error UnsupportedCurrency(address asset); + // CONSTANTS address public immutable factory; // DEFAULT ASSETS - address weth; - address usdc; - address usdt; - - /// @param factory_ Uniswap v3 UniswapV3Factory address - /// @param weth_ Weth contract used in Uniswap v3 Pairs - /// @param usdc_ usdc contract used in Uniswap v3 Pairs - /// @param usdt_ usdt contract used in Uniswap v3 Pairs - constructor(address factory_, address weth_, address usdc_, address usdt_) { - factory = factory_; - weth = weth_; - usdc = usdc_; - usdt = usdt_; + address public immutable weth; + address public immutable usdc; + address public immutable usdt; + + /// @param reg Registry storing constructor parameters + constructor(Registry reg) { + // @param factory_ Uniswap v3 UniswapV3Factory address + // @param weth_ Weth contract used in Uniswap v3 Pairs + // @param usdc_ usdc contract used in Uniswap v3 Pairs + // @param usdt_ usdt contract used in Uniswap v3 Pairs + (factory, weth, usdc, usdt) = abi.decode(reg.get("UniswapV3Wrapper"), (address, address, address, address)); } /** @@ -68,6 +71,18 @@ contract UniswapV3Wrapper is BaseWrapper, IUniswapV3FlashCallback { return amount >= max ? type(uint256).max : _flashFee(asset, amount); } + function _flashLoan(address asset, uint256 amount, bytes memory data) internal override { + IUniswapV3Pool pool = cheapestPool(asset, amount); + if (address(pool) == address(0)) revert UnsupportedCurrency(asset); + + address asset0 = address(pool.token0()); + address asset1 = address(pool.token1()); + uint256 amount0 = asset == asset0 ? amount : 0; + uint256 amount1 = asset == asset1 ? amount : 0; + + pool.flash(address(this), amount0, amount1, abi.encode(asset0, asset1, pool.fee(), amount, data)); + } + /// @inheritdoc IUniswapV3FlashCallback function uniswapV3FlashCallback( uint256 fee0, // Fee on Asset0 @@ -79,7 +94,7 @@ contract UniswapV3Wrapper is BaseWrapper, IUniswapV3FlashCallback { { (address asset, address other, uint24 feeTier, uint256 amount, bytes memory data) = abi.decode(params, (address, address, uint24, uint256, bytes)); - require(msg.sender == address(_pool(asset, other, feeTier)), "UniswapV3Wrapper: Unknown pool"); + if (msg.sender != address(_pool(asset, other, feeTier))) revert UnknownPool(); uint256 fee = fee0 > 0 ? fee0 : fee1; _bridgeToCallback(asset, amount, fee, data); diff --git a/src/utils/Arrays.sol b/src/utils/Arrays.sol index e326d71..53fffe4 100644 --- a/src/utils/Arrays.sol +++ b/src/utils/Arrays.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Thanks to ultrasecr.eth -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; library Arrays { function toArray(uint256 n) internal pure returns (uint256[] memory arr) { diff --git a/src/utils/RevertMsgExtractor.sol b/src/utils/RevertMsgExtractor.sol index 7e190a1..7f18ab0 100644 --- a/src/utils/RevertMsgExtractor.sol +++ b/src/utils/RevertMsgExtractor.sol @@ -2,7 +2,7 @@ // Taken from // https://github.com/sushiswap/BoringSolidity/blob/441e51c0544cf2451e6116fe00515e71d7c42e2c/src/BoringBatchable.sol -pragma solidity >=0.6.0; +pragma solidity ^0.8.19; library RevertMsgExtractor { /// @dev Helper function to extract a useful revert message from a failed call. @@ -11,6 +11,7 @@ library RevertMsgExtractor { // If the _res length is less than 68, then the transaction failed silently (without a revert message) if (returnData.length < 68) return "Transaction reverted silently"; + // solhint-disable-next-line no-inline-assembly assembly { // Slice the sighash. returnData := add(returnData, 0x04) diff --git a/src/utils/TransferHelper.sol b/src/utils/TransferHelper.sol index 7a4a6fc..5fde0da 100644 --- a/src/utils/TransferHelper.sol +++ b/src/utils/TransferHelper.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT // Taken from https://github.com/Uniswap/uniswap-lib/blob/master/src/libraries/TransferHelper.sol -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import { ERC20 } from "solmate/tokens/ERC20.sol"; -import "./RevertMsgExtractor.sol"; +import { RevertMsgExtractor } from "./RevertMsgExtractor.sol"; // helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false // USDT is a well known token that returns nothing for its transfer, transferFrom, and approve functions @@ -16,7 +16,8 @@ library TransferHelper { /// @param value The value of the transfer function safeTransfer(address asset, address to, uint256 value) internal { (bool success, bytes memory data) = - address(asset).call(abi.encodeWithSelector(ERC20.transfer.selector, to, value)); + // solhint-disable-next-line avoid-low-level-calls + address(asset).call(abi.encodeWithSelector(ERC20.transfer.selector, to, value)); if (!(success && (data.length == 0 || abi.decode(data, (bool))))) revert(RevertMsgExtractor.getRevertMsg(data)); } } diff --git a/test/AaveWrapper.t.sol b/test/AaveWrapper.t.sol index 007c2e1..5273e59 100644 --- a/test/AaveWrapper.t.sol +++ b/test/AaveWrapper.t.sol @@ -68,11 +68,11 @@ contract AaveWrapperTest is PRBTest, StdCheats { } function test_executeOperation_permissions() public { - vm.expectRevert("AaveFlashLoanProvider: not pool"); + vm.expectRevert(AaveWrapper.NotPool.selector); wrapper.executeOperation({ asset: address(dai), amount: 1e18, fee: 0, initiator: address(wrapper), params: "" }); vm.prank(provider.getPool()); - vm.expectRevert("AaveFlashLoanProvider: not initiator"); + vm.expectRevert(AaveWrapper.NotInitiator.selector); wrapper.executeOperation({ asset: address(dai), amount: 1e18, fee: 0, initiator: address(0x666), params: "" }); } } diff --git a/test/BalancerWrapper.t.sol b/test/BalancerWrapper.t.sol index eab254f..cb0a3ef 100644 --- a/test/BalancerWrapper.t.sol +++ b/test/BalancerWrapper.t.sol @@ -73,7 +73,7 @@ contract BalancerWrapperTest is PRBTest, StdCheats { } function test_receiveFlashLoan_permissions() public { - vm.expectRevert("BalancerWrapper: not balancer"); + vm.expectRevert(BalancerWrapper.NotBalancer.selector); wrapper.receiveFlashLoan({ assets: address(dai).toArray(), amounts: uint256(1e18).toArray(), @@ -82,7 +82,7 @@ contract BalancerWrapperTest is PRBTest, StdCheats { }); vm.prank(address(balancer)); - vm.expectRevert("BalancerWrapper: params hash mismatch"); + vm.expectRevert(BalancerWrapper.HashMismatch.selector); wrapper.receiveFlashLoan({ assets: address(dai).toArray(), amounts: uint256(1e18).toArray(), diff --git a/test/MockBorrower.sol b/test/MockBorrower.sol index ec4e6c3..c76dc23 100644 --- a/test/MockBorrower.sol +++ b/test/MockBorrower.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "erc7399/IERC7399.sol"; diff --git a/test/UniswapV3Wrapper.t.sol b/test/UniswapV3Wrapper.t.sol index 99d4e25..045c787 100644 --- a/test/UniswapV3Wrapper.t.sol +++ b/test/UniswapV3Wrapper.t.sol @@ -6,6 +6,7 @@ import { console2 } from "forge-std/console2.sol"; import { StdCheats } from "forge-std/StdCheats.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; +import { Registry } from "lib/registry/src/Registry.sol"; import { MockBorrower } from "./MockBorrower.sol"; import { UniswapV3Wrapper } from "../src/uniswapV3/UniswapV3Wrapper.sol"; @@ -37,7 +38,9 @@ contract UniswapV3WrapperTest is PRBTest, StdCheats { weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; wbtc = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; - wrapper = new UniswapV3Wrapper(address(factory), weth, usdc, usdt); + Registry registry = new Registry(address(this)); + registry.set("UniswapV3Wrapper", abi.encode(address(factory), weth, usdc, usdt)); + wrapper = new UniswapV3Wrapper(registry); borrower = new MockBorrower(wrapper); deal(address(usdc), address(this), 1e6); // For fees } @@ -81,7 +84,7 @@ contract UniswapV3WrapperTest is PRBTest, StdCheats { } function test_uniswapV3FlashCallback_permissions() public { - vm.expectRevert("UniswapV3Wrapper: Unknown pool"); + vm.expectRevert(UniswapV3Wrapper.UnknownPool.selector); wrapper.uniswapV3FlashCallback({ fee0: 0, fee1: 0,