Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into stake-rename
Browse files Browse the repository at this point in the history
  • Loading branch information
nkuba committed Apr 10, 2024
2 parents c6acdce + e94a4ff commit fd154c0
Show file tree
Hide file tree
Showing 18 changed files with 2,574 additions and 43 deletions.
1 change: 1 addition & 0 deletions core/.solhintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
contracts/test/
243 changes: 243 additions & 0 deletions core/contracts/MezoAllocator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ZeroAddress} from "./utils/Errors.sol";
import "./stBTC.sol";
import "./interfaces/IDispatcher.sol";

/// @title IMezoPortal
/// @dev Interface for the Mezo's Portal contract.
interface IMezoPortal {
/// @notice DepositInfo keeps track of the deposit balance and unlock time.
/// Each deposit is tracked separately and associated with a specific
/// token. Some tokens can be deposited but can not be locked - in
/// that case the unlockAt is the block timestamp of when the deposit
/// was created. The same is true for tokens that can be locked but
/// the depositor decided not to lock them.
struct DepositInfo {
uint96 balance;
uint32 unlockAt;
}

/// @notice Deposit and optionally lock tokens for the given period.
/// @dev Lock period will be normalized to weeks. If non-zero, it must not
/// be shorter than the minimum lock period and must not be longer than
/// the maximum lock period.
/// @param token token address to deposit
/// @param amount amount of tokens to deposit
/// @param lockPeriod lock period in seconds, 0 to not lock the deposit
function deposit(address token, uint96 amount, uint32 lockPeriod) external;

/// @notice Withdraw deposited tokens.
/// Deposited lockable tokens can be withdrawn at any time if
/// there is no lock set on the deposit or the lock period has passed.
/// There is no way to withdraw locked deposit. Tokens that are not
/// lockable can be withdrawn at any time. Deposit can be withdrawn
/// partially.
/// @param token deposited token address
/// @param depositId id of the deposit
/// @param amount amount of the token to be withdrawn from the deposit
function withdraw(address token, uint256 depositId, uint96 amount) external;

/// @notice The number of deposits created. Includes the deposits that
/// were fully withdrawn. This is also the identifier of the most
/// recently created deposit.
function depositCount() external view returns (uint256);

/// @notice Get the balance and unlock time of a given deposit.
/// @param depositor depositor address
/// @param token token address to get the balance
/// @param depositId id of the deposit
function getDeposit(
address depositor,
address token,
uint256 depositId
) external view returns (DepositInfo memory);
}

