diff --git a/core/.solhintignore b/core/.solhintignore index c2658d7d1..e2af04a45 100644 --- a/core/.solhintignore +++ b/core/.solhintignore @@ -1 +1,2 @@ node_modules/ +contracts/test/ \ No newline at end of file diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol new file mode 100644 index 000000000..02d194388 --- /dev/null +++ b/core/contracts/MezoAllocator.sol @@ -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; + } +} diff --git a/core/contracts/interfaces/IDispatcher.sol b/core/contracts/interfaces/IDispatcher.sol new file mode 100644 index 000000000..4c762240c --- /dev/null +++ b/core/contracts/interfaces/IDispatcher.sol @@ -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); +} diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index d456735a0..c021ad298 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -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 @@ -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; @@ -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(); } @@ -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 @@ -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); } diff --git a/core/contracts/test/MezoPortalStub.sol b/core/contracts/test/MezoPortalStub.sol new file mode 100644 index 000000000..d38b86de8 --- /dev/null +++ b/core/contracts/test/MezoPortalStub.sol @@ -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 + ); + } +} diff --git a/core/deploy/00_resolve_mezo_portal.ts b/core/deploy/00_resolve_mezo_portal.ts new file mode 100644 index 000000000..412c468d6 --- /dev/null +++ b/core/deploy/00_resolve_mezo_portal.ts @@ -0,0 +1,38 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { log } = deployments + const { deployer } = await getNamedAccounts() + + const mezoPortal = await deployments.getOrNull("MezoPortal") + + if (mezoPortal && isNonZeroAddress(mezoPortal.address)) { + log(`using MezoPortal contract at ${mezoPortal.address}`) + } else if ( + (hre.network.config as HardhatNetworkConfig)?.forking?.enabled && + hre.network.name !== "hardhat" + ) { + throw new Error("deployed MezoPortal contract not found") + } else { + log("deploying Mezo Portal contract stub") + + await deployments.deploy("MezoPortal", { + contract: "MezoPortalStub", + args: [], + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + } +} + +export default func + +func.tags = ["MezoPortal"] diff --git a/core/deploy/02_deploy_mezo_allocator.ts b/core/deploy/02_deploy_mezo_allocator.ts new file mode 100644 index 000000000..f1002afba --- /dev/null +++ b/core/deploy/02_deploy_mezo_allocator.ts @@ -0,0 +1,30 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments, helpers } = hre + const { deployer } = await getNamedAccounts() + + const tbtc = await deployments.get("TBTC") + const stbtc = await deployments.get("stBTC") + const mezoPortal = await deployments.get("MezoPortal") + + const mezoAllocator = await deployments.deploy("MezoAllocator", { + from: deployer, + args: [mezoPortal.address, tbtc.address, stbtc.address], + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + + if (hre.network.tags.etherscan) { + await helpers.etherscan.verify(mezoAllocator) + } + + // TODO: Add Tenderly verification +} + +export default func + +func.tags = ["MezoAllocator"] +func.dependencies = ["TBTC", "stBTC", "MezoPortal"] diff --git a/core/deploy/11_acre_update_dispatcher.ts b/core/deploy/11_stbtc_update_dispatcher.ts similarity index 76% rename from core/deploy/11_acre_update_dispatcher.ts rename to core/deploy/11_stbtc_update_dispatcher.ts index 6ddb718d8..9fc08fc51 100644 --- a/core/deploy/11_acre_update_dispatcher.ts +++ b/core/deploy/11_stbtc_update_dispatcher.ts @@ -5,7 +5,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre const { deployer } = await getNamedAccounts() - const dispatcher = await deployments.get("Dispatcher") + const dispatcher = await deployments.get("MezoAllocator") await deployments.execute( "stBTC", @@ -17,5 +17,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["AcreUpdateDispatcher"] -func.dependencies = ["stBTC", "Dispatcher"] +func.tags = ["stBTCUpdateDispatcher"] +func.dependencies = ["stBTC", "MezoAllocator"] diff --git a/core/deploy/12_dispatcher_update_maintainer.ts b/core/deploy/12_mezo_allocator_update_maintainer.ts similarity index 61% rename from core/deploy/12_dispatcher_update_maintainer.ts rename to core/deploy/12_mezo_allocator_update_maintainer.ts index 8f616fac0..5eb82de37 100644 --- a/core/deploy/12_dispatcher_update_maintainer.ts +++ b/core/deploy/12_mezo_allocator_update_maintainer.ts @@ -1,19 +1,24 @@ import type { HardhatRuntimeEnvironment } from "hardhat/types" import type { DeployFunction } from "hardhat-deploy/types" +import { waitConfirmationsNumber } from "../helpers/deployment" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre const { deployer, maintainer } = await getNamedAccounts() await deployments.execute( - "Dispatcher", - { from: deployer, log: true, waitConfirmations: 1 }, - "updateMaintainer", + "MezoAllocator", + { + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "addMaintainer", maintainer, ) } export default func -func.tags = ["DispatcherUpdateMaintainer"] +func.tags = ["MezoAllocatorAddMaintainer"] func.dependencies = ["Dispatcher"] diff --git a/core/deploy/24_transfer_ownership_mezo_allocator.ts b/core/deploy/24_transfer_ownership_mezo_allocator.ts new file mode 100644 index 000000000..f6add8a43 --- /dev/null +++ b/core/deploy/24_transfer_ownership_mezo_allocator.ts @@ -0,0 +1,39 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer, governance } = await getNamedAccounts() + const { log } = deployments + + log(`transferring ownership of MezoAllocator contract to ${governance}`) + + await deployments.execute( + "MezoAllocator", + { + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "transferOwnership", + governance, + ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "MezoAllocator", + { + from: governance, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }, + "acceptOwnership", + ) + } +} + +export default func + +func.tags = ["TransferOwnershipMezoAllocator"] +func.dependencies = ["MezoAllocator"] diff --git a/core/external/mainnet/MezoPortal.json b/core/external/mainnet/MezoPortal.json new file mode 100644 index 000000000..07a2d5bce --- /dev/null +++ b/core/external/mainnet/MezoPortal.json @@ -0,0 +1,828 @@ +{ + "address": "0xAB13B8eecf5AA2460841d75da5d5D861fD5B8A39", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "name": "DepositLocked", + "type": "error" + }, + { + "inputs": [], + "name": "DepositNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "IncorrectAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + } + ], + "name": "IncorrectDepositor", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "lockPeriod", + "type": "uint256" + } + ], + "name": "IncorrectLockPeriod", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "ability", + "type": "uint8" + } + ], + "name": "IncorrectTokenAbility", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "IncorrectTokenAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "depositBalance", + "type": "uint256" + } + ], + "name": "InsufficientDepositAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "InsufficientTokenAbility", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "LockPeriodOutOfRange", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "newUnlockAt", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "existingUnlockAt", + "type": "uint32" + } + ], + "name": "LockPeriodTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "TokenAlreadySupported", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TokenNotSupported", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "Locked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "maxLockPeriod", + "type": "uint32" + } + ], + "name": "MaxLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "minLockPeriod", + "type": "uint32" + } + ], + "name": "MinLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "SupportedTokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken", + "name": "supportedToken", + "type": "tuple" + } + ], + "name": "addSupportedToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "depositFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + } + ], + "name": "getDeposit", + "outputs": [ + { + "components": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "internalType": "struct Portal.DepositInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken[]", + "name": "supportedTokens", + "type": "tuple[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "lock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "receiveApproval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_maxLockPeriod", + "type": "uint32" + } + ], + "name": "setMaxLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minLockPeriod", + "type": "uint32" + } + ], + "name": "setMinLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenAbility", + "outputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x3e13a1ece7173342ac0146c63dbfb2fb60b2badfb0991a493a868ff0db55540b", + "numDeployments": 1, + "implementation": "0xeAAf2B9e90aA6400d83e07606F5F2A5432502216", + "devdoc": "Contract deployed as upgradable proxy" +} \ No newline at end of file diff --git a/core/external/sepolia/MezoPortal.json b/core/external/sepolia/MezoPortal.json new file mode 100644 index 000000000..2ba1a0e07 --- /dev/null +++ b/core/external/sepolia/MezoPortal.json @@ -0,0 +1,828 @@ +{ + "address": "0x6978E3e11b8Bc34ea836C1706fC742aC4Cb6b0Db", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "name": "DepositLocked", + "type": "error" + }, + { + "inputs": [], + "name": "DepositNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "IncorrectAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + } + ], + "name": "IncorrectDepositor", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "lockPeriod", + "type": "uint256" + } + ], + "name": "IncorrectLockPeriod", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "ability", + "type": "uint8" + } + ], + "name": "IncorrectTokenAbility", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "IncorrectTokenAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "depositBalance", + "type": "uint256" + } + ], + "name": "InsufficientDepositAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "InsufficientTokenAbility", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "LockPeriodOutOfRange", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "newUnlockAt", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "existingUnlockAt", + "type": "uint32" + } + ], + "name": "LockPeriodTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "TokenAlreadySupported", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "TokenNotSupported", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Deposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "Locked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "maxLockPeriod", + "type": "uint32" + } + ], + "name": "MaxLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "minLockPeriod", + "type": "uint32" + } + ], + "name": "MinLockPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "name": "SupportedTokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken", + "name": "supportedToken", + "type": "tuple" + } + ], + "name": "addSupportedToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "depositCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "depositFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "depositor", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + } + ], + "name": "getDeposit", + "outputs": [ + { + "components": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint32", + "name": "unlockAt", + "type": "uint32" + } + ], + "internalType": "struct Portal.DepositInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "enum Portal.TokenAbility", + "name": "tokenAbility", + "type": "uint8" + } + ], + "internalType": "struct Portal.SupportedToken[]", + "name": "supportedTokens", + "type": "tuple[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "lockPeriod", + "type": "uint32" + } + ], + "name": "lock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minLockPeriod", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "receiveApproval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_maxLockPeriod", + "type": "uint32" + } + ], + "name": "setMaxLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minLockPeriod", + "type": "uint32" + } + ], + "name": "setMinLockPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "tokenAbility", + "outputs": [ + { + "internalType": "enum Portal.TokenAbility", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "depositId", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "amount", + "type": "uint96" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0xc4b6c7f1865b884f292f456a648464bfa3e5f67133d4b0a92abd11c48945fee2", + "numDeployments": 1, + "implementation": "0x7641f007de71e849c1b75A3e430e8CA13d4bF646", + "devdoc": "Contract deployed as upgradable proxy" +} \ No newline at end of file diff --git a/core/test/BitcoinRedeemer.test.ts b/core/test/BitcoinRedeemer.test.ts index 2874cf9cd..396391945 100644 --- a/core/test/BitcoinRedeemer.test.ts +++ b/core/test/BitcoinRedeemer.test.ts @@ -133,12 +133,7 @@ describe("BitcoinRedeemer", () => { to1e18(1), tbtcRedemptionData.redemptionData, ), - ) - .to.be.revertedWithCustomError( - stbtc, - "ERC4626ExceededMaxRedeem", - ) - .withArgs(await depositor.getAddress(), to1e18(1), 0) + ).to.be.revertedWithCustomError(stbtc, "ERC20InsufficientBalance") }) }) @@ -170,16 +165,10 @@ describe("BitcoinRedeemer", () => { amountToRedeem, tbtcRedemptionData.redemptionData, ), + ).to.be.revertedWithCustomError( + stbtc, + "ERC20InsufficientBalance", ) - .to.be.revertedWithCustomError( - stbtc, - "ERC4626ExceededMaxRedeem", - ) - .withArgs( - await depositor.getAddress(), - amountToRedeem, - depositAmount, - ) }) }) diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts index 5914e98bb..d318d9652 100644 --- a/core/test/Deployment.test.ts +++ b/core/test/Deployment.test.ts @@ -6,30 +6,30 @@ import { helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { deployment } from "./helpers/context" -import type { StBTC as stBTC, Dispatcher, TestERC20 } from "../typechain" +import type { StBTC as stBTC, TestERC20, MezoAllocator } from "../typechain" const { getNamedSigners } = helpers.signers async function fixture() { - const { tbtc, stbtc, dispatcher } = await deployment() + const { tbtc, stbtc, mezoAllocator } = await deployment() const { governance, maintainer, treasury } = await getNamedSigners() - return { stbtc, dispatcher, tbtc, governance, maintainer, treasury } + return { stbtc, mezoAllocator, tbtc, governance, maintainer, treasury } } describe("Deployment", () => { let stbtc: stBTC - let dispatcher: Dispatcher + let mezoAllocator: MezoAllocator let tbtc: TestERC20 let maintainer: HardhatEthersSigner let treasury: HardhatEthersSigner before(async () => { - ;({ stbtc, dispatcher, tbtc, maintainer, treasury } = + ;({ stbtc, mezoAllocator, tbtc, maintainer, treasury } = await loadFixture(fixture)) }) - describe("Acre", () => { + describe("stBTC", () => { describe("constructor", () => { context("when treasury has been set", () => { it("should be set to a treasury address", async () => { @@ -45,7 +45,7 @@ describe("Deployment", () => { it("should be set to a dispatcher address by the deployment script", async () => { const actualDispatcher = await stbtc.dispatcher() - expect(actualDispatcher).to.be.equal(await dispatcher.getAddress()) + expect(actualDispatcher).to.be.equal(await mezoAllocator.getAddress()) }) it("should approve max amount for the dispatcher", async () => { @@ -61,13 +61,13 @@ describe("Deployment", () => { }) }) - describe("Dispatcher", () => { + describe("MezoAllocator", () => { describe("updateMaintainer", () => { context("when a new maintainer has been set", () => { it("should be set to a new maintainer address", async () => { - const actualMaintainer = await dispatcher.maintainer() + const isMaintainer = await mezoAllocator.isMaintainer(maintainer) - expect(actualMaintainer).to.be.equal(await maintainer.getAddress()) + expect(isMaintainer).to.be.equal(true) }) }) }) diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts index 4e1e8a987..dfe7dba92 100644 --- a/core/test/Dispatcher.test.ts +++ b/core/test/Dispatcher.test.ts @@ -28,8 +28,8 @@ async function fixture() { return { dispatcher, governance, thirdParty, maintainer, vault, tbtc, stbtc } } - -describe("Dispatcher", () => { +// TODO: Remove these tests once Distpather contract is removed from the project. +describe.skip("Dispatcher", () => { let dispatcher: Dispatcher let vault: TestERC4626 let tbtc: TestERC20 diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts new file mode 100644 index 000000000..23467a497 --- /dev/null +++ b/core/test/MezoAllocator.test.ts @@ -0,0 +1,443 @@ +import { helpers } from "hardhat" +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { expect } from "chai" +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" + +import { ContractTransactionResponse, ZeroAddress } from "ethers" +import { beforeAfterSnapshotWrapper, deployment } from "./helpers" + +import { + StBTC as stBTC, + TestERC20, + MezoAllocator, + IMezoPortal, +} from "../typechain" + +import { to1e18 } from "./utils" + +const { getNamedSigners, getUnnamedSigners } = helpers.signers + +async function fixture() { + const { tbtc, stbtc, mezoAllocator, mezoPortal } = await deployment() + const { governance, maintainer } = await getNamedSigners() + const [depositor, thirdParty] = await getUnnamedSigners() + + return { + governance, + thirdParty, + depositor, + maintainer, + tbtc, + stbtc, + mezoAllocator, + mezoPortal, + } +} + +describe("MezoAllocator", () => { + let tbtc: TestERC20 + let stbtc: stBTC + let mezoAllocator: MezoAllocator + let mezoPortal: IMezoPortal + + let thirdParty: HardhatEthersSigner + let depositor: HardhatEthersSigner + let maintainer: HardhatEthersSigner + let governance: HardhatEthersSigner + + before(async () => { + ;({ + thirdParty, + depositor, + maintainer, + governance, + tbtc, + stbtc, + mezoAllocator, + mezoPortal, + } = await loadFixture(fixture)) + }) + + describe("allocate", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not a maintainer", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).allocate(), + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotMaintainer") + }) + }) + + context("when the caller is maintainer", () => { + context("when a first deposit is made", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(6)) + tx = await mezoAllocator.connect(maintainer).allocate() + }) + + it("should deposit and transfer tBTC to Mezo Portal", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [to1e18(6)], + ) + }) + + it("should not store any tBTC in Mezo Allocator", async () => { + expect( + await tbtc.balanceOf(await mezoAllocator.getAddress()), + ).to.equal(0) + }) + + it("should increment the deposit id", async () => { + const actualDepositId = await mezoAllocator.depositId() + expect(actualDepositId).to.equal(1) + }) + + it("should increase tracked deposit balance amount", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(to1e18(6)) + }) + + it("should emit DepositAllocated event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositAllocated") + .withArgs(0, 1, to1e18(6), to1e18(6)) + }) + }) + + context("when a second deposit is made", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(5)) + + tx = await mezoAllocator.connect(maintainer).allocate() + }) + + it("should increment the deposit id", async () => { + const actualDepositId = await mezoAllocator.depositId() + expect(actualDepositId).to.equal(2) + }) + + it("should emit DepositAllocated event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositAllocated") + .withArgs(1, 2, to1e18(5), to1e18(11)) + }) + + it("should deposit and transfer tBTC to Mezo Portal", async () => { + expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( + to1e18(11), + ) + }) + + it("should increase tracked deposit balance amount", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(to1e18(11)) + }) + + it("should not store any tBTC in Mezo Allocator", async () => { + expect( + await tbtc.balanceOf(await mezoAllocator.getAddress()), + ).to.equal(0) + }) + + it("should not store any tBTC in stBTC", async () => { + expect(await tbtc.balanceOf(await stbtc.getAddress())).to.equal(0) + }) + }) + }) + }) + + describe("withdraw", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not stBTC", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).withdraw(1n), + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotStbtc") + }) + }) + + context("when the caller is stBTC contract", () => { + context("when there is no deposit", () => { + it("should revert", async () => { + await expect(stbtc.withdraw(1n, depositor, depositor)) + .to.be.revertedWithCustomError(tbtc, "ERC20InsufficientBalance") + .withArgs(await mezoPortal.getAddress(), 0, 1n) + }) + }) + + context("when there is a deposit", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(depositor, to1e18(5)) + await tbtc.approve(await stbtc.getAddress(), to1e18(5)) + await stbtc.connect(depositor).deposit(to1e18(5), depositor) + await mezoAllocator.connect(maintainer).allocate() + }) + + context("when the deposit is not fully withdrawn", () => { + before(async () => { + tx = await stbtc.withdraw(to1e18(2), depositor, depositor) + }) + + it("should transfer 2 tBTC back to a depositor", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [depositor.address], + [to1e18(2)], + ) + }) + + it("should emit DepositWithdrawn event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositWithdrawn") + .withArgs(1, to1e18(2)) + }) + + it("should decrease tracked deposit balance amount", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(to1e18(3)) + }) + + it("should decrease Mezo Portal balance", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(2)], + ) + }) + }) + + context("when the deposit is fully withdrawn", () => { + before(async () => { + tx = await stbtc.withdraw(to1e18(3), depositor, depositor) + }) + + it("should transfer 3 tBTC back to a depositor", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [depositor.address], + [to1e18(3)], + ) + }) + + it("should emit DepositWithdrawn event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositWithdrawn") + .withArgs(1, to1e18(3)) + }) + + it("should decrease tracked deposit balance amount to zero", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(0) + }) + + it("should decrease Mezo Portal balance", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(3)], + ) + }) + }) + }) + }) + }) + + describe("totalAssets", () => { + beforeAfterSnapshotWrapper() + + context("when there is no deposit", () => { + it("should return 0", async () => { + const totalAssets = await mezoAllocator.totalAssets() + expect(totalAssets).to.equal(0) + }) + }) + + context("when there is a deposit", () => { + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(5)) + await mezoAllocator.connect(maintainer).allocate() + }) + + it("should return the total assets value", async () => { + const totalAssets = await mezoAllocator.totalAssets() + expect(totalAssets).to.equal(to1e18(5)) + }) + }) + }) + + describe("addMaintainer", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not a governance", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).addMaintainer(depositor.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when a caller is governance", () => { + context("when a maintainer is added", () => { + let tx: ContractTransactionResponse + + before(async () => { + tx = await mezoAllocator + .connect(governance) + .addMaintainer(thirdParty.address) + }) + + it("should add a maintainer", async () => { + expect(await mezoAllocator.isMaintainer(thirdParty.address)).to.equal( + true, + ) + }) + + it("should emit MaintainerAdded event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "MaintainerAdded") + .withArgs(thirdParty.address) + }) + + it("should add a new maintainer to the list", async () => { + const maintainers = await mezoAllocator.getMaintainers() + expect(maintainers).to.deep.equal([ + maintainer.address, + thirdParty.address, + ]) + }) + + it("should not allow to add the same maintainer twice", async () => { + await expect( + mezoAllocator.connect(governance).addMaintainer(thirdParty.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "MaintainerAlreadyRegistered", + ) + }) + + it("should not allow to add a zero address as a maintainer", async () => { + await expect( + mezoAllocator.connect(governance).addMaintainer(ZeroAddress), + ).to.be.revertedWithCustomError(mezoAllocator, "ZeroAddress") + }) + }) + }) + }) + + describe("removeMaintainer", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not a governance", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).removeMaintainer(depositor.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when a caller is governance", () => { + context("when a maintainer is removed", () => { + let tx: ContractTransactionResponse + + before(async () => { + await mezoAllocator + .connect(governance) + .addMaintainer(thirdParty.address) + tx = await mezoAllocator + .connect(governance) + .removeMaintainer(thirdParty.address) + }) + + it("should remove a maintainer", async () => { + expect(await mezoAllocator.isMaintainer(thirdParty.address)).to.equal( + false, + ) + }) + + it("should emit MaintainerRemoved event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "MaintainerRemoved") + .withArgs(thirdParty.address) + }) + + it("should remove a maintainer from the list", async () => { + const maintainers = await mezoAllocator.getMaintainers() + expect(maintainers).to.deep.equal([maintainer.address]) + }) + + it("should not allow to remove a maintainer twice", async () => { + await expect( + mezoAllocator + .connect(governance) + .removeMaintainer(thirdParty.address), + ).to.be.revertedWithCustomError( + mezoAllocator, + "MaintainerNotRegistered", + ) + }) + }) + }) + }) + + describe("releaseDeposit", () => { + beforeAfterSnapshotWrapper() + + context("when a caller is not governance", () => { + it("should revert", async () => { + await expect( + mezoAllocator.connect(thirdParty).releaseDeposit(), + ).to.be.revertedWithCustomError( + mezoAllocator, + "OwnableUnauthorizedAccount", + ) + }) + }) + + context("when the caller is governance", () => { + context("when there is a deposit", () => { + let tx: ContractTransactionResponse + + before(async () => { + await tbtc.mint(await stbtc.getAddress(), to1e18(5)) + await mezoAllocator.connect(maintainer).allocate() + tx = await mezoAllocator.connect(governance).releaseDeposit() + }) + + it("should emit DepositReleased event", async () => { + await expect(tx) + .to.emit(mezoAllocator, "DepositReleased") + .withArgs(1, to1e18(5)) + }) + + it("should decrease tracked deposit balance amount to zero", async () => { + const depositBalance = await mezoAllocator.depositBalance() + expect(depositBalance).to.equal(0) + }) + + it("should decrease Mezo Portal balance", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [mezoPortal, stbtc], + [-to1e18(5), to1e18(5)], + ) + }) + }) + }) + }) +}) diff --git a/core/test/helpers/context.ts b/core/test/helpers/context.ts index 5d6f518ea..68584f5ae 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -7,6 +7,8 @@ import type { BridgeStub, TestERC4626, TBTCVaultStub, + MezoAllocator, + MezoPortalStub, BitcoinDepositor, BitcoinRedeemer, TestTBTC, @@ -29,6 +31,9 @@ export async function deployment() { const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") const vault: TestERC4626 = await getDeployedContract("Vault") + const mezoAllocator: MezoAllocator = + await getDeployedContract("MezoAllocator") + const mezoPortal: MezoPortalStub = await getDeployedContract("MezoPortal") return { tbtc, @@ -39,5 +44,7 @@ export async function deployment() { tbtcVault, dispatcher, vault, + mezoAllocator, + mezoPortal, } } diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 729cdcafe..0346f69c4 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -12,12 +12,12 @@ import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { to1e18 } from "./utils" -import type { StBTC as stBTC, TestERC20, Dispatcher } from "../typechain" +import type { StBTC as stBTC, TestERC20, MezoAllocator } from "../typechain" const { getNamedSigners, getUnnamedSigners } = helpers.signers async function fixture() { - const { tbtc, stbtc, dispatcher } = await deployment() + const { tbtc, stbtc, mezoAllocator } = await deployment() const { governance, treasury, pauseAdmin } = await getNamedSigners() const [depositor1, depositor2, thirdParty] = await getUnnamedSigners() @@ -31,10 +31,10 @@ async function fixture() { tbtc, depositor1, depositor2, - dispatcher, governance, thirdParty, treasury, + mezoAllocator, pauseAdmin, } } @@ -46,7 +46,7 @@ describe("stBTC", () => { let stbtc: stBTC let tbtc: TestERC20 - let dispatcher: Dispatcher + let mezoAllocator: MezoAllocator let governance: HardhatEthersSigner let depositor1: HardhatEthersSigner @@ -61,10 +61,10 @@ describe("stBTC", () => { tbtc, depositor1, depositor2, - dispatcher, governance, thirdParty, treasury, + mezoAllocator, pauseAdmin, } = await loadFixture(fixture)) @@ -1098,7 +1098,7 @@ describe("stBTC", () => { before(async () => { // Dispatcher is set by the deployment scripts. See deployment tests // where initial parameters are checked. - dispatcherAddress = await dispatcher.getAddress() + dispatcherAddress = await mezoAllocator.getAddress() newDispatcher = await ethers.Wallet.createRandom().getAddress() stbtcAddress = await stbtc.getAddress()