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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT: feature: Yield Sharing #1105

Closed
wants to merge 13 commits into from
33 changes: 33 additions & 0 deletions contracts/interfaces/IYieldDeposit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-only

pragma solidity ^0.8.18;

interface IYieldDeposit {
/* ========== VIEWS ========== */

function getCurrentTokenRate(address tokenAddress) external returns (uint);

/* ==== MUTATIVE FUNCTIONS ==== */

function deposit(address tokenAddress, uint256 amount) external;

function withdraw(address tokenAddress, uint amount) external;

function withdrawAll(address tokenAddress) external;

function withdrawAvailableYield(address tokenAddress) external;

function updateCoverPricePercentage(address tokenAddress, uint16 newCoverPricePercentage) external;

/* ========== EVENTS AND ERRORS ========== */

event TokenDeposited(address from, uint256 depositAmount, uint256 priceRate);
event TokenWithdrawn(address from, uint256 withdrawalAmount, uint256 priceRate);

error TokenNotSupported();
error InvalidDepositAmount();
error InvalidWithdrawalAmount(uint maxWithdrawalAmount);
error InvalidTokenRate();
error NoYieldAvailable();
error InsufficientDepositForWithdrawal();
}
8 changes: 7 additions & 1 deletion contracts/mocks/common/ChainlinkAggregatorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
pragma solidity ^0.8.18;

contract ChainlinkAggregatorMock {

uint public latestAnswer;
uint public decimals;
uint80 public _roundId;

function setDecimals(uint _decimals) public {
decimals = _decimals;
Expand All @@ -15,4 +15,10 @@ contract ChainlinkAggregatorMock {
latestAnswer = _latestAnswer;
}

function latestRoundData()
public view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (_roundId, int256(latestAnswer), block.timestamp, block.timestamp, _roundId);
}
}
182 changes: 182 additions & 0 deletions contracts/modules/capital/YieldDeposit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: GPL-3.0-only

pragma solidity ^0.8.18;

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts-v4/access/Ownable.sol";
import "@openzeppelin/contracts-v4/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol";

import "../../interfaces/IYieldDeposit.sol";

/// @title Yield Deposit
/// @notice Contract for depositing yield bearing tokens and using the yield to manually purchase cover for the user

contract YieldDeposit is IYieldDeposit, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;

/* ========== STATE VARIABLES ========== */

mapping(address => address) public priceFeedOracle;
mapping(address => uint16) public tokenCoverPricePercentages;
mapping(address => mapping(address => uint)) public userTokenDepositValue; // user > token > depositValue

uint public totalDepositValue; // the total all deposits valued at the priceRate at the time of each deposit
uint public totalYieldWithdrawn;

/* ========== CONSTANTS ========== */

uint private constant PRICE_DENOMINATOR = 10_000;
uint private constant RATE_DENOMINATOR = 1e18;

/* ========== CONSTRUCTOR ========== */

constructor(address _manager) {
transferOwnership(_manager);
}

function listToken(address tokenAddress, address priceFeedAddress, uint16 coverPricePercentage) external onlyOwner {
priceFeedOracle[tokenAddress] = priceFeedAddress;
tokenCoverPricePercentages[tokenAddress] = coverPricePercentage;
}

/**
* @notice Deposits a specified amount of tokens into the contract.
* @dev User must withdraw first to change their deposit amount.
* Reverts with `InvalidDepositAmount` if the deposit amount is zero or negative.
* @param amount The quantity of tokens to deposit.
*/
function deposit(address tokenAddress, uint256 amount) external {
if (amount <= 0) {
revert InvalidDepositAmount();
}

uint currentRate = getCurrentTokenRate(tokenAddress);
uint userDepositValue = (amount * currentRate) / RATE_DENOMINATOR;

IERC20 token = IERC20(tokenAddress);
token.safeTransferFrom(msg.sender, address(this), amount);

totalDepositValue += userDepositValue;
userTokenDepositValue[msg.sender][tokenAddress] += userDepositValue;

emit TokenDeposited(msg.sender, amount, currentRate);
}