/// @notice MezoAllocator routes tBTC to/from MezoPortal.
contract MezoAllocator is IDispatcher, Ownable2Step {
using SafeERC20 for IERC20;

/// @notice Address of the MezoPortal contract.
IMezoPortal public immutable mezoPortal;
/// @notice tBTC token contract.
IERC20 public immutable tbtc;
/// @notice stBTC token vault contract.
stBTC public immutable stbtc;
/// @notice Keeps track of the addresses that are allowed to trigger deposit
/// allocations.
mapping(address => bool) public isMaintainer;
/// @notice List of maintainers.
address[] public maintainers;
/// @notice keeps track of the latest deposit ID assigned in Mezo Portal.
uint256 public depositId;
/// @notice Keeps track of the total amount of tBTC allocated to MezoPortal.
uint96 public depositBalance;

/// @notice Emitted when tBTC is deposited to MezoPortal.
event DepositAllocated(
uint256 indexed oldDepositId,
uint256 indexed newDepositId,
uint256 addedAmount,
uint256 newDepositAmount
);
/// @notice Emitted when tBTC is withdrawn from MezoPortal.
event DepositWithdrawn(uint256 indexed depositId, uint256 amount);
/// @notice Emitted when the maintainer address is updated.
event MaintainerAdded(address indexed maintainer);
/// @notice Emitted when the maintainer address is updated.
event MaintainerRemoved(address indexed maintainer);
/// @notice Emitted when tBTC is released from MezoPortal.
event DepositReleased(uint256 indexed depositId, uint256 amount);
/// @notice Reverts if the caller is not a maintainer.
error CallerNotMaintainer();
/// @notice Reverts if the caller is not the stBTC contract.
error CallerNotStbtc();
/// @notice Reverts if the maintainer is not registered.
error MaintainerNotRegistered();
/// @notice Reverts if the maintainer has been already registered.
error MaintainerAlreadyRegistered();

modifier onlyMaintainer() {
if (!isMaintainer[msg.sender]) {
revert CallerNotMaintainer();
}
_;
}

/// @notice Initializes the MezoAllocator contract.
/// @param _mezoPortal Address of the MezoPortal contract.
/// @param _tbtc Address of the tBTC token contract.
constructor(
address _mezoPortal,
IERC20 _tbtc,
stBTC _stbtc
) Ownable(msg.sender) {
if (_mezoPortal == address(0)) {
revert ZeroAddress();
}
if (address(_tbtc) == address(0)) {
revert ZeroAddress();
}
if (address(_stbtc) == address(0)) {
revert ZeroAddress();
}
mezoPortal = IMezoPortal(_mezoPortal);
tbtc = _tbtc;
stbtc = _stbtc;
}

/// @notice Allocate tBTC to MezoPortal. Each allocation creates a new "rolling"
/// deposit meaning that the previous Acre's deposit is fully withdrawn
/// before a new deposit with added amount is created. This mimics a
/// "top up" functionality with the difference that a new deposit id
/// is created and the previous deposit id is no longer in use.
/// @dev This function can be invoked periodically by a maintainer.
function allocate() external onlyMaintainer {
if (depositBalance > 0) {
// Free all Acre's tBTC from MezoPortal before creating a new deposit.
// slither-disable-next-line reentrancy-no-eth
mezoPortal.withdraw(address(tbtc), depositId, depositBalance);
}

// Fetch unallocated tBTC from stBTC contract.
uint256 addedAmount = tbtc.balanceOf(address(stbtc));
// slither-disable-next-line arbitrary-send-erc20
tbtc.safeTransferFrom(address(stbtc), address(this), addedAmount);

// Create a new deposit in the MezoPortal.
depositBalance = uint96(tbtc.balanceOf(address(this)));
tbtc.forceApprove(address(mezoPortal), depositBalance);
// 0 denotes no lock period for this deposit.
mezoPortal.deposit(address(tbtc), depositBalance, 0);
uint256 oldDepositId = depositId;
// MezoPortal doesn't return depositId, so we have to read depositCounter
// which assigns depositId to the current deposit.
depositId = mezoPortal.depositCount();

// slither-disable-next-line reentrancy-events
emit DepositAllocated(
oldDepositId,
depositId,
addedAmount,
depositBalance
);
}

/// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC.
/// This function can withdraw partial or a full amount of tBTC from
/// MezoPortal for a given deposit id.
/// @param amount Amount of tBTC to withdraw.
function withdraw(uint256 amount) external {
if (msg.sender != address(stbtc)) revert CallerNotStbtc();

emit DepositWithdrawn(depositId, amount);
mezoPortal.withdraw(address(tbtc), depositId, uint96(amount));
// slither-disable-next-line reentrancy-benign
depositBalance -= uint96(amount);
tbtc.safeTransfer(address(stbtc), amount);
}

/// @notice Releases deposit in full from MezoPortal.
/// @dev This is a special function that can be used to migrate funds during
/// allocator upgrade or in case of emergencies.
function releaseDeposit() external onlyOwner {
uint96 amount = mezoPortal
.getDeposit(address(this), address(tbtc), depositId)
.balance;

emit DepositReleased(depositId, amount);
depositBalance = 0;
mezoPortal.withdraw(address(tbtc), depositId, amount);
tbtc.safeTransfer(address(stbtc), tbtc.balanceOf(address(this)));
}

/// @notice Updates the maintainer address.
/// @param maintainerToAdd Address of the new maintainer.
function addMaintainer(address maintainerToAdd) external onlyOwner {
if (maintainerToAdd == address(0)) {
revert ZeroAddress();
}
if (isMaintainer[maintainerToAdd]) {
revert MaintainerAlreadyRegistered();
}
maintainers.push(maintainerToAdd);
isMaintainer[maintainerToAdd] = true;

emit MaintainerAdded(maintainerToAdd);
}

/// @notice Removes the maintainer address.
/// @param maintainerToRemove Address of the maintainer to remove.
function removeMaintainer(address maintainerToRemove) external onlyOwner {
if (!isMaintainer[maintainerToRemove]) {
revert MaintainerNotRegistered();
}
delete (isMaintainer[maintainerToRemove]);

for (uint256 i = 0; i < maintainers.length; i++) {
if (maintainers[i] == maintainerToRemove) {
maintainers[i] = maintainers[maintainers.length - 1];
// slither-disable-next-line costly-loop
maintainers.pop();
break;
}
}

emit MaintainerRemoved(maintainerToRemove);
}

/// @notice Returns the total amount of tBTC allocated to MezoPortal.
function totalAssets() external view returns (uint256) {
return depositBalance;
}

/// @notice Returns the list of maintainers.
function getMaintainers() external view returns (address[] memory) {
return maintainers;
}
}
12 changes: 12 additions & 0 deletions core/contracts/interfaces/IDispatcher.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;

