From 8e2b141336854fb2f410db61e689005ee70ea56c Mon Sep 17 00:00:00 2001 From: alcueca Date: Tue, 16 Apr 2024 10:59:35 +0100 Subject: [PATCH 01/19] draft: Gnosis Safe Flash Loans --- src/gnosissafe/GnosisSafeWrapper.sol | 69 +++++++++++++++++++++++ src/gnosissafe/interfaces/IGnosisSafe.sol | 18 ++++++ src/gnosissafe/lib/Enum.sol | 11 ++++ 3 files changed, 98 insertions(+) create mode 100644 src/gnosissafe/GnosisSafeWrapper.sol create mode 100644 src/gnosissafe/interfaces/IGnosisSafe.sol create mode 100644 src/gnosissafe/lib/Enum.sol diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol new file mode 100644 index 0000000..b78dad0 --- /dev/null +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// Thanks to ultrasecr.eth +pragma solidity ^0.8.19; + +import { IGnosisSafe } from "./interfaces/IGnosisSafe.sol"; + +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; + +import { Enum } from "./lib/Enum.sol"; +import { BaseWrapper, IERC7399, IERC20 } from "../BaseWrapper.sol"; + +/// @dev MorphoBlue Flash Lender that uses MorphoBlue as source of liquidity. +contract GnosisSafeWrapper is BaseWrapper, AccessControl { + error UnsupportedAsset(address asset); + error FailedTransfer(address asset, uint256 amount); + error InsufficientRepayment(address asset, uint256 amount); + + event LendingDataSet(address asset, uint248 fee, bool enabled); + + struct LendingData { + uint248 fee; // 1 = 0.01% + bool enabled; + } + + IGnosisSafe public immutable safe; + + mapping (address asset => LendingData data) public lending; + + constructor(IGnosisSafe _safe) { + safe = _safe; + } + + /// @inheritdoc IERC7399 + function maxFlashLoan(address asset) public view returns (uint256) { + if (lending[asset].enabled == false) return 0; + return IERC20(asset).balanceOf(address(safe)); + } + + /// @inheritdoc IERC7399 + function flashFee(address asset, uint256 amount) public view returns (uint256) { + uint256 max = maxFlashLoan(asset); + if (max == 0) revert UnsupportedAsset(asset); + return amount >= max ? type(uint256).max : amount * lending[asset].fee / 10000; + } + + function _flashLoan(address asset, uint256 amount, bytes memory params) internal override { + + Data memory decodedParams = abi.decode(params, (Data)); + + if (lending[asset].enabled == false) revert UnsupportedAsset(asset); + uint256 fee = flashFee(asset, amount); + uint256 balanceAfter = IERC20(asset).balanceOf(address(safe)) + fee; + + // Take assets from safe + bytes memory transferCall = abi.encodeWithSignature("transfer(address,uint256)", decodedParams.loanReceiver, amount); + if (!safe.execTransactionFromModule(asset, 0, transferCall, Enum.Operation.Call)) revert FailedTransfer(asset, amount); + + // Call callback + _bridgeToCallback(asset, amount, 0, params); + + // Make sure assets are back in safe (think about reentrancy) + if (IERC20(asset).balanceOf(address(safe)) < balanceAfter) revert InsufficientRepayment(asset, amount + fee); + } + + function setLendingData(address asset, uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { + lending[asset] = LendingData({ fee: fee, enabled: enabled }); + emit LendingDataSet(asset, fee, enabled); + } +} diff --git a/src/gnosissafe/interfaces/IGnosisSafe.sol b/src/gnosissafe/interfaces/IGnosisSafe.sol new file mode 100644 index 0000000..ac77f0e --- /dev/null +++ b/src/gnosissafe/interfaces/IGnosisSafe.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0; + +import { Enum } from "../lib/Enum.sol"; + +interface IGnosisSafe { + /// @dev Allows a Module to execute a Safe transaction without any further confirmations. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction. + function execTransactionFromModule( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation + ) external returns (bool success); +} \ No newline at end of file diff --git a/src/gnosissafe/lib/Enum.sol b/src/gnosissafe/lib/Enum.sol new file mode 100644 index 0000000..5a4fbe3 --- /dev/null +++ b/src/gnosissafe/lib/Enum.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0; + +/// @title Enum - Collection of enums +/// @author Richard Meissner - +contract Enum { + enum Operation { + Call, + DelegateCall + } +} \ No newline at end of file From 822dc8c7ddf7347c111aaf1b9bd5b80588c77788 Mon Sep 17 00:00:00 2001 From: alcueca Date: Tue, 16 Apr 2024 11:03:37 +0100 Subject: [PATCH 02/19] always be linting --- src/gnosissafe/GnosisSafeWrapper.sol | 14 ++++++++------ src/gnosissafe/interfaces/IGnosisSafe.sol | 6 ++++-- src/gnosissafe/lib/Enum.sol | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index b78dad0..a5f9f4a 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -24,7 +24,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { IGnosisSafe public immutable safe; - mapping (address asset => LendingData data) public lending; + mapping(address asset => LendingData data) public lending; constructor(IGnosisSafe _safe) { safe = _safe; @@ -40,20 +40,22 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { function flashFee(address asset, uint256 amount) public view returns (uint256) { uint256 max = maxFlashLoan(asset); if (max == 0) revert UnsupportedAsset(asset); - return amount >= max ? type(uint256).max : amount * lending[asset].fee / 10000; + return amount >= max ? type(uint256).max : amount * lending[asset].fee / 10_000; } function _flashLoan(address asset, uint256 amount, bytes memory params) internal override { - Data memory decodedParams = abi.decode(params, (Data)); - + if (lending[asset].enabled == false) revert UnsupportedAsset(asset); uint256 fee = flashFee(asset, amount); uint256 balanceAfter = IERC20(asset).balanceOf(address(safe)) + fee; // Take assets from safe - bytes memory transferCall = abi.encodeWithSignature("transfer(address,uint256)", decodedParams.loanReceiver, amount); - if (!safe.execTransactionFromModule(asset, 0, transferCall, Enum.Operation.Call)) revert FailedTransfer(asset, amount); + bytes memory transferCall = + abi.encodeWithSignature("transfer(address,uint256)", decodedParams.loanReceiver, amount); + if (!safe.execTransactionFromModule(asset, 0, transferCall, Enum.Operation.Call)) { + revert FailedTransfer(asset, amount); + } // Call callback _bridgeToCallback(asset, amount, 0, params); diff --git a/src/gnosissafe/interfaces/IGnosisSafe.sol b/src/gnosissafe/interfaces/IGnosisSafe.sol index ac77f0e..42d0990 100644 --- a/src/gnosissafe/interfaces/IGnosisSafe.sol +++ b/src/gnosissafe/interfaces/IGnosisSafe.sol @@ -14,5 +14,7 @@ interface IGnosisSafe { uint256 value, bytes calldata data, Enum.Operation operation - ) external returns (bool success); -} \ No newline at end of file + ) + external + returns (bool success); +} diff --git a/src/gnosissafe/lib/Enum.sol b/src/gnosissafe/lib/Enum.sol index 5a4fbe3..08efdb2 100644 --- a/src/gnosissafe/lib/Enum.sol +++ b/src/gnosissafe/lib/Enum.sol @@ -8,4 +8,4 @@ contract Enum { Call, DelegateCall } -} \ No newline at end of file +} From 7c51f899ac6cccf4b47e1acc8dc497d81348ae69 Mon Sep 17 00:00:00 2001 From: alcueca Date: Tue, 16 Apr 2024 11:05:23 +0100 Subject: [PATCH 03/19] Fix comments --- src/gnosissafe/GnosisSafeWrapper.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index a5f9f4a..9fd2b14 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -9,7 +9,7 @@ import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol" import { Enum } from "./lib/Enum.sol"; import { BaseWrapper, IERC7399, IERC20 } from "../BaseWrapper.sol"; -/// @dev MorphoBlue Flash Lender that uses MorphoBlue as source of liquidity. +/// @dev Safe Gnosis Flash Lender that uses individual Gnosis Safe contracts as source of liquidity. contract GnosisSafeWrapper is BaseWrapper, AccessControl { error UnsupportedAsset(address asset); error FailedTransfer(address asset, uint256 amount); @@ -64,6 +64,10 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { if (IERC20(asset).balanceOf(address(safe)) < balanceAfter) revert InsufficientRepayment(asset, amount + fee); } + /// @dev Set lending data for an asset. + /// @param asset Address of the asset. + /// @param fee Fee for the flash loan (FP 1e-4) + /// @param enabled Whether the asset is enabled for flash loans. function setLendingData(address asset, uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { lending[asset] = LendingData({ fee: fee, enabled: enabled }); emit LendingDataSet(asset, fee, enabled); From 6fc145390a790feba313b97886735b6607cf747e Mon Sep 17 00:00:00 2001 From: alcueca Date: Tue, 16 Apr 2024 12:10:46 +0100 Subject: [PATCH 04/19] Not sure what's wrong with the transfer --- src/gnosissafe/GnosisSafeWrapper.sol | 3 +- src/gnosissafe/interfaces/IGnosisSafe.sol | 5 ++ test/GnosisSafeWrapper.t.sol | 98 +++++++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 test/GnosisSafeWrapper.t.sol diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index 9fd2b14..ff673d1 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -26,7 +26,8 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { mapping(address asset => LendingData data) public lending; - constructor(IGnosisSafe _safe) { + constructor(address owner, IGnosisSafe _safe) { + _grantRole(DEFAULT_ADMIN_ROLE, owner); safe = _safe; } diff --git a/src/gnosissafe/interfaces/IGnosisSafe.sol b/src/gnosissafe/interfaces/IGnosisSafe.sol index 42d0990..4eaa870 100644 --- a/src/gnosissafe/interfaces/IGnosisSafe.sol +++ b/src/gnosissafe/interfaces/IGnosisSafe.sol @@ -17,4 +17,9 @@ interface IGnosisSafe { ) external returns (bool success); + + /// @notice Enables the module `module` for the Safe. + /// @dev This can only be done via a Safe transaction. + /// @param module Module to be whitelisted. + function enableModule(address module) external; } diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol new file mode 100644 index 0000000..92721c8 --- /dev/null +++ b/test/GnosisSafeWrapper.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.19 <0.9.0; + +import { Test } from "forge-std/Test.sol"; +import { console2 } from "forge-std/console2.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; + +import { IERC20Metadata as IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { IGnosisSafe } from "src/gnosissafe/interfaces/IGnosisSafe.sol"; +import { MockBorrower } from "./MockBorrower.sol"; +import { GnosisSafeWrapper } from "src/gnosissafe/GnosisSafeWrapper.sol"; + +/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +/// https://book.getfoundry.sh/forge/writing-tests +contract GnosisSafeWrapperTest is Test { + using SafeERC20 for IERC20; + + GnosisSafeWrapper internal wrapper; + MockBorrower internal borrower; + address internal USDT; + address internal USDC; + IGnosisSafe internal safe; + + /// @dev A function invoked before each test case is run. + function setUp() public virtual { + // Revert if there is no API key. + string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); + if (bytes(alchemyApiKey).length == 0) { + revert("API_KEY_ALCHEMY variable missing"); + } + + vm.createSelectFork({ urlOrAlias: "gnosis", blockNumber: 33_471_377 }); + safe = IGnosisSafe(0x0faAe596Ce5d762BcD78b4415992726492570B54); + USDT = 0x4ECaBa5870353805a9F068101A40E0f32ed605C6; + USDC = 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83; + + wrapper = new GnosisSafeWrapper(address(this), safe); + borrower = new MockBorrower(wrapper); + + deal(USDT, address(safe), 100e18); + deal(USDT, address(borrower), 1e18); + + vm.prank(address(safe)); + safe.enableModule(address(wrapper)); + + wrapper.setLendingData(USDT, 10, true); + + } + + /// @dev Basic test. Run it with `forge test -vvv` to see the console log. + function test_flashFee() external { + console2.log("test_flashFee"); + assertEq(wrapper.flashFee(USDT, 1e18), 1e15, "Flash fee not right"); + } + + function test_maxFlashLoan() external { + console2.log("test_maxFlashLoan"); + assertEq(wrapper.maxFlashLoan(USDT), 100e18, "Max flash loan not right"); + } + + function test_maxFlashLoan_unsupportedAsset() external { + console2.log("test_maxFlashLoan"); + assertEq(wrapper.maxFlashLoan(USDC), 0, "Max flash loan not right"); + } + + function test_flashFee_unsupportedAsset() external { + console2.log("test_flashFee"); + vm.expectRevert(abi.encodeWithSelector(GnosisSafeWrapper.UnsupportedAsset.selector, USDC)); + wrapper.flashFee(USDC, 1e18); + } + + function test_flashFee_insufficientLiquidity() external { + console2.log("test_flashFee"); + assertEq(wrapper.flashFee(USDT, 20_000e18), type(uint256).max, "Flash fee not right"); + } + + function test_flashLoanDebug() external { + console2.log("test_flashLoan"); + uint256 loan = 10e18; + uint256 fee = wrapper.flashFee(USDT, loan); + IERC20(USDT).safeTransfer(address(borrower), fee); + bytes memory result = borrower.flashBorrow(USDT, loan); + + // Test the return values passed through the wrapper + (bytes32 callbackReturn) = abi.decode(result, (bytes32)); + assertEq(uint256(callbackReturn), uint256(borrower.ERC3156PP_CALLBACK_SUCCESS()), "Callback failed"); + + // Test the borrower state during the callback + assertEq(borrower.flashInitiator(), address(borrower)); + assertEq(address(borrower.flashAsset()), address(USDT)); + assertEq(borrower.flashAmount(), loan); + assertEq(borrower.flashBalance(), loan + fee); // The amount we transferred to pay for fees, plus the amount we + // borrowed + assertEq(borrower.flashFee(), fee); + } +} From 49e6de0af1e4d9fb531aeeaf0ad1da57fb983ec6 Mon Sep 17 00:00:00 2001 From: alcueca Date: Wed, 17 Apr 2024 10:40:44 +0100 Subject: [PATCH 05/19] tests complete --- src/gnosissafe/GnosisSafeWrapper.sol | 17 +++++++++--- test/GnosisSafeWrapper.t.sol | 39 +++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index ff673d1..6c63323 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -26,8 +26,9 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { mapping(address asset => LendingData data) public lending; - constructor(address owner, IGnosisSafe _safe) { - _grantRole(DEFAULT_ADMIN_ROLE, owner); + /// @param _safe The Gnosis Safe to use as the source of liquidity, and as the owner of this contract. + constructor(IGnosisSafe _safe) { + _grantRole(DEFAULT_ADMIN_ROLE, address(_safe)); safe = _safe; } @@ -59,12 +60,22 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { } // Call callback - _bridgeToCallback(asset, amount, 0, params); + _bridgeToCallback(asset, amount, fee, params); // Make sure assets are back in safe (think about reentrancy) if (IERC20(asset).balanceOf(address(safe)) < balanceAfter) revert InsufficientRepayment(asset, amount + fee); } + /// @dev Transfer the assets to the loan receiver. + /// Overriden because the provider can send the funds directly + function _transferAssets(address asset, uint256 amount, address loanReceiver) internal override { } + + /// @dev Where should the end client send the funds to repay the loan + /// Overriden because the provider can receive the funds directly + function _repayTo() internal view override returns (address) { + return address(safe); + } + /// @dev Set lending data for an asset. /// @param asset Address of the asset. /// @param fee Fee for the flash loan (FP 1e-4) diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol index 92721c8..3fcbada 100644 --- a/test/GnosisSafeWrapper.t.sol +++ b/test/GnosisSafeWrapper.t.sol @@ -6,7 +6,9 @@ import { console2 } from "forge-std/console2.sol"; import { StdCheats } from "forge-std/StdCheats.sol"; import { IERC20Metadata as IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; import { IGnosisSafe } from "src/gnosissafe/interfaces/IGnosisSafe.sol"; import { MockBorrower } from "./MockBorrower.sol"; @@ -36,25 +38,39 @@ contract GnosisSafeWrapperTest is Test { USDT = 0x4ECaBa5870353805a9F068101A40E0f32ed605C6; USDC = 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83; - wrapper = new GnosisSafeWrapper(address(this), safe); + wrapper = new GnosisSafeWrapper(safe); borrower = new MockBorrower(wrapper); deal(USDT, address(safe), 100e18); - deal(USDT, address(borrower), 1e18); - vm.prank(address(safe)); + vm.startPrank(address(safe)); safe.enableModule(address(wrapper)); - wrapper.setLendingData(USDT, 10, true); + vm.stopPrank(); + } + function test_setLendingData_unauthorized() external { + console2.log("test_setLendingData_unauthorized"); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), wrapper.DEFAULT_ADMIN_ROLE() + ) + ); + wrapper.setLendingData(USDT, 10, true); } - /// @dev Basic test. Run it with `forge test -vvv` to see the console log. function test_flashFee() external { console2.log("test_flashFee"); assertEq(wrapper.flashFee(USDT, 1e18), 1e15, "Flash fee not right"); } + function test_setLendingData_changeFee() external { + console2.log("test_setLendingData_changeFee"); + vm.prank(address(safe)); + wrapper.setLendingData(USDT, 1, true); + assertEq(wrapper.flashFee(USDT, 1e18), 1e14, "Flash fee not right"); + } + function test_maxFlashLoan() external { console2.log("test_maxFlashLoan"); assertEq(wrapper.maxFlashLoan(USDT), 100e18, "Max flash loan not right"); @@ -76,11 +92,12 @@ contract GnosisSafeWrapperTest is Test { assertEq(wrapper.flashFee(USDT, 20_000e18), type(uint256).max, "Flash fee not right"); } - function test_flashLoanDebug() external { + function test_flashLoan() external { console2.log("test_flashLoan"); uint256 loan = 10e18; uint256 fee = wrapper.flashFee(USDT, loan); - IERC20(USDT).safeTransfer(address(borrower), fee); + deal(USDT, address(borrower), fee); + bytes memory result = borrower.flashBorrow(USDT, loan); // Test the return values passed through the wrapper @@ -95,4 +112,12 @@ contract GnosisSafeWrapperTest is Test { // borrowed assertEq(borrower.flashFee(), fee); } + + function test_setLendingData_disable() external { + console2.log("test_setLendingData_disable"); + vm.prank(address(safe)); + wrapper.setLendingData(USDT, 10, false); + vm.expectRevert(abi.encodeWithSelector(GnosisSafeWrapper.UnsupportedAsset.selector, USDT)); + borrower.flashBorrow(USDT, 1); + } } From 8aae47e37f99a037739246dc15ac8513777e97a5 Mon Sep 17 00:00:00 2001 From: alcueca Date: Wed, 17 Apr 2024 10:51:15 +0100 Subject: [PATCH 06/19] Better README --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9610e69..497fac5 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,20 @@ When a contract requires constructor parameters which vary per network, these ar [Registry](https://github.com/alcueca/registry) deployed at 0x1BFf8Eee6ECF1c8155E81dba8894CE9cF49a220c in each supported network. +## Gnosis Safe Wrapper + +The [Gnosis Safe Wrapper](src/gnosissafe/GnosisSafeWrapper.sol) is intended for individual users to enable it as a module in their Gnosis Safe, so that they can lend their own assets and earn a fee. Please let us know if you do it and we'll add your Safe to the list above. + ## Flash Loans For detail on executing flash loans, please refer to the -[ERC7399](https://github.com/ethereum/EIPs/blob/d072207e24e3cc12b6315909e6a65275a38e1984/EIPS/eip-7399.md) EIP. +[ERC7399](https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7399.md) EIP. + +## Safety + +This is __experimental software__ and is provided on an "as is" and "as available" basis. + +While care has been taken during development, and most contracts have seen significant use, __we do not give any warranties__ and __will not be liable for any loss__ incurred through any use of this codebase. ## Using This Repository From e413d0a92feeff0a8d7e7979add1c2f9eb0412a4 Mon Sep 17 00:00:00 2001 From: alcueca Date: Wed, 17 Apr 2024 10:56:56 +0100 Subject: [PATCH 07/19] lint me, my friend, lint me, again --- README.md | 9 ++++++--- src/gnosissafe/GnosisSafeWrapper.sol | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 497fac5..178bf88 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ network. ## Gnosis Safe Wrapper -The [Gnosis Safe Wrapper](src/gnosissafe/GnosisSafeWrapper.sol) is intended for individual users to enable it as a module in their Gnosis Safe, so that they can lend their own assets and earn a fee. Please let us know if you do it and we'll add your Safe to the list above. +The [Gnosis Safe Wrapper](src/gnosissafe/GnosisSafeWrapper.sol) is intended for individual users to enable it as a +module in their Gnosis Safe, so that they can lend their own assets and earn a fee. Please let us know if you do it and +we'll add your Safe to the list above. ## Flash Loans @@ -65,9 +67,10 @@ For detail on executing flash loans, please refer to the ## Safety -This is __experimental software__ and is provided on an "as is" and "as available" basis. +This is **experimental software** and is provided on an "as is" and "as available" basis. -While care has been taken during development, and most contracts have seen significant use, __we do not give any warranties__ and __will not be liable for any loss__ incurred through any use of this codebase. +While care has been taken during development, and most contracts have seen significant use, **we do not give any +warranties** and **will not be liable for any loss** incurred through any use of this codebase. ## Using This Repository diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index 6c63323..acd05e0 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -68,7 +68,8 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { /// @dev Transfer the assets to the loan receiver. /// Overriden because the provider can send the funds directly - function _transferAssets(address asset, uint256 amount, address loanReceiver) internal override { } + // solhint-disable-next-line no-empty-blocks + function _transferAssets(address, uint256, address) internal override { } /// @dev Where should the end client send the funds to repay the loan /// Overriden because the provider can receive the funds directly From d6daecb53c737b9cab7e58dfbc2bcad71811265f Mon Sep 17 00:00:00 2001 From: alcueca Date: Wed, 17 Apr 2024 11:10:45 +0100 Subject: [PATCH 08/19] Moved tests to mainnet --- test/GnosisSafeWrapper.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol index 3fcbada..ee02c84 100644 --- a/test/GnosisSafeWrapper.t.sol +++ b/test/GnosisSafeWrapper.t.sol @@ -33,10 +33,10 @@ contract GnosisSafeWrapperTest is Test { revert("API_KEY_ALCHEMY variable missing"); } - vm.createSelectFork({ urlOrAlias: "gnosis", blockNumber: 33_471_377 }); - safe = IGnosisSafe(0x0faAe596Ce5d762BcD78b4415992726492570B54); - USDT = 0x4ECaBa5870353805a9F068101A40E0f32ed605C6; - USDC = 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83; + vm.createSelectFork({ urlOrAlias: "mainnet", blockNumber: 19_674_449 }); + safe = IGnosisSafe(0xfA6DaAF31F8E2498b5D4C43E59c6eDd345D951F5); + USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; wrapper = new GnosisSafeWrapper(safe); borrower = new MockBorrower(wrapper); From 54ae085598077060b76e5bef535b43b5cb523cbb Mon Sep 17 00:00:00 2001 From: alcueca Date: Wed, 17 Apr 2024 12:03:24 +0100 Subject: [PATCH 09/19] setLendingDataAll --- src/gnosissafe/GnosisSafeWrapper.sol | 32 ++++++++++++++++++++++------ test/GnosisSafeWrapper.t.sol | 18 ++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index acd05e0..d83f016 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -23,6 +23,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { } IGnosisSafe public immutable safe; + address public constant ALL_ASSETS = address(0); mapping(address asset => LendingData data) public lending; @@ -34,22 +35,30 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { /// @inheritdoc IERC7399 function maxFlashLoan(address asset) public view returns (uint256) { - if (lending[asset].enabled == false) return 0; - return IERC20(asset).balanceOf(address(safe)); + if (lending[asset].enabled == true || lending[ALL_ASSETS].enabled == true) { + return IERC20(asset).balanceOf(address(safe)); + } else { + return 0; + } } /// @inheritdoc IERC7399 function flashFee(address asset, uint256 amount) public view returns (uint256) { uint256 max = maxFlashLoan(asset); - if (max == 0) revert UnsupportedAsset(asset); - return amount >= max ? type(uint256).max : amount * lending[asset].fee / 10_000; + if (max == 0) revert UnsupportedAsset(asset); // TODO: Should we revert on tokens that are enabled but have zero + // liquidity? + if (amount >= max) { + return type(uint256).max; + } else { + uint256 fee = lending[ALL_ASSETS].enabled == true ? lending[ALL_ASSETS].fee : lending[asset].fee; + return amount * fee / 10_000; + } } function _flashLoan(address asset, uint256 amount, bytes memory params) internal override { Data memory decodedParams = abi.decode(params, (Data)); - if (lending[asset].enabled == false) revert UnsupportedAsset(asset); - uint256 fee = flashFee(asset, amount); + uint256 fee = flashFee(asset, amount); // Checks for unsupported assets uint256 balanceAfter = IERC20(asset).balanceOf(address(safe)) + fee; // Take assets from safe @@ -62,7 +71,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { // Call callback _bridgeToCallback(asset, amount, fee, params); - // Make sure assets are back in safe (think about reentrancy) + // Make sure assets are back in safe (TODO: think about reentrancy) if (IERC20(asset).balanceOf(address(safe)) < balanceAfter) revert InsufficientRepayment(asset, amount + fee); } @@ -82,7 +91,16 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { /// @param fee Fee for the flash loan (FP 1e-4) /// @param enabled Whether the asset is enabled for flash loans. function setLendingData(address asset, uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (asset == ALL_ASSETS) revert UnsupportedAsset(asset); // address(0) is reserved for the all assets override lending[asset] = LendingData({ fee: fee, enabled: enabled }); emit LendingDataSet(asset, fee, enabled); } + + /// @dev Set a lending data override for all assets. + /// @param fee Fee for the flash loan (FP 1e-4) + /// @param enabled Whether the lending data override is enabled for flash loans. + function setLendingDataAll(uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { + lending[ALL_ASSETS] = LendingData({ fee: fee, enabled: enabled }); + emit LendingDataSet(ALL_ASSETS, fee, enabled); + } } diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol index ee02c84..ffd80ee 100644 --- a/test/GnosisSafeWrapper.t.sol +++ b/test/GnosisSafeWrapper.t.sol @@ -71,6 +71,24 @@ contract GnosisSafeWrapperTest is Test { assertEq(wrapper.flashFee(USDT, 1e18), 1e14, "Flash fee not right"); } + function test_setLendingDataAll_changeFee() external { + console2.log("test_setLendingDataAll_changeFee"); + vm.prank(address(safe)); + wrapper.setLendingDataAll(1, true); + assertEq(wrapper.flashFee(USDT, 1e18), 1e14, "Flash fee not right"); + deal(USDC, address(safe), 100e18); + assertEq(wrapper.flashFee(USDC, 1e18), 1e14, "Flash fee not right"); + } + + function test_setLendingDataAll_disable() external { + console2.log("test_setLendingDataAll_changeFee"); + vm.startPrank(address(safe)); + wrapper.setLendingDataAll(1, true); + wrapper.setLendingDataAll(1, false); + vm.stopPrank(); + assertEq(wrapper.flashFee(USDT, 1e18), 1e15, "Flash fee not right"); + } + function test_maxFlashLoan() external { console2.log("test_maxFlashLoan"); assertEq(wrapper.maxFlashLoan(USDT), 100e18, "Max flash loan not right"); From 0c06c38a7758a1cb4f1d1171f08ab65e8082753a Mon Sep 17 00:00:00 2001 From: alcueca Date: Thu, 18 Apr 2024 07:09:31 +0100 Subject: [PATCH 10/19] Now with a clone factory --- src/gnosissafe/GnosisSafeWrapper.sol | 21 +++--- src/gnosissafe/GnosisSafeWrapperFactory.sol | 71 +++++++++++++++++++ test/GnosisSafeWrapper.t.sol | 76 ++++++++++++++++++--- 3 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 src/gnosissafe/GnosisSafeWrapperFactory.sol diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index d83f016..e5ac266 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -5,32 +5,35 @@ pragma solidity ^0.8.19; import { IGnosisSafe } from "./interfaces/IGnosisSafe.sol"; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { Enum } from "./lib/Enum.sol"; import { BaseWrapper, IERC7399, IERC20 } from "../BaseWrapper.sol"; /// @dev Safe Gnosis Flash Lender that uses individual Gnosis Safe contracts as source of liquidity. -contract GnosisSafeWrapper is BaseWrapper, AccessControl { +contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { error UnsupportedAsset(address asset); error FailedTransfer(address asset, uint256 amount); error InsufficientRepayment(address asset, uint256 amount); event LendingDataSet(address asset, uint248 fee, bool enabled); + event SafeSet(IGnosisSafe safe); struct LendingData { uint248 fee; // 1 = 0.01% bool enabled; } - IGnosisSafe public immutable safe; address public constant ALL_ASSETS = address(0); - mapping(address asset => LendingData data) public lending; + IGnosisSafe public safe; - /// @param _safe The Gnosis Safe to use as the source of liquidity, and as the owner of this contract. - constructor(IGnosisSafe _safe) { - _grantRole(DEFAULT_ADMIN_ROLE, address(_safe)); - safe = _safe; + mapping(address asset => LendingData data) public lending; + + function initialize(address _safe) public initializer { + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + safe = IGnosisSafe(_safe); + emit SafeSet(safe); } /// @inheritdoc IERC7399 @@ -90,7 +93,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { /// @param asset Address of the asset. /// @param fee Fee for the flash loan (FP 1e-4) /// @param enabled Whether the asset is enabled for flash loans. - function setLendingData(address asset, uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { + function lend(address asset, uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { if (asset == ALL_ASSETS) revert UnsupportedAsset(asset); // address(0) is reserved for the all assets override lending[asset] = LendingData({ fee: fee, enabled: enabled }); emit LendingDataSet(asset, fee, enabled); @@ -99,7 +102,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl { /// @dev Set a lending data override for all assets. /// @param fee Fee for the flash loan (FP 1e-4) /// @param enabled Whether the lending data override is enabled for flash loans. - function setLendingDataAll(uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { + function lendAll(uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) { lending[ALL_ASSETS] = LendingData({ fee: fee, enabled: enabled }); emit LendingDataSet(ALL_ASSETS, fee, enabled); } diff --git a/src/gnosissafe/GnosisSafeWrapperFactory.sol b/src/gnosissafe/GnosisSafeWrapperFactory.sol new file mode 100644 index 0000000..467d5dc --- /dev/null +++ b/src/gnosissafe/GnosisSafeWrapperFactory.sol @@ -0,0 +1,71 @@ +//SPDX-License-Identifier: MIT +pragma solidity >= 0.8.19; + +import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; + +import { GnosisSafeWrapper } from "./GnosisSafeWrapper.sol"; + + +contract GnosisSafeWrapperFactory { + + event LenderCreated(address safe, GnosisSafeWrapper lender); + event LendingDataSet(address safe, address asset, uint248 fee, bool enabled); + + address public constant ALL_ASSETS = address(0); + + GnosisSafeWrapper immutable public template; + + mapping(address safe => GnosisSafeWrapper lender) public lenders; + + constructor() { + template = new GnosisSafeWrapper(); + } + + function _deploy(address safe) internal returns (GnosisSafeWrapper lender) { + lender = GnosisSafeWrapper(Clones.cloneDeterministic(address(template), bytes20(safe))); + lender.initialize(safe); + lenders[safe] = lender; + emit LenderCreated(safe, lender); + } + + function deploy(address safe) public returns (GnosisSafeWrapper lender) { + lender = _deploy(safe); + } + + function predictLenderAddress(address safe) public view returns (address lender) { + lender = Clones.predictDeterministicAddress(address(template), bytes20(safe)); + } + + function myLender() public view returns (address lender) { + lender = Clones.predictDeterministicAddress(address(template), bytes20(msg.sender)); + } + + function lending(address asset) public view returns (uint248 fee, bool enabled) { + return lenders[msg.sender].lending(asset); + } + + function lending(address safe, address asset) public view returns (uint248 fee, bool enabled) { + return lenders[safe].lending(asset); + } + + /// @dev Set lending data for an asset. + /// @param asset Address of the asset. + /// @param fee Fee for the flash loan (FP 1e-4) + /// @param enabled Whether the asset is enabled for flash loans. + function lend(address asset, uint248 fee, bool enabled) public { + GnosisSafeWrapper lender = lenders[msg.sender]; + if (lender == GnosisSafeWrapper(address(0))) lender = _deploy(msg.sender); + lender.lend(asset, fee, enabled); + emit LendingDataSet(msg.sender, asset, fee, enabled); + } + + /// @dev Set a lending data override for all assets. + /// @param fee Fee for the flash loan (FP 1e-4) + /// @param enabled Whether the lending data override is enabled for flash loans. + function lendAll(uint248 fee, bool enabled) public { + GnosisSafeWrapper lender = lenders[msg.sender]; + if (lender == GnosisSafeWrapper(address(0))) lender = _deploy(msg.sender); + lender.lendAll(fee, enabled); + emit LendingDataSet(msg.sender, address(0), fee, enabled); + } +} \ No newline at end of file diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol index ffd80ee..11521dd 100644 --- a/test/GnosisSafeWrapper.t.sol +++ b/test/GnosisSafeWrapper.t.sol @@ -12,13 +12,15 @@ import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol" import { IGnosisSafe } from "src/gnosissafe/interfaces/IGnosisSafe.sol"; import { MockBorrower } from "./MockBorrower.sol"; +import { GnosisSafeWrapperFactory } from "src/gnosissafe/GnosisSafeWrapperFactory.sol"; import { GnosisSafeWrapper } from "src/gnosissafe/GnosisSafeWrapper.sol"; /// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: /// https://book.getfoundry.sh/forge/writing-tests -contract GnosisSafeWrapperTest is Test { +abstract contract GnosisSafeWrapperStateZero is Test { using SafeERC20 for IERC20; + GnosisSafeWrapperFactory internal factory; GnosisSafeWrapper internal wrapper; MockBorrower internal borrower; address internal USDT; @@ -38,17 +40,69 @@ contract GnosisSafeWrapperTest is Test { USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - wrapper = new GnosisSafeWrapper(safe); - borrower = new MockBorrower(wrapper); - + factory = new GnosisSafeWrapperFactory(); deal(USDT, address(safe), 100e18); + } +} + +contract GnosisSafeWrapperStateZeroTest is GnosisSafeWrapperStateZero { + function test_deploy() external { + console2.log("test_deploy"); + wrapper = factory.deploy(address(safe)); + assertEq(address(factory.lenders(address(safe))), address(wrapper)); + assertEq(address(wrapper.safe()), address(safe)); + } + + function test_predictLenderAddressDebug() external { + console2.log("test_predictLenderAddress"); + wrapper = factory.deploy(address(safe)); + assertEq(factory.predictLenderAddress(address(safe)), address(wrapper)); + } + + function test_lendDebug() external { + console2.log("test_lend"); + vm.prank(address(safe)); + factory.lend(USDT, 10, true); + wrapper = GnosisSafeWrapper(factory.predictLenderAddress(address(safe))); + (uint256 fee, bool enabled) = wrapper.lending(USDT); + assertEq(fee, 10); + assertEq(enabled, true); + } + function test_lendAll() external { + console2.log("test_lendAll"); + vm.prank(address(safe)); + factory.lendAll(10, true); + wrapper = GnosisSafeWrapper(factory.predictLenderAddress(address(safe))); + (uint256 fee, bool enabled) = wrapper.lending(wrapper.ALL_ASSETS()); + assertEq(fee, 10); + assertEq(enabled, true); + } + + function test_myLender() external { + console2.log("test_myLender"); vm.startPrank(address(safe)); + wrapper = factory.deploy(address(safe)); + assertEq(factory.myLender(), address(wrapper)); + vm.stopPrank(); + } +} + +abstract contract GnosisSafeWrapperWithWrapper is GnosisSafeWrapperStateZero { + function setUp() public override virtual { + super.setUp(); + + vm.startPrank(address(safe)); + wrapper = GnosisSafeWrapper(factory.myLender()); safe.enableModule(address(wrapper)); - wrapper.setLendingData(USDT, 10, true); + factory.lend(USDT, 10, true); vm.stopPrank(); + + borrower = new MockBorrower(wrapper); } +} +contract GnosisSafeWrapperWithWrapperTest is GnosisSafeWrapperWithWrapper { function test_setLendingData_unauthorized() external { console2.log("test_setLendingData_unauthorized"); vm.expectRevert( @@ -56,7 +110,7 @@ contract GnosisSafeWrapperTest is Test { IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), wrapper.DEFAULT_ADMIN_ROLE() ) ); - wrapper.setLendingData(USDT, 10, true); + wrapper.lend(USDT, 10, true); } function test_flashFee() external { @@ -67,14 +121,14 @@ contract GnosisSafeWrapperTest is Test { function test_setLendingData_changeFee() external { console2.log("test_setLendingData_changeFee"); vm.prank(address(safe)); - wrapper.setLendingData(USDT, 1, true); + factory.lend(USDT, 1, true); assertEq(wrapper.flashFee(USDT, 1e18), 1e14, "Flash fee not right"); } function test_setLendingDataAll_changeFee() external { console2.log("test_setLendingDataAll_changeFee"); vm.prank(address(safe)); - wrapper.setLendingDataAll(1, true); + factory.lendAll(1, true); assertEq(wrapper.flashFee(USDT, 1e18), 1e14, "Flash fee not right"); deal(USDC, address(safe), 100e18); assertEq(wrapper.flashFee(USDC, 1e18), 1e14, "Flash fee not right"); @@ -83,8 +137,8 @@ contract GnosisSafeWrapperTest is Test { function test_setLendingDataAll_disable() external { console2.log("test_setLendingDataAll_changeFee"); vm.startPrank(address(safe)); - wrapper.setLendingDataAll(1, true); - wrapper.setLendingDataAll(1, false); + factory.lendAll(1, true); + factory.lendAll(1, false); vm.stopPrank(); assertEq(wrapper.flashFee(USDT, 1e18), 1e15, "Flash fee not right"); } @@ -134,7 +188,7 @@ contract GnosisSafeWrapperTest is Test { function test_setLendingData_disable() external { console2.log("test_setLendingData_disable"); vm.prank(address(safe)); - wrapper.setLendingData(USDT, 10, false); + factory.lend(USDT, 10, false); vm.expectRevert(abi.encodeWithSelector(GnosisSafeWrapper.UnsupportedAsset.selector, USDT)); borrower.flashBorrow(USDT, 1); } From 8900c8a6d750b2c545a5efcf2b9449455883007e Mon Sep 17 00:00:00 2001 From: alcueca Date: Thu, 18 Apr 2024 07:12:13 +0100 Subject: [PATCH 11/19] LINT LINT LINT --- src/gnosissafe/GnosisSafeWrapper.sol | 2 +- src/gnosissafe/GnosisSafeWrapperFactory.sol | 6 ++---- test/GnosisSafeWrapper.t.sol | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index e5ac266..bf198ee 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -29,7 +29,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { IGnosisSafe public safe; mapping(address asset => LendingData data) public lending; - + function initialize(address _safe) public initializer { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); safe = IGnosisSafe(_safe); diff --git a/src/gnosissafe/GnosisSafeWrapperFactory.sol b/src/gnosissafe/GnosisSafeWrapperFactory.sol index 467d5dc..1b4845c 100644 --- a/src/gnosissafe/GnosisSafeWrapperFactory.sol +++ b/src/gnosissafe/GnosisSafeWrapperFactory.sol @@ -5,15 +5,13 @@ import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; import { GnosisSafeWrapper } from "./GnosisSafeWrapper.sol"; - contract GnosisSafeWrapperFactory { - event LenderCreated(address safe, GnosisSafeWrapper lender); event LendingDataSet(address safe, address asset, uint248 fee, bool enabled); address public constant ALL_ASSETS = address(0); - GnosisSafeWrapper immutable public template; + GnosisSafeWrapper public immutable template; mapping(address safe => GnosisSafeWrapper lender) public lenders; @@ -68,4 +66,4 @@ contract GnosisSafeWrapperFactory { lender.lendAll(fee, enabled); emit LendingDataSet(msg.sender, address(0), fee, enabled); } -} \ No newline at end of file +} diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol index 11521dd..e3e7497 100644 --- a/test/GnosisSafeWrapper.t.sol +++ b/test/GnosisSafeWrapper.t.sol @@ -89,7 +89,7 @@ contract GnosisSafeWrapperStateZeroTest is GnosisSafeWrapperStateZero { } abstract contract GnosisSafeWrapperWithWrapper is GnosisSafeWrapperStateZero { - function setUp() public override virtual { + function setUp() public virtual override { super.setUp(); vm.startPrank(address(safe)); @@ -97,7 +97,7 @@ abstract contract GnosisSafeWrapperWithWrapper is GnosisSafeWrapperStateZero { safe.enableModule(address(wrapper)); factory.lend(USDT, 10, true); vm.stopPrank(); - + borrower = new MockBorrower(wrapper); } } From a7f5108f34f6d61678a7ecde8ca973a0ac19ed56 Mon Sep 17 00:00:00 2001 From: alcueca Date: Sat, 20 Apr 2024 06:14:53 +0100 Subject: [PATCH 12/19] added indexed to events --- src/gnosissafe/GnosisSafeWrapper.sol | 2 +- src/gnosissafe/GnosisSafeWrapperFactory.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index bf198ee..67a7bfa 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -16,7 +16,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { error FailedTransfer(address asset, uint256 amount); error InsufficientRepayment(address asset, uint256 amount); - event LendingDataSet(address asset, uint248 fee, bool enabled); + event LendingDataSet(address indexed asset, uint248 fee, bool enabled); event SafeSet(IGnosisSafe safe); struct LendingData { diff --git a/src/gnosissafe/GnosisSafeWrapperFactory.sol b/src/gnosissafe/GnosisSafeWrapperFactory.sol index 1b4845c..2bf7e16 100644 --- a/src/gnosissafe/GnosisSafeWrapperFactory.sol +++ b/src/gnosissafe/GnosisSafeWrapperFactory.sol @@ -6,8 +6,8 @@ import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; import { GnosisSafeWrapper } from "./GnosisSafeWrapper.sol"; contract GnosisSafeWrapperFactory { - event LenderCreated(address safe, GnosisSafeWrapper lender); - event LendingDataSet(address safe, address asset, uint248 fee, bool enabled); + event LenderCreated(address indexed safe, GnosisSafeWrapper lender); + event LendingDataSet(address indexed safe, address indexed asset, uint248 fee, bool enabled); address public constant ALL_ASSETS = address(0); From fa88a936c7468d9d9801c14119e9164e62321c59 Mon Sep 17 00:00:00 2001 From: alcueca Date: Sat, 20 Apr 2024 06:40:55 +0100 Subject: [PATCH 13/19] Removed code duplication and unnecessary storage --- src/gnosissafe/GnosisSafeWrapperFactory.sol | 71 ++++++++++++++------- test/GnosisSafeWrapper.t.sol | 23 ++++--- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapperFactory.sol b/src/gnosissafe/GnosisSafeWrapperFactory.sol index 2bf7e16..2d86cdb 100644 --- a/src/gnosissafe/GnosisSafeWrapperFactory.sol +++ b/src/gnosissafe/GnosisSafeWrapperFactory.sol @@ -6,44 +6,69 @@ import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; import { GnosisSafeWrapper } from "./GnosisSafeWrapper.sol"; contract GnosisSafeWrapperFactory { - event LenderCreated(address indexed safe, GnosisSafeWrapper lender); + event LenderCreated(address indexed safe, GnosisSafeWrapper _lender); event LendingDataSet(address indexed safe, address indexed asset, uint248 fee, bool enabled); address public constant ALL_ASSETS = address(0); GnosisSafeWrapper public immutable template; - mapping(address safe => GnosisSafeWrapper lender) public lenders; - constructor() { template = new GnosisSafeWrapper(); } - function _deploy(address safe) internal returns (GnosisSafeWrapper lender) { - lender = GnosisSafeWrapper(Clones.cloneDeterministic(address(template), bytes20(safe))); - lender.initialize(safe); - lenders[safe] = lender; - emit LenderCreated(safe, lender); + /// @dev Returns true if `_lender` is a contract. + /// @param _lender The address being checked. + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + function _deployed(GnosisSafeWrapper _lender) internal view returns (bool) { + return address(_lender).code.length > 0; } - function deploy(address safe) public returns (GnosisSafeWrapper lender) { - lender = _deploy(safe); + /// @dev Deploy a new Gnosis Safe wrapper for a Gnosis Safe. + /// The factory will become the owner of the wrapper, and the safe will be able to govern the wrapper through the factory. + /// There can ever be only one wrapper per safe + /// @param safe Address of the Gnosis Safe. + function _deploy(address safe) internal returns (GnosisSafeWrapper _lender) { + _lender = GnosisSafeWrapper(Clones.cloneDeterministic(address(template), bytes20(safe))); + _lender.initialize(safe); + emit LenderCreated(safe, _lender); } - function predictLenderAddress(address safe) public view returns (address lender) { - lender = Clones.predictDeterministicAddress(address(template), bytes20(safe)); + function _getOrDeploy(address safe) internal returns (GnosisSafeWrapper _lender) { + _lender = lender(safe); + if (!_deployed(_lender)) _lender = _deploy(safe); } - function myLender() public view returns (address lender) { - lender = Clones.predictDeterministicAddress(address(template), bytes20(msg.sender)); + /// @dev Deploy a new Gnosis Safe wrapper for a Gnosis Safe. + /// @param safe Address of the Gnosis Safe. + function deploy(address safe) public returns (GnosisSafeWrapper _lender) { + _lender = _deploy(safe); } - function lending(address asset) public view returns (uint248 fee, bool enabled) { - return lenders[msg.sender].lending(asset); + /// @dev Get the Gnosis Safe wrapper for a Gnosis Safe. + /// @param safe Address of the Gnosis Safe. + function lender(address safe) public view returns (GnosisSafeWrapper _lender) { + _lender = GnosisSafeWrapper(Clones.predictDeterministicAddress(address(template), bytes20(safe))); } + /// @dev Get the Gnosis Safe wrapper for the sender. + function lender() public view returns (GnosisSafeWrapper _lender) { + _lender = lender(msg.sender); + } + + /// @dev Get the lending data for a Gnosis Safe and asset. + /// @param safe Address of the Gnosis Safe. + /// @param asset Address of the asset. function lending(address safe, address asset) public view returns (uint248 fee, bool enabled) { - return lenders[safe].lending(asset); + return lender(safe).lending(asset); + } + + /// @dev Get the lending data for an asset for the sender. + /// @param asset Address of the asset. + function lending(address asset) public view returns (uint248 fee, bool enabled) { + return lending(msg.sender, asset); } /// @dev Set lending data for an asset. @@ -51,9 +76,8 @@ contract GnosisSafeWrapperFactory { /// @param fee Fee for the flash loan (FP 1e-4) /// @param enabled Whether the asset is enabled for flash loans. function lend(address asset, uint248 fee, bool enabled) public { - GnosisSafeWrapper lender = lenders[msg.sender]; - if (lender == GnosisSafeWrapper(address(0))) lender = _deploy(msg.sender); - lender.lend(asset, fee, enabled); + GnosisSafeWrapper _lender = _getOrDeploy(msg.sender); + _lender.lend(asset, fee, enabled); emit LendingDataSet(msg.sender, asset, fee, enabled); } @@ -61,9 +85,8 @@ contract GnosisSafeWrapperFactory { /// @param fee Fee for the flash loan (FP 1e-4) /// @param enabled Whether the lending data override is enabled for flash loans. function lendAll(uint248 fee, bool enabled) public { - GnosisSafeWrapper lender = lenders[msg.sender]; - if (lender == GnosisSafeWrapper(address(0))) lender = _deploy(msg.sender); - lender.lendAll(fee, enabled); - emit LendingDataSet(msg.sender, address(0), fee, enabled); + GnosisSafeWrapper _lender = _getOrDeploy(msg.sender); + _lender.lendAll(fee, enabled); + emit LendingDataSet(msg.sender, ALL_ASSETS, fee, enabled); } } diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol index e3e7497..8cc6a0b 100644 --- a/test/GnosisSafeWrapper.t.sol +++ b/test/GnosisSafeWrapper.t.sol @@ -27,6 +27,10 @@ abstract contract GnosisSafeWrapperStateZero is Test { address internal USDC; IGnosisSafe internal safe; + function _deployed(GnosisSafeWrapper _lender) internal view returns (bool) { + return address(_lender).code.length > 0; + } + /// @dev A function invoked before each test case is run. function setUp() public virtual { // Revert if there is no API key. @@ -49,21 +53,20 @@ contract GnosisSafeWrapperStateZeroTest is GnosisSafeWrapperStateZero { function test_deploy() external { console2.log("test_deploy"); wrapper = factory.deploy(address(safe)); - assertEq(address(factory.lenders(address(safe))), address(wrapper)); assertEq(address(wrapper.safe()), address(safe)); } - function test_predictLenderAddressDebug() external { - console2.log("test_predictLenderAddress"); + function test_lender() external { + console2.log("test_lender"); wrapper = factory.deploy(address(safe)); - assertEq(factory.predictLenderAddress(address(safe)), address(wrapper)); + assertEq(address(factory.lender(address(safe))), address(wrapper)); } function test_lendDebug() external { console2.log("test_lend"); vm.prank(address(safe)); factory.lend(USDT, 10, true); - wrapper = GnosisSafeWrapper(factory.predictLenderAddress(address(safe))); + wrapper = factory.lender(address(safe)); (uint256 fee, bool enabled) = wrapper.lending(USDT); assertEq(fee, 10); assertEq(enabled, true); @@ -73,17 +76,17 @@ contract GnosisSafeWrapperStateZeroTest is GnosisSafeWrapperStateZero { console2.log("test_lendAll"); vm.prank(address(safe)); factory.lendAll(10, true); - wrapper = GnosisSafeWrapper(factory.predictLenderAddress(address(safe))); + wrapper = factory.lender(address(safe)); (uint256 fee, bool enabled) = wrapper.lending(wrapper.ALL_ASSETS()); assertEq(fee, 10); assertEq(enabled, true); } - function test_myLender() external { - console2.log("test_myLender"); + function test_lenderNoParams() external { + console2.log("test_lenderNoParams"); vm.startPrank(address(safe)); wrapper = factory.deploy(address(safe)); - assertEq(factory.myLender(), address(wrapper)); + assertEq(address(factory.lender()), address(wrapper)); vm.stopPrank(); } } @@ -93,7 +96,7 @@ abstract contract GnosisSafeWrapperWithWrapper is GnosisSafeWrapperStateZero { super.setUp(); vm.startPrank(address(safe)); - wrapper = GnosisSafeWrapper(factory.myLender()); + wrapper = factory.lender(); safe.enableModule(address(wrapper)); factory.lend(USDT, 10, true); vm.stopPrank(); From 494e31d094062354e72420a6c4a258b2f31ee7a0 Mon Sep 17 00:00:00 2001 From: alcueca Date: Sat, 20 Apr 2024 06:42:06 +0100 Subject: [PATCH 14/19] Missing natspec --- src/gnosissafe/GnosisSafeWrapperFactory.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gnosissafe/GnosisSafeWrapperFactory.sol b/src/gnosissafe/GnosisSafeWrapperFactory.sol index 2d86cdb..53b7a35 100644 --- a/src/gnosissafe/GnosisSafeWrapperFactory.sol +++ b/src/gnosissafe/GnosisSafeWrapperFactory.sol @@ -36,6 +36,8 @@ contract GnosisSafeWrapperFactory { emit LenderCreated(safe, _lender); } + /// @dev Get the Gnosis Safe wrapper for a Gnosis Safe, deploying it if it doesn't exist. + /// @param safe Address of the Gnosis Safe. function _getOrDeploy(address safe) internal returns (GnosisSafeWrapper _lender) { _lender = lender(safe); if (!_deployed(_lender)) _lender = _deploy(safe); From c1480dd9928a120778e7c81c4e52f741a2b463af Mon Sep 17 00:00:00 2001 From: alcueca Date: Sat, 20 Apr 2024 07:02:35 +0100 Subject: [PATCH 15/19] Friendlier UX --- README.md | 34 ++++++++++++++++-- src/gnosissafe/GnosisSafeWrapperFactory.sol | 35 ++++++++++++++----- test/GnosisSafeWrapper.t.sol | 38 ++++++++++----------- 3 files changed, 76 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 178bf88..5c46db9 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,37 @@ network. ## Gnosis Safe Wrapper -The [Gnosis Safe Wrapper](src/gnosissafe/GnosisSafeWrapper.sol) is intended for individual users to enable it as a -module in their Gnosis Safe, so that they can lend their own assets and earn a fee. Please let us know if you do it and -we'll add your Safe to the list above. +The [Gnosis Safe Wrapper](src/gnosissafe/GnosisSafeWrapper.sol) is intended for individual users to flash lend their own +assets held in a Gnosis Safe and earn a fee. To enable it, from your own Gnosis Safe, execute a transaction bundle to +enable the GnosisSafeWrapperFactory and set the fees for individual assets. + +``` +safe.enableModule(gnosisSafeWrapperFactory); +gnosisSafeWrapperFactory.lend(asset, fee); +... +``` + +or an override to lend all assets in the safe: + +``` +safe.enableModule(gnosisSafeWrapperFactory); +gnosisSafeWrapperFactory.lendAll(fee); +... +``` + +The `fee` parameter can be zero for free flash loans. To disable lending, execute from your safe the following command: + +``` +gnosisSafeWrapperFactory.disableLend(asset); +... +``` + +If you set a lending override, you can disable it to go back to individual asset configuration: + +``` +gnosisSafeWrapperFactory.disableLendAll(); +... +``` ## Flash Loans diff --git a/src/gnosissafe/GnosisSafeWrapperFactory.sol b/src/gnosissafe/GnosisSafeWrapperFactory.sol index 53b7a35..f8fa04c 100644 --- a/src/gnosissafe/GnosisSafeWrapperFactory.sol +++ b/src/gnosissafe/GnosisSafeWrapperFactory.sol @@ -27,7 +27,8 @@ contract GnosisSafeWrapperFactory { } /// @dev Deploy a new Gnosis Safe wrapper for a Gnosis Safe. - /// The factory will become the owner of the wrapper, and the safe will be able to govern the wrapper through the factory. + /// The factory will become the owner of the wrapper, and the safe will be able to govern the wrapper through the + /// factory. /// There can ever be only one wrapper per safe /// @param safe Address of the Gnosis Safe. function _deploy(address safe) internal returns (GnosisSafeWrapper _lender) { @@ -76,19 +77,35 @@ contract GnosisSafeWrapperFactory { /// @dev Set lending data for an asset. /// @param asset Address of the asset. /// @param fee Fee for the flash loan (FP 1e-4) - /// @param enabled Whether the asset is enabled for flash loans. - function lend(address asset, uint248 fee, bool enabled) public { + function lend(address asset, uint248 fee) public { GnosisSafeWrapper _lender = _getOrDeploy(msg.sender); - _lender.lend(asset, fee, enabled); - emit LendingDataSet(msg.sender, asset, fee, enabled); + _lender.lend(asset, fee, true); + emit LendingDataSet(msg.sender, asset, fee, true); + } + + /// @dev Disable lending for an asset. + /// @param asset Address of the asset. + function disableLend(address asset) public { + GnosisSafeWrapper _lender = _getOrDeploy(msg.sender); + (uint248 fee,) = _lender.lending(asset); + _lender.lend(asset, fee, false); + emit LendingDataSet(msg.sender, asset, fee, false); } /// @dev Set a lending data override for all assets. /// @param fee Fee for the flash loan (FP 1e-4) - /// @param enabled Whether the lending data override is enabled for flash loans. - function lendAll(uint248 fee, bool enabled) public { + function lendAll(uint248 fee) public { + GnosisSafeWrapper _lender = _getOrDeploy(msg.sender); + _lender.lendAll(fee, true); + emit LendingDataSet(msg.sender, ALL_ASSETS, fee, true); + } + + /// @dev Disable the lending override for all assets. + /// @notice If you have individual lending data set for assets, this will not affect them. + function disableLendAll() public { GnosisSafeWrapper _lender = _getOrDeploy(msg.sender); - _lender.lendAll(fee, enabled); - emit LendingDataSet(msg.sender, ALL_ASSETS, fee, enabled); + (uint248 fee,) = _lender.lending(ALL_ASSETS); + _lender.lendAll(fee, false); + emit LendingDataSet(msg.sender, ALL_ASSETS, fee, false); } } diff --git a/test/GnosisSafeWrapper.t.sol b/test/GnosisSafeWrapper.t.sol index 8cc6a0b..04c6825 100644 --- a/test/GnosisSafeWrapper.t.sol +++ b/test/GnosisSafeWrapper.t.sol @@ -62,10 +62,10 @@ contract GnosisSafeWrapperStateZeroTest is GnosisSafeWrapperStateZero { assertEq(address(factory.lender(address(safe))), address(wrapper)); } - function test_lendDebug() external { + function test_lend() external { console2.log("test_lend"); vm.prank(address(safe)); - factory.lend(USDT, 10, true); + factory.lend(USDT, 10); wrapper = factory.lender(address(safe)); (uint256 fee, bool enabled) = wrapper.lending(USDT); assertEq(fee, 10); @@ -75,7 +75,7 @@ contract GnosisSafeWrapperStateZeroTest is GnosisSafeWrapperStateZero { function test_lendAll() external { console2.log("test_lendAll"); vm.prank(address(safe)); - factory.lendAll(10, true); + factory.lendAll(10); wrapper = factory.lender(address(safe)); (uint256 fee, bool enabled) = wrapper.lending(wrapper.ALL_ASSETS()); assertEq(fee, 10); @@ -98,7 +98,7 @@ abstract contract GnosisSafeWrapperWithWrapper is GnosisSafeWrapperStateZero { vm.startPrank(address(safe)); wrapper = factory.lender(); safe.enableModule(address(wrapper)); - factory.lend(USDT, 10, true); + factory.lend(USDT, 10); vm.stopPrank(); borrower = new MockBorrower(wrapper); @@ -106,8 +106,8 @@ abstract contract GnosisSafeWrapperWithWrapper is GnosisSafeWrapperStateZero { } contract GnosisSafeWrapperWithWrapperTest is GnosisSafeWrapperWithWrapper { - function test_setLendingData_unauthorized() external { - console2.log("test_setLendingData_unauthorized"); + function test_lend_unauthorized() external { + console2.log("test_lend_unauthorized"); vm.expectRevert( abi.encodeWithSelector( IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), wrapper.DEFAULT_ADMIN_ROLE() @@ -121,27 +121,27 @@ contract GnosisSafeWrapperWithWrapperTest is GnosisSafeWrapperWithWrapper { assertEq(wrapper.flashFee(USDT, 1e18), 1e15, "Flash fee not right"); } - function test_setLendingData_changeFee() external { - console2.log("test_setLendingData_changeFee"); + function test_lend_changeFee() external { + console2.log("test_lend_changeFee"); vm.prank(address(safe)); - factory.lend(USDT, 1, true); + factory.lend(USDT, 1); assertEq(wrapper.flashFee(USDT, 1e18), 1e14, "Flash fee not right"); } - function test_setLendingDataAll_changeFee() external { - console2.log("test_setLendingDataAll_changeFee"); + function test_lendAll_changeFee() external { + console2.log("test_lendAll_changeFee"); vm.prank(address(safe)); - factory.lendAll(1, true); + factory.lendAll(1); assertEq(wrapper.flashFee(USDT, 1e18), 1e14, "Flash fee not right"); deal(USDC, address(safe), 100e18); assertEq(wrapper.flashFee(USDC, 1e18), 1e14, "Flash fee not right"); } - function test_setLendingDataAll_disable() external { - console2.log("test_setLendingDataAll_changeFee"); + function test_lendAll_disable() external { + console2.log("test_lendAll_changeFee"); vm.startPrank(address(safe)); - factory.lendAll(1, true); - factory.lendAll(1, false); + factory.lendAll(1); + factory.disableLendAll(); vm.stopPrank(); assertEq(wrapper.flashFee(USDT, 1e18), 1e15, "Flash fee not right"); } @@ -188,10 +188,10 @@ contract GnosisSafeWrapperWithWrapperTest is GnosisSafeWrapperWithWrapper { assertEq(borrower.flashFee(), fee); } - function test_setLendingData_disable() external { - console2.log("test_setLendingData_disable"); + function test_lend_disable() external { + console2.log("test_lend_disable"); vm.prank(address(safe)); - factory.lend(USDT, 10, false); + factory.disableLend(USDT); vm.expectRevert(abi.encodeWithSelector(GnosisSafeWrapper.UnsupportedAsset.selector, USDT)); borrower.flashBorrow(USDT, 1); } From 8922834605a47c43b275a43237c49898e55ba442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= <38806121+alcueca@users.noreply.github.com> Date: Sun, 21 Apr 2024 07:29:37 +0100 Subject: [PATCH 16/19] Held at gunpoint, basically --- src/gnosissafe/GnosisSafeWrapper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index 67a7bfa..64009aa 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -17,7 +17,7 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { error InsufficientRepayment(address asset, uint256 amount); event LendingDataSet(address indexed asset, uint248 fee, bool enabled); - event SafeSet(IGnosisSafe safe); + event SafeSet(IGnosisSafe indexed safe); struct LendingData { uint248 fee; // 1 = 0.01% From 56927df6d6bd3bc45fb041b578b7e506e5f559f9 Mon Sep 17 00:00:00 2001 From: alcueca Date: Tue, 23 Apr 2024 09:55:34 +0100 Subject: [PATCH 17/19] Slapped nonReentrant on --- src/gnosissafe/GnosisSafeWrapper.sol | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index 67a7bfa..d2cb739 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -6,12 +6,13 @@ import { IGnosisSafe } from "./interfaces/IGnosisSafe.sol"; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { Enum } from "./lib/Enum.sol"; import { BaseWrapper, IERC7399, IERC20 } from "../BaseWrapper.sol"; /// @dev Safe Gnosis Flash Lender that uses individual Gnosis Safe contracts as source of liquidity. -contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { +contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable, ReentrancyGuard { error UnsupportedAsset(address asset); error FailedTransfer(address asset, uint256 amount); error InsufficientRepayment(address asset, uint256 amount); @@ -58,11 +59,14 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { } } - function _flashLoan(address asset, uint256 amount, bytes memory params) internal override { + /// @dev Serve a flash loan. + /// Because in reentrant flash loans the fees repaid would be double counted, we are making this nonReentrant. + /// If the Safe uses another module that expects repayments, it should be disabled during the flash loan. + function _flashLoan(address asset, uint256 amount, bytes memory params) internal override nonReentrant { Data memory decodedParams = abi.decode(params, (Data)); uint256 fee = flashFee(asset, amount); // Checks for unsupported assets - uint256 balanceAfter = IERC20(asset).balanceOf(address(safe)) + fee; + uint256 safeBalance = IERC20(asset).balanceOf(address(safe)); // Take assets from safe bytes memory transferCall = @@ -74,8 +78,8 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { // Call callback _bridgeToCallback(asset, amount, fee, params); - // Make sure assets are back in safe (TODO: think about reentrancy) - if (IERC20(asset).balanceOf(address(safe)) < balanceAfter) revert InsufficientRepayment(asset, amount + fee); + // Make sure assets are back in safe + if (IERC20(asset).balanceOf(address(safe)) < safeBalance + fee) revert InsufficientRepayment(asset, amount + fee); } /// @dev Transfer the assets to the loan receiver. From d827d7b7deaa78e709d8e7084e5d355a89dfd1d2 Mon Sep 17 00:00:00 2001 From: alcueca Date: Tue, 23 Apr 2024 09:56:35 +0100 Subject: [PATCH 18/19] lint --- src/gnosissafe/GnosisSafeWrapper.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index e44a078..b784500 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -79,7 +79,9 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable, Reentra _bridgeToCallback(asset, amount, fee, params); // Make sure assets are back in safe - if (IERC20(asset).balanceOf(address(safe)) < safeBalance + fee) revert InsufficientRepayment(asset, amount + fee); + if (IERC20(asset).balanceOf(address(safe)) < safeBalance + fee) { + revert InsufficientRepayment(asset, amount + fee); + } } /// @dev Transfer the assets to the loan receiver. From 0e3b094179a91cdf92ebbe5f87a57b6bbaea37da Mon Sep 17 00:00:00 2001 From: alcueca Date: Tue, 23 Apr 2024 10:06:54 +0100 Subject: [PATCH 19/19] Now with reentrancy support --- src/gnosissafe/GnosisSafeWrapper.sol | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/gnosissafe/GnosisSafeWrapper.sol b/src/gnosissafe/GnosisSafeWrapper.sol index b784500..4e80837 100644 --- a/src/gnosissafe/GnosisSafeWrapper.sol +++ b/src/gnosissafe/GnosisSafeWrapper.sol @@ -6,13 +6,15 @@ import { IGnosisSafe } from "./interfaces/IGnosisSafe.sol"; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { Enum } from "./lib/Enum.sol"; import { BaseWrapper, IERC7399, IERC20 } from "../BaseWrapper.sol"; /// @dev Safe Gnosis Flash Lender that uses individual Gnosis Safe contracts as source of liquidity. -contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable, ReentrancyGuard { +contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable { + using SafeERC20 for IERC20; + error UnsupportedAsset(address asset); error FailedTransfer(address asset, uint256 amount); error InsufficientRepayment(address asset, uint256 amount); @@ -60,13 +62,10 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable, Reentra } /// @dev Serve a flash loan. - /// Because in reentrant flash loans the fees repaid would be double counted, we are making this nonReentrant. - /// If the Safe uses another module that expects repayments, it should be disabled during the flash loan. - function _flashLoan(address asset, uint256 amount, bytes memory params) internal override nonReentrant { + function _flashLoan(address asset, uint256 amount, bytes memory params) internal override { Data memory decodedParams = abi.decode(params, (Data)); uint256 fee = flashFee(asset, amount); // Checks for unsupported assets - uint256 safeBalance = IERC20(asset).balanceOf(address(safe)); // Take assets from safe bytes memory transferCall = @@ -78,9 +77,11 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable, Reentra // Call callback _bridgeToCallback(asset, amount, fee, params); - // Make sure assets are back in safe - if (IERC20(asset).balanceOf(address(safe)) < safeBalance + fee) { + // Repay to the Safe. The assets are temporary held by this wrapper to support reentrancy. + if (IERC20(asset).balanceOf(address(this)) < amount + fee) { revert InsufficientRepayment(asset, amount + fee); + } else { + IERC20(asset).safeTransfer(address(safe), amount + fee); } } @@ -89,12 +90,6 @@ contract GnosisSafeWrapper is BaseWrapper, AccessControl, Initializable, Reentra // solhint-disable-next-line no-empty-blocks function _transferAssets(address, uint256, address) internal override { } - /// @dev Where should the end client send the funds to repay the loan - /// Overriden because the provider can receive the funds directly - function _repayTo() internal view override returns (address) { - return address(safe); - } - /// @dev Set lending data for an asset. /// @param asset Address of the asset. /// @param fee Fee for the flash loan (FP 1e-4)