diff --git a/core/contracts/MezoAllocator.sol b/core/contracts/MezoAllocator.sol index edf078132..02d194388 100644 --- a/core/contracts/MezoAllocator.sol +++ b/core/contracts/MezoAllocator.sol @@ -91,16 +91,20 @@ contract MezoAllocator is IDispatcher, Ownable2Step { event MaintainerAdded(address indexed maintainer); /// @notice Emitted when the maintainer address is updated. event MaintainerRemoved(address indexed maintainer); - /// @notice Reverts if the caller is not an authorized account. - error NotAuthorized(); + /// @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 caller is already a maintainer. + /// @notice Reverts if the maintainer has been already registered. error MaintainerAlreadyRegistered(); modifier onlyMaintainer() { if (!isMaintainer[msg.sender]) { - revert NotAuthorized(); + revert CallerNotMaintainer(); } _; } @@ -169,7 +173,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { /// MezoPortal for a given deposit id. /// @param amount Amount of tBTC to withdraw. function withdraw(uint256 amount) external { - if (msg.sender != address(stbtc)) revert NotAuthorized(); + if (msg.sender != address(stbtc)) revert CallerNotStbtc(); emit DepositWithdrawn(depositId, amount); mezoPortal.withdraw(address(tbtc), depositId, uint96(amount)); @@ -178,6 +182,20 @@ contract MezoAllocator is IDispatcher, Ownable2Step { 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 { @@ -214,7 +232,7 @@ contract MezoAllocator is IDispatcher, Ownable2Step { } /// @notice Returns the total amount of tBTC allocated to MezoPortal. - function totalAssets() external view returns (uint256 totalAmount) { + function totalAssets() external view returns (uint256) { return depositBalance; } diff --git a/core/deploy/12_mezo_allocator_update_maintainer.ts b/core/deploy/12_mezo_allocator_update_maintainer.ts index 1be91a2a9..3d1bd2c5f 100644 --- a/core/deploy/12_mezo_allocator_update_maintainer.ts +++ b/core/deploy/12_mezo_allocator_update_maintainer.ts @@ -20,5 +20,5 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func -func.tags = ["MezoAllocatorUpdateMaintainer"] func.dependencies = ["MezoAllocator"] +func.tags = ["MezoAllocatorAddMaintainer"] diff --git a/core/test/MezoAllocator.test.ts b/core/test/MezoAllocator.test.ts index 5d012d664..23467a497 100644 --- a/core/test/MezoAllocator.test.ts +++ b/core/test/MezoAllocator.test.ts @@ -65,7 +65,7 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).allocate(), - ).to.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotMaintainer") }) }) @@ -79,8 +79,10 @@ describe("MezoAllocator", () => { }) it("should deposit and transfer tBTC to Mezo Portal", async () => { - expect(await tbtc.balanceOf(await mezoPortal.getAddress())).to.equal( - to1e18(6), + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [to1e18(6)], ) }) @@ -95,6 +97,11 @@ describe("MezoAllocator", () => { 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") @@ -153,7 +160,7 @@ describe("MezoAllocator", () => { it("should revert", async () => { await expect( mezoAllocator.connect(thirdParty).withdraw(1n), - ).to.be.revertedWithCustomError(mezoAllocator, "NotAuthorized") + ).to.be.revertedWithCustomError(mezoAllocator, "CallerNotStbtc") }) }) @@ -201,9 +208,11 @@ describe("MezoAllocator", () => { }) it("should decrease Mezo Portal balance", async () => { - expect( - await tbtc.balanceOf(await mezoPortal.getAddress()), - ).to.equal(to1e18(3)) + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(2)], + ) }) }) @@ -232,9 +241,11 @@ describe("MezoAllocator", () => { }) it("should decrease Mezo Portal balance", async () => { - expect( - await tbtc.balanceOf(await mezoPortal.getAddress()), - ).to.equal(0) + await expect(tx).to.changeTokenBalances( + tbtc, + [await mezoPortal.getAddress()], + [-to1e18(3)], + ) }) }) }) @@ -383,4 +394,50 @@ describe("MezoAllocator", () => { }) }) }) + + 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)], + ) + }) + }) + }) + }) })