Skip to content

Commit

Permalink
Deposit to ERC4626 Vaults (#71)
Browse files Browse the repository at this point in the history
- Added allocation functionality that deposits a single asset (tBTC) to
an authorized vault
- Added and adjusted deployment scripts
- Changed unit tests and added an integration test that validates the
flow of assets from Acre contract through Dispatcher that deposits
assets to an authorized ERC4626 Vault
  • Loading branch information
nkuba authored Jan 3, 2024
2 parents d3556ac + 023f1d0 commit 6463c79
Show file tree
Hide file tree
Showing 13 changed files with 635 additions and 62 deletions.
56 changes: 54 additions & 2 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,19 +18,41 @@ 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 {
// Minimum amount for a single deposit operation.
using SafeERC20 for IERC20;

/// Dispatcher contract that routes tBTC from Acre to a given vault and back.
Dispatcher public dispatcher;
/// Minimum amount for a single deposit operation.
uint256 public minimumDepositAmount;
// Maximum total amount of tBTC token held by Acre.
/// Maximum total amount of tBTC token held by Acre.
uint256 public maximumTotalAssets;

/// Emitted when a referral is used.
/// @param referral Used for referral program.
/// @param assets Amount of tBTC tokens staked.
event StakeReferral(bytes32 indexed referral, uint256 assets);

/// Emitted when deposit parameters are updated.
/// @param minimumDepositAmount New value of the minimum deposit amount.
/// @param maximumTotalAssets New value of the maximum total assets amount.
event DepositParametersUpdated(
uint256 minimumDepositAmount,
uint256 maximumTotalAssets
);

/// Emitted when the dispatcher contract is updated.
/// @param oldDispatcher Address of the old dispatcher contract.
/// @param newDispatcher Address of the new dispatcher contract.
event DispatcherUpdated(address oldDispatcher, address newDispatcher);

/// Reverts if the amount is less than the minimum deposit amount.
/// @param amount Amount to check.
/// @param min Minimum amount to check 'amount' against.
error DepositAmountLessThanMin(uint256 amount, uint256 min);

/// Reverts if the address is zero.
error ZeroAddress();

constructor(
IERC20 tbtc
) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") Ownable(msg.sender) {
Expand Down Expand Up @@ -59,6 +83,34 @@ contract Acre is ERC4626, Ownable {
);
}

// 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 Mints shares to receiver by depositing exactly amount of
/// tBTC tokens.
/// @dev Takes into account a deposit parameter, minimum deposit amount,
Expand Down
135 changes: 120 additions & 15 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,91 @@
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
/// a given vault and back. Vaults supply yield strategies with TBTC that
/// @notice Dispatcher is a contract that routes tBTC from Acre (stBTC) to
/// yield vaults and back. Vaults supply yield strategies with tBTC that
/// generate yield for Bitcoin holders.
contract Dispatcher is Ownable {
error VaultAlreadyAuthorized();
error VaultUnauthorized();
contract Dispatcher is Router, Ownable {
using SafeERC20 for IERC20;

/// Struct holds information about a vault.
struct VaultInfo {
bool authorized;
}

/// @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
/// implemented externally. As long as it complies with ERC4626
/// standard and is authorized by the owner it can be plugged into
/// Acre.
/// The main Acre contract holding tBTC deposited by stakers.
Acre public immutable acre;
/// tBTC token contract.
IERC20 public immutable tbtc;
/// Address of the maintainer bot.
address public maintainer;

/// 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
/// implemented externally. As long as it complies with ERC4626
/// standard and is authorized by the owner it can be plugged into
/// Acre.
address[] public vaults;
/// Mapping of vaults to their information.
mapping(address => VaultInfo) public vaultsInfo;

/// Emitted when a vault is authorized.
/// @param vault Address of the vault.
event VaultAuthorized(address indexed vault);

/// Emitted when a vault is deauthorized.
/// @param vault Address of the vault.
event VaultDeauthorized(address indexed vault);

constructor() Ownable(msg.sender) {}
/// Emitted when tBTC is routed to a vault.
/// @param vault Address of the vault.
/// @param amount Amount of tBTC.
/// @param sharesOut Amount of shares received by Acre.
event DepositAllocated(
address indexed vault,
uint256 amount,
uint256 sharesOut
);

/// Emitted when the maintainer address is updated.
/// @param maintainer Address of the new maintainer.
event MaintainerUpdated(address indexed maintainer);

/// Reverts if the vault is already authorized.
error VaultAlreadyAuthorized();

/// Reverts if the vault is not authorized.
error VaultUnauthorized();

/// Reverts if the caller is not the maintainer.
error NotMaintainer();

/// Reverts if the address is zero.
error ZeroAddress();

/// Modifier that reverts if the caller is not the maintainer.
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 +99,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 +117,58 @@ contract Dispatcher is Ownable {
emit VaultDeauthorized(vault);
}

function getVaults() external view returns (address[] memory) {
/// @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 {
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() public 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) public view returns (bool) {
return vaultsInfo[vault].authorized;
}

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

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 {
/// Thrown when amount of shares received is below the min set by caller.
/// @param vault Address of the vault.
/// @param sharesOut Amount of shares received by Acre.
/// @param minSharesOut Minimum amount of shares expected to receive.
error MinSharesError(
address vault,
uint256 sharesOut,
uint256 minSharesOut
);

/// @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(address(vault), sharesOut, minSharesOut);
}
}
}
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) {}
}
27 changes: 27 additions & 0 deletions core/deploy/00_resolve_testing_erc4626.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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")

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
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"]
Loading

0 comments on commit 6463c79

Please sign in to comment.