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

Compounding support via ERC4626 #83

Open
wants to merge 34 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1655825
Modularize zkBob pool and add energy redeemer example
k1rill-fedoseev Jun 27, 2023
3d1f1b3
Migration script example
k1rill-fedoseev Jul 12, 2023
40208f9
init commit
AlexSaplin Aug 1, 2023
d37fc33
Modularize zkBob pool and add energy redeemer example
k1rill-fedoseev Jun 27, 2023
93aa5d7
Migration script example
k1rill-fedoseev Jul 12, 2023
0c412d0
Init pool index
k1rill-fedoseev Jul 31, 2023
39254e9
Update ZkBobPoolUSDCMigrated
k1rill-fedoseev Aug 3, 2023
1fc52b9
upd
AlexSaplin Aug 8, 2023
5d14b23
Merge remote-tracking branch 'origin/feat/energy-redeemer' into feat/…
AlexSaplin Aug 9, 2023
3dd6b18
upd
AlexSaplin Aug 10, 2023
89d3633
upd
AlexSaplin Aug 11, 2023
f635da8
Fixes
AlexSaplin Aug 21, 2023
3d4e626
claim fix
AlexSaplin Aug 21, 2023
f9ad65b
emergency withdraw support
AlexSaplin Aug 28, 2023
8aa7023
main fixes
AlexSaplin Sep 11, 2023
50004e2
Merge remote-tracking branch 'origin/develop' into feat/yield-erc4626…
AlexSaplin Sep 11, 2023
7deed0e
merge conflicts resolved
AlexSaplin Sep 11, 2023
1497ddb
Merge remote-tracking branch 'origin/develop' into feat/yield-erc4626…
AlexSaplin Sep 12, 2023
b05d627
withdraw from compounding support in forced exit
AlexSaplin Sep 12, 2023
7c80c21
updated aave-vault
AlexSaplin Sep 12, 2023
cae0b71
test fix denominator
AlexSaplin Sep 12, 2023
8eae668
fix: migration test
k1rill-fedoseev Sep 21, 2023
3d7474e
chore: cleanup
k1rill-fedoseev Sep 21, 2023
43cbede
chore: lint
k1rill-fedoseev Sep 22, 2023
4e16314
chore: repack and optimize yield params struct
k1rill-fedoseev Sep 22, 2023
c823c8f
chore: code cleanup
k1rill-fedoseev Sep 22, 2023
a98f251
fix: code coverage
k1rill-fedoseev Sep 22, 2023
77e9893
fix forks + removed emergency withdraw
AlexSaplin Sep 28, 2023
979c506
Merge remote-tracking branch 'origin/feat/yield-erc4626-support' into…
AlexSaplin Sep 28, 2023
45ce1ff
chore: lint
AlexSaplin Sep 28, 2023
fa8d190
fix
AlexSaplin Oct 11, 2023
fd37d8a
chore: lint
AlexSaplin Oct 11, 2023
d0e529e
fix
AlexSaplin Oct 11, 2023
5040e52
fix updateYieldParams test
AlexSaplin Oct 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
path = lib/base58-solidity
url = https://github.com/storyicon/base58-solidity
branch = v1.0.2
[submodule "lib/@aave/aave-vault"]
path = lib/@aave/aave-vault
url = https://github.com/zkBob/Aave-Vault
k1rill-fedoseev marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ fs_permissions = [
{ access = "read", path = "./out" },
{ access = "read", path = "./scripts/vanityaddr/contracts" }
]
auto_detect_remappings = false

