From 8e64232d9040ccc1a296266ea3c729018e358576 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 12 Feb 2024 17:06:52 -0700 Subject: [PATCH 1/2] build: partner program --- .../accountants/HealthCheckAccountant.sol | 14 ++- contracts/partners/FeeSplitter.sol | 110 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 contracts/partners/FeeSplitter.sol diff --git a/contracts/accountants/HealthCheckAccountant.sol b/contracts/accountants/HealthCheckAccountant.sol index b6afab9..98a2f33 100644 --- a/contracts/accountants/HealthCheckAccountant.sol +++ b/contracts/accountants/HealthCheckAccountant.sol @@ -80,6 +80,11 @@ contract HealthCheckAccountant { _; } + modifier onlyFeeManagerOrRecipient() { + _checkFeeManagerOrRecipient(); + _; + } + modifier onlyAddedVaults() { _checkVaultIsAdded(); _; @@ -96,6 +101,13 @@ contract HealthCheckAccountant { ); } + function _checkFeeManagerOrRecipient() internal view virtual { + require( + msg.sender == feeRecipient || msg.sender == feeManager, + "!recipient" + ); + } + function _checkVaultIsAdded() internal view virtual { require(vaults[msg.sender], "vault not added"); } @@ -518,7 +530,7 @@ contract HealthCheckAccountant { function distribute( address token, uint256 amount - ) public virtual onlyFeeManager { + ) public virtual onlyFeeManagerOrRecipient { ERC20(token).safeTransfer(feeRecipient, amount); emit DistributeRewards(token, amount); diff --git a/contracts/partners/FeeSplitter.sol b/contracts/partners/FeeSplitter.sol new file mode 100644 index 0000000..5e8f60f --- /dev/null +++ b/contracts/partners/FeeSplitter.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.18; + +import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; +import {Governance2Step} from "@periphery/utils/Governance2Step.sol"; +import {HealthCheckAccountant} from "../accountants/HealthCheckAccountant.sol"; + +contract FeeSplitter is Governance2Step { + + struct Vault { + address shareHolder; + uint256 currentDeposits; + uint256 lastDeposit; + } + struct Partner { + address govenator; + address feeRecipient; + mapping(address => Vault) vault; + } + + uint256 internal constant WAD = 1e18; + + mapping(bytes32 => Partner) public partners; // Use uint and start at 1 to keep track of number? + + address public accountant; + + constructor(address _governance, address _accountant) Governance2Step(_governance){ + accountant = _accountant; + } + + function deposit(address _vault, bytes32 _partnerId, uint256 _amount) external virtual returns (uint256) { + // Accrue reward balance + + // Deposit in vault and send to vault holder. + + // Track the amount of shares now and update tracked balance + } + + function redeem(address _vault, bytes32 _partnerId, uint256 _amount, address _receiver, uint256 maxLoss) external virtual returns (uint256) { + // Accrue reward balance + + // Withdraw on behalf of holder + + // Lower amount accounting for the time till now + } + + function addPartner() external virtual onlyGovernance { + // Add partner logic + } + + function addPartnerVaults(bytes32 partnerId, address[] memory vaults, address[] memory holders) external virtual onlyGovernance { + // check vault is eligible through the accountant? + // Add each vault to the partner mapping + } + + + function removePartnerVaults(bytes32 partnerId, address[] memory vaults) external virtual onlyGovernance { + // Remove each vault to the partner mapping + } + + function removePartner() external onlyGovernance { + // Remove partner logic + } + + function setAccountant(address _newAccountant) external virtual onlyGovernance { + accountant = _newAccountant; + } + + function updateInfo() external { + // Update info logic + } + + function claimFees(bytes32 partnerId, address[] memory vaults) external virtual returns (uint256[] memory claimed) { + Partner storage partner = partners[partnerId]; + require(partner.govenator != address(0), "!partner"); + require(msg.sender == partner.govenator || msg.sender == governance, "!allowed"); + + claimed = new uint256[](vaults.length); + address vault; + address holder; + address recipient = partner.feeRecipient; + for (uint256 i = 0; i < vaults.length; i++) { + vault = vaults[i]; + holder = partner.vault[vault].shareHolder; + require(holder != address(0), "vault not added"); + + HealthCheckAccountant(accountant).distribute(vault); + claimed[i] = _claimFees(vault, holder, recipient); + } + + return claimed; + } + + /** + * TODO: + Track claimed balance and timestamp to not repay + Track fee share balance so the percent isn't dependant on order claimed + Use a time weighted balance of holder so cant deposit right before the fee claim + */ + function _claimFees(address vault, address holder, address recipient) internal virtual returns (uint256 feesClaimed) { + // % of tvl in 1e18 scale + uint256 percent = IVault(vault).balanceOf(holder) * WAD / IVault(vault).totalAssets(); + + // total fees owned + feesClaimed = IVault(vault).balanceOf(address(this)) * percent / WAD; + + IVault(vault).transfer(recipient, feesClaimed); + } + +} \ No newline at end of file From f815cfc73fdb74d21d9073e0f539a0680aa225d1 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 12 Feb 2024 18:05:33 -0700 Subject: [PATCH 2/2] build: deposit and withdraw --- contracts/partners/FeeSplitter.sol | 162 ++++++++++++++---- .../test_healthcheck_accountant.py | 2 +- tests/accountants/test_refund_accountant.py | 2 +- 3 files changed, 135 insertions(+), 31 deletions(-) diff --git a/contracts/partners/FeeSplitter.sol b/contracts/partners/FeeSplitter.sol index 5e8f60f..5c76a0e 100644 --- a/contracts/partners/FeeSplitter.sol +++ b/contracts/partners/FeeSplitter.sol @@ -1,16 +1,21 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.18; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; import {Governance2Step} from "@periphery/utils/Governance2Step.sol"; import {HealthCheckAccountant} from "../accountants/HealthCheckAccountant.sol"; contract FeeSplitter is Governance2Step { + using SafeERC20 for ERC20; struct Vault { address shareHolder; uint256 currentDeposits; - uint256 lastDeposit; + uint256 lastUpdate; } struct Partner { address govenator; @@ -18,43 +23,110 @@ contract FeeSplitter is Governance2Step { mapping(address => Vault) vault; } + modifier updateFees(address _vault, bytes32 _partnerId) { + _updateFees(_vault, _partnerId); + _; + } + + function _updateFees(address _vault, bytes32 _partnerId) internal { + Vault memory vault = partners[_partnerId].vault[_vault]; + require(vault.shareHolder != address(0), "not active"); + uint256 balance = Math.min( + vault.currentDeposits, + IVault(_vault).balanceOf(address(vault.shareHolder)) + ); + uint256 time = block.timestamp - vault.lastUpdate; + earned[_vault][_partnerId] += + (vaultRate[_vault] * balance * time) / + WAD; + partners[_partnerId].vault[_vault].lastUpdate = block.timestamp; + } + uint256 internal constant WAD = 1e18; - + mapping(bytes32 => Partner) public partners; // Use uint and start at 1 to keep track of number? + mapping(address => uint256) public vaultRate; + + // TODO Combine into one mapping + mapping(address => mapping(bytes32 => uint256)) public earned; + mapping(address => mapping(bytes32 => uint256)) public paid; + address public accountant; - constructor(address _governance, address _accountant) Governance2Step(_governance){ + constructor( + address _governance, + address _accountant + ) Governance2Step(_governance) { accountant = _accountant; } - function deposit(address _vault, bytes32 _partnerId, uint256 _amount) external virtual returns (uint256) { - // Accrue reward balance - + function deposit( + address _vault, + bytes32 _partnerId, + uint256 _amount + ) external virtual updateFees(_vault, _partnerId) returns (uint256 shares) { // Deposit in vault and send to vault holder. + address asset = IVault(_vault).asset(); + + ERC20(asset).safeTransferFrom(msg.sender, address(this), _amount); + + _checkAllowance(_vault, asset); + + shares = IVault(_vault).deposit( + _amount, + partners[_partnerId].vault[_vault].shareHolder + ); // Track the amount of shares now and update tracked balance + partners[_partnerId].vault[_vault].currentDeposits += _amount; } - function redeem(address _vault, bytes32 _partnerId, uint256 _amount, address _receiver, uint256 maxLoss) external virtual returns (uint256) { - // Accrue reward balance - + function redeem( + address _vault, + bytes32 _partnerId, + uint256 _amount, + address _receiver, + uint256 maxLoss + ) + external + virtual + updateFees(_vault, _partnerId) + returns (uint256 withdrawn) + { // Withdraw on behalf of holder + withdrawn = IVault(_vault).redeem( + _amount, + msg.sender, + _receiver, + maxLoss + ); // Lower amount accounting for the time till now + uint256 deposits = partners[_partnerId].vault[_vault].currentDeposits; + partners[_partnerId].vault[_vault].currentDeposits = deposits > + withdrawn + ? deposits - withdrawn + : 0; } function addPartner() external virtual onlyGovernance { // Add partner logic } - function addPartnerVaults(bytes32 partnerId, address[] memory vaults, address[] memory holders) external virtual onlyGovernance { + function addPartnerVaults( + bytes32 partnerId, + address[] calldata vaults, + address[] calldata holders + ) external virtual onlyGovernance { // check vault is eligible through the accountant? // Add each vault to the partner mapping } - - function removePartnerVaults(bytes32 partnerId, address[] memory vaults) external virtual onlyGovernance { + function removePartnerVaults( + bytes32 partnerId, + address[] calldata vaults + ) external virtual onlyGovernance { // Remove each vault to the partner mapping } @@ -62,7 +134,9 @@ contract FeeSplitter is Governance2Step { // Remove partner logic } - function setAccountant(address _newAccountant) external virtual onlyGovernance { + function setAccountant( + address _newAccountant + ) external virtual onlyGovernance { accountant = _newAccountant; } @@ -70,22 +144,31 @@ contract FeeSplitter is Governance2Step { // Update info logic } - function claimFees(bytes32 partnerId, address[] memory vaults) external virtual returns (uint256[] memory claimed) { - Partner storage partner = partners[partnerId]; + function claimFees( + bytes32 _partnerId, + address[] calldata vaults + ) external virtual returns (uint256[] memory claimed) { + Partner storage partner = partners[_partnerId]; require(partner.govenator != address(0), "!partner"); - require(msg.sender == partner.govenator || msg.sender == governance, "!allowed"); + require( + msg.sender == partner.govenator || msg.sender == governance, + "!allowed" + ); claimed = new uint256[](vaults.length); address vault; - address holder; address recipient = partner.feeRecipient; for (uint256 i = 0; i < vaults.length; i++) { vault = vaults[i]; - holder = partner.vault[vault].shareHolder; - require(holder != address(0), "vault not added"); - HealthCheckAccountant(accountant).distribute(vault); - claimed[i] = _claimFees(vault, holder, recipient); + _updateFees(vault, _partnerId); + + uint256 toPay = earned[vault][_partnerId] - paid[vault][_partnerId]; + + _claimFees(vault, recipient, toPay); + + claimed[i] = toPay; + paid[vault][_partnerId] += toPay; } return claimed; @@ -97,14 +180,35 @@ contract FeeSplitter is Governance2Step { Track fee share balance so the percent isn't dependant on order claimed Use a time weighted balance of holder so cant deposit right before the fee claim */ - function _claimFees(address vault, address holder, address recipient) internal virtual returns (uint256 feesClaimed) { - // % of tvl in 1e18 scale - uint256 percent = IVault(vault).balanceOf(holder) * WAD / IVault(vault).totalAssets(); - - // total fees owned - feesClaimed = IVault(vault).balanceOf(address(this)) * percent / WAD; + function _claimFees( + address vault, + address recipient, + uint256 toClaim + ) internal virtual { + if (IVault(vault).balanceOf(address(this)) < toClaim) { + HealthCheckAccountant(accountant).distribute(vault); + } - IVault(vault).transfer(recipient, feesClaimed); + IVault(vault).transfer(recipient, toClaim); } -} \ No newline at end of file + /** + * @dev Internal safe function to make sure the contract you want to + * interact with has enough allowance to pull the desired tokens. + * + * @param _contract The address of the contract that will move the token. + * @param _token The ERC-20 token that will be getting spent. + */ + function _checkAllowance( + address _contract, + address _token + ) internal virtual { + // Yearn vaults don't lower allowance if set to max uint + if ( + ERC20(_token).allowance(address(this), _contract) != + type(uint256).max + ) { + ERC20(_token).safeApprove(_contract, type(uint256).max); + } + } +} diff --git a/tests/accountants/test_healthcheck_accountant.py b/tests/accountants/test_healthcheck_accountant.py index bb871d1..93c29cd 100644 --- a/tests/accountants/test_healthcheck_accountant.py +++ b/tests/accountants/test_healthcheck_accountant.py @@ -641,7 +641,7 @@ def test_distribute( assert vault.balanceOf(daddy.address) == 0 assert vault.balanceOf(fee_recipient.address) == 0 - with ape.reverts("!fee manager"): + with ape.reverts("!recipient"): accountant.distribute(vault.address, sender=user) tx = accountant.distribute(vault.address, sender=daddy) diff --git a/tests/accountants/test_refund_accountant.py b/tests/accountants/test_refund_accountant.py index 49fbf17..88c1c3f 100644 --- a/tests/accountants/test_refund_accountant.py +++ b/tests/accountants/test_refund_accountant.py @@ -653,7 +653,7 @@ def test_distribute( assert vault.balanceOf(daddy.address) == 0 assert vault.balanceOf(fee_recipient.address) == 0 - with ape.reverts("!fee manager"): + with ape.reverts("!recipient"): accountant.distribute(vault.address, sender=user) tx = accountant.distribute(vault.address, sender=daddy)