/// @title IDispatcher
/// @notice Interface for the Dispatcher contract.
interface IDispatcher {
/// @notice Withdraw assets from the Dispatcher.
function withdraw(uint256 amount) external;

/// @notice Returns the total amount of assets held by the Dispatcher.
function totalAssets() external view returns (uint256);
}
39 changes: 36 additions & 3 deletions core/contracts/stBTC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol";
import "./Dispatcher.sol";
import "./PausableOwnable.sol";
import "./lib/ERC4626Fees.sol";
import "./interfaces/IDispatcher.sol";
import {ZeroAddress} from "./utils/Errors.sol";

/// @title stBTC
Expand All @@ -24,8 +25,9 @@ import {ZeroAddress} from "./utils/Errors.sol";
contract stBTC is ERC4626Fees, PausableOwnable {
using SafeERC20 for IERC20;

/// Dispatcher contract that routes tBTC from stBTC to a given vault and back.
Dispatcher public dispatcher;
/// Dispatcher contract that routes tBTC from stBTC to a given allocation
/// contract and back.
IDispatcher public dispatcher;

/// Address of the treasury wallet, where fees should be transferred to.
address public treasury;
Expand Down Expand Up @@ -123,7 +125,7 @@ contract stBTC is ERC4626Fees, PausableOwnable {
/// @notice Updates the dispatcher contract and gives it an unlimited
/// allowance to transfer deposited tBTC.
/// @param newDispatcher Address of the new dispatcher contract.
function updateDispatcher(Dispatcher newDispatcher) external onlyOwner {
function updateDispatcher(IDispatcher newDispatcher) external onlyOwner {
if (address(newDispatcher) == address(0)) {
revert ZeroAddress();
}
Expand Down Expand Up @@ -162,6 +164,13 @@ contract stBTC is ERC4626Fees, PausableOwnable {
emit ExitFeeBasisPointsUpdated(newExitFeeBasisPoints);
}

/// @notice Returns the total amount of assets held by the vault across all
/// allocations and this contract.
function totalAssets() public view override returns (uint256) {
return
IERC20(asset()).balanceOf(address(this)) + dispatcher.totalAssets();
}

/// @notice Calls `receiveApproval` function on spender previously approving
/// the spender to withdraw from the caller multiple times, up to
/// the `amount` amount. If this function is called again, it
Expand Down Expand Up @@ -232,19 +241,43 @@ contract stBTC is ERC4626Fees, PausableOwnable {
}
}

/// @notice Withdraws assets from the vault and transfers them to the
/// receiver.
/// @dev Withdraw unallocated assets first and and if not enough, then pull
/// the assets from the dispatcher.
/// @param assets Amount of assets to withdraw.
/// @param receiver The address to which the assets will be transferred.
/// @param owner The address of the owner of the shares.
function withdraw(
uint256 assets,
address receiver,
address owner
) public override whenNotPaused returns (uint256) {
uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this));
if (assets > currentAssetsBalance) {
dispatcher.withdraw(assets - currentAssetsBalance);
}

return super.withdraw(assets, receiver, owner);
}

/// @notice Redeems shares for assets and transfers them to the receiver.
/// @dev Redeem unallocated assets first and and if not enough, then pull
/// the assets from the dispatcher.
/// @param shares Amount of shares to redeem.
/// @param receiver The address to which the assets will be transferred.
/// @param owner The address of the owner of the shares.
function redeem(
uint256 shares,
address receiver,
address owner
) public override whenNotPaused returns (uint256) {
uint256 assets = convertToAssets(shares);
uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this));
if (assets > currentAssetsBalance) {
dispatcher.withdraw(assets - currentAssetsBalance);
}

return super.redeem(shares, receiver, owner);
}

Expand Down
35 changes: 35 additions & 0 deletions core/contracts/test/MezoPortalStub.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract MezoPortalStub {
using SafeERC20 for IERC20;

uint256 public depositCount;

function withdraw(
address token,
uint256 depositId,
uint96 amount
) external {
IERC20(token).safeTransfer(msg.sender, amount);
}

function deposit(address token, uint96 amount, uint32 lockPeriod) external {
depositCount++;
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
}

function getDeposit(
address depositor,
address token,
uint256 depositId
) external view returns (uint96 balance, uint256 unlockAt) {
return (
uint96(IERC20(token).balanceOf(address(this))),
block.timestamp
);
}
}
Loading

0 comments on commit fd154c0

Please sign in to comment.