From 6e9c67a070ccac36a63de249a214fa33f0a85a06 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 1 Feb 2023 12:41:46 +0100 Subject: [PATCH 1/2] brought in SugarVault from storming0x --- packages/foundry/src/utils/SugarVault.sol | 127 ++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 packages/foundry/src/utils/SugarVault.sol diff --git a/packages/foundry/src/utils/SugarVault.sol b/packages/foundry/src/utils/SugarVault.sol new file mode 100644 index 00000000..05946535 --- /dev/null +++ b/packages/foundry/src/utils/SugarVault.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import "../interfaces/IERC4626.sol"; + +/******************** + * + * This contract is a POC as an example of a ERC4626 integration meant for instructive purposes. + * The donator assigns approves a token to Sugar Contract and can StartSharingYield with a receiver. + * The receiver can claim the donated yield that's "streamed" every time yield is realized in vault. + * The donator can stopSharingYield() any time and receiver won't be able to claim yield after this. + * + * + * // TODO: tokenized this into an NFT or two NFT one for donator and one for receiver keeping same IDs + * // TODO: add support for migrating yield source as admin. Require token matches + ********************* */ + +contract SugarVault is Ownable { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event StartShare(address indexed receiver, address indexed donator, uint256 amount); + + event StopShare(address indexed donator, uint256 amountReturned); + + event Claimed(address indexed receiver, address indexed donator, uint256 claimed, uint256 newShareAmount); + + /*////////////////////////////////////////////////////////////// + METADATA STORAGE/LOGIC + //////////////////////////////////////////////////////////////*/ + IERC4626 public immutable vault; + IERC20 public immutable token; + mapping(address => uint256) public tokenBalances; + mapping(address => uint256) public shareBalances; + mapping(address => address) public donatorToReceiver; + mapping(address => mapping(address => bool)) public receiverToDonator; + uint256 public dust = 1e16; + + constructor(address _vault) { + vault = IERC4626(_vault); + token = IERC20(vault.asset()); + token.approve(address(vault), type(uint256).max); + } + + function startSharingYield(address receiver, uint256 amount) public returns (uint256 shares) { + require(receiver != address(0), "ADDRESS_NOT_ZERO"); + require(receiver != msg.sender, "SELF_SHARE"); + require(amount > 0, "AMT_NOT_ZERO"); + require(donatorToReceiver[msg.sender] == address(0), "DONATOR_NOT_SET"); + SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount); + shares = vault.deposit(amount, address(this)); + tokenBalances[msg.sender] += amount; + shareBalances[msg.sender] += shares; + // donator can only have one receiver for yield and can switch it here + donatorToReceiver[msg.sender] = receiver; + receiverToDonator[receiver][msg.sender] = true; + + emit StartShare(msg.sender, receiver, amount); + } + + function stopSharingYield() public returns (uint256 amount) { + uint256 _shares = shareBalances[msg.sender]; + uint256 _tokenBalance = tokenBalances[msg.sender]; + require(_shares > 0, "NO_SHARES"); + require(_tokenBalance > 0, "NO_TOKEN"); + + shareBalances[msg.sender] = 0; + tokenBalances[msg.sender] = 0; + address _receiver = donatorToReceiver[msg.sender]; + donatorToReceiver[msg.sender] = address(0); + receiverToDonator[_receiver][msg.sender] = false; + + amount = vault.redeem(_shares, msg.sender, address(this)); + + emit StopShare(msg.sender, amount); + + // TODO: add emergency exit to withdraw and realize loss in case it happens + require(amount >= _tokenBalance, "LOSS"); + } + + function claimYield(address _donator) public returns (uint256 claimed) { + require(donatorToReceiver[_donator] == msg.sender, "NOT_RECEIVER"); + // NOTE: checks in startSharingYield ensure these values are not zero + uint256 _shares = shareBalances[_donator]; + uint256 _tokenBalance = tokenBalances[_donator]; + + // NOTE: we add dust thresold to assure precision + require(vault.convertToAssets(_shares) > _tokenBalance + dust, "NO_YIELD"); + + uint256 _remainingShares = vault.convertToShares(_tokenBalance + dust); + require(_shares > _remainingShares, "LOSS"); + + uint256 _sharesToClaim = _shares - _remainingShares; + + shareBalances[_donator] = _remainingShares; + + claimed = vault.redeem(_sharesToClaim, msg.sender, address(this)); + // NOTE: ensure donator still has deposited capital after side effect + require(vault.convertToAssets(_remainingShares) >= _tokenBalance, "CLAIM_EXCEED"); + + emit Claimed(msg.sender, _donator, claimed, _remainingShares); + } + + function claimable(address _donator, address _receiver) external view returns (uint256 amount) { + if (donatorToReceiver[_donator] != _receiver) return 0; + + // NOTE: checks in startSharingYield ensure these values are not zero + uint256 _shares = shareBalances[_donator]; + uint256 _tokenBalance = tokenBalances[_donator]; + if (_shares == 0 || _tokenBalance == 0) return 0; + + // NOTE: we add dust thresold to assure precision + if (vault.convertToAssets(_shares) < _tokenBalance + dust) return 0; + + uint256 _remainingShares = vault.convertToShares(_tokenBalance + dust); + if (_shares <= _remainingShares) return 0; + + uint256 _sharesToClaim = _shares - _remainingShares; + + amount = vault.previewRedeem(_sharesToClaim); + } +} \ No newline at end of file From 4f330262fe722e4f6b71492b090a719dfe956cc0 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 1 Feb 2023 13:00:36 +0100 Subject: [PATCH 2/2] brought in ArrakisAdapter from Midas --- packages/foundry/src/utils/SugarVault.sol | 127 ------------------ .../vault/adapter/arrakis/ArrakisAdapter.sol | 78 +++++++++++ 2 files changed, 78 insertions(+), 127 deletions(-) delete mode 100644 packages/foundry/src/utils/SugarVault.sol create mode 100644 packages/foundry/src/vault/adapter/arrakis/ArrakisAdapter.sol diff --git a/packages/foundry/src/utils/SugarVault.sol b/packages/foundry/src/utils/SugarVault.sol deleted file mode 100644 index 05946535..00000000 --- a/packages/foundry/src/utils/SugarVault.sol +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Docgen-SOLC: 0.8.15 - -pragma solidity ^0.8.15; - -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import "../interfaces/IERC4626.sol"; - -/******************** - * - * This contract is a POC as an example of a ERC4626 integration meant for instructive purposes. - * The donator assigns approves a token to Sugar Contract and can StartSharingYield with a receiver. - * The receiver can claim the donated yield that's "streamed" every time yield is realized in vault. - * The donator can stopSharingYield() any time and receiver won't be able to claim yield after this. - * - * - * // TODO: tokenized this into an NFT or two NFT one for donator and one for receiver keeping same IDs - * // TODO: add support for migrating yield source as admin. Require token matches - ********************* */ - -contract SugarVault is Ownable { - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event StartShare(address indexed receiver, address indexed donator, uint256 amount); - - event StopShare(address indexed donator, uint256 amountReturned); - - event Claimed(address indexed receiver, address indexed donator, uint256 claimed, uint256 newShareAmount); - - /*////////////////////////////////////////////////////////////// - METADATA STORAGE/LOGIC - //////////////////////////////////////////////////////////////*/ - IERC4626 public immutable vault; - IERC20 public immutable token; - mapping(address => uint256) public tokenBalances; - mapping(address => uint256) public shareBalances; - mapping(address => address) public donatorToReceiver; - mapping(address => mapping(address => bool)) public receiverToDonator; - uint256 public dust = 1e16; - - constructor(address _vault) { - vault = IERC4626(_vault); - token = IERC20(vault.asset()); - token.approve(address(vault), type(uint256).max); - } - - function startSharingYield(address receiver, uint256 amount) public returns (uint256 shares) { - require(receiver != address(0), "ADDRESS_NOT_ZERO"); - require(receiver != msg.sender, "SELF_SHARE"); - require(amount > 0, "AMT_NOT_ZERO"); - require(donatorToReceiver[msg.sender] == address(0), "DONATOR_NOT_SET"); - SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount); - shares = vault.deposit(amount, address(this)); - tokenBalances[msg.sender] += amount; - shareBalances[msg.sender] += shares; - // donator can only have one receiver for yield and can switch it here - donatorToReceiver[msg.sender] = receiver; - receiverToDonator[receiver][msg.sender] = true; - - emit StartShare(msg.sender, receiver, amount); - } - - function stopSharingYield() public returns (uint256 amount) { - uint256 _shares = shareBalances[msg.sender]; - uint256 _tokenBalance = tokenBalances[msg.sender]; - require(_shares > 0, "NO_SHARES"); - require(_tokenBalance > 0, "NO_TOKEN"); - - shareBalances[msg.sender] = 0; - tokenBalances[msg.sender] = 0; - address _receiver = donatorToReceiver[msg.sender]; - donatorToReceiver[msg.sender] = address(0); - receiverToDonator[_receiver][msg.sender] = false; - - amount = vault.redeem(_shares, msg.sender, address(this)); - - emit StopShare(msg.sender, amount); - - // TODO: add emergency exit to withdraw and realize loss in case it happens - require(amount >= _tokenBalance, "LOSS"); - } - - function claimYield(address _donator) public returns (uint256 claimed) { - require(donatorToReceiver[_donator] == msg.sender, "NOT_RECEIVER"); - // NOTE: checks in startSharingYield ensure these values are not zero - uint256 _shares = shareBalances[_donator]; - uint256 _tokenBalance = tokenBalances[_donator]; - - // NOTE: we add dust thresold to assure precision - require(vault.convertToAssets(_shares) > _tokenBalance + dust, "NO_YIELD"); - - uint256 _remainingShares = vault.convertToShares(_tokenBalance + dust); - require(_shares > _remainingShares, "LOSS"); - - uint256 _sharesToClaim = _shares - _remainingShares; - - shareBalances[_donator] = _remainingShares; - - claimed = vault.redeem(_sharesToClaim, msg.sender, address(this)); - // NOTE: ensure donator still has deposited capital after side effect - require(vault.convertToAssets(_remainingShares) >= _tokenBalance, "CLAIM_EXCEED"); - - emit Claimed(msg.sender, _donator, claimed, _remainingShares); - } - - function claimable(address _donator, address _receiver) external view returns (uint256 amount) { - if (donatorToReceiver[_donator] != _receiver) return 0; - - // NOTE: checks in startSharingYield ensure these values are not zero - uint256 _shares = shareBalances[_donator]; - uint256 _tokenBalance = tokenBalances[_donator]; - if (_shares == 0 || _tokenBalance == 0) return 0; - - // NOTE: we add dust thresold to assure precision - if (vault.convertToAssets(_shares) < _tokenBalance + dust) return 0; - - uint256 _remainingShares = vault.convertToShares(_tokenBalance + dust); - if (_shares <= _remainingShares) return 0; - - uint256 _sharesToClaim = _shares - _remainingShares; - - amount = vault.previewRedeem(_sharesToClaim); - } -} \ No newline at end of file diff --git a/packages/foundry/src/vault/adapter/arrakis/ArrakisAdapter.sol b/packages/foundry/src/vault/adapter/arrakis/ArrakisAdapter.sol new file mode 100644 index 00000000..9fe32aaa --- /dev/null +++ b/packages/foundry/src/vault/adapter/arrakis/ArrakisAdapter.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.10; + +import { MidasERC4626 } from "./MidasERC4626.sol"; +import { FlywheelCore } from "flywheel-v2/FlywheelCore.sol"; +import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; +import { RewardsClaimer } from "../RewardsClaimer.sol"; + +import { ERC20Upgradeable } from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; + +interface IGuniPool { + function deposit(uint256) external; + + function withdraw(uint256) external; + + function stake(address) external view returns (uint256); + + function totalStake() external view returns (uint256); + + function userInfo(address) external view returns (uint256, uint256); + + function pendingMIMO(address) external view returns (uint256); + + function releaseMIMO(address) external; +} + +contract ArrakisERC4626 is MidasERC4626, RewardsClaimer { + using FixedPointMathLib for uint256; + + IGuniPool public pool; + FlywheelCore public flywheel; + + function initialize( + ERC20Upgradeable asset, + FlywheelCore _flywheel, + IGuniPool _pool, + address _rewardsDestination, + ERC20Upgradeable[] memory _rewardTokens + ) public initializer { + __MidasER4626_init(asset); + __RewardsClaimer_init(_rewardsDestination, _rewardTokens); + + performanceFee = 5e16; + pool = _pool; + flywheel = _flywheel; + asset.approve(address(pool), type(uint256).max); + } + + function totalAssets() public view override returns (uint256) { + return paused() ? _asset().balanceOf(address(this)) : pool.stake(address(this)); + } + + function balanceOfUnderlying(address account) public view returns (uint256) { + return convertToAssets(balanceOf(account)); + } + + function afterDeposit(uint256 amount, uint256) internal override { + pool.deposit(amount); + } + + function beforeWithdraw(uint256 amount, uint256) internal override { + pool.withdraw(amount); + } + + function beforeClaim() internal override { + pool.releaseMIMO(address(this)); + } + + function emergencyWithdrawAndPause() external override onlyOwner { + pool.withdraw(pool.stake(address(this))); + _pause(); + } + + function unpause() external override onlyOwner { + _unpause(); + pool.deposit(_asset().balanceOf(address(this))); + } +} \ No newline at end of file