/**
* @notice Withdraws the entire principal amount previously deposited by the caller.
* @dev Transfers the deposited tokens back to the caller and resets their deposited balance to zero.
* This action is only possible if the caller has a positive deposited balance.
* @param tokenAddress - TODO
*/
function withdraw(address tokenAddress, uint amount) external nonReentrant {
uint userDepositValue = userTokenDepositValue[msg.sender][tokenAddress];
if (userDepositValue <= 0) {
revert InsufficientDepositForWithdrawal();
}

uint currentRate = getCurrentTokenRate(tokenAddress);
uint maxWithdrawalAmount = userDepositValue / currentRate;

if (amount < 0) {
revert InvalidWithdrawalAmount(maxWithdrawalAmount);
}

if (amount > maxWithdrawalAmount) {
revert InvalidWithdrawalAmount(maxWithdrawalAmount);
}

uint withdrawalValue = amount * currentRate;

totalDepositValue -= withdrawalValue;
userTokenDepositValue[msg.sender][tokenAddress] -= withdrawalValue;

IERC20 token = IERC20(tokenAddress);
token.safeTransfer(msg.sender, amount);

emit TokenWithdrawn(msg.sender, amount, currentRate);
}

/**
* @notice Withdraws the entire principal amount previously deposited by the caller.
* TODO: finish
*/
function withdrawAll(address tokenAddress) external nonReentrant {
uint userDepositValue = userTokenDepositValue[msg.sender][tokenAddress];
if (userDepositValue <= 0) {
revert InsufficientDepositForWithdrawal();
}

uint currentRate = getCurrentTokenRate(tokenAddress);
uint withdrawalAmount = userDepositValue / currentRate; // withdraw max amount
uint withdrawalValue = withdrawalAmount * currentRate;

totalDepositValue -= withdrawalValue;
userTokenDepositValue[msg.sender][tokenAddress] -= withdrawalValue;

IERC20 token = IERC20(tokenAddress);
token.safeTransfer(msg.sender, withdrawalAmount);

emit TokenWithdrawn(msg.sender, withdrawalAmount, currentRate);
}

/**
* @dev Gets the current token price from the price feed contract.
* @return uint The current token price.
* @param tokenAddress - TODO
*/
function getCurrentTokenRate(address tokenAddress) public view returns (uint) {
address priceFeedAddress = priceFeedOracle[tokenAddress];
if (priceFeedAddress == address(0)) {
revert TokenNotSupported();
}

AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
(, int256 price, , , ) = priceFeed.latestRoundData();

if (price <= 0) {
revert InvalidTokenRate();
}

return uint256(price);
}

/**
* @notice Allows the contract owner to withdraw the accumulated yield.
* @dev The yield is defined as the difference between the current token balance of the contract
* and the total principal. Reverts with `NoYieldAvailable` if there is no yield.
*/
function withdrawAvailableYield(address tokenAddress) external onlyOwner nonReentrant {
uint currentRate = getCurrentTokenRate(tokenAddress);
uint usersMaxWithdrawalAmount = totalDepositValue / currentRate;

IERC20 token = IERC20(tokenAddress);
uint totalDepositAmount = token.balanceOf(address(this));
uint totalYield = totalDepositAmount - usersMaxWithdrawalAmount;

uint availableYield = totalYield - totalYieldWithdrawn;
if (availableYield == 0) {
revert NoYieldAvailable();
}

totalYieldWithdrawn += availableYield;

token.safeTransfer(owner(), availableYield);
}

/**
* @dev Updates the cover price percentage. Only the owner can call this function.
* @param tokenAddress - TODO
* @param newCoverPricePercentage The new cover price percentage to set.
*/
function updateCoverPricePercentage(address tokenAddress, uint16 newCoverPricePercentage) external onlyOwner {
// ensure tokenAddress has corresponding priceOracle
address priceFeedAddress = priceFeedOracle[tokenAddress];
if (priceFeedAddress == address(0)) {
revert TokenNotSupported();
}

tokenCoverPricePercentages[tokenAddress] = newCoverPricePercentage;
}
}
1 change: 1 addition & 0 deletions deployments/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const contractList = [
'CoverBroker',
'wNXM',
'YieldTokenIncidents',
'YieldDeposit',
];

const updateVersion = () => {
Expand Down
Loading
Loading