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 all 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
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;
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.
/// 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 {
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() 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
nkuba marked this conversation as resolved.
Show resolved Hide resolved
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