Skip to content

Commit

Permalink
Add allocations to the Dispatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
nkuba committed Dec 14, 2023
1 parent 03b5784 commit cf57c89
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 229 deletions.
8 changes: 7 additions & 1 deletion core/contracts/Acre.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract Acre is ERC4626, Ownable {
using SafeERC20 for IERC20;

error CallerNotDispatcher();

event StakeReferral(bytes32 indexed referral, uint256 assets);

Dispatcher public dispatcher;
Expand All @@ -31,6 +31,12 @@ contract Acre is ERC4626, Ownable {
IERC20 tbtc
) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") Ownable(msg.sender) {}

/// @notice Returns the total amount of the underlying asset that is “managed”
/// by Acre.
function totalAssets() public view virtual override returns (uint256) {
return
IERC20(asset()).balanceOf(address(this)) + dispatcher.totalAssets();
}

/// @notice Stakes a given amount of tBTC token and mints shares to a
/// receiver.
Expand Down
142 changes: 118 additions & 24 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,46 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
import "./Router.sol";
import "./Acre.sol";

// @notice Interface the Vaults connected to the Dispatcher contract should
// implement.
interface IVault is IERC4626 {}

/// a given vault and back. Vaults supply yield strategies with TBTC that
/// generate yield for Bitcoin holders.
contract Dispatcher is Router, Ownable {
using SafeERC20 for IERC20;

error VaultAlreadyAuthorized();
error VaultUnauthorized();
error InvalidVaultsWeight(uint16 vaultsWeight, uint16 vaultsTotalWeight);
error TotalAmountZero();

struct VaultInfo {
bool authorized;
uint16 weight;
}

Acre acre;
IERC20 tbtc;
Acre public acre;
IERC20 public tbtc;

uint16 public vaultsTotalWeight = 1000;

/// @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.
address[] public vaults;
mapping(address => VaultInfo) public vaultsInfo;
IVault[] public vaults;
mapping(IVault => VaultInfo) public vaultsInfo;

event VaultAuthorized(address indexed vault);
event VaultDeauthorized(address indexed vault);
event VaultWeightUpdated(
address indexed vault,
uint16 newWeight,
uint16 oldWeight
);

constructor(Acre _acre, IERC20 _tbtc) Ownable(msg.sender) {
acre = _acre;
Expand All @@ -42,25 +56,25 @@ contract Dispatcher is Router, Ownable {

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

vaults.push(vault);
vaultsInfo[vault].authorized = true;

emit VaultAuthorized(vault);
emit VaultAuthorized(address(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 {
function deauthorizeVault(IVault vault) external onlyOwner {
if (!isVaultAuthorized(vault)) {
revert VaultUnauthorized();
}

vaultsInfo[vault].authorized = false;
delete vaultsInfo[vault];

for (uint256 i = 0; i < vaults.length; i++) {
if (vaults[i] == vault) {
Expand All @@ -71,25 +85,43 @@ contract Dispatcher is Router, Ownable {
}
}

emit VaultDeauthorized(vault);
emit VaultDeauthorized(address(vault));
}

function setVaultWeights(
IVault[] memory vaultsToSet,
uint16[] memory newWeights
) external onlyOwner {
for (uint256 i = 0; i < vaultsToSet.length; i++) {
IVault vault = vaultsToSet[i];
uint16 newWeight = newWeights[i];

if (newWeight > 0 && !isVaultAuthorized(vault)) {
revert VaultUnauthorized();
}

uint16 oldWeight = vaultsInfo[vault].weight;
vaultsInfo[vault].weight = newWeight;

emit VaultWeightUpdated(address(vault), newWeight, oldWeight);
}
}

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

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


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

Expand All @@ -98,25 +130,25 @@ contract Dispatcher is Router, Ownable {
IERC20(tbtc).safeTransferFrom(address(acre), address(this), amount);
IERC20(tbtc).approve(address(vault), amount);

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

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

IERC20(vault).approve(address(vault), shares);

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

// TODO: Add access restriction
// TODO: Add access restriction
function redeemFromVault(
IERC4626 vault,
IVault vault,
uint256 shares,
uint256 minAmountOut
) public returns (uint256 amountOut) {
Expand All @@ -128,11 +160,73 @@ contract Dispatcher is Router, Ownable {
// TODO: Add function to withdrawMax

// TODO: Check possibilities of Dispatcher upgrades and shares migration.
function migrateShares(IERC4626[] calldata _vaults) public onlyOwner {
function migrateShares(IVault[] calldata _vaults) public onlyOwner {
address newDispatcher = address(acre.dispatcher());

for (uint i=0; i<_vaults.length; i++) {
_vaults[i].transfer(newDispatcher, _vaults[i].balanceOf(address(this)));
for (uint i = 0; i < _vaults.length; i++) {
_vaults[i].transfer(
newDispatcher,
_vaults[i].balanceOf(address(this))
);
}
}

function vaultsWeight() internal view returns (uint16 totalWeight) {
for (uint256 i = 0; i < vaults.length; i++) {
totalWeight += vaultsInfo[vaults[i]].weight;
}
}

function totalAssets() public view returns (uint256 totalAmount) {
// Balance deployed in Vaults.
for (uint256 i = 0; i < vaults.length; i++) {
IVault vault = IVault(vaults[i]);
totalAmount += vault.convertToAssets(
vault.balanceOf(address(this))
);
}

// Unused balance in Dispatcher.
// TODO: It is not expected the Dispatcher will hold any tBTC, we should
// add a function that would sweep tBTC from Dispatcher to Acre contract.
totalAmount += tbtc.balanceOf(address(this));
}

// TODO: This solution expects all tBTC to be withdrawn from all the Vaults before
// allocation. We may need improved solution to calculate exactly how much
// tBTC should be deposited or withdrawn from each vault.
// TODO: Make callable only by the maintainer bot.
// TODO: Add pre-calculated minSharesOut values for each deposit.
// TODO: Consider having constant total weight, e.g. 1000, so the vaults can
// have
function allocate() public {
uint16 vaultsWeight = vaultsWeight();
if (
vaultsTotalWeight == 0 ||
vaultsWeight == 0 ||
vaultsWeight > vaultsTotalWeight
) revert InvalidVaultsWeight(vaultsWeight, vaultsTotalWeight);

// tBTC held by Dispatcher and registered Vaults.
uint256 totalAmount = totalAssets();

// Unallocated tBTC in the Acre contract.
totalAmount += tbtc.balanceOf(address(acre));
if (totalAmount == 0) revert TotalAmountZero();

for (uint256 i = 0; i < vaults.length; i++) {
IVault vault = vaults[i];

uint256 vaultAmount = (totalAmount * vaultsInfo[vault].weight) /
vaultsTotalWeight;
if (vaultAmount == 0) continue;

// TODO: Pre-calculate the minSharesOut value off-chain as a slippage protection
// before calling the allocate function.
uint256 minSharesOut = vault.previewDeposit(vaultAmount);

// Allocate tBTC to Vault.
depositToVault(vault, vaultAmount, minSharesOut);
}
}
}
10 changes: 7 additions & 3 deletions core/contracts/Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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
Expand Down Expand Up @@ -48,7 +47,10 @@ abstract contract Router {
uint256 amount,
uint256 maxSharesOut
) internal virtual returns (uint256 sharesOut) {
if ((sharesOut = vault.withdraw(amount, to, address(this))) > maxSharesOut) {
if (
(sharesOut = vault.withdraw(amount, to, address(this))) >
maxSharesOut
) {
revert MaxSharesError();
}
}
Expand All @@ -63,7 +65,9 @@ abstract contract Router {
uint256 shares,
uint256 minAmountOut
) internal virtual returns (uint256 amountOut) {
if ((amountOut = vault.redeem(shares, to, address(this))) < minAmountOut) {
if (
(amountOut = vault.redeem(shares, to, address(this))) < minAmountOut
) {
revert MinAmountError();
}
}
Expand Down
Loading

0 comments on commit cf57c89

Please sign in to comment.