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

[WIP] Dispatcher #69

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 31 additions & 2 deletions core/contracts/Acre.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
pragma solidity ^0.8.21;

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

import "./Dispatcher.sol";

/// @title Acre
/// @notice This contract implements the ERC-4626 tokenized vault standard. By
Expand All @@ -14,12 +18,19 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
/// of yield-bearing vaults. This contract facilitates the minting and
/// burning of shares (stBTC), which are represented as standard ERC20
/// tokens, providing a seamless exchange with tBTC tokens.
contract Acre is ERC4626 {
contract Acre is ERC4626, Ownable {
using SafeERC20 for IERC20;

error CallerNotDispatcher();

event StakeReferral(bytes32 indexed referral, uint256 assets);

Dispatcher public dispatcher;

constructor(
IERC20 tbtc
) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") {}
) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") Ownable(msg.sender) {}


/// @notice Stakes a given amount of tBTC token and mints shares to a
/// receiver.
Expand All @@ -43,4 +54,22 @@ contract Acre is ERC4626 {

return shares;
}

function upgradeDispatcher(Dispatcher _newDispatcher) public onlyOwner {
if (address(dispatcher) != address(0)) {
IERC20(asset()).approve(address(dispatcher), 0);
// TODO: Remove dispatcher's approvals for the vaults.
}

dispatcher = _newDispatcher;

IERC20(asset()).approve(address(dispatcher), type(uint256).max);
}

function approveVaultSharesForDispatcher(address vault, uint256 amount) external {
if (msg.sender != address(dispatcher)) revert CallerNotDispatcher();

// TODO: Emit event
IERC20(vault).safeIncreaseAllowance(address(dispatcher), amount);
}
}
77 changes: 72 additions & 5 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,28 @@
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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
/// generate yield for Bitcoin holders.
contract Dispatcher is Ownable {
contract Dispatcher is Router, Ownable {
using SafeERC20 for IERC20;


error VaultAlreadyAuthorized();
error VaultUnauthorized();

struct VaultInfo {
bool authorized;
}

Acre acre;
IERC20 tbtc;

/// @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 @@ -27,7 +36,10 @@ contract Dispatcher is Ownable {
event VaultAuthorized(address indexed vault);
event VaultDeauthorized(address indexed vault);

constructor() Ownable(msg.sender) {}
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.
Expand All @@ -39,13 +51,15 @@ contract Dispatcher is Ownable {
vaults.push(vault);
vaultsInfo[vault].authorized = true;

acre.approveVaultSharesForDispatcher(vault, type(uint256).max);

emit VaultAuthorized(vault);
}

/// @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 @@ -60,10 +74,63 @@ contract Dispatcher is Ownable {
}
}

acre.approveVaultSharesForDispatcher(vault, 0);

emit VaultDeauthorized(vault);
}

function isVaultAuthorized(address vault) public view returns (bool){
return vaultsInfo[vault].authorized;
}

function getVaults() external view returns (address[] memory) {
return vaults;
}


// TODO: Add access restriction
function depositToVault(
IERC4626 vault,
uint256 amount,
uint256 minSharesOut
) public returns (uint256 sharesOut) {
if (!isVaultAuthorized(address(vault))) {
revert VaultUnauthorized();
}

require(vault.asset() == address(tbtc), "vault asset is not tbtc");

IERC20(tbtc).safeTransferFrom(address(acre), address(this), amount);
IERC20(tbtc).approve(address(vault), amount);

Router.deposit(vault, address(acre), amount, minSharesOut);
}

// TODO: Add access restriction
function withdrawFromVault(
IERC4626 vault,
uint256 amount,
uint256 maxSharesOut
) public returns (uint256 sharesOut) {
uint256 shares = vault.previewWithdraw(amount);

IERC20(vault).safeTransferFrom(address(acre), address(this), shares);
IERC20(vault).approve(address(vault), shares);

Router.withdraw(vault, address(acre), amount, maxSharesOut);
}

// TODO: Add access restriction
function redeemFromVault(
IERC4626 vault,
uint256 shares,
uint256 minAmountOut
) public returns (uint256 amountOut) {
IERC20(vault).safeTransferFrom(address(acre), address(this), shares);
IERC20(vault).approve(address(vault), shares);

Router.redeem(vault, address(acre), shares, minAmountOut);
}

// TODO: Add function to withdrawMax
}
70 changes: 70 additions & 0 deletions core/contracts/Router.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/interfaces/IERC20.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";


// TODO: Consider deploying ERC4626RouterBase from the ERC4626 Alliance.
// TODO: Think about adding reentrancy guard
// TODO: Add ACL
abstract contract Router {
using SafeERC20 for IERC20;

/// @notice thrown when amount of assets received is below the min set by caller
error MinAmountError();

/// @notice thrown when amount of shares received is below the min set by caller
error MinSharesError();

/// @notice thrown when amount of assets received is above the max set by caller
error MaxAmountError();

/// @notice thrown when amount of shares received is above the max set by caller
error MaxSharesError();

// Copied from ERC4626RouterBase
// Differences:
// - internal instead of public
function deposit(
IERC4626 vault,
address to,
uint256 amount,
uint256 minSharesOut
) internal virtual returns (uint256 sharesOut) {
if ((sharesOut = vault.deposit(amount, to)) < minSharesOut) {
revert MinSharesError();
}
}

// Copied from ERC4626RouterBase
// Difference:
// - internal instead of public
// - use address(this) as owner instead of msg.sender
function withdraw(
IERC4626 vault,
address to,
uint256 amount,
uint256 maxSharesOut
) internal virtual returns (uint256 sharesOut) {
if ((sharesOut = vault.withdraw(amount, to, address(this))) > maxSharesOut) {
revert MaxSharesError();
}
}

// Copied from ERC4626RouterBase
// Difference:
// - internal instead of public
// - use address(this) as owner instead of msg.sender
function redeem(
IERC4626 vault,
address to,
uint256 shares,
uint256 minAmountOut
) internal virtual returns (uint256 amountOut) {
if ((amountOut = vault.redeem(shares, to, address(this))) < minAmountOut) {
revert MinAmountError();
}
}
}
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) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,31 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
const { deployer } = await getNamedAccounts()

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

await deployments.deploy("Dispatcher", {
from: deployer,
args: [],
args: [acre.address, tbtc.address],
log: true,
waitConfirmations: 1,
})

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

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

// TODO: Add Etherscan verification
// TODO: Add Tenderly verification
}

export default func

func.tags = ["Dispatcher"]
func.dependencies = ["Acre"]
func.dependencies = ["Acre", "TBTC"]
2 changes: 0 additions & 2 deletions core/deploy/21_transfer_ownership_acre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,3 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
export default func

func.tags = ["TransferOwnershipAcre"]
// TODO: Enable once Acre extends Ownable
func.skip = async () => true
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployer, governance } = await getNamedAccounts()
const { log } = deployments

log(`transferring ownership of Dispatcher contract to ${governance}`)
log(`transferring ownership of AcreRouter contract to ${governance}`)

await deployments.execute(
"Dispatcher",
Expand All @@ -18,5 +18,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {

export default func

func.tags = ["TransferOwnershipAcreRouter"]
func.tags = ["TransferOwnershipDispatcher"]
func.dependencies = ["Dispatcher"]
Loading
Loading