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

Deposit to ERC4626 Vaults #71

Merged
merged 36 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
dfec9bf
Adding deposit and redeem functionality
dimpar Dec 8, 2023
54a1562
Resolved conflicts with the base branched
dimpar Dec 12, 2023
1166e5e
Drafting integration flows for assets
dimpar Dec 12, 2023
f60a634
Merge remote-tracking branch 'origin' into deposit-redeem-to-vaults
dimpar Dec 13, 2023
1df0676
Allocate tBTC to Yield Modules
dimpar Dec 27, 2023
e6c6222
Adding maintainer modifier and smaller refactorings
dimpar Dec 28, 2023
3cb3bdf
Adding maintainer as a named account
dimpar Dec 28, 2023
1101708
Updating deployment scripts
dimpar Dec 28, 2023
6ec7df5
Updating unit tests and adding some integration tests for assets allo…
dimpar Dec 28, 2023
3837758
Merge remote-tracking branch 'origin' into deposit-redeem-to-vaults
dimpar Dec 28, 2023
fcac792
Resetting approval for the old dispatcher
dimpar Dec 29, 2023
5313aa6
Skipping deployment of TestERC4626 Vault for Mainnet
dimpar Dec 29, 2023
e7f48cb
Renames and smaller refactorings
dimpar Dec 29, 2023
899bb70
Merge remote-tracking branch 'origin' into deposit-redeem-to-vaults
dimpar Dec 29, 2023
10b0806
Slithering
dimpar Dec 29, 2023
ea667ed
Adding TODO to revoke share tokens approval from the old dispatcher …
dimpar Dec 29, 2023
12b9d5f
Simplifying tBTC token approvals by using OZ forceApprove
dimpar Dec 29, 2023
ed7297b
Merge remote-tracking branch 'origin' into deposit-redeem-to-vaults
dimpar Dec 29, 2023
6d0d574
Refactoring skip flag for Mainnet
dimpar Dec 29, 2023
c0d9147
Adding event when updating Dispatcher plus smaller comments
dimpar Dec 29, 2023
c73202e
Updating dispatcher in 'before' instead of 'it'
dimpar Dec 29, 2023
39ff3d7
Refactorings in Dispatcher
dimpar Dec 29, 2023
659556b
Extracting function calls under tests to before clause
dimpar Dec 29, 2023
147f9cc
Adding tests for deployment scripts around dispatcher and maintainer
dimpar Jan 2, 2024
241a504
Extracting common code to before clause
dimpar Jan 2, 2024
591447a
Moving depositToVault tests under Dipatcher.test.ts + smaller cleanups
dimpar Jan 2, 2024
c18d23b
Reorder of the functions in Dispatcher.sol
dimpar Jan 2, 2024
9e6c630
Adding withArgs checks for CustomErrors validations
dimpar Jan 2, 2024
19e7890
Adding NatSpec comments
dimpar Jan 3, 2024
ce9e973
Smaller docs improvements
dimpar Jan 3, 2024
a59d6f4
Removing allowStubs tag check. Not needed.
dimpar Jan 3, 2024
4bd6ca9
Function reorder. External goes above public
dimpar Jan 3, 2024
73ccd1c
getVaults() making public instead of external
dimpar Jan 3, 2024
2ef73ab
Smaller cleanups, renames and adding an additional test
dimpar Jan 3, 2024
324d71e
Merge remote-tracking branch 'origin' into deposit-redeem-to-vaults
dimpar Jan 3, 2024
023f1d0
Cleanups across Solidity contracts and tests
dimpar Jan 3, 2024
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
36 changes: 36 additions & 0 deletions core/contracts/Acre.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Dispatcher.sol";

