From bd0559a611f04f2849e46537e3edec71d8a04537 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 8 Dec 2023 10:14:12 +0100 Subject: [PATCH 1/3] WIP Dispatcher and funds routing This is WIP code to check how dispatching to vaults could work --- core/contracts/Acre.sol | 20 +++- core/contracts/Dispatcher.sol | 34 ++++++ core/contracts/Router.sol | 67 +++++++++++ core/contracts/test/TestERC4626.sol | 12 ++ core/deploy/02_deploy_dispatcher.ts | 24 ++++ core/deploy/21_transfer_ownership_acre.ts | 2 - .../22_transfer_ownership_dispatcher.ts | 21 ++++ core/test/Dispatcher.test.ts | 109 ++++++++++++++++++ core/test/helpers/context.ts | 5 +- 9 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 core/contracts/Dispatcher.sol create mode 100644 core/contracts/Router.sol create mode 100644 core/contracts/test/TestERC4626.sol create mode 100644 core/deploy/02_deploy_dispatcher.ts create mode 100644 core/deploy/22_transfer_ownership_dispatcher.ts create mode 100644 core/test/Dispatcher.test.ts diff --git a/core/contracts/Acre.sol b/core/contracts/Acre.sol index 0e1cf640f..d4b229295 100644 --- a/core/contracts/Acre.sol +++ b/core/contracts/Acre.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "./Dispatcher.sol"; /// @title Acre /// @notice This contract implements the ERC-4626 tokenized vault standard. By @@ -14,12 +17,15 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; /// of yield-bearing vaults. This contract facilitates the minting and /// burning of shares (stBTC), which are represented as standard ERC20 /// tokens, providing a seamless exchange with tBTC tokens. -contract Acre is ERC4626 { +contract Acre is ERC4626, Ownable { event StakeReferral(bytes32 indexed referral, uint256 assets); + Dispatcher public dispatcher; + constructor( IERC20 tbtc - ) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") {} + ) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") Ownable(msg.sender) {} + /// @notice Stakes a given amount of tBTC token and mints shares to a /// receiver. @@ -43,4 +49,14 @@ contract Acre is ERC4626 { return shares; } + + function upgradeDispatcher(Dispatcher _newDispatcher) public onlyOwner { + if (address(dispatcher) != address(0)) { + IERC20(asset()).approve(address(dispatcher), 0); + } + + dispatcher = _newDispatcher; + + IERC20(asset()).approve(address(dispatcher), type(uint256).max); + } } diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol new file mode 100644 index 000000000..cb24deb2d --- /dev/null +++ b/core/contracts/Dispatcher.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "./Router.sol"; + +contract Dispatcher is Router, Ownable { + using SafeERC20 for IERC20; + + Acre acre; + + constructor(Acre _acre) Ownable(msg.sender) { + acre = _acre; + } + + function assetsHolder() public virtual override returns (address){ + return address(acre); + } + + function sharesHolder() public virtual override returns (address){ + return address(this); + } + + function migrateShares(IERC4626[] calldata _vaults) public onlyOwner { + address newDispatcher = address(acre.dispatcher()); + + for (uint i=0; i<_vaults.length; i++) { + _vaults[i].transfer(newDispatcher, _vaults[i].balanceOf(address(this))); + } + } +} diff --git a/core/contracts/Router.sol b/core/contracts/Router.sol new file mode 100644 index 000000000..6c7923c5a --- /dev/null +++ b/core/contracts/Router.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/interfaces/IERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./Acre.sol"; + + +// TODO: Consider deploying ERC4626RouterBase from the ERC4626 Alliance. +// TODO: Think about adding reentrancy guard +// TODO: Add ACL + +abstract contract Router { + using SafeERC20 for IERC20; + + /// @notice thrown when amount of assets received is below the min set by caller + error MinAmountError(); + + /// @notice thrown when amount of shares received is below the min set by caller + error MinSharesError(); + + /// @notice thrown when amount of assets received is above the max set by caller + error MaxAmountError(); + + /// @notice thrown when amount of shares received is above the max set by caller + error MaxSharesError(); + + + function assetsHolder() public virtual returns (address); + function sharesHolder() public virtual returns (address); + + function deposit( + IERC4626 vault, + uint256 amount, + uint256 minSharesOut + ) public returns (uint256 sharesOut) { + IERC20(vault.asset()).safeTransferFrom(assetsHolder(), address(this), amount); + + IERC20(vault.asset()).approve(address(vault), amount); + + if ((sharesOut = vault.deposit(amount, sharesHolder())) < minSharesOut) { + revert MinSharesError(); + } + } + + + function withdraw( + IERC4626 vault, + uint256 amount, + uint256 maxSharesOut + ) public returns (uint256 sharesOut) { + if ((sharesOut = vault.withdraw(amount, assetsHolder(), sharesHolder())) > maxSharesOut) { + revert MaxSharesError(); + } + } + + function redeem( + IERC4626 vault, + uint256 shares, + uint256 minAmountOut + ) public returns (uint256 amountOut) { + if ((amountOut = vault.redeem(shares, assetsHolder(), sharesHolder())) < minAmountOut) { + revert MinAmountError(); + } + } +} diff --git a/core/contracts/test/TestERC4626.sol b/core/contracts/test/TestERC4626.sol new file mode 100644 index 000000000..acf09928e --- /dev/null +++ b/core/contracts/test/TestERC4626.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; + +contract TestERC4626 is ERC4626 { + constructor( + IERC20 asset, + string memory tokenName, + string memory tokenSymbol + ) ERC4626(asset) ERC20(tokenName, tokenSymbol) {} +} diff --git a/core/deploy/02_deploy_dispatcher.ts b/core/deploy/02_deploy_dispatcher.ts new file mode 100644 index 000000000..b27730f62 --- /dev/null +++ b/core/deploy/02_deploy_dispatcher.ts @@ -0,0 +1,24 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer } = await getNamedAccounts() + + const acre = await deployments.get("Acre") + + await deployments.deploy("Dispatcher", { + from: deployer, + args: [acre.address], + log: true, + waitConfirmations: 1, + }) + + // TODO: Add Etherscan verification + // TODO: Add Tenderly verification +} + +export default func + +func.tags = ["Dispatcher"] +func.dependencies = ["Acre"] diff --git a/core/deploy/21_transfer_ownership_acre.ts b/core/deploy/21_transfer_ownership_acre.ts index c62708641..09a875a1d 100644 --- a/core/deploy/21_transfer_ownership_acre.ts +++ b/core/deploy/21_transfer_ownership_acre.ts @@ -19,5 +19,3 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["TransferOwnershipAcre"] -// TODO: Enable once Acre extends Ownable -func.skip = async () => true diff --git a/core/deploy/22_transfer_ownership_dispatcher.ts b/core/deploy/22_transfer_ownership_dispatcher.ts new file mode 100644 index 000000000..77103ddb7 --- /dev/null +++ b/core/deploy/22_transfer_ownership_dispatcher.ts @@ -0,0 +1,21 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer, governance } = await getNamedAccounts() + const { log } = deployments + + log(`transferring ownership of AcreRouter contract to ${governance}`) + + await deployments.execute( + "Dispatcher", + { from: deployer, log: true, waitConfirmations: 1 }, + "transferOwnership", + governance, + ) +} + +export default func + +func.tags = ["TransferOwnershipDispatcher"] diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts new file mode 100644 index 000000000..77955cb1c --- /dev/null +++ b/core/test/Dispatcher.test.ts @@ -0,0 +1,109 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { expect } from "chai" + +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" + +import { ethers } from "hardhat" +import { deployment } from "./helpers/context" +import { getNamedSigner, getUnnamedSigner } from "./helpers/signer" + +import { to1e18 } from "./utils" + +import type { Acre, Dispatcher, TestERC4626, TestERC20 } from "../typechain" + +async function fixture() { + const { tbtc, acre, dispatcher } = await deployment() + + const { governance } = await getNamedSigner() + + const [staker1] = await getUnnamedSigner() + + await acre + .connect(governance) + .upgradeDispatcher(await dispatcher.getAddress()) + + const vault: TestERC4626 = await ethers.deployContract("TestERC4626", [ + await tbtc.getAddress(), + "Test Vault Token", + "vToken", + ]) + await vault.waitForDeployment() + + return { acre, tbtc, dispatcher, vault, staker1 } +} + +describe("Dispatcher", () => { + const staker1Amount = to1e18(1000) + + let acre: Acre + let tbtc: TestERC20 + let dispatcher: Dispatcher + let vault: TestERC4626 + + let staker1: HardhatEthersSigner + + before(async () => { + ;({ acre, tbtc, dispatcher, vault, staker1 } = await loadFixture(fixture)) + }) + + it("test deposit and withdraw", async () => { + // Mint tBTC for staker. + await tbtc.mint(staker1.address, staker1Amount) + + // Stake tBTC in Acre. + await tbtc.approve(await acre.getAddress(), staker1Amount) + await acre.connect(staker1).deposit(staker1Amount, staker1.address) + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount, + ) + + const vaultDepositAmount = to1e18(500) + const expectedSharesDeposit = vaultDepositAmount + + await dispatcher.deposit( + await vault.getAddress(), + vaultDepositAmount, + expectedSharesDeposit, + ) + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount - vaultDepositAmount, + ) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await vault.getAddress())).to.be.equal( + vaultDepositAmount, + ) + + expect(await vault.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedSharesDeposit, + ) + + // // Simulate Vault generating yield. + // const yieldAmount = to1e18(200) + // await tbtc.mint(await vault.getAddress(), yieldAmount) + + // Partial withdrawal. + const amountToWithdraw1 = to1e18(300) + const expectedSharesWithdraw = to1e18(300) + await dispatcher.withdraw( + await vault.getAddress(), + amountToWithdraw1, + expectedSharesWithdraw, + ) + + expect(await vault.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedSharesDeposit - expectedSharesWithdraw, + ) + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount - vaultDepositAmount + amountToWithdraw1, + ) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await vault.getAddress())).to.be.equal( + vaultDepositAmount - amountToWithdraw1, + ) + }) +}) diff --git a/core/test/helpers/context.ts b/core/test/helpers/context.ts index ec4c56cbd..d043238f7 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -2,7 +2,7 @@ import { deployments } from "hardhat" import { getDeployedContract } from "./contract" -import type { Acre, AcreRouter, TestERC20 } from "../../typechain" +import type { Acre, AcreRouter, Dispatcher, TestERC20 } from "../../typechain" // eslint-disable-next-line import/prefer-default-export export async function deployment() { @@ -11,6 +11,7 @@ export async function deployment() { const tbtc: TestERC20 = await getDeployedContract("TBTC") const acre: Acre = await getDeployedContract("Acre") const acreRouter: AcreRouter = await getDeployedContract("AcreRouter") + const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") - return { tbtc, acre, acreRouter } + return { tbtc, acre, acreRouter, dispatcher } } From d0be970387fd04feb172f2acdc01ee0d81522398 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 8 Dec 2023 13:23:43 +0100 Subject: [PATCH 2/3] Check for zero address --- core/contracts/Dispatcher.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol index cb24deb2d..d175a5fe9 100644 --- a/core/contracts/Dispatcher.sol +++ b/core/contracts/Dispatcher.sol @@ -27,6 +27,8 @@ contract Dispatcher is Router, Ownable { function migrateShares(IERC4626[] calldata _vaults) public onlyOwner { address newDispatcher = address(acre.dispatcher()); + require(newDispatcher != address(0), "new dispatcher address cannot be zero address"); + for (uint i=0; i<_vaults.length; i++) { _vaults[i].transfer(newDispatcher, _vaults[i].balanceOf(address(this))); } From 4e211f8e04dfc386b241c0e77884f029d5b7488e Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 13 Dec 2023 12:33:21 +0100 Subject: [PATCH 3/3] Modify routing logic With changes in this commit the vault shares will be held by the main Acre contract. --- core/contracts/Acre.sol | 13 ++ core/contracts/Dispatcher.sol | 67 ++++++-- core/contracts/Router.sol | 39 ++--- core/deploy/02_deploy_acre_router.ts | 22 --- core/deploy/02_deploy_dispatcher.ts | 15 +- .../22_transfer_ownership_acre_router.ts | 22 --- .../22_transfer_ownership_dispatcher.ts | 1 + core/test/Dispatcher.Routing.POC.test.ts | 147 ++++++++++++++++++ 8 files changed, 250 insertions(+), 76 deletions(-) delete mode 100644 core/deploy/02_deploy_acre_router.ts delete mode 100644 core/deploy/22_transfer_ownership_acre_router.ts create mode 100644 core/test/Dispatcher.Routing.POC.test.ts diff --git a/core/contracts/Acre.sol b/core/contracts/Acre.sol index d4b229295..34f2de84b 100644 --- a/core/contracts/Acre.sol +++ b/core/contracts/Acre.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Dispatcher.sol"; @@ -18,6 +19,10 @@ import "./Dispatcher.sol"; /// burning of shares (stBTC), which are represented as standard ERC20 /// tokens, providing a seamless exchange with tBTC tokens. contract Acre is ERC4626, Ownable { + using SafeERC20 for IERC20; + + error CallerNotDispatcher(); + event StakeReferral(bytes32 indexed referral, uint256 assets); Dispatcher public dispatcher; @@ -53,10 +58,18 @@ contract Acre is ERC4626, Ownable { function upgradeDispatcher(Dispatcher _newDispatcher) public onlyOwner { if (address(dispatcher) != address(0)) { IERC20(asset()).approve(address(dispatcher), 0); + // TODO: Remove dispatcher's approvals for the vaults. } dispatcher = _newDispatcher; IERC20(asset()).approve(address(dispatcher), type(uint256).max); } + + function approveVaultSharesForDispatcher(address vault, uint256 amount) external { + if (msg.sender != address(dispatcher)) revert CallerNotDispatcher(); + + // TODO: Emit event + IERC20(vault).safeIncreaseAllowance(address(dispatcher), amount); + } } diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol index 4028cbdab..f2b49a28e 100644 --- a/core/contracts/Dispatcher.sol +++ b/core/contracts/Dispatcher.sol @@ -6,12 +6,14 @@ import "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Router.sol"; +import "./Acre.sol"; /// a given vault and back. Vaults supply yield strategies with TBTC that /// generate yield for Bitcoin holders. -contract Dispatcher is Ownable { +contract Dispatcher is Router, Ownable { using SafeERC20 for IERC20; + error VaultAlreadyAuthorized(); error VaultUnauthorized(); @@ -20,6 +22,7 @@ contract Dispatcher is Ownable { } Acre acre; + IERC20 tbtc; /// @notice Authorized Yield Vaults that implement ERC4626 standard. These /// vaults deposit assets to yield strategies, e.g. Uniswap V3 @@ -33,8 +36,9 @@ contract Dispatcher is Ownable { event VaultAuthorized(address indexed vault); event VaultDeauthorized(address indexed vault); - constructor(Acre _acre) Ownable(msg.sender) { + constructor(Acre _acre, IERC20 _tbtc) Ownable(msg.sender) { acre = _acre; + tbtc = _tbtc; } /// @notice Adds a vault to the list of authorized vaults. @@ -47,13 +51,15 @@ contract Dispatcher is Ownable { vaults.push(vault); vaultsInfo[vault].authorized = true; + acre.approveVaultSharesForDispatcher(vault, type(uint256).max); + emit VaultAuthorized(vault); } /// @notice Removes a vault from the list of authorized vaults. /// @param vault Address of the vault to remove. function deauthorizeVault(address vault) external onlyOwner { - if (!vaultsInfo[vault].authorized) { + if (!isVaultAuthorized(vault)) { revert VaultUnauthorized(); } @@ -68,26 +74,63 @@ contract Dispatcher is Ownable { } } + acre.approveVaultSharesForDispatcher(vault, 0); + emit VaultDeauthorized(vault); } + function isVaultAuthorized(address vault) public view returns (bool){ + return vaultsInfo[vault].authorized; + } + function getVaults() external view returns (address[] memory) { return vaults; } - return address(acre); + +// TODO: Add access restriction + function depositToVault( + IERC4626 vault, + uint256 amount, + uint256 minSharesOut + ) public returns (uint256 sharesOut) { + if (!isVaultAuthorized(address(vault))) { + revert VaultUnauthorized(); + } + + require(vault.asset() == address(tbtc), "vault asset is not tbtc"); + + IERC20(tbtc).safeTransferFrom(address(acre), address(this), amount); + IERC20(tbtc).approve(address(vault), amount); + + Router.deposit(vault, address(acre), amount, minSharesOut); } - function sharesHolder() public virtual override returns (address){ - return address(this); +// TODO: Add access restriction + function withdrawFromVault( + IERC4626 vault, + uint256 amount, + uint256 maxSharesOut + ) public returns (uint256 sharesOut) { + uint256 shares = vault.previewWithdraw(amount); + + IERC20(vault).safeTransferFrom(address(acre), address(this), shares); + IERC20(vault).approve(address(vault), shares); + + Router.withdraw(vault, address(acre), amount, maxSharesOut); } - function migrateShares(IERC4626[] calldata _vaults) public onlyOwner { - address newDispatcher = address(acre.dispatcher()); +// TODO: Add access restriction + function redeemFromVault( + IERC4626 vault, + uint256 shares, + uint256 minAmountOut + ) public returns (uint256 amountOut) { + IERC20(vault).safeTransferFrom(address(acre), address(this), shares); + IERC20(vault).approve(address(vault), shares); - require(newDispatcher != address(0), "new dispatcher address cannot be zero address"); + Router.redeem(vault, address(acre), shares, minAmountOut); + } - for (uint i=0; i<_vaults.length; i++) { - _vaults[i].transfer(newDispatcher, _vaults[i].balanceOf(address(this))); - } + // TODO: Add function to withdrawMax } diff --git a/core/contracts/Router.sol b/core/contracts/Router.sol index 6c7923c5a..06d765a39 100644 --- a/core/contracts/Router.sol +++ b/core/contracts/Router.sol @@ -2,15 +2,13 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/interfaces/IERC20.sol"; -import "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./Acre.sol"; // TODO: Consider deploying ERC4626RouterBase from the ERC4626 Alliance. // TODO: Think about adding reentrancy guard // TODO: Add ACL - abstract contract Router { using SafeERC20 for IERC20; @@ -26,41 +24,46 @@ abstract contract Router { /// @notice thrown when amount of shares received is above the max set by caller error MaxSharesError(); - - function assetsHolder() public virtual returns (address); - function sharesHolder() public virtual returns (address); - + // Copied from ERC4626RouterBase + // Differences: + // - internal instead of public function deposit( IERC4626 vault, + address to, uint256 amount, uint256 minSharesOut - ) public returns (uint256 sharesOut) { - IERC20(vault.asset()).safeTransferFrom(assetsHolder(), address(this), amount); - - IERC20(vault.asset()).approve(address(vault), amount); - - if ((sharesOut = vault.deposit(amount, sharesHolder())) < minSharesOut) { + ) internal virtual returns (uint256 sharesOut) { + if ((sharesOut = vault.deposit(amount, to)) < minSharesOut) { revert MinSharesError(); } } - + // Copied from ERC4626RouterBase + // Difference: + // - internal instead of public + // - use address(this) as owner instead of msg.sender function withdraw( IERC4626 vault, + address to, uint256 amount, uint256 maxSharesOut - ) public returns (uint256 sharesOut) { - if ((sharesOut = vault.withdraw(amount, assetsHolder(), sharesHolder())) > maxSharesOut) { + ) internal virtual returns (uint256 sharesOut) { + if ((sharesOut = vault.withdraw(amount, to, address(this))) > maxSharesOut) { revert MaxSharesError(); } } + // Copied from ERC4626RouterBase + // Difference: + // - internal instead of public + // - use address(this) as owner instead of msg.sender function redeem( IERC4626 vault, + address to, uint256 shares, uint256 minAmountOut - ) public returns (uint256 amountOut) { - if ((amountOut = vault.redeem(shares, assetsHolder(), sharesHolder())) < minAmountOut) { + ) internal virtual returns (uint256 amountOut) { + if ((amountOut = vault.redeem(shares, to, address(this))) < minAmountOut) { revert MinAmountError(); } } diff --git a/core/deploy/02_deploy_acre_router.ts b/core/deploy/02_deploy_acre_router.ts deleted file mode 100644 index bf99d4d73..000000000 --- a/core/deploy/02_deploy_acre_router.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { getNamedAccounts, deployments } = hre - const { deployer } = await getNamedAccounts() - - await deployments.deploy("Dispatcher", { - from: deployer, - args: [], - log: true, - waitConfirmations: 1, - }) - - // TODO: Add Etherscan verification - // TODO: Add Tenderly verification -} - -export default func - -func.tags = ["Dispatcher"] -func.dependencies = ["Acre"] diff --git a/core/deploy/02_deploy_dispatcher.ts b/core/deploy/02_deploy_dispatcher.ts index b27730f62..0d3e7e44e 100644 --- a/core/deploy/02_deploy_dispatcher.ts +++ b/core/deploy/02_deploy_dispatcher.ts @@ -6,14 +6,25 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployer } = await getNamedAccounts() const acre = await deployments.get("Acre") + // const router = await deployments.get("Router") + const tbtc = await deployments.get("TBTC") await deployments.deploy("Dispatcher", { from: deployer, - args: [acre.address], + args: [acre.address, tbtc.address], log: true, waitConfirmations: 1, }) + const dispatcher = await deployments.get("Dispatcher") + + await deployments.execute( + "Acre", + { from: deployer, log: true, waitConfirmations: 1 }, + "upgradeDispatcher", + dispatcher.address, + ) + // TODO: Add Etherscan verification // TODO: Add Tenderly verification } @@ -21,4 +32,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["Dispatcher"] -func.dependencies = ["Acre"] +func.dependencies = ["Acre", "TBTC"] diff --git a/core/deploy/22_transfer_ownership_acre_router.ts b/core/deploy/22_transfer_ownership_acre_router.ts deleted file mode 100644 index 686d378c4..000000000 --- a/core/deploy/22_transfer_ownership_acre_router.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" -import type { DeployFunction } from "hardhat-deploy/types" - -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { getNamedAccounts, deployments } = hre - const { deployer, governance } = await getNamedAccounts() - const { log } = deployments - - log(`transferring ownership of Dispatcher contract to ${governance}`) - - await deployments.execute( - "Dispatcher", - { from: deployer, log: true, waitConfirmations: 1 }, - "transferOwnership", - governance, - ) -} - -export default func - -func.tags = ["TransferOwnershipAcreRouter"] -func.dependencies = ["Dispatcher"] diff --git a/core/deploy/22_transfer_ownership_dispatcher.ts b/core/deploy/22_transfer_ownership_dispatcher.ts index 77103ddb7..9428a3d71 100644 --- a/core/deploy/22_transfer_ownership_dispatcher.ts +++ b/core/deploy/22_transfer_ownership_dispatcher.ts @@ -19,3 +19,4 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { export default func func.tags = ["TransferOwnershipDispatcher"] +func.dependencies = ["Dispatcher"] diff --git a/core/test/Dispatcher.Routing.POC.test.ts b/core/test/Dispatcher.Routing.POC.test.ts new file mode 100644 index 000000000..4777355fc --- /dev/null +++ b/core/test/Dispatcher.Routing.POC.test.ts @@ -0,0 +1,147 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { expect } from "chai" + +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" + +import { ethers } from "hardhat" +import { deployment } from "./helpers/context" +import { getNamedSigner, getUnnamedSigner } from "./helpers/signer" + +import { to1e18 } from "./utils" + +import type { Acre, Dispatcher, TestERC4626, TestERC20 } from "../typechain" + +async function fixture() { + const { tbtc, acre, dispatcher } = await deployment() + + const { governance } = await getNamedSigner() + + const [staker1] = await getUnnamedSigner() + + await acre + .connect(governance) + .upgradeDispatcher(await dispatcher.getAddress()) + + const vault: TestERC4626 = await ethers.deployContract("TestERC4626", [ + await tbtc.getAddress(), + "Test Vault Token", + "vToken", + ]) + await vault.waitForDeployment() + + // Authorize vault. + await dispatcher.connect(governance).authorizeVault(await vault.getAddress()) + + return { acre, tbtc, dispatcher, vault, staker1 } +} + +describe.only("Dispatcher", () => { + const staker1Amount = to1e18(1000) + + let acre: Acre + let tbtc: TestERC20 + let dispatcher: Dispatcher + let vault: TestERC4626 + + let staker1: HardhatEthersSigner + + before(async () => { + ;({ acre, tbtc, dispatcher, vault, staker1 } = await loadFixture(fixture)) + }) + + it("test deposit and withdraw", async () => { + // Mint tBTC for staker. + await tbtc.mint(staker1.address, staker1Amount) + + // Stake tBTC in Acre. + await tbtc.approve(await acre.getAddress(), staker1Amount) + await acre.connect(staker1).deposit(staker1Amount, staker1.address) + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount, + ) + + const vaultDepositAmount = to1e18(500) + const expectedSharesDeposit = vaultDepositAmount + + await dispatcher.depositToVault( + await vault.getAddress(), + vaultDepositAmount, + expectedSharesDeposit, + ) + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount - vaultDepositAmount, + ) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await vault.getAddress())).to.be.equal( + vaultDepositAmount, + ) + + expect(await vault.balanceOf(await acre.getAddress())).to.be.equal( + expectedSharesDeposit, + ) + expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + + // Simulate Vault generating yield. + const yieldAmount = to1e18(300) + await tbtc.mint(await vault.getAddress(), yieldAmount) + + // Partial withdraw. + const amountToWithdraw1 = to1e18(320) + // TODO: Clarify why we have to add 1 (rounding issue)? + const expectedSharesWithdraw = to1e18(200) + 1n // 500 * 320 / 800 = 200 + + expect(await vault.previewWithdraw(amountToWithdraw1)).to.be.equal( + expectedSharesWithdraw, + ) + + await dispatcher.withdrawFromVault( + await vault.getAddress(), + amountToWithdraw1, + expectedSharesWithdraw, + ) + + expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await vault.balanceOf(await acre.getAddress())).to.be.equal( + expectedSharesDeposit - expectedSharesWithdraw, + ) + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount - vaultDepositAmount + amountToWithdraw1, + ) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await vault.getAddress())).to.be.equal( + vaultDepositAmount + yieldAmount - amountToWithdraw1, + ) + + // Partial redeem. + const sharesToRedeem = to1e18(250) + const expectedAmountRedeem = to1e18(400) // 800 * 250 / 500 + + await dispatcher.redeemFromVault( + await vault.getAddress(), + sharesToRedeem, + expectedAmountRedeem, + ) + + expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await vault.balanceOf(await acre.getAddress())).to.be.equal( + expectedSharesDeposit - expectedSharesWithdraw - sharesToRedeem, + ) + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount - + vaultDepositAmount + + amountToWithdraw1 + + expectedAmountRedeem, + ) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await vault.getAddress())).to.be.equal( + vaultDepositAmount + + yieldAmount - + amountToWithdraw1 - + expectedAmountRedeem, + ) + }) +})