Skip to content

Commit e94a4ff

Browse files
authored
Mezo Allocator (#326)
Depends on #314 This PR adds the first tBTC Allocator with depositing functionality only that will work with Mezo L2. The gateway for tBTC allocation on Mezo is called Mezo Portal and this is where tBTC are allocated from Acre. As of now, 100% of assets will flow to Mezo Portal upon staking in Acre. In the future the % of allocation might change once other L2/DeFi are introduced. Deposits are created as "rolling" deposits, meaning that the "old" Acre's deposit is drained and all the assets are moved to a newly created deposit. A new id is assigned and replaces the old one. The "old" deposit with zero balance is abandoned. Mezo Allocator now serves as a "Dispatcher" contract in stBTC because there is only one Mezo Allocator. Once we have more allocators, then the Dispatcher contract will need to be implemented and switched with the current Mezo Allocator contract in stBTC. MezoAllocator will be included in Dispatcher's authorized list of allocators.
2 parents bd4f25f + ba01b07 commit e94a4ff

18 files changed

+2574
-43
lines changed

core/.solhintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules/
2+
contracts/test/

core/contracts/MezoAllocator.sol

+243
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
pragma solidity ^0.8.21;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import "@openzeppelin/contracts/access/Ownable2Step.sol";
7+
import {ZeroAddress} from "./utils/Errors.sol";
8+
import "./stBTC.sol";
9+
import "./interfaces/IDispatcher.sol";
10+
11+
/// @title IMezoPortal
12+
/// @dev Interface for the Mezo's Portal contract.
13+
interface IMezoPortal {
14+
/// @notice DepositInfo keeps track of the deposit balance and unlock time.
15+
/// Each deposit is tracked separately and associated with a specific
16+
/// token. Some tokens can be deposited but can not be locked - in
17+
/// that case the unlockAt is the block timestamp of when the deposit
18+
/// was created. The same is true for tokens that can be locked but
19+
/// the depositor decided not to lock them.
20+
struct DepositInfo {
21+
uint96 balance;
22+
uint32 unlockAt;
23+
}
24+
25+
/// @notice Deposit and optionally lock tokens for the given period.
26+
/// @dev Lock period will be normalized to weeks. If non-zero, it must not
27+
/// be shorter than the minimum lock period and must not be longer than
28+
/// the maximum lock period.
29+
/// @param token token address to deposit
30+
/// @param amount amount of tokens to deposit
31+
/// @param lockPeriod lock period in seconds, 0 to not lock the deposit
32+
function deposit(address token, uint96 amount, uint32 lockPeriod) external;
33+
34+
/// @notice Withdraw deposited tokens.
35+
/// Deposited lockable tokens can be withdrawn at any time if
36+
/// there is no lock set on the deposit or the lock period has passed.
37+
/// There is no way to withdraw locked deposit. Tokens that are not
38+
/// lockable can be withdrawn at any time. Deposit can be withdrawn
39+
/// partially.
40+
/// @param token deposited token address
41+
/// @param depositId id of the deposit
42+
/// @param amount amount of the token to be withdrawn from the deposit
43+
function withdraw(address token, uint256 depositId, uint96 amount) external;
44+
45+
/// @notice The number of deposits created. Includes the deposits that
46+
/// were fully withdrawn. This is also the identifier of the most
47+
/// recently created deposit.
48+
function depositCount() external view returns (uint256);
49+
50+
/// @notice Get the balance and unlock time of a given deposit.
51+
/// @param depositor depositor address
52+
/// @param token token address to get the balance
53+
/// @param depositId id of the deposit
54+
function getDeposit(
55+
address depositor,
56+
address token,
57+
uint256 depositId
58+
) external view returns (DepositInfo memory);
59+
}
60+
61+
/// @notice MezoAllocator routes tBTC to/from MezoPortal.
62+
contract MezoAllocator is IDispatcher, Ownable2Step {
63+
using SafeERC20 for IERC20;
64+
65+
/// @notice Address of the MezoPortal contract.
66+
IMezoPortal public immutable mezoPortal;
67+
/// @notice tBTC token contract.
68+
IERC20 public immutable tbtc;
69+
/// @notice stBTC token vault contract.
70+
stBTC public immutable stbtc;
71+
/// @notice Keeps track of the addresses that are allowed to trigger deposit
72+
/// allocations.
73+
mapping(address => bool) public isMaintainer;
74+
/// @notice List of maintainers.
75+
address[] public maintainers;
76+
/// @notice keeps track of the latest deposit ID assigned in Mezo Portal.
77+
uint256 public depositId;
78+
/// @notice Keeps track of the total amount of tBTC allocated to MezoPortal.
79+
uint96 public depositBalance;
80+
81+
/// @notice Emitted when tBTC is deposited to MezoPortal.
82+
event DepositAllocated(
83+
uint256 indexed oldDepositId,
84+
uint256 indexed newDepositId,
85+
uint256 addedAmount,
86+
uint256 newDepositAmount
87+
);
88+
/// @notice Emitted when tBTC is withdrawn from MezoPortal.
89+
event DepositWithdrawn(uint256 indexed depositId, uint256 amount);
90+
/// @notice Emitted when the maintainer address is updated.
91+
event MaintainerAdded(address indexed maintainer);
92+
/// @notice Emitted when the maintainer address is updated.
93+
event MaintainerRemoved(address indexed maintainer);
94+
/// @notice Emitted when tBTC is released from MezoPortal.
95+
event DepositReleased(uint256 indexed depositId, uint256 amount);
96+
/// @notice Reverts if the caller is not a maintainer.
97+
error CallerNotMaintainer();
98+
/// @notice Reverts if the caller is not the stBTC contract.
99+
error CallerNotStbtc();
100+
/// @notice Reverts if the maintainer is not registered.
101+
error MaintainerNotRegistered();
102+
/// @notice Reverts if the maintainer has been already registered.
103+
error MaintainerAlreadyRegistered();
104+
105+
modifier onlyMaintainer() {
106+
if (!isMaintainer[msg.sender]) {
107+
revert CallerNotMaintainer();
108+
}
109+
_;
110+
}
111+
112+
/// @notice Initializes the MezoAllocator contract.
113+
/// @param _mezoPortal Address of the MezoPortal contract.
114+
/// @param _tbtc Address of the tBTC token contract.
115+
constructor(
116+
address _mezoPortal,
117+
IERC20 _tbtc,
118+
stBTC _stbtc
119+
) Ownable(msg.sender) {
120+
if (_mezoPortal == address(0)) {
121+
revert ZeroAddress();
122+
}
123+
if (address(_tbtc) == address(0)) {
124+
revert ZeroAddress();
125+
}
126+
if (address(_stbtc) == address(0)) {
127+
revert ZeroAddress();
128+
}
129+
mezoPortal = IMezoPortal(_mezoPortal);
130+
tbtc = _tbtc;
131+
stbtc = _stbtc;
132+
}
133+
134+
/// @notice Allocate tBTC to MezoPortal. Each allocation creates a new "rolling"
135+
/// deposit meaning that the previous Acre's deposit is fully withdrawn
136+
/// before a new deposit with added amount is created. This mimics a
137+
/// "top up" functionality with the difference that a new deposit id
138+
/// is created and the previous deposit id is no longer in use.
139+
/// @dev This function can be invoked periodically by a maintainer.
140+
function allocate() external onlyMaintainer {
141+
if (depositBalance > 0) {
142+
// Free all Acre's tBTC from MezoPortal before creating a new deposit.
143+
// slither-disable-next-line reentrancy-no-eth
144+
mezoPortal.withdraw(address(tbtc), depositId, depositBalance);
145+
}
146+
147+
// Fetch unallocated tBTC from stBTC contract.
148+
uint256 addedAmount = tbtc.balanceOf(address(stbtc));
149+
// slither-disable-next-line arbitrary-send-erc20
150+
tbtc.safeTransferFrom(address(stbtc), address(this), addedAmount);
151+
152+
// Create a new deposit in the MezoPortal.
153+
depositBalance = uint96(tbtc.balanceOf(address(this)));
154+
tbtc.forceApprove(address(mezoPortal), depositBalance);
155+
// 0 denotes no lock period for this deposit.
156+
mezoPortal.deposit(address(tbtc), depositBalance, 0);
157+
uint256 oldDepositId = depositId;
158+
// MezoPortal doesn't return depositId, so we have to read depositCounter
159+
// which assigns depositId to the current deposit.
160+
depositId = mezoPortal.depositCount();
161+
162+
// slither-disable-next-line reentrancy-events
163+
emit DepositAllocated(
164+
oldDepositId,
165+
depositId,
166+
addedAmount,
167+
depositBalance
168+
);
169+
}
170+
171+
/// @notice Withdraws tBTC from MezoPortal and transfers it to stBTC.
172+
/// This function can withdraw partial or a full amount of tBTC from
173+
/// MezoPortal for a given deposit id.
174+
/// @param amount Amount of tBTC to withdraw.
175+
function withdraw(uint256 amount) external {
176+
if (msg.sender != address(stbtc)) revert CallerNotStbtc();
177+
178+
emit DepositWithdrawn(depositId, amount);
179+
mezoPortal.withdraw(address(tbtc), depositId, uint96(amount));
180+
// slither-disable-next-line reentrancy-benign
181+
depositBalance -= uint96(amount);
182+
tbtc.safeTransfer(address(stbtc), amount);
183+
}
184+
185+
/// @notice Releases deposit in full from MezoPortal.
186+
/// @dev This is a special function that can be used to migrate funds during
187+
/// allocator upgrade or in case of emergencies.
188+
function releaseDeposit() external onlyOwner {
189+
uint96 amount = mezoPortal
190+
.getDeposit(address(this), address(tbtc), depositId)
191+
.balance;
192+
193+
emit DepositReleased(depositId, amount);
194+
depositBalance = 0;
195+
mezoPortal.withdraw(address(tbtc), depositId, amount);
196+
tbtc.safeTransfer(address(stbtc), tbtc.balanceOf(address(this)));
197+
}
198+
199+
/// @notice Updates the maintainer address.
200+
/// @param maintainerToAdd Address of the new maintainer.
201+
function addMaintainer(address maintainerToAdd) external onlyOwner {
202+
if (maintainerToAdd == address(0)) {
203+
revert ZeroAddress();
204+
}
205+
if (isMaintainer[maintainerToAdd]) {
206+
revert MaintainerAlreadyRegistered();
207+
}
208+
maintainers.push(maintainerToAdd);
209+
isMaintainer[maintainerToAdd] = true;
210+
211+
emit MaintainerAdded(maintainerToAdd);
212+
}
213+
214+
/// @notice Removes the maintainer address.
215+
/// @param maintainerToRemove Address of the maintainer to remove.
216+
function removeMaintainer(address maintainerToRemove) external onlyOwner {
217+
if (!isMaintainer[maintainerToRemove]) {
218+
revert MaintainerNotRegistered();
219+
}
220+
delete (isMaintainer[maintainerToRemove]);
221+
222+
for (uint256 i = 0; i < maintainers.length; i++) {
223+
if (maintainers[i] == maintainerToRemove) {
224+
maintainers[i] = maintainers[maintainers.length - 1];
225+
// slither-disable-next-line costly-loop
226+
maintainers.pop();
227+
break;
228+
}
229+
}
230+
231+
emit MaintainerRemoved(maintainerToRemove);
232+
}
233+
234+
/// @notice Returns the total amount of tBTC allocated to MezoPortal.
235+
function totalAssets() external view returns (uint256) {
236+
return depositBalance;
237+
}
238+
239+
/// @notice Returns the list of maintainers.
240+
function getMaintainers() external view returns (address[] memory) {
241+
return maintainers;
242+
}
243+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
pragma solidity ^0.8.21;
3+
4+
/// @title IDispatcher
5+
/// @notice Interface for the Dispatcher contract.
6+
interface IDispatcher {
7+
/// @notice Withdraw assets from the Dispatcher.
8+
function withdraw(uint256 amount) external;
9+
10+
/// @notice Returns the total amount of assets held by the Dispatcher.
11+
function totalAssets() external view returns (uint256);
12+
}

core/contracts/stBTC.sol

+36-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol";
88
import "./Dispatcher.sol";
99
import "./PausableOwnable.sol";
1010
import "./lib/ERC4626Fees.sol";
11+
import "./interfaces/IDispatcher.sol";
1112
import {ZeroAddress} from "./utils/Errors.sol";
1213

1314
/// @title stBTC
@@ -25,8 +26,9 @@ import {ZeroAddress} from "./utils/Errors.sol";
2526
contract stBTC is ERC4626Fees, PausableOwnable {
2627
using SafeERC20 for IERC20;
2728

28-
/// Dispatcher contract that routes tBTC from stBTC to a given vault and back.
29-
Dispatcher public dispatcher;
29+
/// Dispatcher contract that routes tBTC from stBTC to a given allocation
30+
/// contract and back.
31+
IDispatcher public dispatcher;
3032

3133
/// Address of the treasury wallet, where fees should be transferred to.
3234
address public treasury;
@@ -128,7 +130,7 @@ contract stBTC is ERC4626Fees, PausableOwnable {
128130
/// @notice Updates the dispatcher contract and gives it an unlimited
129131
/// allowance to transfer staked tBTC.
130132
/// @param newDispatcher Address of the new dispatcher contract.
131-
function updateDispatcher(Dispatcher newDispatcher) external onlyOwner {
133+
function updateDispatcher(IDispatcher newDispatcher) external onlyOwner {
132134
if (address(newDispatcher) == address(0)) {
133135
revert ZeroAddress();
134136
}
@@ -175,6 +177,13 @@ contract stBTC is ERC4626Fees, PausableOwnable {
175177
emit ExitFeeBasisPointsUpdated(newExitFeeBasisPoints);
176178
}
177179

180+
/// @notice Returns the total amount of assets held by the vault across all
181+
/// allocations and this contract.
182+
function totalAssets() public view override returns (uint256) {
183+
return
184+
IERC20(asset()).balanceOf(address(this)) + dispatcher.totalAssets();
185+
}
186+
178187
/// @notice Calls `receiveApproval` function on spender previously approving
179188
/// the spender to withdraw from the caller multiple times, up to
180189
/// the `amount` amount. If this function is called again, it
@@ -245,19 +254,43 @@ contract stBTC is ERC4626Fees, PausableOwnable {
245254
}
246255
}
247256

257+
/// @notice Withdraws assets from the vault and transfers them to the
258+
/// receiver.
259+
/// @dev Withdraw unallocated assets first and and if not enough, then pull
260+
/// the assets from the dispatcher.
261+
/// @param assets Amount of assets to withdraw.
262+
/// @param receiver The address to which the assets will be transferred.
263+
/// @param owner The address of the owner of the shares.
248264
function withdraw(
249265
uint256 assets,
250266
address receiver,
251267
address owner
252268
) public override whenNotPaused returns (uint256) {
269+
uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this));
270+
if (assets > currentAssetsBalance) {
271+
dispatcher.withdraw(assets - currentAssetsBalance);
272+
}
273+
253274
return super.withdraw(assets, receiver, owner);
254275
}
255276

277+
/// @notice Redeems shares for assets and transfers them to the receiver.
278+
/// @dev Redeem unallocated assets first and and if not enough, then pull
279+
/// the assets from the dispatcher.
280+
/// @param shares Amount of shares to redeem.
281+
/// @param receiver The address to which the assets will be transferred.
282+
/// @param owner The address of the owner of the shares.
256283
function redeem(
257284
uint256 shares,
258285
address receiver,
259286
address owner
260287
) public override whenNotPaused returns (uint256) {
288+
uint256 assets = convertToAssets(shares);
289+
uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this));
290+
if (assets > currentAssetsBalance) {
291+
dispatcher.withdraw(assets - currentAssetsBalance);
292+
}
293+
261294
return super.redeem(shares, receiver, owner);
262295
}
263296

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.21;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
7+
contract MezoPortalStub {
8+
using SafeERC20 for IERC20;
9+
10+
uint256 public depositCount;
11+
12+
function withdraw(
13+
address token,
14+
uint256 depositId,
15+
uint96 amount
16+
) external {
17+
IERC20(token).safeTransfer(msg.sender, amount);
18+
}
19+
20+
function deposit(address token, uint96 amount, uint32 lockPeriod) external {
21+
depositCount++;
22+
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
23+
}
24+
25+
function getDeposit(
26+
address depositor,
27+
address token,
28+
uint256 depositId
29+
) external view returns (uint96 balance, uint256 unlockAt) {
30+
return (
31+
uint96(IERC20(token).balanceOf(address(this))),
32+
block.timestamp
33+
);
34+
}
35+
}

0 commit comments

Comments
 (0)