/// @title Acre
/// @notice This contract implements the ERC-4626 tokenized vault standard. By
Expand All @@ -16,6 +18,10 @@ import "@openzeppelin/contracts/access/Ownable.sol";
/// burning of shares (stBTC), which are represented as standard ERC20
/// tokens, providing a seamless exchange with tBTC tokens.
contract Acre is ERC4626, Ownable {
using SafeERC20 for IERC20;

// Dispatcher contract that routes tBTC from Acre to a given vault and back.
nkuba marked this conversation as resolved.
Show resolved Hide resolved
Dispatcher public dispatcher;
nkuba marked this conversation as resolved.
Show resolved Hide resolved
// Minimum amount for a single deposit operation.
uint256 public minimumDepositAmount;
// Maximum total amount of tBTC token held by Acre.
Expand All @@ -26,8 +32,10 @@ contract Acre is ERC4626, Ownable {
uint256 minimumDepositAmount,
uint256 maximumTotalAssets
);
event DispatcherUpdated(address oldDispatcher, address newDispatcher);

error DepositAmountLessThanMin(uint256 amount, uint256 min);
error ZeroAddress();

constructor(
IERC20 tbtc
Expand Down Expand Up @@ -119,6 +127,34 @@ contract Acre is ERC4626, Ownable {
return shares;
}

// TODO: Implement a governed upgrade process that initiates an update and
// then finalizes it after a delay.
/// @notice Updates the dispatcher contract and gives it an unlimited
/// allowance to transfer staked tBTC.
/// @param newDispatcher Address of the new dispatcher contract.
function updateDispatcher(Dispatcher newDispatcher) external onlyOwner {
if (address(newDispatcher) == address(0)) {
revert ZeroAddress();
}

address oldDispatcher = address(dispatcher);

emit DispatcherUpdated(oldDispatcher, address(newDispatcher));
dispatcher = newDispatcher;

// TODO: Once withdrawal/rebalancing is implemented, we need to revoke the
// approval of the vaults share tokens from the old dispatcher and approve
// a new dispatcher to manage the share tokens.

if (oldDispatcher != address(0)) {
// Setting allowance to zero for the old dispatcher
IERC20(asset()).forceApprove(oldDispatcher, 0);
}

// Setting allowance to max for the new dispatcher
IERC20(asset()).forceApprove(address(dispatcher), type(uint256).max);
}

/// @notice Returns the maximum amount of the tBTC token that can be
/// deposited into the vault for the receiver, through a deposit
/// call. It takes into account the deposit parameter, maximum total
Expand Down
96 changes: 89 additions & 7 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/interfaces/IERC4626.sol";
import "./Router.sol";
import "./Acre.sol";

/// @title Dispatcher
/// @notice Dispatcher is a contract that routes TBTC from stBTC (Acre) to
/// @notice Dispatcher is a contract that routes tBTC from Acre (stBTC) to
/// a given vault and back. Vaults supply yield strategies with TBTC that
nkuba marked this conversation as resolved.
Show resolved Hide resolved
/// generate yield for Bitcoin holders.
contract Dispatcher is Ownable {
error VaultAlreadyAuthorized();
error VaultUnauthorized();
contract Dispatcher is Router, Ownable {
using SafeERC20 for IERC20;

struct VaultInfo {
bool authorized;
}

// Depositing tBTC into Acre returns stBTC.
nkuba marked this conversation as resolved.
Show resolved Hide resolved
Acre public immutable acre;
// tBTC token contract.
IERC20 public immutable tbtc;
// Address of the maintainer bot.
address public maintainer;

/// @notice Authorized Yield Vaults that implement ERC4626 standard. These
/// vaults deposit assets to yield strategies, e.g. Uniswap V3
/// WBTC/TBTC pool. Vault can be a part of Acre ecosystem or can be
Expand All @@ -26,13 +36,34 @@ contract Dispatcher is Ownable {

event VaultAuthorized(address indexed vault);
event VaultDeauthorized(address indexed vault);
event DepositAllocated(
address indexed vault,
uint256 amount,
uint256 sharesOut
);
event MaintainerUpdated(address indexed maintainer);

error VaultAlreadyAuthorized();
error VaultUnauthorized();
error NotMaintainer();
error ZeroAddress();

constructor() Ownable(msg.sender) {}
modifier onlyMaintainer() {
if (msg.sender != maintainer) {
revert NotMaintainer();
}
_;
}

constructor(Acre _acre, IERC20 _tbtc) Ownable(msg.sender) {
acre = _acre;
tbtc = _tbtc;
}

/// @notice Adds a vault to the list of authorized vaults.
/// @param vault Address of the vault to add.
function authorizeVault(address vault) external onlyOwner {
if (vaultsInfo[vault].authorized) {
if (isVaultAuthorized(vault)) {
revert VaultAlreadyAuthorized();
}

Expand All @@ -45,7 +76,7 @@ contract Dispatcher is Ownable {
/// @notice Removes a vault from the list of authorized vaults.
/// @param vault Address of the vault to remove.
function deauthorizeVault(address vault) external onlyOwner {
if (!vaultsInfo[vault].authorized) {
if (!isVaultAuthorized(vault)) {
revert VaultUnauthorized();
}

Expand All @@ -63,7 +94,58 @@ contract Dispatcher is Ownable {
emit VaultDeauthorized(vault);
}

/// @notice Updates the maintainer address.
/// @param newMaintainer Address of the new maintainer.
function updateMaintainer(address newMaintainer) external onlyOwner {
if (newMaintainer == address(0)) {
revert ZeroAddress();
}

maintainer = newMaintainer;

emit MaintainerUpdated(maintainer);
}

/// TODO: make this function internal once the allocation distribution is
/// implemented
/// @notice Routes tBTC from Acre to a vault. Can be called by the maintainer
/// only.
/// @param vault Address of the vault to route the assets to.
/// @param amount Amount of tBTC to deposit.
/// @param minSharesOut Minimum amount of shares to receive by Acre.
function depositToVault(
address vault,
uint256 amount,
uint256 minSharesOut
) public onlyMaintainer {
nkuba marked this conversation as resolved.
Show resolved Hide resolved
if (!isVaultAuthorized(vault)) {
revert VaultUnauthorized();
}

// slither-disable-next-line arbitrary-send-erc20
tbtc.safeTransferFrom(address(acre), address(this), amount);
tbtc.forceApprove(address(vault), amount);

uint256 sharesOut = deposit(
IERC4626(vault),
address(acre),
amount,
minSharesOut
);
// slither-disable-next-line reentrancy-events
emit DepositAllocated(vault, amount, sharesOut);
}

/// @notice Returns the list of authorized vaults.
function getVaults() external view returns (address[] memory) {
return vaults;
}

/// @notice Returns true if the vault is authorized.
/// @param vault Address of the vault to check.
function isVaultAuthorized(address vault) internal view returns (bool) {
nkuba marked this conversation as resolved.
Show resolved Hide resolved
return vaultsInfo[vault].authorized;
}

/// TODO: implement redeem() / withdraw() functions
}
34 changes: 34 additions & 0 deletions core/contracts/Router.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/interfaces/IERC20.sol";
r-czajkowski marked this conversation as resolved.
Show resolved Hide resolved
import "@openzeppelin/contracts/interfaces/IERC4626.sol";

/// @title Router
/// @notice Router is a contract that routes tBTC from stBTC (Acre) to
/// a given vault and back. Vaults supply yield strategies with tBTC that
/// generate yield for Bitcoin holders.
abstract contract Router {
using SafeERC20 for IERC20;
r-czajkowski marked this conversation as resolved.
Show resolved Hide resolved

/// @notice thrown when amount of shares received is below the min set by caller
error MinSharesError();
nkuba marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Routes funds from stBTC (Acre) to a vault. The amount of tBTC to
/// Shares of deposited tBTC are minted to the stBTC contract.
/// @param vault Address of the vault to route the funds to.
/// @param receiver Address of the receiver of the shares.
/// @param amount Amount of tBTC to deposit.
/// @param minSharesOut Minimum amount of shares to receive.
function deposit(
IERC4626 vault,
address receiver,
uint256 amount,
uint256 minSharesOut
) internal returns (uint256 sharesOut) {
if ((sharesOut = vault.deposit(amount, receiver)) < minSharesOut) {
revert MinSharesError();
}
}
}
12 changes: 12 additions & 0 deletions core/contracts/test/TestERC4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

contract TestERC4626 is ERC4626 {
constructor(
IERC20 asset,
string memory tokenName,
string memory tokenSymbol
) ERC4626(asset) ERC20(tokenName, tokenSymbol) {}
}
30 changes: 30 additions & 0 deletions core/deploy/00_resolve_testing_erc4626.ts
nkuba marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
const { log } = deployments
const { deployer } = await getNamedAccounts()

const tBTC = await deployments.get("TBTC")

if (hre.network.tags.allowStubs) {
nkuba marked this conversation as resolved.
Show resolved Hide resolved
log("deploying Mock ERC4626 Vault")

await deployments.deploy("Vault", {
contract: "TestERC4626",
from: deployer,
args: [tBTC.address, "MockVault", "MV"],
log: true,
waitConfirmations: 1,
})
}
}

export default func

func.tags = ["TestERC4626"]
func.dependencies = ["TBTC"]

func.skip = async (hre: HardhatRuntimeEnvironment): Promise<boolean> =>
hre.network.name === "mainnet"
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
const { deployer } = await getNamedAccounts()

const tbtc = await deployments.get("TBTC")
const acre = await deployments.get("Acre")

await deployments.deploy("Dispatcher", {
from: deployer,
args: [],
args: [acre.address, tbtc.address],
log: true,
waitConfirmations: 1,
})
Expand Down
21 changes: 21 additions & 0 deletions core/deploy/11_acre_update_dispatcher.ts
r-czajkowski marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
const { deployer } = await getNamedAccounts()

const dispatcher = await deployments.get("Dispatcher")

await deployments.execute(
"Acre",
{ from: deployer, log: true, waitConfirmations: 1 },
"updateDispatcher",
dispatcher.address,
)
}

export default func

func.tags = ["AcreUpdateDispatcher"]
func.dependencies = ["Acre", "Dispatcher"]
19 changes: 19 additions & 0 deletions core/deploy/12_dispatcher_update_maintainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
const { deployer, maintainer } = await getNamedAccounts()

await deployments.execute(
"Dispatcher",
{ from: deployer, log: true, waitConfirmations: 1 },
"updateMaintainer",
maintainer,
)
}

export default func

func.tags = ["DispatcherUpdateMaintainer"]
func.dependencies = ["Dispatcher"]
9 changes: 7 additions & 2 deletions core/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,13 @@ const config: HardhatUserConfig = {
},
governance: {
default: 2,
sepolia: 0,
mainnet: "",
sepolia: 0, // TODO: updated to the actual address once available
mainnet: "", // TODO: updated to the actual address once available
},
maintainer: {
default: 3,
sepolia: 0, // TODO: updated to the actual address once available
mainnet: "", // TODO: updated to the actual address once available
},
},

Expand Down
Loading
Loading