[fmt]
line_length = 120
Expand Down
1 change: 1 addition & 0 deletions lib/@aave/aave-vault
Submodule aave-vault added at 73613b
16 changes: 14 additions & 2 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
@openzeppelin/contracts/=lib/@openzeppelin/contracts/contracts/
@gnosis/auction/=lib/@gnosis/auction/contracts/
@aave/=lib/@aave/
@base58-solidity/=lib/base58-solidity/contracts/
@gnosis/=lib/@gnosis/
@gnosis/auction/=lib/@gnosis/auction/contracts/
@openzeppelin/interfaces/=lib/@openzeppelin/contracts/contracts/interfaces/
@openzeppelin/contracts/=lib/@openzeppelin/contracts/contracts
@uniswap/=lib/@uniswap/
@aave-v3-core/=lib/@aave/aave-vault/lib/aave-v3-core/contracts
@aave-v3-periphery/=lib/@aave/aave-vault/lib/aave-v3-periphery/contracts/
ds-test/=lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/@aave/aave-vault/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
@openzeppelin-upgradeable/=lib/@aave/aave-vault/lib/openzeppelin-contracts-upgradeable/contracts
openzeppelin-contracts/=lib/@aave/aave-vault/lib/openzeppelin-contracts/
solmate/=lib/@aave/aave-vault/lib/solmate/src/
139 changes: 139 additions & 0 deletions script/scripts/CompoundingUSDCPoolMigration.s.sol

Large diffs are not rendered by default.

164 changes: 164 additions & 0 deletions src/zkbob/ZkBobCompoundingMixin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.15;

import "@openzeppelin/contracts/interfaces/IERC4626.sol";
import "./ZkBobPool.sol";

