diff --git a/.env.example b/.env.example index 63858a6..fab2ac3 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,7 @@ ADMIN= # USDC token address. Leave empty to deploy a test one instead. USDC= +# Deposits to Liquidity Hub are only allowed till this limit is reached. +ASSETS_LIMIT= BASETEST_PRIVATE_KEY= VERIFY=false diff --git a/contracts/LiquidityHub.sol b/contracts/LiquidityHub.sol index 8fdf86f..504eab9 100644 --- a/contracts/LiquidityHub.sol +++ b/contracts/LiquidityHub.sol @@ -6,40 +6,56 @@ 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 AssetsLimitSet(uint256 oldLimit, uint256 newLimit); error ZeroAddress(); error NotImplemented(); error IncompatibleAssetsAndShares(); + error AssetsLimitIsTooBig(); /// @custom:storage-location erc7201:sprinter.storage.LiquidityHub struct LiquidityHubStorage { uint256 totalAssets; + uint256 assetsLimit; } 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, + uint256 newAssetsLimit + ) external initializer() { ERC4626Upgradeable.__ERC4626_init(asset_); require( IERC20Metadata(address(asset_)).decimals() <= IERC20Metadata(address(SHARES)).decimals(), @@ -48,6 +64,28 @@ 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); + _setAssetsLimit(newAssetsLimit); + } + + function adjustTotalAssets(uint256 amount, bool isIncrease) external onlyRole(ASSETS_ADJUST_ROLE) { + LiquidityHubStorage storage $ = _getStorage(); + uint256 assets = $.totalAssets; + uint256 newAssets = isIncrease ? assets + amount : assets - amount; + $.totalAssets = newAssets; + emit TotalAssetsAdjustment(assets, newAssets); + } + + function setAssetsLimit(uint256 newAssetsLimit) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setAssetsLimit(newAssetsLimit); + } + + function _setAssetsLimit(uint256 newAssetsLimit) internal { + require(newAssetsLimit <= type(uint256).max / 10 ** _decimalsOffset(), AssetsLimitIsTooBig()); + LiquidityHubStorage storage $ = _getStorage(); + uint256 oldLimit = $.assetsLimit; + $.assetsLimit = newAssetsLimit; + emit AssetsLimitSet(oldLimit, newAssetsLimit); } function name() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) { @@ -91,26 +129,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 assetsLimit() public view returns (uint256) { + return _getStorage().assetsLimit; + } + + function maxDeposit(address) public view virtual override returns (uint256) { + uint256 total = totalAssets(); + uint256 limit = assetsLimit(); + if (total >= limit) { + return 0; + } + return limit - total; + } + + function maxMint(address) public view virtual override returns (uint256) { + return _convertToShares(maxDeposit(address(0)), Math.Rounding.Floor); + } + + 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 { @@ -128,8 +219,12 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable { } function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { - super._deposit(caller, receiver, assets, shares); - _getStorage().totalAssets += assets; + LiquidityHubStorage storage $ = _getStorage(); + SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(LIQUIDITY_POOL), assets); + _mint(receiver, shares); + $.totalAssets += assets; + LIQUIDITY_POOL.deposit(); + emit Deposit(caller, receiver, assets, shares); } function _withdraw( @@ -139,8 +234,14 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable { uint256 assets, uint256 shares ) internal virtual override { - _getStorage().totalAssets -= assets; - super._withdraw(caller, receiver, owner, assets, shares); + LiquidityHubStorage storage $ = _getStorage(); + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + $.totalAssets -= assets; + _burn(owner, shares); + 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..ab7efac 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -2,16 +2,20 @@ import dotenv from "dotenv"; dotenv.config(); import hre from "hardhat"; -import {isAddress} from "ethers"; -import {getContractAt, getCreateAddress, deploy} from "../test/helpers"; +import {isAddress, MaxUint256, getBigInt} from "ethers"; +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; + const maxLimit: bigint = MaxUint256 / 10n ** 12n; + const assetsLimit: bigint = getBigInt(process.env.ASSETS_LIMIT || maxLimit); let usdc: string; if (isAddress(process.env.USDC)) { usdc = process.env.USDC; @@ -20,6 +24,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 +37,11 @@ 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, assetsLimit + )).data; const liquidityHubProxy = (await verifier.deploy( "TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2}, liquidityHubImpl.target, admin, liquidityHubInit @@ -43,6 +52,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/scripts/helpers.ts b/scripts/helpers.ts index 2ce91c6..355dd1a 100644 --- a/scripts/helpers.ts +++ b/scripts/helpers.ts @@ -7,12 +7,19 @@ export function assert(condition: boolean, message: string): void { throw new Error(message); }; -function sleep(msec: number): Promise { +export function sleep(msec: number): Promise { return new Promise((resolve) => { setTimeout(() => resolve(true), msec); }); }; +export function isSet(input?: string): boolean { + if (input) { + return input.length > 0; + } + return false; +}; + export function getVerifier() { interface VerificationInput { address: string; diff --git a/test/LiquidityHub.ts b/test/LiquidityHub.ts index c339bb4..d032fa7 100644 --- a/test/LiquidityHub.ts +++ b/test/LiquidityHub.ts @@ -3,19 +3,27 @@ import { } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import {expect} from "chai"; import hre from "hardhat"; +import {Signature, resolveAddress, MaxUint256, getBigInt} from "ethers"; 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 +36,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, getBigInt(MaxUint256) * USDC / LP) + ).data; const liquidityHubProxy = (await deploy( "TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2}, liquidityHubImpl.target, admin, liquidityHubInit @@ -39,19 +49,25 @@ 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, USDC, LP} = 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); expect(await liquidityHub.totalAssets()).to.equal(0n); expect(await liquidityHub.allowance(user.address, user2.address)).to.equal(0n); + expect(await liquidityHub.assetsLimit()).to.equal(getBigInt(MaxUint256) * USDC / LP); + expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(getBigInt(MaxUint256) * USDC / LP); + expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(getBigInt(MaxUint256) * USDC / LP * LP / USDC); await expect(liquidityHub.name()) .to.be.revertedWithCustomError(liquidityHub, "NotImplemented()"); @@ -68,7 +84,7 @@ 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} = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -78,18 +94,19 @@ describe("LiquidityHub", function () { .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); 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(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); }); 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} = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -99,25 +116,26 @@ describe("LiquidityHub", function () { .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); 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); 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(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); }); 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} = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -127,18 +145,19 @@ describe("LiquidityHub", function () { .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); 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(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); }); 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} = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -149,7 +168,7 @@ describe("LiquidityHub", function () { .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) .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); @@ -157,10 +176,11 @@ describe("LiquidityHub", function () { expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.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} = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -171,7 +191,7 @@ describe("LiquidityHub", function () { .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) .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); @@ -179,10 +199,14 @@ describe("LiquidityHub", function () { expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.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, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -194,7 +218,7 @@ describe("LiquidityHub", function () { .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) .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); @@ -203,10 +227,14 @@ describe("LiquidityHub", function () { expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user2.address)).to.equal(10n * USDC); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.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, + } = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -218,7 +246,7 @@ describe("LiquidityHub", function () { .withArgs(user.address, ZERO_ADDRESS, 10n * LP); await expect(tx) .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); @@ -227,10 +255,11 @@ describe("LiquidityHub", function () { expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); expect(await usdc.balanceOf(user2.address)).to.equal(10n * USDC); expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.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} = await loadFixture(deployAll); await usdc.connect(deployer).transfer(user.address, 10n * USDC); await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); @@ -240,7 +269,7 @@ describe("LiquidityHub", function () { .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 liquidityHub.connect(user).withdraw(1n * USDC, user.address, user.address); const tx2 = liquidityHub.connect(user).deposit(7n * USDC, user.address); await expect(tx2) @@ -248,7 +277,7 @@ describe("LiquidityHub", function () { .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 liquidityHub.connect(user).withdraw(4n * USDC, user.address, user.address); expect(await lpToken.balanceOf(user.address)).to.equal(5n * LP); expect(await lpToken.totalSupply()).to.equal(5n * LP); @@ -256,6 +285,439 @@ describe("LiquidityHub", function () { expect(await liquidityHub.totalAssets()).to.equal(5n * USDC); 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(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(5n * USDC); + }); + + 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(lpToken, "Transfer") + .withArgs(ZERO_ADDRESS, user.address, 10n * LP); + await expect(tx) + .to.emit(usdc, "Transfer") + .withArgs(user.address, liquidityPool.target, 10n * USDC); + 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(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + + await expect(liquidityHub.connect(admin).adjustTotalAssets(0n, INCREASE)) + .to.emit(liquidityHub, "TotalAssetsAdjustment") + .withArgs(10n * USDC, 10n * USDC); + 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(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + }); + + 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 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 allow 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); + 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(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + 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(admin).adjustTotalAssets(120n * USDC, INCREASE); + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, 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.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); + 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(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + 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(admin).adjustTotalAssets(20n * USDC, DECREASE); + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, 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.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("Should allow withdrawals 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, 10n * USDC); + await usdc.connect(deployer).transfer(user2.address, 20n * USDC); + await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); + await usdc.connect(user2).approve(liquidityHub.target, 20n * USDC); + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, user2.address); + 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(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + 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(0n); + expect(await usdc.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(30n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + await liquidityHub.connect(admin).adjustTotalAssets(12n * USDC, INCREASE); + expect(await liquidityHub.totalAssets()).to.equal(42n * USDC); + await liquidityHub.connect(user).redeem(5n * LP, user.address, user.address); + await liquidityHub.connect(user2).redeem(10n * LP, user2.address, user2.address); + expect(await lpToken.balanceOf(user.address)).to.equal(5n * LP); + expect(await lpToken.balanceOf(user2.address)).to.equal(10n * LP); + expect(await lpToken.totalSupply()).to.equal(15n * LP); + expect(await liquidityHub.totalSupply()).to.equal(15n * LP); + expect(await liquidityHub.totalAssets()).to.equal(21n * USDC); + expect(await liquidityHub.balanceOf(user.address)).to.equal(5n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(10n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(7n * USDC); + expect(await usdc.balanceOf(user2.address)).to.equal(14n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(9n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + }); + + it("Should allow withdrawals 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, 10n * USDC); + await usdc.connect(deployer).transfer(user2.address, 20n * USDC); + await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); + await usdc.connect(user2).approve(liquidityHub.target, 20n * USDC); + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + await liquidityHub.connect(user2).deposit(20n * USDC, user2.address); + 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(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + 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(0n); + expect(await usdc.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(30n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + + await liquidityHub.connect(admin).adjustTotalAssets(27n * USDC, DECREASE); + expect(await liquidityHub.totalAssets()).to.equal(3n * USDC); + await liquidityHub.connect(user).redeem(10n * LP, user.address, user.address); + await liquidityHub.connect(user2).redeem(20n * LP, user2.address, user2.address); + 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(0n); + expect(await liquidityHub.totalAssets()).to.equal(0n); + expect(await liquidityHub.balanceOf(user.address)).to.equal(0n); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(user.address)).to.equal(1n * USDC); + expect(await usdc.balanceOf(user2.address)).to.equal(2n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(27n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + }); + + it("Should allow deposits and withdrawals 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); + 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(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + 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(admin).adjustTotalAssets(12n * USDC, INCREASE); + expect(await liquidityHub.totalAssets()).to.equal(42n * USDC); + await liquidityHub.connect(user).redeem(5n * LP, user.address, user.address); + await liquidityHub.connect(user2).redeem(10n * LP, user2.address, user2.address); + await liquidityHub.connect(user).deposit(7n * USDC, user.address); + await liquidityHub.connect(user2).deposit(14n * USDC, user2.address); + 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(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(42n * USDC); + 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); + }); + + it("Should allow deposits and withdrawals 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); + 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(30n * LP); + expect(await liquidityHub.totalAssets()).to.equal(30n * USDC); + 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(admin).adjustTotalAssets(27n * USDC, DECREASE); + expect(await liquidityHub.totalAssets()).to.equal(3n * USDC); + await liquidityHub.connect(user).redeem(10n * LP, user.address, user.address); + await liquidityHub.connect(user2).redeem(20n * LP, user2.address, user2.address); + await liquidityHub.connect(user).deposit(6n * USDC, user.address); + await liquidityHub.connect(user2).deposit(12n * USDC, user2.address); + expect(await lpToken.balanceOf(user.address)).to.equal(6n * LP); + expect(await lpToken.balanceOf(user2.address)).to.equal(12n * LP); + expect(await lpToken.totalSupply()).to.equal(18n * LP); + expect(await liquidityHub.totalSupply()).to.equal(18n * LP); + expect(await liquidityHub.totalAssets()).to.equal(18n * USDC); + expect(await liquidityHub.balanceOf(user.address)).to.equal(6n * LP); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(12n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(5n * USDC); + expect(await usdc.balanceOf(user2.address)).to.equal(10n * USDC); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(45n * USDC); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + }); + + it("Should allow to deposit with permit", async function () { + const {lpToken, liquidityHub, usdc, deployer, user, user2, USDC, LP, liquidityPool} = await loadFixture(deployAll); + + const domain = { + name: "Circle USD", + version: "1", + chainId: hre.network.config.chainId, + verifyingContract: await resolveAddress(usdc), + }; + + const types = { + Permit: [ + {name: "owner", type: "address"}, + {name: "spender", type: "address"}, + {name: "value", type: "uint256"}, + {name: "nonce", type: "uint256"}, + {name: "deadline", type: "uint256"}, + ], + }; + + await usdc.connect(deployer).transfer(user.address, 10n * USDC); + const permitSig = Signature.from(await user.signTypedData(domain, types, { + owner: user.address, + spender: liquidityHub.target, + value: 10n * USDC, + nonce: 0n, + deadline: 2000000000n, + })); + const tx = liquidityHub.connect(user).depositWithPermit( + 10n * USDC, + user2.address, + 2000000000n, + permitSig.v, + permitSig.r, + permitSig.s, + ); + await expect(tx) + .to.emit(lpToken, "Transfer") + .withArgs(ZERO_ADDRESS, user2.address, 10n * LP); + await expect(tx) + .to.emit(usdc, "Transfer") + .withArgs(user.address, liquidityPool.target, 10n * USDC); + expect(await lpToken.balanceOf(user.address)).to.equal(0n); + expect(await lpToken.balanceOf(user2.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(0n); + expect(await liquidityHub.balanceOf(user2.address)).to.equal(10n * LP); + expect(await usdc.balanceOf(user.address)).to.equal(0n); + expect(await usdc.balanceOf(user2.address)).to.equal(0n); + expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + expect(await usdc.allowance(user.address, liquidityHub.target)).to.equal(0n); + }); + + it("Should allow admin to set assets limit", async function () { + const {liquidityHub, deployer, admin, user, usdc, lpToken, USDC, LP, liquidityPool} = await loadFixture(deployAll); + + const tx = liquidityHub.connect(admin).setAssetsLimit(0n); + await expect(tx) + .to.emit(liquidityHub, "AssetsLimitSet") + .withArgs(getBigInt(MaxUint256) * USDC / LP, 0n); + + expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(0n); + expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(0n); + + await usdc.connect(deployer).transfer(user.address, 10n * USDC); + await usdc.connect(user).approve(liquidityHub.target, 10n * USDC); + + await expect(liquidityHub.connect(user).deposit(1n, user.address)) + .to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxDeposit(address,uint256,uint256)"); + await expect(liquidityHub.connect(user).mint(LP, user.address)) + .to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxMint(address,uint256,uint256)"); + + const tx2 = liquidityHub.connect(admin).setAssetsLimit(100n * USDC); + await expect(tx2) + .to.emit(liquidityHub, "AssetsLimitSet") + .withArgs(0n, 100n * USDC); + + expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(100n * USDC); + expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(100n * LP); + + await liquidityHub.connect(user).deposit(10n * USDC, user.address); + + expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(90n * USDC); + expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(90n * 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(0n); + expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC); + + await expect(liquidityHub.connect(user).deposit(90n * USDC + 1n, user.address)) + .to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxDeposit(address,uint256,uint256)"); + await expect(liquidityHub.connect(user).mint(90n * LP + 1n, user.address)) + .to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxMint(address,uint256,uint256)"); + + await liquidityHub.connect(admin).adjustTotalAssets(10n * USDC, INCREASE); + + expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(80n * USDC); + expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(40n * LP); + + await expect(liquidityHub.connect(user).deposit(80n * USDC + 1n, user.address)) + .to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxDeposit(address,uint256,uint256)"); + await expect(liquidityHub.connect(user).mint(40n * LP + 1n, user.address)) + .to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxMint(address,uint256,uint256)"); + + await liquidityHub.connect(admin).adjustTotalAssets(10n * USDC, DECREASE); + + expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(90n * USDC); + expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(90n * LP); + }); + + it("Should not allow others to set assets limit", async function () { + const {liquidityHub, user} = await loadFixture(deployAll); + + await expect(liquidityHub.connect(user).setAssetsLimit(0n)) + .to.be.revertedWithCustomError(liquidityHub, "AccessControlUnauthorizedAccount(address,bytes32)"); }); }); 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); }