Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gnosis Safe Flash Loans #39

Merged
merged 20 commits into from
Apr 23, 2024
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,51 @@ 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 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

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

Expand Down
110 changes: 110 additions & 0 deletions src/gnosissafe/GnosisSafeWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.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 {
using SafeERC20 for IERC20;

error UnsupportedAsset(address asset);
error FailedTransfer(address asset, uint256 amount);
error InsufficientRepayment(address asset, uint256 amount);

event LendingDataSet(address indexed asset, uint248 fee, bool enabled);
event SafeSet(IGnosisSafe indexed safe);

struct LendingData {
uint248 fee; // 1 = 0.01%
bool enabled;
}

address public constant ALL_ASSETS = address(0);

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);
emit SafeSet(safe);
}

/// @inheritdoc IERC7399
function maxFlashLoan(address asset) public view returns (uint256) {
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); // 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;
}
}

/// @dev Serve a flash loan.
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

// 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, fee, params);

// 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);
}
}

/// @dev Transfer the assets to the loan receiver.
/// Overriden because the provider can send the funds directly
// solhint-disable-next-line no-empty-blocks
function _transferAssets(address, uint256, address) internal override { }

/// @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 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 lendAll(uint248 fee, bool enabled) public onlyRole(DEFAULT_ADMIN_ROLE) {
lending[ALL_ASSETS] = LendingData({ fee: fee, enabled: enabled });
emit LendingDataSet(ALL_ASSETS, fee, enabled);
}
}
111 changes: 111 additions & 0 deletions src/gnosissafe/GnosisSafeWrapperFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//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 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;

constructor() {
template = new GnosisSafeWrapper();
}

/// @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;
}

/// @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);
}

/// @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);
}

/// @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);
}

/// @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 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.
/// @param asset Address of the asset.
/// @param fee Fee for the flash loan (FP 1e-4)
function lend(address asset, uint248 fee) public {
GnosisSafeWrapper _lender = _getOrDeploy(msg.sender);
_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)
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);
(uint248 fee,) = _lender.lending(ALL_ASSETS);
_lender.lendAll(fee, false);
emit LendingDataSet(msg.sender, ALL_ASSETS, fee, false);
}
}
25 changes: 25 additions & 0 deletions src/gnosissafe/interfaces/IGnosisSafe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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);

/// @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;
}
11 changes: 11 additions & 0 deletions src/gnosissafe/lib/Enum.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0;

/// @title Enum - Collection of enums
/// @author Richard Meissner - <richard@gnosis.pm>
contract Enum {
enum Operation {
Call,
DelegateCall
}
}
Loading
Loading