/**
* @title ZkBobCompoundingMixin
*/
abstract contract ZkBobCompoundingMixin is ZkBobPool {
using SafeERC20 for IERC20;

uint256 public investedAssetsAmount;

struct YieldParams {
// ERC4626 vault address (or address(0) if not set)
address yield;
// expected amount of underlying tokens to be left at the pool after successful rebalance
uint96 buffer;
// operator address (or address(0) if permissionless)
address yieldOperator;
// slippage/rounding protection buffer, small part of accumulated interest that is non-claimable
uint96 dust;
// address to receive accumulated interest during the rebalance
address interestReceiver;
// maximum amount of underlying tokens that can be invested into vault
uint256 maxInvestedAmount;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the scenario of negative apr considered?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2 possible snearios - we want to close the position in the vault or don't want.
It can be checked by comparing to variables - totalInvestedAssets and previewWithdraw(totalInvestedShares)
So, offchain bot can monitor these 2 variables and notify, when our tvl become less than users deposits. After that the team can decide, if position should be closed.
Insufficiency in tvl can be fullfilled by us just by transferring tokens.
So, in code function for closing position should be added, and possibility to decrease totalInvestedAmount manually by transferring tokens to pool


YieldParams public yieldParams;

event UpdateYieldParams(YieldParams yieldParams);
event Claimed(address indexed yield, uint256 amount);
event Rebalance(address indexed yield, uint256 withdrawn, uint256 deposited);

// @inheritdoc ZkBobPool
function _withdrawToken(address _user, uint256 _tokenAmount) internal override {
uint256 underlyingBalance = IERC20(token).balanceOf(address(this));
if (underlyingBalance < _tokenAmount) {
k1rill-fedoseev marked this conversation as resolved.
Show resolved Hide resolved
(address yieldAddress, uint256 buffer) = (yieldParams.yield, yieldParams.buffer);
uint256 remainder = _tokenAmount - underlyingBalance;
uint256 investedAssets = investedAssetsAmount;
uint256 withdrawAmount = investedAssets > remainder + buffer ? remainder + buffer : investedAssets;
investedAssetsAmount = investedAssets - withdrawAmount;
IERC4626(yieldAddress).withdraw(withdrawAmount, address(this), address(this));
emit Rebalance(yieldAddress, withdrawAmount, 0);
}
IERC20(token).safeTransfer(_user, _tokenAmount);
}

/**
* @dev Updates yield parameters.
* Callable only by the contract owner / proxy admin.
* @param _yieldParams new yield parameters.
*/
function updateYieldParams(YieldParams memory _yieldParams) external onlyOwner {
k1rill-fedoseev marked this conversation as resolved.
Show resolved Hide resolved
address yieldAddress = yieldParams.yield;
require(
_yieldParams.yield == yieldAddress || investedAssetsAmount == 0, "ZkBobCompounding: another yield is active"
);
require(
_yieldParams.yield == address(0) || _yieldParams.interestReceiver != address(0),
"ZkBobCompounding: zero interest receiver"
);

if (_yieldParams.yield != yieldAddress && yieldAddress != address(0)) {
_claim(yieldAddress, yieldParams.interestReceiver, 0);
}

yieldParams = _yieldParams;

emit UpdateYieldParams(_yieldParams);
}

/**
* @dev Rebalances yield bearing tokens.
* @param minRebalanceAmount minimum amount of token to move between underlying balance and yield.
* @param maxRebalanceAmount maximum amount of token to move between underlying balance and yield.
*/
function rebalance(uint256 minRebalanceAmount, uint256 maxRebalanceAmount) external {
(address yieldAddress, address operator, uint256 buffer, uint256 maxInvestedAmount) =
(yieldParams.yield, yieldParams.yieldOperator, yieldParams.buffer, yieldParams.maxInvestedAmount);

require(yieldAddress != address(0), "ZkBobCompounding: yield not enabled");
require(operator == address(0) || operator == msg.sender || _isOwner(), "ZkBobCompounding: not authorized");

uint256 underlyingBalance = IERC20(token).balanceOf(address(this));
uint256 investedAssets = investedAssetsAmount;

if (underlyingBalance < buffer || investedAssets > maxInvestedAmount) {
uint256 withdrawAmount;
if (underlyingBalance < buffer) {
withdrawAmount = buffer - underlyingBalance;
if (withdrawAmount > investedAssets) {
withdrawAmount = investedAssets;
}
} else {
withdrawAmount = investedAssets - maxInvestedAmount;
}
if (withdrawAmount > maxRebalanceAmount) {
withdrawAmount = maxRebalanceAmount;
}
require(
withdrawAmount > 0 && withdrawAmount >= minRebalanceAmount, "ZkBobCompounding: insufficient rebalance"
);
investedAssetsAmount = investedAssets - withdrawAmount;
IERC4626(yieldAddress).withdraw(withdrawAmount, address(this), address(this));
emit Rebalance(yieldAddress, withdrawAmount, 0);
} else {
uint256 depositAmount = underlyingBalance - buffer;
if (investedAssets + depositAmount > maxInvestedAmount) {
depositAmount = maxInvestedAmount - investedAssets;
}
if (depositAmount > maxRebalanceAmount) {
depositAmount = maxRebalanceAmount;
}
require(
depositAmount > 0 && depositAmount >= minRebalanceAmount, "ZkBobCompounding: insufficient rebalance"
);
investedAssetsAmount = investedAssets + depositAmount;
IERC20(token).approve(yieldAddress, depositAmount);
IERC4626(yieldAddress).deposit(depositAmount, address(this));
emit Rebalance(yieldAddress, 0, depositAmount);
}
}

/**
* @dev Collects accumulated fees and generated yield for the specific collateral.
* Callable only by the contract owner / proxy admin / yield admin.
* @param minClaimAmount minimum amount of token to claim.
* @return Claimed amount.
*/
function claim(uint256 minClaimAmount) external returns (uint256) {
(address yieldAddress, address operator, uint256 dust, address interestReceiver) =
(yieldParams.yield, yieldParams.yieldOperator, yieldParams.dust, yieldParams.interestReceiver);

require(yieldAddress != address(0), "ZkBobCompounding: yield not enabled");
require(operator == address(0) || operator == msg.sender || _isOwner(), "ZkBobCompounding: not authorized");

uint256 claimed = _claim(yieldAddress, interestReceiver, dust);

require(claimed > 0 && claimed >= minClaimAmount, "ZkBobCompounding: not enough to claim");

return claimed;
}

function _claim(address yieldAddress, address interestReceiver, uint256 dust) internal returns (uint256) {
uint256 shares = IERC4626(yieldAddress).balanceOf(address(this));
uint256 lockedAssets = investedAssetsAmount + dust;
uint256 availableAssets = IERC4626(yieldAddress).convertToAssets(shares);

if (availableAssets <= lockedAssets) {
return 0;
}

uint256 claimAmount = availableAssets - lockedAssets;
IERC4626(yieldAddress).withdraw(claimAmount, interestReceiver, address(this));

emit Claimed(yieldAddress, claimAmount);

return claimAmount;
}
}
17 changes: 17 additions & 0 deletions src/zkbob/ZkBobNonCompoudingMixin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.15;

import "./ZkBobPool.sol";

/**
* @title ZkBobNonCompoundingMixin
*/
abstract contract ZkBobNonCompoundingMixin is ZkBobPool {
using SafeERC20 for IERC20;

// @inheritdoc ZkBobPool
function _withdrawToken(address _user, uint256 _tokenAmount) internal override {
IERC20(token).safeTransfer(_user, _tokenAmount);
}
}
13 changes: 10 additions & 3 deletions src/zkbob/ZkBobPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex
*/
function _withdrawNative(address _user, uint256 _tokenAmount) internal virtual returns (uint256);

/**
* @dev Withdraws given amount of tokens to the provided address.
* @param _user token receiver address.
* @param _tokenAmount amount to tokens to withdraw.
*/
function _withdrawToken(address _user, uint256 _tokenAmount) internal virtual;

/**
* @dev Performs token transfer using a signed permit signature.
* @param _user token depositor address, should correspond to the signature author.
Expand Down Expand Up @@ -270,7 +277,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex
}

if (withdraw_amount > 0) {
IERC20(token).safeTransfer(user, withdraw_amount);
_withdrawToken(user, withdraw_amount);
}

if (energy_amount < 0) {
Expand Down Expand Up @@ -431,7 +438,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex
}
nullifiers[_nullifier] = poolIndex | uint256(0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000);

IERC20(token).safeTransfer(_to, _amount * TOKEN_DENOMINATOR / TOKEN_NUMERATOR);
_withdrawToken(_to, _amount * TOKEN_DENOMINATOR / TOKEN_NUMERATOR);

emit ForcedExit(poolIndex, _nullifier, _to, _amount);
}
Expand Down Expand Up @@ -463,7 +470,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex
);
uint256 fee = accumulatedFee[_operator] * TOKEN_DENOMINATOR / TOKEN_NUMERATOR;
require(fee > 0, "ZkBobPool: no fee to withdraw");
IERC20(token).safeTransfer(_to, fee);
_withdrawToken(_to, fee);
accumulatedFee[_operator] = 0;
emit WithdrawFee(_operator, fee);
}
Expand Down
3 changes: 2 additions & 1 deletion src/zkbob/ZkBobPoolBOB.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ pragma solidity 0.8.15;
import "./ZkBobPool.sol";
import "./ZkBobTokenSellerMixin.sol";
import "./ZkBobSaltedPermitMixin.sol";
import "./ZkBobNonCompoudingMixin.sol";

/**
* @title ZkBobPoolBOB
* Shielded transactions pool for BOB tokens.
*/
contract ZkBobPoolBOB is ZkBobPool, ZkBobTokenSellerMixin, ZkBobSaltedPermitMixin {
contract ZkBobPoolBOB is ZkBobPool, ZkBobTokenSellerMixin, ZkBobSaltedPermitMixin, ZkBobNonCompoundingMixin {
constructor(
uint256 __pool_id,
address _token,
Expand Down
3 changes: 2 additions & 1 deletion src/zkbob/ZkBobPoolERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ pragma solidity 0.8.15;
import "./ZkBobPool.sol";
import "./ZkBobTokenSellerMixin.sol";
import "./ZkBobPermit2Mixin.sol";
import "./ZkBobCompoundingMixin.sol";

/**
* @title ZkBobPoolERC20
* Shielded transactions pool for ERC20 tokens
*/
contract ZkBobPoolERC20 is ZkBobPool, ZkBobTokenSellerMixin, ZkBobPermit2Mixin {
contract ZkBobPoolERC20 is ZkBobPool, ZkBobTokenSellerMixin, ZkBobPermit2Mixin, ZkBobCompoundingMixin {
constructor(
uint256 __pool_id,
address _token,
Expand Down
3 changes: 2 additions & 1 deletion src/zkbob/ZkBobPoolETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ pragma solidity 0.8.15;
import "./ZkBobPool.sol";
import "./ZkBobWETHMixin.sol";
import "./ZkBobPermit2Mixin.sol";
import "./ZkBobCompoundingMixin.sol";

/**
* @title ZkBobPoolETH
* Shielded transactions pool for native and wrapped native tokens.
*/
contract ZkBobPoolETH is ZkBobPool, ZkBobWETHMixin, ZkBobPermit2Mixin {
contract ZkBobPoolETH is ZkBobPool, ZkBobWETHMixin, ZkBobPermit2Mixin, ZkBobCompoundingMixin {
constructor(
uint256 __pool_id,
address _token,
Expand Down
3 changes: 2 additions & 1 deletion src/zkbob/ZkBobPoolUSDC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ pragma solidity 0.8.15;
import "./ZkBobPool.sol";
import "./ZkBobTokenSellerMixin.sol";
import "./ZkBobUSDCPermitMixin.sol";
import "./ZkBobCompoundingMixin.sol";

/**
* @title ZkBobPoolUSDC
* Shielded transactions pool for USDC tokens supporting USDC transfer authorizations
*/
contract ZkBobPoolUSDC is ZkBobPool, ZkBobTokenSellerMixin, ZkBobUSDCPermitMixin {
contract ZkBobPoolUSDC is ZkBobPool, ZkBobTokenSellerMixin, ZkBobUSDCPermitMixin, ZkBobCompoundingMixin {
constructor(
uint256 __pool_id,
address _token,
Expand Down
27 changes: 27 additions & 0 deletions test/interfaces/IZkBobPoolAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ import "../../src/interfaces/IOperatorManager.sol";
import "../../src/interfaces/IEnergyRedeemer.sol";

interface IZkBobPoolAdmin {
struct YieldParams {
// ERC4626 vault address (or address(0) if not set)
address yield;
// expected amount of underlying tokens to be left at the pool after successful rebalance
uint96 buffer;
// operator address (or address(0) if permissionless)
address yieldOperator;
// slippage/rounding protection buffer, small part of accumulated interest that is non-claimable
uint96 dust;
// address to receive accumulated interest during the rebalance
address interestReceiver;
// maximum amount of underlying tokens that can be invested into vault
uint256 maxInvestedAmount;
}

function denominator() external pure returns (uint256);

function pool_index() external view returns (uint256);
Expand Down Expand Up @@ -82,4 +97,16 @@ interface IZkBobPoolAdmin {
function direct_deposit_queue() external view returns (address);

function pool_id() external view returns (uint256);

function investedAssetsAmount() external view returns (uint256);

function yieldParams() external view returns (YieldParams memory);

function updateYieldParams(YieldParams memory _yieldParams) external;

function rebalance(uint256 minRebalanceAmount, uint256 maxRebalanceAmount) external;

function claim(uint256 minClaimAmount) external returns (uint256);

function emergencyWithdraw(uint256 targetAmount) external;
}
8 changes: 5 additions & 3 deletions test/shared/Env.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ address constant mockImpl = address(0xdead);
address constant bobVanityAddr = address(0xB0B195aEFA3650A6908f15CdaC7D92F8a5791B0B);
bytes32 constant bobSalt = bytes32(uint256(285834900769));

uint256 constant forkBlockMainnet = 16200000;
string constant forkRpcUrlMainnet = "https://rpc.ankr.com/eth";
uint256 constant forkBlockMainnet = 17000000;
string constant forkRpcUrlMainnet =
"https://rpc.ankr.com/eth/9459a7b0289d1177790c6d0e02b5d2c852d173cfae0ce30ba12b1e7ad3b73cc8";
uint256 constant forkBlockPolygon = 37000000;
string constant forkRpcUrlPolygon = "https://rpc.ankr.com/polygon";
string constant forkRpcUrlPolygon =
"https://rpc.ankr.com/polygon/9459a7b0289d1177790c6d0e02b5d2c852d173cfae0ce30ba12b1e7ad3b73cc8";
uint256 constant forkBlockOptimism = 52000000;
string constant forkRpcUrlOptimism = "https://1rpc.io/op";
Loading
Loading