From cf57c89f851fdb31e7de10a1eb18e6200c6b5682 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 14 Dec 2023 16:04:36 +0100 Subject: [PATCH] Add allocations to the Dispatcher --- core/contracts/Acre.sol | 8 +- core/contracts/Dispatcher.sol | 142 ++++++++-- core/contracts/Router.sol | 10 +- core/test/Dispatcher-RoutingPoC.test.ts | 109 -------- core/test/Dispatcher.Routing.POC.test.ts | 319 ++++++++++++++++------- 5 files changed, 359 insertions(+), 229 deletions(-) delete mode 100644 core/test/Dispatcher-RoutingPoC.test.ts diff --git a/core/contracts/Acre.sol b/core/contracts/Acre.sol index 446703371..2b76a2bdb 100644 --- a/core/contracts/Acre.sol +++ b/core/contracts/Acre.sol @@ -22,7 +22,7 @@ contract Acre is ERC4626, Ownable { using SafeERC20 for IERC20; error CallerNotDispatcher(); - + event StakeReferral(bytes32 indexed referral, uint256 assets); Dispatcher public dispatcher; @@ -31,6 +31,12 @@ contract Acre is ERC4626, Ownable { IERC20 tbtc ) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") Ownable(msg.sender) {} + /// @notice Returns the total amount of the underlying asset that is “managed” + /// by Acre. + function totalAssets() public view virtual override returns (uint256) { + return + IERC20(asset()).balanceOf(address(this)) + dispatcher.totalAssets(); + } /// @notice Stakes a given amount of tBTC token and mints shares to a /// receiver. diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol index 0997e0bdd..6d51f4eb8 100644 --- a/core/contracts/Dispatcher.sol +++ b/core/contracts/Dispatcher.sol @@ -8,6 +8,10 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import "./Router.sol"; import "./Acre.sol"; +// @notice Interface the Vaults connected to the Dispatcher contract should +// implement. +interface IVault is IERC4626 {} + /// a given vault and back. Vaults supply yield strategies with TBTC that /// generate yield for Bitcoin holders. contract Dispatcher is Router, Ownable { @@ -15,13 +19,18 @@ contract Dispatcher is Router, Ownable { error VaultAlreadyAuthorized(); error VaultUnauthorized(); + error InvalidVaultsWeight(uint16 vaultsWeight, uint16 vaultsTotalWeight); + error TotalAmountZero(); struct VaultInfo { bool authorized; + uint16 weight; } - Acre acre; - IERC20 tbtc; + Acre public acre; + IERC20 public tbtc; + + uint16 public vaultsTotalWeight = 1000; /// @notice Authorized Yield Vaults that implement ERC4626 standard. These /// vaults deposit assets to yield strategies, e.g. Uniswap V3 @@ -29,11 +38,16 @@ contract Dispatcher is Router, Ownable { /// implemented externally. As long as it complies with ERC4626 /// standard and is authorized by the owner it can be plugged into /// Acre. - address[] public vaults; - mapping(address => VaultInfo) public vaultsInfo; + IVault[] public vaults; + mapping(IVault => VaultInfo) public vaultsInfo; event VaultAuthorized(address indexed vault); event VaultDeauthorized(address indexed vault); + event VaultWeightUpdated( + address indexed vault, + uint16 newWeight, + uint16 oldWeight + ); constructor(Acre _acre, IERC20 _tbtc) Ownable(msg.sender) { acre = _acre; @@ -42,7 +56,7 @@ contract Dispatcher is Router, Ownable { /// @notice Adds a vault to the list of authorized vaults. /// @param vault Address of the vault to add. - function authorizeVault(address vault) external onlyOwner { + function authorizeVault(IVault vault) external onlyOwner { if (vaultsInfo[vault].authorized) { revert VaultAlreadyAuthorized(); } @@ -50,17 +64,17 @@ contract Dispatcher is Router, Ownable { vaults.push(vault); vaultsInfo[vault].authorized = true; - emit VaultAuthorized(vault); + emit VaultAuthorized(address(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 { + function deauthorizeVault(IVault vault) external onlyOwner { if (!isVaultAuthorized(vault)) { revert VaultUnauthorized(); } - vaultsInfo[vault].authorized = false; + delete vaultsInfo[vault]; for (uint256 i = 0; i < vaults.length; i++) { if (vaults[i] == vault) { @@ -71,25 +85,43 @@ contract Dispatcher is Router, Ownable { } } - emit VaultDeauthorized(vault); + emit VaultDeauthorized(address(vault)); + } + + function setVaultWeights( + IVault[] memory vaultsToSet, + uint16[] memory newWeights + ) external onlyOwner { + for (uint256 i = 0; i < vaultsToSet.length; i++) { + IVault vault = vaultsToSet[i]; + uint16 newWeight = newWeights[i]; + + if (newWeight > 0 && !isVaultAuthorized(vault)) { + revert VaultUnauthorized(); + } + + uint16 oldWeight = vaultsInfo[vault].weight; + vaultsInfo[vault].weight = newWeight; + + emit VaultWeightUpdated(address(vault), newWeight, oldWeight); + } } - function isVaultAuthorized(address vault) public view returns (bool){ + function isVaultAuthorized(IVault vault) public view returns (bool) { return vaultsInfo[vault].authorized; } - function getVaults() external view returns (address[] memory) { + function getVaults() external view returns (IVault[] memory) { return vaults; } - -// TODO: Add access restriction + // TODO: Add access restriction function depositToVault( - IERC4626 vault, + IVault vault, uint256 amount, uint256 minSharesOut ) public returns (uint256 sharesOut) { - if (!isVaultAuthorized(address(vault))) { + if (!isVaultAuthorized(vault)) { revert VaultUnauthorized(); } @@ -98,12 +130,12 @@ contract Dispatcher is Router, Ownable { IERC20(tbtc).safeTransferFrom(address(acre), address(this), amount); IERC20(tbtc).approve(address(vault), amount); - Router.deposit(vault, address(this), amount, minSharesOut); + return Router.deposit(vault, address(this), amount, minSharesOut); } -// TODO: Add access restriction + // TODO: Add access restriction function withdrawFromVault( - IERC4626 vault, + IVault vault, uint256 amount, uint256 maxSharesOut ) public returns (uint256 sharesOut) { @@ -111,12 +143,12 @@ contract Dispatcher is Router, Ownable { IERC20(vault).approve(address(vault), shares); - Router.withdraw(vault, address(acre), amount, maxSharesOut); + return Router.withdraw(vault, address(acre), amount, maxSharesOut); } -// TODO: Add access restriction + // TODO: Add access restriction function redeemFromVault( - IERC4626 vault, + IVault vault, uint256 shares, uint256 minAmountOut ) public returns (uint256 amountOut) { @@ -128,11 +160,73 @@ contract Dispatcher is Router, Ownable { // TODO: Add function to withdrawMax // TODO: Check possibilities of Dispatcher upgrades and shares migration. - function migrateShares(IERC4626[] calldata _vaults) public onlyOwner { + function migrateShares(IVault[] 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))); + for (uint i = 0; i < _vaults.length; i++) { + _vaults[i].transfer( + newDispatcher, + _vaults[i].balanceOf(address(this)) + ); + } + } + + function vaultsWeight() internal view returns (uint16 totalWeight) { + for (uint256 i = 0; i < vaults.length; i++) { + totalWeight += vaultsInfo[vaults[i]].weight; + } + } + + function totalAssets() public view returns (uint256 totalAmount) { + // Balance deployed in Vaults. + for (uint256 i = 0; i < vaults.length; i++) { + IVault vault = IVault(vaults[i]); + totalAmount += vault.convertToAssets( + vault.balanceOf(address(this)) + ); + } + + // Unused balance in Dispatcher. + // TODO: It is not expected the Dispatcher will hold any tBTC, we should + // add a function that would sweep tBTC from Dispatcher to Acre contract. + totalAmount += tbtc.balanceOf(address(this)); + } + + // TODO: This solution expects all tBTC to be withdrawn from all the Vaults before + // allocation. We may need improved solution to calculate exactly how much + // tBTC should be deposited or withdrawn from each vault. + // TODO: Make callable only by the maintainer bot. + // TODO: Add pre-calculated minSharesOut values for each deposit. + // TODO: Consider having constant total weight, e.g. 1000, so the vaults can + // have + function allocate() public { + uint16 vaultsWeight = vaultsWeight(); + if ( + vaultsTotalWeight == 0 || + vaultsWeight == 0 || + vaultsWeight > vaultsTotalWeight + ) revert InvalidVaultsWeight(vaultsWeight, vaultsTotalWeight); + + // tBTC held by Dispatcher and registered Vaults. + uint256 totalAmount = totalAssets(); + + // Unallocated tBTC in the Acre contract. + totalAmount += tbtc.balanceOf(address(acre)); + if (totalAmount == 0) revert TotalAmountZero(); + + for (uint256 i = 0; i < vaults.length; i++) { + IVault vault = vaults[i]; + + uint256 vaultAmount = (totalAmount * vaultsInfo[vault].weight) / + vaultsTotalWeight; + if (vaultAmount == 0) continue; + + // TODO: Pre-calculate the minSharesOut value off-chain as a slippage protection + // before calling the allocate function. + uint256 minSharesOut = vault.previewDeposit(vaultAmount); + + // Allocate tBTC to Vault. + depositToVault(vault, vaultAmount, minSharesOut); } } } diff --git a/core/contracts/Router.sol b/core/contracts/Router.sol index 06d765a39..8bbcd0e1e 100644 --- a/core/contracts/Router.sol +++ b/core/contracts/Router.sol @@ -5,7 +5,6 @@ import "@openzeppelin/contracts/interfaces/IERC20.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - // TODO: Consider deploying ERC4626RouterBase from the ERC4626 Alliance. // TODO: Think about adding reentrancy guard // TODO: Add ACL @@ -48,7 +47,10 @@ abstract contract Router { uint256 amount, uint256 maxSharesOut ) internal virtual returns (uint256 sharesOut) { - if ((sharesOut = vault.withdraw(amount, to, address(this))) > maxSharesOut) { + if ( + (sharesOut = vault.withdraw(amount, to, address(this))) > + maxSharesOut + ) { revert MaxSharesError(); } } @@ -63,7 +65,9 @@ abstract contract Router { uint256 shares, uint256 minAmountOut ) internal virtual returns (uint256 amountOut) { - if ((amountOut = vault.redeem(shares, to, address(this))) < minAmountOut) { + if ( + (amountOut = vault.redeem(shares, to, address(this))) < minAmountOut + ) { revert MinAmountError(); } } diff --git a/core/test/Dispatcher-RoutingPoC.test.ts b/core/test/Dispatcher-RoutingPoC.test.ts deleted file mode 100644 index 77955cb1c..000000000 --- a/core/test/Dispatcher-RoutingPoC.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -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/Dispatcher.Routing.POC.test.ts b/core/test/Dispatcher.Routing.POC.test.ts index 5ba33db31..2c0afb822 100644 --- a/core/test/Dispatcher.Routing.POC.test.ts +++ b/core/test/Dispatcher.Routing.POC.test.ts @@ -1,4 +1,8 @@ -import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" +import { + SnapshotRestorer, + takeSnapshot, + loadFixture, +} from "@nomicfoundation/hardhat-toolbox/network-helpers" import { expect } from "chai" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" @@ -22,126 +26,257 @@ async function fixture() { .connect(governance) .upgradeDispatcher(await dispatcher.getAddress()) - const vault: TestERC4626 = await ethers.deployContract("TestERC4626", [ + const vault1: TestERC4626 = await ethers.deployContract("TestERC4626", [ await tbtc.getAddress(), - "Test Vault Token", - "vToken", + "Test Vault Token 1", + "vToken1", ]) - await vault.waitForDeployment() + await vault1.waitForDeployment() // Authorize vault. - await dispatcher.connect(governance).authorizeVault(await vault.getAddress()) + await dispatcher.connect(governance).authorizeVault(await vault1.getAddress()) - return { acre, tbtc, dispatcher, vault, staker1 } + const vault2: TestERC4626 = await ethers.deployContract("TestERC4626", [ + await tbtc.getAddress(), + "Test Vault Token 2", + "vToken2", + ]) + await vault2.waitForDeployment() + + // Authorize vault. + await dispatcher.connect(governance).authorizeVault(await vault2.getAddress()) + + return { acre, tbtc, dispatcher, vault1, vault2, governance, staker1 } } -describe.only("Dispatcher", () => { +describe("Dispatcher Routing", () => { + let snapshot: SnapshotRestorer + const staker1Amount = to1e18(1000) let acre: Acre let tbtc: TestERC20 let dispatcher: Dispatcher - let vault: TestERC4626 + let vault1: TestERC4626 + let vault2: TestERC4626 + let governance: HardhatEthersSigner let staker1: HardhatEthersSigner before(async () => { - ;({ acre, tbtc, dispatcher, vault, staker1 } = await loadFixture(fixture)) - }) + ;({ acre, tbtc, dispatcher, vault1, vault2, governance, 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(0) - expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal( - expectedSharesDeposit, - ) - - // 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( - expectedSharesDeposit - expectedSharesWithdraw, - ) - expect(await vault.balanceOf(await acre.getAddress())).to.be.equal(0) + beforeEach(async () => { + snapshot = await takeSnapshot() 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, + staker1Amount, ) + }) - expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal( - expectedSharesDeposit - expectedSharesWithdraw - sharesToRedeem, - ) - expect(await vault.balanceOf(await acre.getAddress())).to.be.equal(0) + afterEach(async () => { + await snapshot.restore() + }) - expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( - staker1Amount - - vaultDepositAmount + - amountToWithdraw1 + + describe("depositToVault, withdrawFromVault, redeemFromVault", () => { + it("with one vault", async () => { + const vaultDepositAmount = to1e18(500) + const expectedSharesDeposit = vaultDepositAmount + + await dispatcher.depositToVault( + await vault1.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 vault1.getAddress())).to.be.equal( + vaultDepositAmount, + ) + + expect(await vault1.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await vault1.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedSharesDeposit, + ) + + expect(await acre.totalAssets()).to.be.equal(staker1Amount) + + // Simulate Vault generating yield. + const yieldAmount = to1e18(300) + await tbtc.mint(await vault1.getAddress(), yieldAmount) + + // TODO: Clarify why we have to subtract 1 (rounding issue)? + expect(await acre.totalAssets()).to.be.equal( + staker1Amount + yieldAmount - 1n, + ) + + // 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 vault1.previewWithdraw(amountToWithdraw1)).to.be.equal( + expectedSharesWithdraw, + ) + + await dispatcher.withdrawFromVault( + await vault1.getAddress(), + amountToWithdraw1, + expectedSharesWithdraw, + ) + + expect(await vault1.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedSharesDeposit - expectedSharesWithdraw, + ) + expect(await vault1.balanceOf(await acre.getAddress())).to.be.equal(0) + + 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 vault1.getAddress())).to.be.equal( + vaultDepositAmount + yieldAmount - amountToWithdraw1, + ) + + // Partial redeem. + const sharesToRedeem = to1e18(250) + const expectedAmountRedeem = to1e18(400) // 800 * 250 / 500 + + await dispatcher.redeemFromVault( + await vault1.getAddress(), + sharesToRedeem, 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, - ) + ) + + expect(await vault1.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedSharesDeposit - expectedSharesWithdraw - sharesToRedeem, + ) + expect(await vault1.balanceOf(await acre.getAddress())).to.be.equal(0) + + 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 vault1.getAddress())).to.be.equal( + vaultDepositAmount + + yieldAmount - + amountToWithdraw1 - + expectedAmountRedeem, + ) + + // TODO: Clarify why we have to subtract 1 (rounding issue)? + expect(await acre.totalAssets()).to.be.equal( + staker1Amount + yieldAmount - 1n, + ) + }) + }) + + describe("allocate", () => { + it("with one vault with 100% weight", async () => { + const vault1Weight = 1000 + const vault1DepositAmount = staker1Amount + const expectedVault1Shares = staker1Amount + + await dispatcher + .connect(governance) + .setVaultWeights([await vault1.getAddress()], [vault1Weight]) + + await dispatcher.allocate() + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await vault1.getAddress())).to.be.equal( + vault1DepositAmount, + ) + + expect(await vault1.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await vault1.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedVault1Shares, + ) + + expect(await acre.totalAssets()).to.be.equal(staker1Amount) + }) + + it("with one vault with 70% weight", async () => { + const vault1Weight = 700n + const vault1DepositAmount = (staker1Amount * vault1Weight) / 1000n + const expectedVault1Shares = (staker1Amount * vault1Weight) / 1000n + + await dispatcher + .connect(governance) + .setVaultWeights([await vault1.getAddress()], [vault1Weight]) + + await dispatcher.allocate() + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal( + staker1Amount - vault1DepositAmount, + ) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await vault1.getAddress())).to.be.equal( + vault1DepositAmount, + ) + + expect(await vault1.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await vault1.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedVault1Shares, + ) + + expect(await acre.totalAssets()).to.be.equal(staker1Amount) + }) + + it("with two vaults with 80%/20% weights", async () => { + const vault1Weight = 800n + const vault1DepositAmount = (staker1Amount * vault1Weight) / 1000n + const expectedVault1Shares = (staker1Amount * vault1Weight) / 1000n + + const vault2Weight = 200n + const vault2DepositAmount = (staker1Amount * vault2Weight) / 1000n + const expectedVault2Shares = (staker1Amount * vault2Weight) / 1000n + + await dispatcher + .connect(governance) + .setVaultWeights( + [await vault1.getAddress(), await vault2.getAddress()], + [vault1Weight, vault2Weight], + ) + + await dispatcher.allocate() + + expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0) + + expect(await tbtc.balanceOf(await vault1.getAddress())).to.be.equal( + vault1DepositAmount, + ) + expect(await vault1.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await vault1.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedVault1Shares, + ) + + expect(await tbtc.balanceOf(await vault2.getAddress())).to.be.equal( + vault2DepositAmount, + ) + expect(await vault2.balanceOf(await acre.getAddress())).to.be.equal(0) + expect(await vault2.balanceOf(await dispatcher.getAddress())).to.be.equal( + expectedVault2Shares, + ) + + expect(await acre.totalAssets()).to.be.equal(staker1Amount) + }) }) })