diff --git a/contracts/LiquidityHub.sol b/contracts/LiquidityHub.sol index 8fdf86f..8d014f8 100644 --- a/contracts/LiquidityHub.sol +++ b/contracts/LiquidityHub.sol @@ -6,40 +6,73 @@ import { IERC20Metadata, ERC20Upgradeable, ERC4626Upgradeable, + SafeERC20, Math } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {AccessControlUpgradeable} from '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; import {ERC7201Helper} from './utils/ERC7201Helper.sol'; import {IManagedToken} from './interfaces/IManagedToken.sol'; +import {ILiquidityPool} from './interfaces/ILiquidityPool.sol'; contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable { using Math for uint256; IManagedToken immutable public SHARES; - bytes32 public constant ASSETS_UPDATE_ROLE = "ASSETS_UPDATE_ROLE"; + ILiquidityPool immutable public LIQUIDITY_POOL; + bytes32 public constant ASSETS_ADJUST_ROLE = "ASSETS_ADJUST_ROLE"; + + event TotalAssetsAdjustment(uint256 oldAssets, uint256 newAssets); + event DepositRequest(address caller, address receiver, uint256 assets); + event WithdrawRequest(address caller, address receiver, address owner, uint256 shares); error ZeroAddress(); error NotImplemented(); error IncompatibleAssetsAndShares(); + struct AdjustmentRecord { + uint256 totalAssets; + uint256 totalShares; + } + + struct PendingDeposit { + uint256 assets; + uint256 adjustmentId; + } + + struct PendingWithdraw { + uint256 shares; + uint256 adjustmentId; + } + /// @custom:storage-location erc7201:sprinter.storage.LiquidityHub struct LiquidityHubStorage { uint256 totalAssets; + uint256 totalShares; + uint256 depositedAssets; + uint256 burnedShares; + uint256 releasedAssets; + uint256 lastAdjustmentId; + mapping(uint256 adjustmentId => AdjustmentRecord) adjustmentRecords; + mapping(address receiver => PendingDeposit) pendingDeposits; + mapping(address receiver => PendingWithdraw) pendingWithdrawals; } bytes32 private constant StorageLocation = 0xb877bfaae1674461dd1960c90f24075e3de3265a91f6906fe128ab8da6ba1700; - constructor(address shares) { + constructor(address shares, address liquidityPool) { ERC7201Helper.validateStorageLocation( StorageLocation, 'sprinter.storage.LiquidityHub' ); if (shares == address(0)) revert ZeroAddress(); + if (liquidityPool == address(0)) revert ZeroAddress(); SHARES = IManagedToken(shares); + LIQUIDITY_POOL = ILiquidityPool(liquidityPool); _disableInitializers(); } - function initialize(IERC20 asset_, address admin) external initializer() { + function initialize(IERC20 asset_, address admin, address adjuster) external initializer() { ERC4626Upgradeable.__ERC4626_init(asset_); require( IERC20Metadata(address(asset_)).decimals() <= IERC20Metadata(address(SHARES)).decimals(), @@ -48,6 +81,26 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable { // Deliberately not initializing ERC20Upgradable because its // functionality is delegated to SHARES. _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(ASSETS_ADJUST_ROLE, adjuster); + } + + function adjustTotalAssets(uint256 amount, bool isIncrease) external onlyRole(ASSETS_ADJUST_ROLE) { + LiquidityHubStorage storage $ = _getStorage(); + uint256 adjustmentId = ++$.lastAdjustmentId; + AdjustmentRecord storage adjustmentRecord = $.adjustmentRecords[adjustmentId]; + uint256 assets = $.totalAssets; + uint256 newAssets = isIncrease ? assets + amount : assets - amount; + uint256 supplyShares = $.totalShares; + uint256 mintingShares = _toShares($.depositedAssets, supplyShares, newAssets, Math.Rounding.Floor); + uint256 releasingAssets = _toAssets($.burnedShares, supplyShares, newAssets, Math.Rounding.Floor); + $.totalAssets = newAssets + $.depositedAssets - releasingAssets; + $.totalShares = supplyShares + mintingShares - $.burnedShares; + $.depositedAssets = 0; + $.burnedShares = 0; + $.releasedAssets += releasingAssets; + adjustmentRecord.totalAssets = $.totalAssets; + adjustmentRecord.totalShares = $.totalShares; + emit TotalAssetsAdjustment(assets, newAssets); } function name() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) { @@ -63,11 +116,11 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable { } function totalSupply() public view virtual override(IERC20, ERC20Upgradeable) returns (uint256) { - return IERC20(address(SHARES)).totalSupply(); + return _getStorage().totalShares; } function balanceOf(address owner) public view virtual override(IERC20, ERC20Upgradeable) returns (uint256) { - return IERC20(address(SHARES)).balanceOf(owner); + return IERC20(address(SHARES)).balanceOf(owner) + _simulateSettleDeposit(owner); } function transfer(address, uint256) public pure override(IERC20, ERC20Upgradeable) returns (bool) { @@ -91,26 +144,79 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable { return _getStorage().totalAssets; } - function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) { - (uint256 supplyShares, uint256 supplyAssets) = _getTotals(); + function releasedAssets() external view returns (uint256) { + return _getStorage().releasedAssets; + } + + function settle(address receiver) external { + _settleDeposit(receiver, receiver); + _settleWithdraw(receiver, receiver, receiver); + } + + function settleDeposit(address receiver) external { + _settleDeposit(receiver, receiver); + } + + function settleWithdraw(address receiver) external { + _settleWithdraw(receiver, receiver, receiver); + } + + function depositWithPermit( + uint256 assets, + address receiver, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + IERC20Permit(asset()).permit( + _msgSender(), + address(this), + assets, + deadline, + v, + r, + s + ); + deposit(assets, receiver); + } + + function _toShares( + uint256 assets, + uint256 supplyShares, + uint256 supplyAssets, + Math.Rounding rounding + ) internal view returns (uint256) { + (supplyShares, supplyAssets) = _getTotals(supplyShares, supplyAssets); return assets.mulDiv(supplyShares, supplyAssets, rounding); } - function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) { - (uint256 supplyShares, uint256 supplyAssets) = _getTotals(); + function _toAssets( + uint256 shares, + uint256 supplyShares, + uint256 supplyAssets, + Math.Rounding rounding + ) internal view returns (uint256) { + (supplyShares, supplyAssets) = _getTotals(supplyShares, supplyAssets); return shares.mulDiv(supplyAssets, supplyShares, rounding); } - function _getTotals() internal view returns (uint256 supply, uint256 assets) { - supply = totalSupply(); - if (supply == 0) { - supply = 10 ** _decimalsOffset(); + function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) { + return _toShares(assets, totalSupply(), totalAssets(), rounding); + } + + function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) { + return _toAssets(shares, totalSupply(), totalAssets(), rounding); + } + + function _getTotals(uint256 supplyShares, uint256 supplyAssets) internal view returns (uint256, uint256) { + if (supplyShares == 0) { + supplyShares = 10 ** _decimalsOffset(); } - assets = totalAssets(); - if (assets == 0) { - assets = 1; + if (supplyAssets == 0) { + supplyAssets = 1; } - return (supply, assets); + return (supplyShares, supplyAssets); } function _update(address from, address to, uint256 value) internal virtual override { @@ -127,20 +233,91 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable { SHARES.spendAllowance(owner, spender, value); } - function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { - super._deposit(caller, receiver, assets, shares); - _getStorage().totalAssets += assets; + function _deposit(address caller, address receiver, uint256 assets, uint256 /*shares*/) internal virtual override { + _settleWithdraw(caller, caller, caller); + _settleDeposit(caller, receiver); + LiquidityHubStorage storage $ = _getStorage(); + SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(LIQUIDITY_POOL), assets); + PendingDeposit storage pendingDeposit = $.pendingDeposits[receiver]; + pendingDeposit.assets += assets; + pendingDeposit.adjustmentId = $.lastAdjustmentId; + $.depositedAssets += assets; + LIQUIDITY_POOL.deposit(); + emit DepositRequest(caller, receiver, assets); + } + + function _simulateSettleDeposit(address receiver) internal view returns (uint256) { + LiquidityHubStorage storage $ = _getStorage(); + PendingDeposit storage pendingDeposit = $.pendingDeposits[receiver]; + uint256 assets = pendingDeposit.assets; + if (assets == 0) { + return 0; + } + uint256 settleAdjustmentId = pendingDeposit.adjustmentId + 1; + if (settleAdjustmentId > $.lastAdjustmentId) { + return 0; + } + AdjustmentRecord memory adjustmentRecord = $.adjustmentRecords[settleAdjustmentId]; + uint256 shares = _toShares( + assets, adjustmentRecord.totalShares, adjustmentRecord.totalAssets, Math.Rounding.Floor + ); + return shares; + } + + function _settleDeposit(address caller, address receiver) internal { + uint256 shares = _simulateSettleDeposit(receiver); + if (shares == 0) { + return; + } + PendingDeposit storage pendingDeposit = _getStorage().pendingDeposits[receiver]; + uint256 assets = pendingDeposit.assets; + pendingDeposit.assets = 0; + pendingDeposit.adjustmentId = 0; + _mint(receiver, shares); + emit Deposit(caller, receiver, assets, shares); } function _withdraw( address caller, address receiver, address owner, - uint256 assets, + uint256 /*assets*/, uint256 shares ) internal virtual override { - _getStorage().totalAssets -= assets; - super._withdraw(caller, receiver, owner, assets, shares); + _settleDeposit(caller, owner); + _settleWithdraw(caller, receiver, owner); + LiquidityHubStorage storage $ = _getStorage(); + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + PendingWithdraw storage pendingWithdraw = $.pendingWithdrawals[receiver]; + pendingWithdraw.shares += shares; + pendingWithdraw.adjustmentId = $.lastAdjustmentId; + $.burnedShares += shares; + _burn(owner, shares); + emit WithdrawRequest(caller, receiver, owner, shares); + } + + function _settleWithdraw(address caller, address receiver, address owner) internal { + LiquidityHubStorage storage $ = _getStorage(); + PendingWithdraw storage pendingWithdraw = $.pendingWithdrawals[receiver]; + uint256 shares = pendingWithdraw.shares; + if (shares == 0) { + return; + } + uint256 settleAdjustmentId = pendingWithdraw.adjustmentId + 1; + if (settleAdjustmentId > $.lastAdjustmentId) { + return; + } + pendingWithdraw.shares = 0; + pendingWithdraw.adjustmentId = 0; + AdjustmentRecord memory adjustmentRecord = $.adjustmentRecords[settleAdjustmentId]; + uint256 assets = _toAssets( + shares, adjustmentRecord.totalShares, adjustmentRecord.totalAssets, Math.Rounding.Floor + ); + $.releasedAssets -= assets; + LIQUIDITY_POOL.withdraw(receiver, assets); + emit Withdraw(caller, receiver, owner, assets, shares); } function _decimalsOffset() internal view virtual override returns (uint8) { diff --git a/contracts/interfaces/ILiquidityPool.sol b/contracts/interfaces/ILiquidityPool.sol new file mode 100644 index 0000000..11e498b --- /dev/null +++ b/contracts/interfaces/ILiquidityPool.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.28; + +interface ILiquidityPool { + function deposit() external; + + function withdraw(address to, uint256 amount) external; +} diff --git a/contracts/testing/TestLiquidityPool.sol b/contracts/testing/TestLiquidityPool.sol new file mode 100644 index 0000000..edd8478 --- /dev/null +++ b/contracts/testing/TestLiquidityPool.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.28; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ILiquidityPool} from "../interfaces/ILiquidityPool.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +contract TestLiquidityPool is ILiquidityPool, AccessControl { + IERC20 private immutable ASSET; + + event Deposit(); + + constructor(IERC20 asset) { + ASSET = asset; + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function deposit() external override { + emit Deposit(); + } + function withdraw(address to, uint256 amount) external override onlyRole(DEFAULT_ADMIN_ROLE) { + SafeERC20.safeTransfer(ASSET, to, amount); + } +} diff --git a/contracts/testing/TestUSDC.sol b/contracts/testing/TestUSDC.sol index 0e68aef..b0f4941 100644 --- a/contracts/testing/TestUSDC.sol +++ b/contracts/testing/TestUSDC.sol @@ -2,9 +2,10 @@ pragma solidity 0.8.28; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -contract TestUSDC is ERC20 { - constructor() ERC20("Circle USD", "USDC") { +contract TestUSDC is ERC20, ERC20Permit { + constructor() ERC20("Circle USD", "USDC") ERC20Permit("Circle USD") { _mint(msg.sender, 1000 * 10 ** decimals()); } diff --git a/hardhat.config.ts b/hardhat.config.ts index 51d4459..3555d13 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,6 +1,10 @@ import {HardhatUserConfig} from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; +function isSet(param?: string) { + return param && param.length > 0; +} + const config: HardhatUserConfig = { solidity: { version: "0.8.28", @@ -15,6 +19,12 @@ const config: HardhatUserConfig = { localhost: { url: "http://127.0.0.1:8545/", }, + basetest: { + chainId: 84532, + url: "https://sepolia.base.org", + accounts: + isSet(process.env.BASETEST_PRIVATE_KEY) ? [process.env.BASETEST_PRIVATE_KEY || ""] : [], + }, }, sourcify: { enabled: true, diff --git a/scripts/deploy.ts b/scripts/deploy.ts index f10e8a3..a7ff526 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -3,15 +3,17 @@ dotenv.config(); import hre from "hardhat"; import {isAddress} from "ethers"; -import {getContractAt, getCreateAddress, deploy} from "../test/helpers"; +import {getContractAt, getCreateAddress, deploy, ZERO_BYTES32} from "../test/helpers"; import {assert, getVerifier} from "./helpers"; import { - TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin + TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin, + TestLiquidityPool, } from "../typechain-types"; async function main() { const [deployer] = await hre.ethers.getSigners(); const admin: string = isAddress(process.env.ADMIN) ? process.env.ADMIN : deployer.address; + const adjuster: string = isAddress(process.env.ADJUSTER) ? process.env.ADJUSTER : deployer.address; let usdc: string; if (isAddress(process.env.USDC)) { usdc = process.env.USDC; @@ -20,6 +22,9 @@ async function main() { usdc = await testUSDC.getAddress(); } + console.log("TEST: Using TEST Liquidity Pool"); + const liquidityPool = (await deploy("TestLiquidityPool", deployer, {}, usdc)) as TestLiquidityPool; + const startingNonce = await deployer.getNonce(); const verifier = getVerifier(); @@ -30,9 +35,9 @@ async function main() { ) as SprinterUSDCLPShare; const liquidityHubImpl = ( - await verifier.deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target) + await verifier.deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target, liquidityPool.target) ) as LiquidityHub; - const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(usdc, admin)).data; + const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(usdc, admin, adjuster)).data; const liquidityHubProxy = (await verifier.deploy( "TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2}, liquidityHubImpl.target, admin, liquidityHubInit @@ -43,6 +48,11 @@ async function main() { assert(liquidityHubAddress == liquidityHubProxy.target, "LiquidityHub address mismatch"); + const DEFAULT_ADMIN_ROLE = ZERO_BYTES32; + + console.log("TEST: Using default admin role for Hub on Pool"); + await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, liquidityHub.target); + console.log(); console.log(`Admin: ${admin}`); console.log(`SprinterUSDCLPShare: ${lpToken.target}`); diff --git a/test/LiquidityHub.ts b/test/LiquidityHub.ts index c339bb4..2622137 100644 --- a/test/LiquidityHub.ts +++ b/test/LiquidityHub.ts @@ -5,17 +5,24 @@ import {expect} from "chai"; import hre from "hardhat"; import { getCreateAddress, getContractAt, deploy, - ZERO_ADDRESS + ZERO_ADDRESS, ZERO_BYTES32, } from "./helpers"; import { - TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin + TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin, + TestLiquidityPool, } from "../typechain-types"; +const INCREASE = true; +const DECREASE = false; + describe("LiquidityHub", function () { const deployAll = async () => { const [deployer, admin, user, user2, user3] = await hre.ethers.getSigners(); + const DEFAULT_ADMIN_ROLE = ZERO_BYTES32; + const usdc = (await deploy("TestUSDC", deployer, {})) as TestUSDC; + const liquidityPool = (await deploy("TestLiquidityPool", deployer, {}, usdc.target)) as TestLiquidityPool; const USDC = 10n ** (await usdc.decimals()); @@ -28,9 +35,11 @@ describe("LiquidityHub", function () { const LP = 10n ** (await lpToken.decimals()); const liquidityHubImpl = ( - await deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target) + await deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target, liquidityPool.target) ) as LiquidityHub; - const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(usdc.target, admin.address)).data; + const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction( + usdc.target, admin.address, admin.address) + ).data; const liquidityHubProxy = (await deploy( "TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2}, liquidityHubImpl.target, admin, liquidityHubInit @@ -39,14 +48,17 @@ describe("LiquidityHub", function () { const liquidityHubProxyAdminAddress = await getCreateAddress(liquidityHubProxy, 1); const liquidityHubAdmin = (await getContractAt("ProxyAdmin", liquidityHubProxyAdminAddress, admin)) as ProxyAdmin; + await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, liquidityHub.target); + return {deployer, admin, user, user2, user3, usdc, lpToken, - liquidityHub, liquidityHubProxy, liquidityHubAdmin, USDC, LP}; + liquidityHub, liquidityHubProxy, liquidityHubAdmin, USDC, LP, liquidityPool}; }; it("Should have default values", async function () { - const {lpToken, liquidityHub, usdc, user, user2} = await loadFixture(deployAll); + const {lpToken, liquidityHub, usdc, user, user2, liquidityPool} = await loadFixture(deployAll); expect(await liquidityHub.SHARES()).to.equal(lpToken.target); + expect(await liquidityHub.LIQUIDITY_POOL()).to.equal(liquidityPool.target); expect(await liquidityHub.asset()).to.equal(usdc.target); expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await liquidityHub.totalSupply()).to.equal(0n); @@ -68,133 +80,228 @@ describe("LiquidityHub", function () { }); it("Should allow to deposit", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); const tx = liquidityHub.connect(user).deposit(10n * USDC, user.address); - await expect(tx) - .to.emit(lpToken, "Transfer") - .withArgs(ZERO_ADDRESS, user.address, 10n * LP); await expect(tx) .to.emit(usdc, "Transfer") - .withArgs(user.address, liquidityHub.target, 10n * USDC); + .withArgs(user.address, liquidityPool.target, 10n * USDC); + await expect(tx) + .to.emit(liquidityHub, "DepositRequest") + .withArgs(user.address, user.address, 10n * USDC); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(0n, 0n); + + const tx2 = liquidityHub.settle(user.address); + await expect(tx2) + .to.emit(liquidityHub, "Deposit") + .withArgs(user.address, user.address, 10n * USDC, 10n * LP); expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP); expect(await lpToken.totalSupply()).to.equal(10n * LP); expect(await liquidityHub.totalSupply()).to.equal(10n * LP); expect(await liquidityHub.totalAssets()).to.equal(10n * USDC); expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP); expect(await usdc.balanceOf(user.address)).to.equal(0n); - expect(await usdc.balanceOf(liquidityHub.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); it("Should allow to deposit twice", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); const tx = liquidityHub.connect(user).deposit(3n * USDC, user.address); - await expect(tx) - .to.emit(lpToken, "Transfer") - .withArgs(ZERO_ADDRESS, user.address, 3n * LP); await expect(tx) .to.emit(usdc, "Transfer") - .withArgs(user.address, liquidityHub.target, 3n * USDC); + .withArgs(user.address, liquidityPool.target, 3n * USDC); + await expect(tx) + .to.emit(liquidityHub, "DepositRequest") + .withArgs(user.address, user.address, 3n * USDC); const tx2 = liquidityHub.connect(user).deposit(7n * USDC, user.address); - await expect(tx2) - .to.emit(lpToken, "Transfer") - .withArgs(ZERO_ADDRESS, user.address, 7n * LP); await expect(tx2) .to.emit(usdc, "Transfer") - .withArgs(user.address, liquidityHub.target, 7n * USDC); + .withArgs(user.address, liquidityPool.target, 7n * USDC); + await expect(tx2) + .to.emit(liquidityHub, "DepositRequest") + .withArgs(user.address, user.address, 7n * USDC); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(0n, 0n); + + const tx3 = liquidityHub.settle(user.address); + await expect(tx3) + .to.emit(liquidityHub, "Deposit") + .withArgs(user.address, user.address, 10n * USDC, 10n * LP); expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP); expect(await lpToken.totalSupply()).to.equal(10n * LP); expect(await liquidityHub.totalSupply()).to.equal(10n * LP); expect(await liquidityHub.totalAssets()).to.equal(10n * USDC); expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP); expect(await usdc.balanceOf(user.address)).to.equal(0n); - expect(await usdc.balanceOf(liquidityHub.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); it("Should allow to mint", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); const tx = liquidityHub.connect(user).mint(10n * LP, user.address); - await expect(tx) - .to.emit(lpToken, "Transfer") - .withArgs(ZERO_ADDRESS, user.address, 10n * LP); await expect(tx) .to.emit(usdc, "Transfer") - .withArgs(user.address, liquidityHub.target, 10n * USDC); + .withArgs(user.address, liquidityPool.target, 10n * USDC); + await expect(tx) + .to.emit(liquidityHub, "DepositRequest") + .withArgs(user.address, user.address, 10n * USDC); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(0n, 0n); + + const tx2 = liquidityHub.settle(user.address); + await expect(tx2) + .to.emit(liquidityHub, "Deposit") + .withArgs(user.address, user.address, 10n * USDC, 10n * LP); expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP); expect(await lpToken.totalSupply()).to.equal(10n * LP); expect(await liquidityHub.totalSupply()).to.equal(10n * LP); expect(await liquidityHub.totalAssets()).to.equal(10n * USDC); expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP); expect(await usdc.balanceOf(user.address)).to.equal(0n); - expect(await usdc.balanceOf(liquidityHub.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); it("Should allow to withdraw", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + await liquidityHub.connect(user).settle(user.address); const tx = liquidityHub.connect(user).withdraw(10n * USDC, user.address, user.address); await expect(tx) .to.emit(lpToken, "Transfer") .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) + .to.emit(liquidityHub, "WithdrawRequest") + .withArgs(user.address, user.address, user.address, 10n * LP); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(10n * USDC, 10n * USDC); + + const tx2 = liquidityHub.settle(user.address); + await expect(tx2) + .to.emit(liquidityHub, "Withdraw") + .withArgs(user.address, user.address, user.address, 10n * USDC, 10n * LP); + await expect(tx2) .to.emit(usdc, "Transfer") - .withArgs(liquidityHub.target, user.address, 10n * USDC); + .withArgs(liquidityPool.target, user.address, 10n * USDC); + expect(await lpToken.balanceOf(user.address)).to.equal(0n); expect(await lpToken.totalSupply()).to.equal(0n); expect(await liquidityHub.totalSupply()).to.equal(0n); expect(await liquidityHub.totalAssets()).to.equal(0n); expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(0n); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); it("Should allow to redeem", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + await liquidityHub.connect(user).settle(user.address); const tx = liquidityHub.connect(user).redeem(10n * LP, user.address, user.address); await expect(tx) .to.emit(lpToken, "Transfer") .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) + .to.emit(liquidityHub, "WithdrawRequest") + .withArgs(user.address, user.address, user.address, 10n * LP); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(10n * USDC, 10n * USDC); + + const tx2 = liquidityHub.settle(user.address); + await expect(tx2) + .to.emit(liquidityHub, "Withdraw") + .withArgs(user.address, user.address, user.address, 10n * USDC, 10n * LP); + await expect(tx2) .to.emit(usdc, "Transfer") - .withArgs(liquidityHub.target, user.address, 10n * USDC); + .withArgs(liquidityPool.target, user.address, 10n * USDC); + expect(await lpToken.balanceOf(user.address)).to.equal(0n); expect(await lpToken.totalSupply()).to.equal(0n); expect(await liquidityHub.totalSupply()).to.equal(0n); expect(await liquidityHub.totalAssets()).to.equal(0n); expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(0n); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); it("Should allow to withdraw from another user", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, user2, user3, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, user2, user3, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); await liquidityHub.connect(user).deposit(10n * USDC, user.address); await lpToken.connect(user).approve(user3.address, 10n * LP); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + await liquidityHub.connect(user).settle(user.address); const tx = liquidityHub.connect(user3).withdraw(10n * USDC, user2.address, user.address); await expect(tx) .to.emit(lpToken, "Transfer") .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) + .to.emit(liquidityHub, "WithdrawRequest") + .withArgs(user3.address, user2.address, user.address, 10n * LP); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(10n * USDC, 10n * USDC); + + const tx2 = liquidityHub.settle(user2.address); + await expect(tx2) + .to.emit(liquidityHub, "Withdraw") + .withArgs(user2.address, user2.address, user2.address, 10n * USDC, 10n * LP); + await expect(tx2) .to.emit(usdc, "Transfer") - .withArgs(liquidityHub.target, user2.address, 10n * USDC); + .withArgs(liquidityPool.target, user2.address, 10n * USDC); + expect(await lpToken.allowance(user.address, user3.address)).to.equal(0n); expect(await lpToken.balanceOf(user.address)).to.equal(0n); expect(await lpToken.totalSupply()).to.equal(0n); @@ -202,23 +309,42 @@ describe("LiquidityHub", function () { expect(await liquidityHub.totalAssets()).to.equal(0n); expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user2.address)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(0n); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); it("Should allow to redeem from another user", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, user2, user3, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, user2, user3, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); await liquidityHub.connect(user).deposit(10n * USDC, user.address); await lpToken.connect(user).approve(user3.address, 10n * LP); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + await liquidityHub.connect(user).settle(user.address); const tx = liquidityHub.connect(user3).redeem(10n * LP, user2.address, user.address); await expect(tx) .to.emit(lpToken, "Transfer") .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) + .to.emit(liquidityHub, "WithdrawRequest") + .withArgs(user3.address, user2.address, user.address, 10n * LP); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(10n * USDC, 10n * USDC); + + const tx2 = liquidityHub.settle(user2.address); + await expect(tx2) + .to.emit(liquidityHub, "Withdraw") + .withArgs(user2.address, user2.address, user2.address, 10n * USDC, 10n * LP); + await expect(tx2) .to.emit(usdc, "Transfer") - .withArgs(liquidityHub.target, user2.address, 10n * USDC); + .withArgs(liquidityPool.target, user2.address, 10n * USDC); + expect(await lpToken.allowance(user.address, user3.address)).to.equal(0n); expect(await lpToken.balanceOf(user.address)).to.equal(0n); expect(await lpToken.totalSupply()).to.equal(0n); @@ -226,36 +352,239 @@ describe("LiquidityHub", function () { expect(await liquidityHub.totalAssets()).to.equal(0n); expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user2.address)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(0n); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); it("Should allow to deposit and withdraw multiple times", async function () { - const {lpToken, liquidityHub, usdc, deployer, user, USDC, LP} = await loadFixture(deployAll); + const { + lpToken, liquidityHub, usdc, deployer, user, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); - const tx = liquidityHub.connect(user).deposit(3n * USDC, user.address); - await expect(tx) - .to.emit(lpToken, "Transfer") - .withArgs(ZERO_ADDRESS, user.address, 3n * LP); - await expect(tx) - .to.emit(usdc, "Transfer") - .withArgs(user.address, liquidityHub.target, 3n * USDC); + await liquidityHub.connect(user).deposit(3n * USDC, user.address); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); await liquidityHub.connect(user).withdraw(1n * USDC, user.address, user.address); - const tx2 = liquidityHub.connect(user).deposit(7n * USDC, user.address); - await expect(tx2) - .to.emit(lpToken, "Transfer") - .withArgs(ZERO_ADDRESS, user.address, 7n * LP); - await expect(tx2) - .to.emit(usdc, "Transfer") - .withArgs(user.address, liquidityHub.target, 7n * USDC); + await liquidityHub.connect(user).deposit(7n * USDC, user.address); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + expect(await liquidityHub.releasedAssets()).to.equal(1n * USDC); await liquidityHub.connect(user).withdraw(4n * USDC, user.address, user.address); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + expect(await liquidityHub.releasedAssets()).to.equal(4n * USDC); + await liquidityHub.connect(user).settle(user.address); expect(await lpToken.balanceOf(user.address)).to.equal(5n * LP); expect(await lpToken.totalSupply()).to.equal(5n * LP); expect(await liquidityHub.totalSupply()).to.equal(5n * LP); expect(await liquidityHub.totalAssets()).to.equal(5n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); expect(await liquidityHub.balanceOf(user.address)).to.equal(5n * LP); expect(await usdc.balanceOf(user.address)).to.equal(5n * USDC); - expect(await usdc.balanceOf(liquidityHub.target)).to.equal(5n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(5n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + }); + + it("Should allow to do initial 0 assets adjustment", async function () { + const { + lpToken, liquidityHub, usdc, deployer, user, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); + + await usdc.connect(deployer).transfer(user.address, 10n * USDC); + await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); + const tx = liquidityHub.connect(user).deposit(10n * USDC, user.address); + await expect(tx) + .to.emit(usdc, "Transfer") + .withArgs(user.address, liquidityPool.target, 10n * USDC); + await expect(tx) + .to.emit(liquidityHub, "DepositRequest") + .withArgs(user.address, user.address, 10n * USDC); + await expect(tx).to.emit(liquidityPool, "Deposit"); + expect(await lpToken.balanceOf(user.address)).to.equal(0n); + expect(await lpToken.totalSupply()).to.equal(0n); + expect(await liquidityHub.totalSupply()).to.equal(0n); + expect(await liquidityHub.totalAssets()).to.equal(0n); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(0n, 0n); + expect(await lpToken.balanceOf(user.address)).to.equal(0n); + expect(await lpToken.totalSupply()).to.equal(0n); + expect(await liquidityHub.totalSupply()).to.equal(10n * LP); + expect(await liquidityHub.totalAssets()).to.equal(10n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + const tx2 = liquidityHub.settle(user.address); + await expect(tx2) + .to.emit(liquidityHub, "Deposit") + .withArgs(user.address, user.address, 10n * USDC, 10n * LP); + expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP); + expect(await lpToken.totalSupply()).to.equal(10n * LP); + expect(await liquidityHub.totalSupply()).to.equal(10n * LP); + expect(await liquidityHub.totalAssets()).to.equal(10n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); }); + + it("Should allow to do 0 assets adjustment on empty hub", async function () { + const { + lpToken, liquidityHub, usdc, + liquidityPool, admin, + } = await loadFixture(deployAll); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(0n, 0n); + expect(await lpToken.totalSupply()).to.equal(0n); + expect(await liquidityHub.totalSupply()).to.equal(0n); + expect(await liquidityHub.totalAssets()).to.equal(0n); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + }); + + it("Should not allow others to do assets adjustment", async function () { + const {liquidityHub, user} = await loadFixture(deployAll); + + await expect(liquidityHub.connect(user).adjustTotalAssets(0n, INCREASE)) + .to.be.revertedWithCustomError(liquidityHub, "AccessControlUnauthorizedAccount(address,bytes32)"); + }); + + it("Should process deposits after adjustment with increased assets", async function () { + const { + lpToken, liquidityHub, usdc, deployer, user, user2, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); + + await usdc.connect(deployer).transfer(user.address, 20n * USDC); + await usdc.connect(deployer).transfer(user2.address, 40n * USDC); + await usdc.connect(user).approve(liquidityHub.target, 20n * USDC); + await usdc.connect(user2).approve(liquidityHub.target, 40n * USDC); + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, user2.address); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + expect(await lpToken.balanceOf(user.address)).to.equal(0n); + expect(await lpToken.balanceOf(user2.address)).to.equal(0n); + expect(await lpToken.totalSupply()).to.equal(0n); + expect(await liquidityHub.totalSupply()).to.equal(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(20n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC); + expect(await usdc.balanceOf(user2.address)).to.equal(20n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(30n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, user2.address); + await liquidityHub.connect(admin).adjustTotalAssets(120n * USDC, INCREASE); + expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP); + expect(await lpToken.balanceOf(user2.address)).to.equal(20n * LP); + expect(await lpToken.totalSupply()).to.equal(30n * LP); + expect(await liquidityHub.totalSupply()).to.equal(36n * LP); + expect(await liquidityHub.totalAssets()).to.equal(180n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(12n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(24n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(60n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + await liquidityHub.settle(user.address); + await liquidityHub.settle(user2.address); + expect(await lpToken.balanceOf(user.address)).to.equal(12n * LP); + expect(await lpToken.balanceOf(user2.address)).to.equal(24n * LP); + expect(await lpToken.totalSupply()).to.equal(36n * LP); + expect(await liquidityHub.totalSupply()).to.equal(36n * LP); + expect(await liquidityHub.totalAssets()).to.equal(180n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(12n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(24n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(60n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + }); + + it("Should process deposits after adjustment with decreased assets", async function () { + const { + lpToken, liquidityHub, usdc, deployer, user, user2, USDC, LP, + liquidityPool, admin, + } = await loadFixture(deployAll); + + await usdc.connect(deployer).transfer(user.address, 20n * USDC); + await usdc.connect(deployer).transfer(user2.address, 40n * USDC); + await usdc.connect(user).approve(liquidityHub.target, 20n * USDC); + await usdc.connect(user2).approve(liquidityHub.target, 40n * USDC); + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, user2.address); + await liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE); + expect(await lpToken.balanceOf(user.address)).to.equal(0n); + expect(await lpToken.balanceOf(user2.address)).to.equal(0n); + expect(await lpToken.totalSupply()).to.equal(0n); + expect(await liquidityHub.totalSupply()).to.equal(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(20n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC); + expect(await usdc.balanceOf(user2.address)).to.equal(20n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(30n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, user2.address); + await liquidityHub.connect(admin).adjustTotalAssets(20n * USDC, DECREASE); + expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP); + expect(await lpToken.balanceOf(user2.address)).to.equal(20n * LP); + expect(await lpToken.totalSupply()).to.equal(30n * LP); + expect(await liquidityHub.totalSupply()).to.equal(120n * LP); + expect(await liquidityHub.totalAssets()).to.equal(40n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(40n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(80n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(60n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + await liquidityHub.settle(user.address); + await liquidityHub.settle(user2.address); + expect(await lpToken.balanceOf(user.address)).to.equal(40n * LP); + expect(await lpToken.balanceOf(user2.address)).to.equal(80n * LP); + expect(await lpToken.totalSupply()).to.equal(120n * LP); + expect(await liquidityHub.totalSupply()).to.equal(120n * LP); + expect(await liquidityHub.totalAssets()).to.equal(40n * USDC); + expect(await liquidityHub.releasedAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(40n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(80n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(60n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + }); + + it.skip("Should process withdrawals after adjustment with increased assets", async function () {}); + + it.skip("Should process withdrawals after adjustment with decreased assets", async function () {}); + + it.skip("Should process deposits and withdrawals after adjustment with increased assets", async function () {}); + + it.skip("Should process deposits and withdrawals after adjustment with decreased assets", async function () {}); }); diff --git a/test/helpers.ts b/test/helpers.ts index 7494390..b7abb37 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -25,5 +25,5 @@ export async function deploy(contractName: string, signer: Signer, txParams: obj export function toBytes32(str: string) { if (str.length > 32) throw new Error("String too long"); - return zeroPadBytes(toUtf8Bytes(str), 64); + return zeroPadBytes(toUtf8Bytes(str), 32); }