Skip to content

Commit

Permalink
CompoundV3ERC4626 vault
Browse files Browse the repository at this point in the history
Permissioned ERC4626 that invests into Compound. Also a user with
HARVEST_ROLE can claim the rewards that would be swapped for the asset
and supplied again to Compound (accounting then as a profit for all the
LPs).

Also did a refactoring in SharedSmartVault, moving the permissions to
PermissionedERC4626.

Code should be complete, but no real tests have been made.
  • Loading branch information
gnarvaja committed Feb 29, 2024
1 parent e65a2c3 commit ff517c2
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 18 deletions.
144 changes: 144 additions & 0 deletions contracts/CompoundV3ERC4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

import {ICompoundV3} from "./interfaces/ICompoundV3.sol";
import {ICometRewards} from "./interfaces/ICometRewards.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";

Check warning on line 9 in contracts/CompoundV3ERC4626.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

imported name MathUpgradeable is not used
import {SwapLibrary} from "@ensuro/swaplibrary/contracts/SwapLibrary.sol";
import {PermissionedERC4626} from "./PermissionedERC4626.sol";

/**
* @title SharedSmartVault
*
* @custom:security-contact security@ensuro.co
* @author Ensuro
*/
contract CompoundV3ERC4626 is PermissionedERC4626 {
using SafeERC20 for IERC20Metadata;
using SwapLibrary for SwapLibrary.SwapConfig;

bytes32 public constant HARVEST_ROLE = keccak256("HARVEST_ROLE");

/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ICompoundV3 internal immutable _cToken;
ICometRewards internal immutable _rewardsManager;

SwapLibrary.SwapConfig _swapConfig;

Check warning on line 29 in contracts/CompoundV3ERC4626.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

Explicitly mark visibility of state

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(ICompoundV3 cToken_, ICometRewards rewardsManager_) {
_cToken = cToken_;
_rewardsManager = rewardsManager_;
_disableInitializers();
}

/**
* @dev Initializes the SharedSmartVault
*/
function initialize(
string memory name_,
string memory symbol_,
address admin_,
SwapLibrary.SwapConfig calldata swapConfig_
) public virtual initializer {
__CompoundV3ERC4626_init(name_, symbol_, admin_, swapConfig_);
}

// solhint-disable-next-line func-name-mixedcase
function __CompoundV3ERC4626_init(
string memory name_,
string memory symbol_,
address admin_,
SwapLibrary.SwapConfig calldata swapConfig_
) internal onlyInitializing {
__PermissionedERC4626_init(name_, symbol_, admin_, IERC20Upgradeable(_cToken.baseToken()));
__CompoundV3ERC4626_init_unchained(swapConfig_);
}

// solhint-disable-next-line func-name-mixedcase
function __CompoundV3ERC4626_init_unchained(SwapLibrary.SwapConfig calldata swapConfig_) internal onlyInitializing {
swapConfig_.validate();
_swapConfig = swapConfig_;
}

/**
* @dev See {IERC4626-maxWithdraw}.
*/
function maxWithdraw(address owner) public view virtual override returns (uint256) {
if (_cToken.isWithdrawPaused()) return 0;
return super.maxWithdraw(owner);
}

/**
* @dev See {IERC4626-maxRedeem}.
*/
function maxRedeem(address owner) public view virtual override returns (uint256) {
if (_cToken.isWithdrawPaused()) return 0;
return super.maxRedeem(owner);
}

/**
* @dev See {IERC4626-maxDeposit}.
*/
function maxDeposit(address owner) public view virtual override returns (uint256) {
if (_cToken.isSupplyPaused()) return 0;
return super.maxDeposit(owner);
}

/**
* @dev See {IERC4626-maxMint}.
*/
function maxMint(address owner) public view virtual override returns (uint256) {
if (_cToken.isSupplyPaused()) return 0;
return super.maxMint(owner);
}

/**
* @dev See {IERC4626-totalAssets}.
*/
function totalAssets() public view virtual override returns (uint256 assets) {
return _cToken.balanceOf(address(this));
}

function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual override {
_cToken.withdraw(address(asset()), assets);
super._withdraw(caller, receiver, owner, assets, shares);
}

function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override {
// Transfers the assets from the caller and supplies to compound
super._deposit(caller, receiver, assets, shares);
_supply(assets);
}

function _supply(uint256 assets) internal {
IERC20Metadata(asset()).approve(address(_cToken), assets);
_cToken.supply(address(asset()), assets);
}

function harvestRewards(uint256 price) external onlyRole(HARVEST_ROLE) {
(address reward, , ) = _rewardsManager.rewardConfig(address(_cToken));
if (reward == address(0)) return;
_rewardsManager.claim(address(_cToken), address(this), true);

uint256 earned = IERC20Metadata(reward).balanceOf(address(this));
uint256 reinvestAmount = _swapConfig.exactInput(reward, asset(), earned, price);
_supply(reinvestAmount);
}

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[48] private __gap;
}
80 changes: 80 additions & 0 deletions contracts/PermissionedERC4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {ERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {ICallable} from "./interfaces/ICallable.sol";

Check warning on line 6 in contracts/PermissionedERC4626.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

imported name ICallable is not used
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";

Check warning on line 9 in contracts/PermissionedERC4626.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

imported name IERC4626 is not used
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";

Check warning on line 12 in contracts/PermissionedERC4626.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

imported name MathUpgradeable is not used

contract PermissionedERC4626 is AccessControlUpgradeable, UUPSUpgradeable, ERC4626Upgradeable {
using SafeERC20 for IERC20Metadata;

bytes32 public constant LP_ROLE = keccak256("LP_ROLE");
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");

error InvalidAsset(address asset);

// solhint-disable-next-line func-name-mixedcase
function __PermissionedERC4626_init(
string memory name_,
string memory symbol_,
address admin_,
IERC20Upgradeable asset_
) internal onlyInitializing {
__UUPSUpgradeable_init();
__AccessControl_init();
if (address(asset_) == address(0)) revert InvalidAsset(address(0));
__ERC4626_init(asset_);
__ERC20_init(name_, symbol_);
__PermissionedERC4626_init_unchained(admin_);
}

// solhint-disable-next-line func-name-mixedcase
function __PermissionedERC4626_init_unchained(address admin_) internal onlyInitializing {
_setupRole(DEFAULT_ADMIN_ROLE, admin_);
}

// solhint-disable-next-line no-empty-blocks
function _authorizeUpgrade(address newImpl) internal view override onlyRole(GUARDIAN_ROLE) {}

/**
* @dev See {IERC4626-mint}.
*/
function mint(uint256 assets, address receiver) public virtual override onlyRole(LP_ROLE) returns (uint256) {
return super.mint(assets, receiver);
}

/**
* @dev See {IERC4626-deposit}.
*/
function deposit(uint256 assets, address receiver) public virtual override onlyRole(LP_ROLE) returns (uint256) {
return super.deposit(assets, receiver);
}

/**
* @dev See {IERC4626-withdraw}.
*/
function withdraw(
uint256 assets,
address receiver,
address owner
) public virtual override onlyRole(LP_ROLE) returns (uint256) {
return super.withdraw(assets, receiver, owner);
}

/**
* @dev See {IERC4626-redeem}.
*/
function redeem(
uint256 assets,
address receiver,
address owner
) public virtual override onlyRole(LP_ROLE) returns (uint256) {
return super.withdraw(assets, receiver, owner);
}
}
22 changes: 4 additions & 18 deletions contracts/SharedSmartVault.sol
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {ERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {ICallable} from "./interfaces/ICallable.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
import {PermissionedERC4626} from "./PermissionedERC4626.sol";

/**
* @title SharedSmartVault
*
* @custom:security-contact security@ensuro.co
* @author Ensuro
*/
contract SharedSmartVault is AccessControlUpgradeable, UUPSUpgradeable, ERC4626Upgradeable {
contract SharedSmartVault is PermissionedERC4626 {
using SafeERC20 for IERC20Metadata;

bytes32 public constant LP_ROLE = keccak256("LP_ROLE");
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");
bytes32 public constant ADD_INVESTMENT_ROLE = keccak256("ADD_INVESTMENT_ROLE");
bytes32 public constant REMOVE_INVESTMENT_ROLE = keccak256("REMOVE_INVESTMENT_ROLE");

Expand All @@ -35,7 +31,6 @@ contract SharedSmartVault is AccessControlUpgradeable, UUPSUpgradeable, ERC4626U
event InvestmentRemoved(IERC4626 investment);

error InvalidSmartVault(address smartVault);
error InvalidAsset(address asset);
error InvalidCollector(address collector);
error InvalidWithdrawer(address withdrawer);
error InvalidInvestment(address investment);
Expand Down Expand Up @@ -78,25 +73,19 @@ contract SharedSmartVault is AccessControlUpgradeable, UUPSUpgradeable, ERC4626U
IERC4626[] calldata investments_,
IERC20Upgradeable asset_
) internal onlyInitializing {
__UUPSUpgradeable_init();
__AccessControl_init();
if (address(asset_) == address(0)) revert InvalidAsset(address(0));
__ERC4626_init(asset_);
__ERC20_init(name_, symbol_);
__SharedSmartVault_init_unchained(admin_, collector_, withdrawer_, investments_);
__PermissionedERC4626_init(name_, symbol_, admin_, asset_);
__SharedSmartVault_init_unchained(collector_, withdrawer_, investments_);
}

// solhint-disable-next-line func-name-mixedcase
function __SharedSmartVault_init_unchained(
address admin_,
ICallable collector_,
ICallable withdrawer_,
IERC4626[] calldata investments_
) internal onlyInitializing {
if (address(collector_) == address(0)) revert InvalidCollector(address(0));
if (address(withdrawer_) == address(0)) revert InvalidWithdrawer(address(0));
if (investments_.length == 0) revert EmptyInvestments(_investments.length);
_setupRole(DEFAULT_ADMIN_ROLE, admin_);
_collector = collector_;
_withdrawer = withdrawer_;
for (uint256 i = 0; i < investments_.length; i++) {
Expand All @@ -106,9 +95,6 @@ contract SharedSmartVault is AccessControlUpgradeable, UUPSUpgradeable, ERC4626U
IERC20Metadata(asset()).approve(_smartVault, type(uint256).max);
}

// solhint-disable-next-line no-empty-blocks
function _authorizeUpgrade(address newImpl) internal view override onlyRole(GUARDIAN_ROLE) {}

function _balance() internal view returns (uint256) {
return IERC20Metadata(asset()).balanceOf(address(this));
}
Expand Down
15 changes: 15 additions & 0 deletions contracts/interfaces/ICometRewards.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

Check warning on line 4 in contracts/interfaces/ICometRewards.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

imported name IERC20Metadata is not used

/**
* @dev Methods of the CometRewards interface we use
* Full interface in
* https://github.com/compound-finance/comet/blob/main/contracts/CometRewards.sol
*/
interface ICometRewards {
function rewardConfig(address cToken) external returns (address token, uint64 rescaleFactor, bool shouldUpscale);

function claim(address comet, address src, bool shouldAccrue) external;
}
23 changes: 23 additions & 0 deletions contracts/interfaces/ICompoundV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

/**
* @dev Methods of the CompoundV3 interface we use
* Full interface in
* https://github.com/compound-finance/comet/blob/main/contracts/CometExtInterface.sol
* https://github.com/compound-finance/comet/blob/main/contracts/CometMainInterface.sol
*/
interface ICompoundV3 is IERC20Metadata {
/**
* @dev Executes the collector and withdrawer task
*/
function baseToken() external view returns (address);

function isSupplyPaused() external view returns (bool);
function isWithdrawPaused() external view returns (bool);

function withdraw(address asset, uint amount) external;

Check warning on line 21 in contracts/interfaces/ICompoundV3.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

Rule is set with explicit type [var/s: uint]
function supply(address asset, uint amount) external;

Check warning on line 22 in contracts/interfaces/ICompoundV3.sol

View workflow job for this annotation

GitHub Actions / Ensuro Tests

Rule is set with explicit type [var/s: uint]
}
Loading

0 comments on commit ff517c2

Please sign in to comment.