Skip to content

Commit

Permalink
Merge branch 'main' of github.com:thesis/acre into stake-form
Browse files Browse the repository at this point in the history
  • Loading branch information
kkosiorowska committed Jan 4, 2024
2 parents 8c8d2a1 + 4648f6b commit acda15a
Show file tree
Hide file tree
Showing 25 changed files with 816 additions and 118 deletions.
33 changes: 20 additions & 13 deletions .github/workflows/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defaults:
working-directory: ./core

jobs:
core-format:
core-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -30,10 +30,20 @@ jobs:
- name: Install Dependencies
run: pnpm install --prefer-offline --frozen-lockfile

- name: Format
run: pnpm run format
- name: Build
run: pnpm run build

core-build:
- name: Upload Build Artifacts
uses: actions/upload-artifact@v3
with:
name: core-build
path: |
core/build/
core/typechain/
if-no-files-found: error

core-format:
needs: [core-build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -50,17 +60,14 @@ jobs:
- name: Install Dependencies
run: pnpm install --prefer-offline --frozen-lockfile

- name: Build
run: pnpm run build

- name: Upload Build Artifacts
uses: actions/upload-artifact@v3
- name: Download Build Artifacts
uses: actions/download-artifact@v3
with:
name: core-build
path: |
core/build/
core/typechain/
if-no-files-found: error
path: core/

- name: Format
run: pnpm run format

core-slither:
needs: [core-build]
Expand Down
16 changes: 15 additions & 1 deletion core/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,19 @@
]
}
]
}
},
"overrides": [
{
"files": ["deploy/*.ts"],
"rules": {
"@typescript-eslint/unbound-method": "off"
}
},
{
"files": ["*.test.ts"],
"rules": {
"@typescript-eslint/no-unused-expressions": "off"
}
}
]
}
1 change: 1 addition & 0 deletions core/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ build/
cache/
export.json
export/
gen/
typechain/
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) {}
}
Loading

0 comments on commit acda15a

Please sign in to comment.