diff --git a/contracts/index/IFeePool.sol b/contracts/index/IFeeVault.sol similarity index 96% rename from contracts/index/IFeePool.sol rename to contracts/index/IFeeVault.sol index a4cce388..0469d35e 100644 --- a/contracts/index/IFeePool.sol +++ b/contracts/index/IFeeVault.sol @@ -2,9 +2,9 @@ pragma solidity 0.6.11; /** - * @notice For pools that can charge an early withdraw fee + * @notice For vaults that can charge an early withdraw fee */ -interface IFeePool { +interface IFeeVault { /** * @notice Log when the arbitrage fee period changes * @param arbitrageFeePeriod The new period diff --git a/contracts/index/ILockingPool.sol b/contracts/index/ILockingVault.sol similarity index 90% rename from contracts/index/ILockingPool.sol rename to contracts/index/ILockingVault.sol index ba523e67..7e43fede 100644 --- a/contracts/index/ILockingPool.sol +++ b/contracts/index/ILockingVault.sol @@ -2,9 +2,9 @@ pragma solidity 0.6.11; /** - * @notice For pools that can be locked and unlocked in emergencies + * @notice For vaults that can be locked and unlocked in emergencies */ -interface ILockingPool { +interface ILockingVault { /** @notice Log when deposits are locked */ event DepositLocked(); diff --git a/contracts/index/IReservePool.sol b/contracts/index/IReserveVault.sol similarity index 87% rename from contracts/index/IReservePool.sol rename to contracts/index/IReserveVault.sol index 3829375b..02c29354 100644 --- a/contracts/index/IReservePool.sol +++ b/contracts/index/IReserveVault.sol @@ -2,9 +2,9 @@ pragma solidity 0.6.11; /** - * @notice For pools that keep a separate reserve of tokens + * @notice For vaults that keep a separate reserve of tokens */ -interface IReservePool { +interface IReserveVault { /** * @notice Log when the percent held in reserve is changed * @param reservePercentage The new percent held in reserve @@ -19,7 +19,7 @@ interface IReservePool { /** * @notice Transfer an amount of tokens to the LP Account - * @dev This should only be callable by the `MetaPoolToken` + * @dev This should only be callable by the `LpAccountFunder` * @param amount The amount of tokens */ function transferToLpAccount(uint256 amount) external; diff --git a/contracts/index/Imports.sol b/contracts/index/Imports.sol index 45909bb4..5045f79f 100644 --- a/contracts/index/Imports.sol +++ b/contracts/index/Imports.sol @@ -2,6 +2,6 @@ pragma solidity 0.6.11; import {IERC4626} from "./IERC4626.sol"; -import {IFeePool} from "./IFeePool.sol"; -import {ILockingPool} from "./ILockingPool.sol"; -import {IReservePool} from "./IReservePool.sol"; +import {IFeeVault} from "./IFeeVault.sol"; +import {ILockingVault} from "./ILockingVault.sol"; +import {IReserveVault} from "./IReserveVault.sol"; diff --git a/contracts/index/IndexToken.sol b/contracts/index/IndexToken.sol index 008abea0..eac94e30 100644 --- a/contracts/index/IndexToken.sol +++ b/contracts/index/IndexToken.sol @@ -19,24 +19,22 @@ import { AggregatorV3Interface, IOracleAdapter } from "contracts/oracle/Imports.sol"; -import {MetaPoolToken} from "contracts/mapt/MetaPoolToken.sol"; -import {IERC4626, IFeePool, ILockingPool, IReservePool} from "./Imports.sol"; +import {IERC4626, IFeeVault, ILockingVault, IReserveVault} from "./Imports.sol"; /** * @notice Collect user deposits so they can be lent to the LP Account - * @notice Depositors share pool liquidity + * @notice Depositors share vault liquidity * @notice Reserves are maintained to process withdrawals * @notice Reserve tokens cannot be lent to the LP Account * @notice If a user withdraws too early after their deposit, there's a fee - * @notice Tokens borrowed from the pool are tracked with the `MetaPoolToken` */ contract IndexToken is IERC4626, IEmergencyExit, - IFeePool, - ILockingPool, - IReservePool, + IFeeVault, + ILockingVault, + IReserveVault, Initializable, AccessControlUpgradeSafe, ReentrancyGuardUpgradeSafe, @@ -81,7 +79,7 @@ contract IndexToken is */ uint256 public override withdrawFee; - /** @notice percentage of pool total value available for immediate withdrawal */ + /** @notice percentage of vault total value available for immediate withdrawal */ uint256 public override reservePercentage; /* ------------------------------- */ @@ -125,7 +123,10 @@ contract IndexToken is _setupRole(DEFAULT_ADMIN_ROLE, addressRegistry.emergencySafeAddress()); _setupRole(ADMIN_ROLE, addressRegistry.adminSafeAddress()); _setupRole(EMERGENCY_ROLE, addressRegistry.emergencySafeAddress()); - _setupRole(CONTRACT_ROLE, addressRegistry.mAptAddress()); + _setupRole( + CONTRACT_ROLE, + addressRegistry.getAddress("lpAccountFunder") + ); arbitrageFeePeriod = 1 days; arbitrageFee = 5; @@ -235,7 +236,7 @@ contract IndexToken is } /** - * @dev May revert if there is not enough in the pool. + * @dev May revert if there is not enough in the vault. */ function redeem( uint256 shares, @@ -401,7 +402,7 @@ contract IndexToken is function getUsdValue(uint256 shareAmount) external view returns (uint256) { if (shareAmount == 0) return 0; require(totalSupply() > 0, "INSUFFICIENT_TOTAL_SUPPLY"); - return shareAmount.mul(getPoolTotalValue()).div(totalSupply()); + return shareAmount.mul(getVaultTotalValue()).div(totalSupply()); } function getReserveTopUpValue() external view override returns (int256) { @@ -529,11 +530,11 @@ contract IndexToken is } /** - * @dev Total value also includes that have been borrowed from the pool - * @dev Typically it is the LP Account that borrows from the pool + * @dev Total value also includes that have been borrowed from the vault + * @dev Typically it is the LP Account that borrows from the vault */ - function getPoolTotalValue() public view returns (uint256) { - uint256 assetValue = _getPoolAssetValue(); + function getVaultTotalValue() public view returns (uint256) { + uint256 assetValue = _getVaultAssetValue(); uint256 mAptValue = _getDeployedValue(); return assetValue.add(mAptValue); } @@ -558,10 +559,10 @@ contract IndexToken is /** * @dev amount of share minted should be in same ratio to share supply - * as deposit value is to pool's total value, i.e.: + * as deposit value is to vault's total value, i.e.: * * mint amount / total supply - * = deposit value / pool total value + * = deposit value / vault total value * * For denominators, pre or post-deposit amounts can be used. * The important thing is they are consistent, i.e. both pre-deposit @@ -581,7 +582,7 @@ contract IndexToken is // mathematically equivalent to: // assets.mul(supply).div(totalAssets()) // but better precision due to avoiding early division - uint256 totalValue = getPoolTotalValue(); + uint256 totalValue = getVaultTotalValue(); uint256 assetPrice = getAssetPrice(); return assets.mul(supply).mul(assetPrice).div(totalValue).div( @@ -605,7 +606,7 @@ contract IndexToken is // mathematically equivalent to: // shares.mul(totalAssets()).div(supply) // but better precision due to avoiding early division - uint256 totalValue = getPoolTotalValue(); + uint256 totalValue = getVaultTotalValue(); uint256 assetPrice = getAssetPrice(); return shares.mul(totalValue).mul(10**decimals).div(assetPrice).div( @@ -614,7 +615,7 @@ contract IndexToken is } function totalAssets() public view virtual override returns (uint256) { - uint256 totalValue = getPoolTotalValue(); + uint256 totalValue = getVaultTotalValue(); uint256 assetPrice = getAssetPrice(); uint256 decimals = IDetailedERC20(asset).decimals(); return totalValue.mul(10**decimals).div(assetPrice); @@ -629,21 +630,21 @@ contract IndexToken is /** * @dev This "top-up" value should satisfy: * - * top-up USD value + pool underlyer USD value - * = (reserve %) * pool deployed value (after unwinding) + * top-up USD value + vault underlyer USD value + * = (reserve %) * vault deployed value (after unwinding) * - * @dev Taking the percentage of the pool's current deployed value + * @dev Taking the percentage of the vault's current deployed value * is not sufficient, because the requirement is to have the * resulting values after unwinding capital satisfy the * above equation. * * More precisely: * - * R_pre = pool underlyer USD value before pushing unwound - * capital to the pool - * R_post = pool underlyer USD value after pushing - * DV_pre = pool's deployed USD value before unwinding - * DV_post = pool's deployed USD value after unwinding + * R_pre = vault underlyer USD value before pushing unwound + * capital to the vault + * R_post = vault underlyer USD value after pushing + * DV_pre = vault's deployed USD value before unwinding + * DV_post = vault's deployed USD value after unwinding * rPerc = the reserve percentage as a whole number * out of 100 * @@ -661,7 +662,7 @@ contract IndexToken is function _getReserveTopUpValue() internal view returns (int256) { uint256 unnormalizedTargetValue = _getDeployedValue().mul(reservePercentage); - uint256 unnormalizedAssetValue = _getPoolAssetValue().mul(100); + uint256 unnormalizedAssetValue = _getVaultAssetValue().mul(100); require( unnormalizedTargetValue <= uint256(type(int256).max), @@ -679,10 +680,10 @@ contract IndexToken is } /** - * @notice Get the USD value of tokens in the pool + * @notice Get the USD value of tokens in the vault * @return The USD value */ - function _getPoolAssetValue() internal view returns (uint256) { + function _getVaultAssetValue() internal view returns (uint256) { return getValueFromAssetAmount( IDetailedERC20(asset).balanceOf(address(this)) @@ -690,14 +691,16 @@ contract IndexToken is } /** - * @notice Get the USD value of tokens owed to the pool - * @dev Tokens from the pool are typically borrowed by the LP Account - * @dev Tokens borrowed from the pool are tracked with mAPT - * @return The USD value + * @notice Get the USD value of tokens owed to the vault + * @dev Tokens from the vault are typically borrowed by the LP Account + * @return The USD value. USD prices have 8 decimals. */ function _getDeployedValue() internal view returns (uint256) { - MetaPoolToken mApt = MetaPoolToken(addressRegistry.mAptAddress()); - return mApt.getDeployedValue(address(this)); + if (totalSupply() == 0) return 0; + + IOracleAdapter oracleAdapter = + IOracleAdapter(addressRegistry.oracleAdapterAddress()); + return oracleAdapter.getTvl(); } function _previewRedeem(uint256 shareAmount, bool arbFee) diff --git a/contracts/index/LpAccountFunder.sol b/contracts/index/LpAccountFunder.sol new file mode 100644 index 00000000..156ecaf8 --- /dev/null +++ b/contracts/index/LpAccountFunder.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.6.11; +pragma experimental ABIEncoderV2; + +import { + AccessControl, + IDetailedERC20, + ReentrancyGuard +} from "contracts/common/Imports.sol"; +import { + Address, + SafeERC20, + SafeMath, + SignedSafeMath +} from "contracts/libraries/Imports.sol"; +import {ILpAccount} from "contracts/lpaccount/Imports.sol"; +import {IAddressRegistryV2} from "contracts/registry/Imports.sol"; +import {ILockingOracle} from "contracts/oracle/Imports.sol"; +import {IERC4626, IReserveVault} from "contracts/index/Imports.sol"; +import { + IErc20Allocation, + IAssetAllocationRegistry, + Erc20AllocationConstants +} from "contracts/tvl/Imports.sol"; + +/** + * @notice This contract is permissioned to transfer funds between the vault + * and the LP Account contract. + */ +contract LpAccountFunder is + AccessControl, + ReentrancyGuard, + Erc20AllocationConstants +{ + using Address for address; + using SafeMath for uint256; + using SignedSafeMath for int256; + using SafeERC20 for IDetailedERC20; + + IAddressRegistryV2 public addressRegistry; + address public indexToken; + + /* ------------------------------- */ + + event AddressRegistryChanged(address); + event IndexTokenChanged(address); + event FundLpAccount(uint256); + event WithdrawFromLpAccount(uint256); + + /** + * @dev Since the proxy delegate calls to this "logic" contract, any + * storage set by the logic contract's constructor during deploy is + * disregarded and this function is needed to initialize the proxy + * contract's storage according to this contract's layout. + * + * Since storage is not set yet, there is no simple way to protect + * calling this function with owner modifiers. Thus the OpenZeppelin + * `initializer` modifier protects this function from being called + * repeatedly. It should be called during the deployment so that + * it cannot be called by someone else later. + */ + constructor(address addressRegistry_, address indexToken_) public { + _setIndexToken(indexToken_); + _setAddressRegistry(addressRegistry_); + _setupRole(DEFAULT_ADMIN_ROLE, addressRegistry.emergencySafeAddress()); + _setupRole(LP_ROLE, addressRegistry.lpSafeAddress()); + _setupRole(EMERGENCY_ROLE, addressRegistry.emergencySafeAddress()); + } + + /** + * @notice Sets the address registry + * @param addressRegistry_ the address of the registry + */ + function emergencySetAddressRegistry(address addressRegistry_) + external + nonReentrant + onlyEmergencyRole + { + _setAddressRegistry(addressRegistry_); + } + + function fundLpAccount() external nonReentrant onlyLpRole { + int256 amount = getRebalanceAmount(); + uint256 fundAmount = _getFundAmount(amount); + + _fundLpAccount(fundAmount); + _registerPoolUnderlyer(); + + emit FundLpAccount(fundAmount); + } + + function withdrawFromLpAccount() external nonReentrant onlyLpRole { + int256 topupAmount = getRebalanceAmount(); + + uint256 lpAccountBalance = getLpAccountBalance(); + uint256 withdrawAmount = + _calculateAmountToWithdraw(topupAmount, lpAccountBalance); + + _withdrawFromLpAccount(withdrawAmount); + emit WithdrawFromLpAccount(withdrawAmount); + } + + /** + * @notice Returns the (signed) top-up amount for each pool ID given. + * A positive (negative) sign means the reserve level is in deficit + * (excess) of required percentage. + * @return rebalanceAmount + */ + function getRebalanceAmount() public view returns (int256 rebalanceAmount) { + rebalanceAmount = IReserveVault(indexToken).getReserveTopUpValue(); + } + + function getLpAccountBalance() + public + view + returns (uint256 lpAccountBalance) + { + IERC4626 vault = IERC4626(indexToken); + IDetailedERC20 asset = IDetailedERC20(vault.asset()); + + address lpAccountAddress = addressRegistry.lpAccountAddress(); + lpAccountBalance = asset.balanceOf(lpAccountAddress); + } + + function _setAddressRegistry(address addressRegistry_) internal { + require(addressRegistry_.isContract(), "INVALID_ADDRESS"); + addressRegistry = IAddressRegistryV2(addressRegistry_); + emit AddressRegistryChanged(addressRegistry_); + } + + function _setIndexToken(address indexToken_) internal { + require(indexToken_.isContract(), "INVALID_ADDRESS"); + indexToken = indexToken_; + emit IndexTokenChanged(indexToken_); + } + + function _fundLpAccount(uint256 amount) internal { + address lpAccountAddress = addressRegistry.lpAccountAddress(); + require(lpAccountAddress != address(0), "INVALID_LP_ACCOUNT"); // defensive check -- should never happen + + IReserveVault(indexToken).transferToLpAccount(amount); + + ILockingOracle oracleAdapter = _getOracleAdapter(); + oracleAdapter.lock(); + } + + /** + * @dev Transfer the specified amounts to pools, doing mAPT burns, + * and checking the transferred tokens have been registered. + */ + function _withdrawFromLpAccount(uint256 amount) internal { + address lpAccount = addressRegistry.lpAccountAddress(); + ILpAccount(lpAccount).transferToPool(indexToken, amount); + + ILockingOracle oracleAdapter = _getOracleAdapter(); + oracleAdapter.lock(); + } + + /** + * @notice Register an asset allocation for the account with each pool underlyer + */ + function _registerPoolUnderlyer() internal { + IAssetAllocationRegistry tvlManager = + IAssetAllocationRegistry(addressRegistry.getAddress("tvlManager")); + IErc20Allocation erc20Allocation = + IErc20Allocation( + address( + tvlManager.getAssetAllocation(Erc20AllocationConstants.NAME) + ) + ); + + IERC4626 vault = IERC4626(indexToken); + IDetailedERC20 asset = IDetailedERC20(vault.asset()); + + if (!erc20Allocation.isErc20TokenRegistered(asset)) { + erc20Allocation.registerErc20Token(asset); + } + } + + function _getOracleAdapter() internal view returns (ILockingOracle) { + address oracleAdapterAddress = addressRegistry.oracleAdapterAddress(); + return ILockingOracle(oracleAdapterAddress); + } + + function _getFundAmount(int256 amount) + internal + pure + returns (uint256 fundAmount) + { + fundAmount = amount < 0 ? uint256(-amount) : 0; + } + + /** + * @dev Calculate amounts used for topup, taking into + * account the available LP Account balances. + */ + function _calculateAmountToWithdraw( + int256 topupAmount, + uint256 lpAccountBalance + ) internal pure returns (uint256 withdrawAmount) { + withdrawAmount = topupAmount > 0 ? uint256(topupAmount) : 0; + withdrawAmount = withdrawAmount > lpAccountBalance + ? lpAccountBalance + : withdrawAmount; + } +} diff --git a/contracts/index/TestIndexToken.sol b/contracts/index/TestIndexToken.sol index 92d95677..3625c7fc 100644 --- a/contracts/index/TestIndexToken.sol +++ b/contracts/index/TestIndexToken.sol @@ -21,8 +21,8 @@ contract TestIndexToken is IndexToken { return _getDeployedValue(); } - function testGetPoolAssetValue() public view returns (uint256) { - return _getPoolAssetValue(); + function testGetVaultAssetValue() public view returns (uint256) { + return _getVaultAssetValue(); } function testGetAssetAmountAfterFees(uint256 amount, bool arbFee) diff --git a/contracts/index/TestLpAccountFunder.sol b/contracts/index/TestLpAccountFunder.sol new file mode 100644 index 00000000..199777ae --- /dev/null +++ b/contracts/index/TestLpAccountFunder.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.6.11; +pragma experimental ABIEncoderV2; + +import {LpAccountFunder, ILockingOracle} from "./LpAccountFunder.sol"; + +contract TestLpAccountFunder is LpAccountFunder { + constructor(address addressRegistry_, address indexToken_) + public + LpAccountFunder(addressRegistry_, indexToken_) + {} // solhint-disable-line no-empty-blocks + + function testFundLpAccount(uint256 amount) external { + _fundLpAccount(amount); + } + + function testWithdrawFromLpAccount(uint256 amount) external { + _withdrawFromLpAccount(amount); + } + + function testRegisterPoolUnderlyer() external { + _registerPoolUnderlyer(); + } + + function testGetOracleAdapter() external view returns (ILockingOracle) { + return _getOracleAdapter(); + } + + function testGetFundAmount(int256 amount) external pure returns (uint256) { + return _getFundAmount(amount); + } + + function testCalculateAmountToWithdraw( + int256 topupAmount, + uint256 lpAccountBalance + ) external pure returns (uint256) { + return _calculateAmountToWithdraw(topupAmount, lpAccountBalance); + } +} diff --git a/test-integration/IndexToken.js b/test-integration/IndexToken.js index cd0f6a5b..630dba67 100644 --- a/test-integration/IndexToken.js +++ b/test-integration/IndexToken.js @@ -1,7 +1,7 @@ const { assert, expect } = require("chai"); const { ethers } = require("hardhat"); const { AddressZero: ZERO_ADDRESS, MaxUint256: MAX_UINT256 } = ethers.constants; -const { impersonateAccount, bytes32 } = require("../utils/helpers"); +const { bytes32 } = require("../utils/helpers"); const timeMachine = require("ganache-time-traveler"); const { WHALE_POOLS, FARM_TOKENS } = require("../utils/constants"); const { @@ -21,6 +21,11 @@ const link = (amount) => tokenAmountToBigNumber(amount, "18"); console.debugging = false; /* ************************ */ +const vaultAssetSymbol = "USDC"; +const vaultAssetAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +// use usdc agg for now +const vaultAggAddress = "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"; + describe("Contract: IndexToken", () => { let deployer; let oracle; @@ -30,6 +35,13 @@ describe("Contract: IndexToken", () => { let anotherUser; let receiver; + let tvlAgg; + let asset; + let oracleAdapter; + let lpAccountFunder; + let addressRegistry; + let indexToken; + before(async () => { [ deployer, @@ -39,6 +51,7 @@ describe("Contract: IndexToken", () => { randomUser, anotherUser, receiver, + lpAccountFunder, ] = await ethers.getSigners(); }); @@ -64,19 +77,8 @@ describe("Contract: IndexToken", () => { await timeMachine.revertToSnapshot(suiteSnapshotId); }); - const symbol = "USDC"; - const tokenAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - const USDC_AGG_ADDRESS = "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"; - - let tvlAgg; - let asset; - let oracleAdapter; - let mApt; - let addressRegistry; - let indexToken; - before("Setup", async () => { - asset = await ethers.getContractAt("IDetailedERC20", tokenAddress); + asset = await ethers.getContractAt("IDetailedERC20", vaultAssetAddress); const paymentAmount = link("1"); const maxSubmissionValue = tokenAmountToBigNumber("1", "20"); @@ -155,30 +157,21 @@ describe("Contract: IndexToken", () => { const proxyAdmin = await ProxyAdmin.deploy(); await proxyAdmin.deployed(); - const MetaPoolToken = await ethers.getContractFactory("TestMetaPoolToken"); - const mAptLogic = await MetaPoolToken.deploy(); - await mAptLogic.deployed(); - - const mAptInitData = MetaPoolToken.interface.encodeFunctionData( - "initialize(address)", - [addressRegistry.address] - ); - const mAptProxy = await TransparentUpgradeableProxy.deploy( - mAptLogic.address, - proxyAdmin.address, - mAptInitData + await addressRegistry.registerAddress( + bytes32("lpAccountFunder"), + lpAccountFunder.address ); - await mAptProxy.deployed(); - mApt = await MetaPoolToken.attach(mAptProxy.address); - await addressRegistry.registerAddress(bytes32("mApt"), mApt.address); + // dummy address needed for oracle adapter deploy + const mAptAddress = await generateContractAddress(deployer); + await addressRegistry.registerAddress(bytes32("mApt"), mAptAddress); const OracleAdapter = await ethers.getContractFactory("OracleAdapter"); oracleAdapter = await OracleAdapter.deploy( addressRegistry.address, tvlAgg.address, - [tokenAddress], - [USDC_AGG_ADDRESS], + [vaultAssetAddress], + [vaultAggAddress], 86400, 86400 ); @@ -206,7 +199,7 @@ describe("Contract: IndexToken", () => { indexToken = await IndexToken.attach(proxy.address); await acquireToken( - WHALE_POOLS[symbol], + WHALE_POOLS[vaultAssetSymbol], randomUser.address, asset, "1000000", @@ -245,8 +238,8 @@ describe("Contract: IndexToken", () => { }); }); - describe("Lock pool", () => { - it("Emergency Safe can lock and unlock pool", async () => { + describe("Lock vault", () => { + it("Emergency Safe can lock and unlock vault", async () => { await expect(indexToken.connect(emergencySafe).emergencyLock()).to.emit( indexToken, "Paused" @@ -269,7 +262,7 @@ describe("Contract: IndexToken", () => { ).to.be.revertedWith("NOT_EMERGENCY_ROLE"); }); - it("Revert when calling deposit/redeem on locked pool", async () => { + it("Revert when calling deposit/redeem on locked vault", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( @@ -283,7 +276,7 @@ describe("Contract: IndexToken", () => { ).to.revertedWith("Pausable: paused"); }); - it("Revert when calling transferToLpAccount on locked pool", async () => { + it("Revert when calling transferToLpAccount on locked vault", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( @@ -317,7 +310,7 @@ describe("Contract: IndexToken", () => { ).to.be.revertedWith("NOT_EMERGENCY_ROLE"); }); - it("Revert deposit when pool is locked", async () => { + it("Revert deposit when vault is locked", async () => { await indexToken.connect(emergencySafe).emergencyLockDeposit(); await expect( @@ -335,14 +328,10 @@ describe("Contract: IndexToken", () => { }); describe("Transfer to LP Account", () => { - it("mAPT can call transferToLpAccount", async () => { - // need to impersonate the mAPT contract and fund it, since its - // address was set as CONTRACT_ROLE upon PoolTokenV2 deployment - const mAptSigner = await impersonateAccount(mApt.address); - + it("LP Account Funder can call transferToLpAccount", async () => { await indexToken.connect(randomUser).deposit(100, receiver.address); - await expect(indexToken.connect(mAptSigner).transferToLpAccount(100)).to - .not.be.reverted; + await expect(indexToken.connect(lpAccountFunder).transferToLpAccount(100)) + .to.not.be.reverted; }); it("Revert when unpermissioned account calls transferToLpAccount", async () => { @@ -376,7 +365,7 @@ describe("Contract: IndexToken", () => { ).to.be.revertedWith("NOT_EMERGENCY_ROLE"); }); - it("Revert redeem when pool is locked", async () => { + it("Revert redeem when vault is locked", async () => { await indexToken.connect(emergencySafe).emergencyLockRedeem(); await expect( @@ -391,6 +380,7 @@ describe("Contract: IndexToken", () => { await indexToken.connect(emergencySafe).emergencyUnlockRedeem(); await indexToken.testMint(randomUser.address, 1); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(0, 100); await expect( indexToken .connect(randomUser) @@ -413,19 +403,19 @@ describe("Contract: IndexToken", () => { it("Should transfer all deposited tokens to the emergencySafe", async () => { await indexToken.connect(randomUser).deposit(100000, receiver.address); - const prevPoolBalance = await asset.balanceOf(indexToken.address); + const prevVaultBalance = await asset.balanceOf(indexToken.address); const prevSafeBalance = await asset.balanceOf(emergencySafe.address); await indexToken.connect(emergencySafe).emergencyExit(asset.address); - const nextPoolBalance = await asset.balanceOf(indexToken.address); + const nextVaultBalance = await asset.balanceOf(indexToken.address); const nextSafeBalance = await asset.balanceOf(emergencySafe.address); - expect(nextPoolBalance).to.equal(0); - expect(nextSafeBalance.sub(prevSafeBalance)).to.equal(prevPoolBalance); + expect(nextVaultBalance).to.equal(0); + expect(nextSafeBalance.sub(prevSafeBalance)).to.equal(prevVaultBalance); }); - it("Should transfer tokens airdropped to the pool", async () => { + it("Should transfer tokens airdropped to the vault", async () => { const symbol = "AAVE"; const token = await ethers.getContractAt( "IDetailedERC20", @@ -440,16 +430,16 @@ describe("Contract: IndexToken", () => { deployer.address ); - const prevPoolBalance = await token.balanceOf(indexToken.address); + const prevVaultBalance = await token.balanceOf(indexToken.address); const prevSafeBalance = await token.balanceOf(emergencySafe.address); await indexToken.connect(emergencySafe).emergencyExit(token.address); - const nextPoolBalance = await token.balanceOf(indexToken.address); + const nextVaultBalance = await token.balanceOf(indexToken.address); const nextSafeBalance = await token.balanceOf(emergencySafe.address); - expect(nextPoolBalance).to.equal(0); - expect(nextSafeBalance.sub(prevSafeBalance)).to.equal(prevPoolBalance); + expect(nextVaultBalance).to.equal(0); + expect(nextSafeBalance.sub(prevSafeBalance)).to.equal(prevVaultBalance); }); it("Should emit the EmergencyExit event", async () => { @@ -473,8 +463,6 @@ describe("Contract: IndexToken", () => { ]; deployedValues.forEach(function (deployedValue) { describe(`Deployed value: ${deployedValue}`, () => { - const mAptSupply = tokenAmountToBigNumber("100"); - async function updateTvlAgg(usdDeployedValue) { if (usdDeployedValue.isZero()) { await oracleAdapter.connect(emergencySafe).emergencySetTvl(0, 100); @@ -487,8 +475,7 @@ describe("Contract: IndexToken", () => { beforeEach(async () => { /* these get rollbacked after each test due to snapshotting */ - // default to giving entire deployed value to the pool - await mApt.testMint(indexToken.address, mAptSupply); + // default to giving entire deployed value to the vault await updateTvlAgg(deployedValue); await oracleAdapter.connect(emergencySafe).emergencyUnlock(); }); @@ -522,9 +509,9 @@ describe("Contract: IndexToken", () => { assert(expectedAptMinted.gt(0)); }); - it("getPoolTotalValue returns value", async () => { - const val = await indexToken.getPoolTotalValue(); - console.debug(`\tPool Total Eth Value ${val.toString()}`); + it("getVaultTotalValue returns value", async () => { + const val = await indexToken.getVaultTotalValue(); + console.debug(`\tVault Total Eth Value ${val.toString()}`); assert(val.gt(0)); }); @@ -557,12 +544,12 @@ describe("Contract: IndexToken", () => { assert(assetAmount.gt(0)); }); - it("_getPoolAssetValue returns correct value", async () => { + it("_getVaultAssetValue returns correct value", async () => { let assetBalance = await asset.balanceOf(indexToken.address); let expectedAssetValue = await indexToken.getValueFromAssetAmount( assetBalance ); - expect(await indexToken.testGetPoolAssetValue()).to.equal( + expect(await indexToken.testGetVaultAssetValue()).to.equal( expectedAssetValue ); @@ -578,7 +565,7 @@ describe("Contract: IndexToken", () => { expectedAssetValue = await indexToken.getValueFromAssetAmount( assetBalance ); - expect(await indexToken.testGetPoolAssetValue()).to.equal( + expect(await indexToken.testGetVaultAssetValue()).to.equal( expectedAssetValue ); }); @@ -587,28 +574,6 @@ describe("Contract: IndexToken", () => { expect(await indexToken.testGetDeployedValue()).to.equal( deployedValue ); - - // transfer quarter of mAPT to another pool - await mApt.testMint(FAKE_ADDRESS, mAptSupply.div(4)); - await mApt.testBurn(indexToken.address, mAptSupply.div(4)); - // unlock oracle adapter after mint/burn - await oracleAdapter.connect(emergencySafe).emergencyUnlock(); - // must update agg so staleness check passes - await updateTvlAgg(deployedValue); - expect(await indexToken.testGetDeployedValue()).to.equal( - deployedValue.mul(3).div(4) - ); - - // transfer same amount again - await mApt.testMint(FAKE_ADDRESS, mAptSupply.div(4)); - await mApt.testBurn(indexToken.address, mAptSupply.div(4)); - // unlock oracle adapter after mint/burn - await oracleAdapter.connect(emergencySafe).emergencyUnlock(); - // must update agg so staleness check passes - await updateTvlAgg(deployedValue); - expect(await indexToken.testGetDeployedValue()).to.equal( - deployedValue.div(2) - ); }); it("getReserveTopUpValue returns correct value", async () => { @@ -627,8 +592,8 @@ describe("Contract: IndexToken", () => { expect(topUpValue).to.be.gt(0); } - const poolAssetValue = await indexToken.testGetPoolAssetValue(); - // assuming we unwind the top-up value from the pool's deployed + const vaultAssetValue = await indexToken.testGetVaultAssetValue(); + // assuming we unwind the top-up value from the vault's deployed // capital, the reserve percentage of resulting deployed value // is what we are targeting const reservePercentage = await indexToken.reservePercentage(); @@ -638,7 +603,7 @@ describe("Contract: IndexToken", () => { .div(100); const tolerance = Math.ceil((await asset.decimals()) / 4); const allowedDeviation = tokenAmountToBigNumber(5, tolerance); - expect(poolAssetValue.add(topUpValue).sub(targetValue)).to.be.lt( + expect(vaultAssetValue.add(topUpValue).sub(targetValue)).to.be.lt( allowedDeviation ); }); @@ -806,7 +771,7 @@ describe("Contract: IndexToken", () => { const aptSupply = tokenAmountToBigNumber("100000"); await indexToken.testMint(deployer.address, aptSupply); - // seed the pool with asset + // seed the vault with asset const reserveBalance = tokenAmountToBigNumber("150000", decimals); await asset .connect(randomUser) @@ -840,8 +805,8 @@ describe("Contract: IndexToken", () => { const aptSupply = tokenAmountToBigNumber("10000"); await indexToken.testMint(deployer.address, aptSupply); - /* Setup pool and user APT amounts: - 1. give pool an asset reserve balance + /* Setup vault and user APT amounts: + 1. give vault an asset reserve balance 2. calculate the reserve's APT amount 3. transfer APT amount less than that to the user */ @@ -955,7 +920,7 @@ describe("Contract: IndexToken", () => { it("Revert when asset amount is greater than reserve", async () => { const decimals = await asset.decimals(); - // seed the pool with asset + // seed the vault with asset const reserveBalance = tokenAmountToBigNumber("150000", decimals); await asset .connect(randomUser) @@ -977,8 +942,8 @@ describe("Contract: IndexToken", () => { const indexSupply = tokenAmountToBigNumber("10000"); await indexToken.testMint(deployer.address, indexSupply); - /* Setup pool and user share amounts: - 1. give pool an asset reserve balance + /* Setup vault and user share amounts: + 1. give vault an asset reserve balance 2. calculate the reserve's share amount 3. transfer share amount less than that to the user */ @@ -1065,9 +1030,9 @@ describe("Contract: IndexToken", () => { deployer.address, tokenAmountToBigNumber("100000") ); - // seed pool with stablecoin + // seed vault with stablecoin await acquireToken( - WHALE_POOLS[symbol], + WHALE_POOLS[vaultAssetSymbol], indexToken.address, asset, "12000000", // 12 MM @@ -1103,7 +1068,7 @@ describe("Contract: IndexToken", () => { ); // seed pool with stablecoin await acquireToken( - WHALE_POOLS[symbol], + WHALE_POOLS[vaultAssetSymbol], indexToken.address, asset, "12000000", // 12 MM diff --git a/test-integration/LpAccountFunder.js b/test-integration/LpAccountFunder.js new file mode 100644 index 00000000..0fdae3d3 --- /dev/null +++ b/test-integration/LpAccountFunder.js @@ -0,0 +1,1547 @@ +const { expect } = require("chai"); +const { artifacts, ethers } = require("hardhat"); +const { BigNumber } = ethers; +const timeMachine = require("ganache-time-traveler"); +const _ = require("lodash"); +const { + tokenAmountToBigNumber, + bytes32, + acquireToken, + getStablecoinAddress, + getAggregatorAddress, +} = require("../utils/helpers"); +const { WHALE_POOLS } = require("../utils/constants"); + +const IDetailedERC20 = artifacts.require("IDetailedERC20"); + +/****************************/ +/* set DEBUG log level here */ +/****************************/ +console.debugging = false; +/****************************/ + +const NETWORK = "MAINNET"; +const SYMBOLS = ["DAI", "USDC", "USDT"]; +const TOKEN_ADDRESSES = SYMBOLS.map((symbol) => + getStablecoinAddress(symbol, NETWORK) +); +const AGG_ADDRESSES = SYMBOLS.map((symbol) => + getAggregatorAddress(`${symbol}-USD`, NETWORK) +); + +const DAI_TOKEN = TOKEN_ADDRESSES[0]; +const USDC_TOKEN = TOKEN_ADDRESSES[1]; +const USDT_TOKEN = TOKEN_ADDRESSES[2]; + +const daiPoolId = bytes32("daiPool"); +const usdcPoolId = bytes32("usdcPool"); +const tetherPoolId = bytes32("usdtPool"); +const ids = [daiPoolId, usdcPoolId, tetherPoolId]; + +describe("LpAccountFunder", () => { + // to-be-deployed contracts + let tvlManager; + let mApt; + let oracleAdapter; + + // signers + let deployer; + let lpAccount; + let emergencySafe; + let adminSafe; + let lpSafe; + let randomUser; + + // existing Mainnet contracts + let addressRegistry; + + let daiPool; + let usdcPool; + let usdtPool; + let pools; + + let daiToken; + let usdcToken; + let usdtToken; + let underlyers; + + // use EVM snapshots for test isolation + let suiteSnapshotId; + + // standard amounts we use in our tests + const dollars = 100; + const daiAmount = tokenAmountToBigNumber(dollars, 18); + const usdcAmount = tokenAmountToBigNumber(dollars, 6); + const usdtAmount = tokenAmountToBigNumber(dollars, 6); + + before(async () => { + const snapshot = await timeMachine.takeSnapshot(); + suiteSnapshotId = snapshot["result"]; + }); + + after(async () => { + await timeMachine.revertToSnapshot(suiteSnapshotId); + }); + + before("Main deployments and upgrades", async () => { + [deployer, emergencySafe, adminSafe, lpSafe, randomUser] = + await ethers.getSigners(); + + const ProxyAdmin = await ethers.getContractFactory("ProxyAdmin"); + + /************************************************/ + /***** Deploy and upgrade Address Registry ******/ + /************************************************/ + const AddressRegistry = await ethers.getContractFactory("AddressRegistry"); + const addressRegistryLogic = await AddressRegistry.deploy(); + const AddressRegistryV2 = await ethers.getContractFactory( + "AddressRegistryV2" + ); + const addressRegistryLogicV2 = await AddressRegistryV2.deploy(); + const addressRegistryAdmin = await ProxyAdmin.deploy(); + + const ProxyConstructorArg = await ethers.getContractFactory( + "ProxyConstructorArg" + ); + const encodedArg = await ( + await ProxyConstructorArg.deploy() + ).getEncodedArg(addressRegistryAdmin.address); + const TransparentUpgradeableProxy = await ethers.getContractFactory( + "TransparentUpgradeableProxy" + ); + const addressRegistryProxy = await TransparentUpgradeableProxy.deploy( + addressRegistryLogic.address, + addressRegistryAdmin.address, + encodedArg + ); + + await addressRegistryAdmin.upgrade( + addressRegistryProxy.address, + addressRegistryLogicV2.address + ); + + addressRegistry = await AddressRegistryV2.attach( + addressRegistryProxy.address + ); + /* The address registry needs multiple addresses registered + * to setup the roles for access control in the contract + * constructors: + * + * MetaPoolToken + * - emergencySafe (emergency role, default admin role) + * - lpSafe (LP role) + * + * PoolTokenV2 + * - emergencySafe (emergency role, default admin role) + * - adminSafe (admin role) + * - mApt (contract role) + * + * Erc20Allocation + * - emergencySafe (default admin role) + * - lpSafe (LP role) + * - mApt (contract role) + * + * TvlManager + * - emergencySafe (emergency role, default admin role) + * - lpSafe (LP role) + * + * OracleAdapter + * - emergencySafe (emergency role, default admin role) + * - adminSafe (admin role) + * - tvlManager (contract role) + * - mApt (contract role) + * + * Note the order of dependencies: a contract requires contracts + * above it in the list to be deployed first. Thus we need + * to deploy in the order given, starting with the Safes. + */ + await addressRegistry.registerAddress( + bytes32("emergencySafe"), + emergencySafe.address + ); + await addressRegistry.registerAddress( + bytes32("adminSafe"), + adminSafe.address + ); + await addressRegistry.registerAddress(bytes32("lpSafe"), lpSafe.address); + + /***********************/ + /***** deploy mAPT *****/ + /***********************/ + const MetaPoolToken = await ethers.getContractFactory( + "TestMetaPoolTokenV2" + ); + const mAptLogic = await MetaPoolToken.deploy(); + + const mAptAdmin = await ProxyAdmin.deploy(); + + const initData = MetaPoolToken.interface.encodeFunctionData( + "initialize(address)", + [addressRegistry.address] + ); + const mAptProxy = await TransparentUpgradeableProxy.deploy( + mAptLogic.address, + mAptAdmin.address, + initData + ); + + mApt = await MetaPoolToken.attach(mAptProxy.address).connect(lpSafe); + await addressRegistry.registerAddress(bytes32("mApt"), mApt.address); + + /*****************************/ + /***** deploy LP Account *****/ + /*****************************/ + const LpAccount = await ethers.getContractFactory("LpAccount"); + const lpAccountLogic = await LpAccount.deploy(); + + const lpAccountAdmin = await ProxyAdmin.deploy(); + + const lpAccountInitData = LpAccount.interface.encodeFunctionData( + "initialize(address)", + [addressRegistry.address] + ); + + const lpAccountProxy = await TransparentUpgradeableProxy.deploy( + lpAccountLogic.address, + lpAccountAdmin.address, + lpAccountInitData + ); + + lpAccount = await LpAccount.attach(lpAccountProxy.address); + await addressRegistry.registerAddress( + bytes32("lpAccount"), + lpAccount.address + ); + + /***********************************/ + /* deploy pools and upgrade to V2 */ + /***********************************/ + const PoolToken = await ethers.getContractFactory("PoolToken"); + const poolLogic = await PoolToken.deploy(); + + const PoolTokenV2 = await ethers.getContractFactory("PoolTokenV2"); + const poolLogicV2 = await PoolTokenV2.deploy(); + + const poolAdmin = await ProxyAdmin.deploy(); + const PoolTokenProxy = await ethers.getContractFactory("PoolTokenProxy"); + + const poolTokenV2InitData = PoolTokenV2.interface.encodeFunctionData( + "initializeUpgrade(address)", + [addressRegistry.address] + ); + + pools = []; + for (const [symbol, tokenAddress, aggAddress] of _.zip( + SYMBOLS, + TOKEN_ADDRESSES, + AGG_ADDRESSES + )) { + const poolProxy = await PoolTokenProxy.deploy( + poolLogic.address, + poolAdmin.address, + tokenAddress, + aggAddress + ); + + await poolAdmin.upgradeAndCall( + poolProxy.address, + poolLogicV2.address, + poolTokenV2InitData + ); + const pool = await PoolTokenV2.attach(poolProxy.address); + + const poolId = bytes32(symbol.toLowerCase() + "Pool"); + await addressRegistry.registerAddress(poolId, pool.address); + + pools.push(pool); + } + daiPool = pools[0]; + usdcPool = pools[1]; + usdtPool = pools[2]; + + /******************************/ + /***** deploy TVL Manager *****/ + /******************************/ + const Erc20Allocation = await ethers.getContractFactory("Erc20Allocation"); + const erc20Allocation = await Erc20Allocation.deploy( + addressRegistry.address + ); + await addressRegistry.registerAddress( + bytes32("erc20Allocation"), + erc20Allocation.address + ); + + const TvlManager = await ethers.getContractFactory("TestTvlManager"); + tvlManager = await TvlManager.deploy(addressRegistry.address); + + await addressRegistry.registerAddress( + bytes32("tvlManager"), + tvlManager.address + ); + + /*********************************/ + /***** deploy Oracle Adapter *****/ + /*********************************/ + + const tvlAggAddress = getAggregatorAddress("TVL", NETWORK); + + const OracleAdapter = await ethers.getContractFactory("OracleAdapter"); + oracleAdapter = await OracleAdapter.deploy( + addressRegistry.address, + tvlAggAddress, + TOKEN_ADDRESSES, + AGG_ADDRESSES, + 86400, + 270 + ); + await oracleAdapter.deployed(); + + await addressRegistry.registerAddress( + bytes32("oracleAdapter"), + oracleAdapter.address + ); + + // set default TVL for tests to zero + await oracleAdapter.connect(emergencySafe).emergencySetTvl(0, 100); + + // registering ERC20 allocation must happen now, since the + // TVL Manager will attempt to lock the Oracle Adapter. + await tvlManager + .connect(adminSafe) + .registerAssetAllocation(erc20Allocation.address); + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); + }); + + before("Attach to Mainnet stablecoin contracts", async () => { + daiToken = await ethers.getContractAt("IDetailedERC20", DAI_TOKEN); + usdcToken = await ethers.getContractAt("IDetailedERC20", USDC_TOKEN); + usdtToken = await ethers.getContractAt("IDetailedERC20", USDT_TOKEN); + underlyers = [daiToken, usdcToken, usdtToken]; + }); + + before("Fund accounts with stables", async () => { + // fund deployer with stablecoins + await acquireToken( + WHALE_POOLS["DAI"], + deployer, + daiToken, + "1000000", + deployer + ); + await acquireToken( + WHALE_POOLS["USDC"], + deployer, + usdcToken, + "1000000", + deployer + ); + await acquireToken( + WHALE_POOLS["USDT"], + deployer, + usdtToken, + "1000000", + deployer + ); + }); + + async function getMintAmount(pool, underlyerAmount) { + const tokenPrice = await pool.getUnderlyerPrice(); + const underlyer = await pool.underlyer(); + const erc20 = await ethers.getContractAt(IDetailedERC20.abi, underlyer); + const decimals = await erc20.decimals(); + const mintAmount = await mApt.testCalculateDelta( + underlyerAmount, + tokenPrice, + decimals + ); + return mintAmount; + } + + describe("Permissions and input validation", () => { + let subSuiteSnapshotId; + let testSnapshotId; + + beforeEach(async () => { + const snapshot = await timeMachine.takeSnapshot(); + testSnapshotId = snapshot["result"]; + }); + + afterEach(async () => { + await timeMachine.revertToSnapshot(testSnapshotId); + }); + + before(async () => { + const snapshot = await timeMachine.takeSnapshot(); + subSuiteSnapshotId = snapshot["result"]; + }); + + after(async () => { + await timeMachine.revertToSnapshot(subSuiteSnapshotId); + }); + + describe("fundLpAccount", () => { + it("Unpermissioned cannot call", async () => { + await expect( + mApt.connect(randomUser).fundLpAccount([]) + ).to.be.revertedWith("NOT_LP_ROLE"); + }); + + it("LP role can call", async () => { + await expect(mApt.connect(lpSafe).fundLpAccount([])).to.not.be.reverted; + }); + + it("Revert on unregistered pool", async () => { + await expect( + mApt + .connect(lpSafe) + .fundLpAccount([daiPoolId, bytes32("invalidPool"), tetherPoolId]) + ).to.be.revertedWith("Missing address"); + }); + }); + + describe("withdrawFromLpAccount", () => { + it("Unpermissioned cannot call", async () => { + await expect( + mApt.connect(randomUser).withdrawFromLpAccount([]) + ).to.be.revertedWith("NOT_LP_ROLE"); + }); + + it("LP role can call", async () => { + await expect(mApt.connect(lpSafe).withdrawFromLpAccount([])).to.not.be + .reverted; + }); + + it("Revert on unregistered pool", async () => { + await expect( + mApt + .connect(lpSafe) + .withdrawFromLpAccount([ + daiPoolId, + bytes32("invalidPool"), + tetherPoolId, + ]) + ).to.be.revertedWith("Missing address"); + }); + }); + }); + + describe("Balances and minting", () => { + let subSuiteSnapshotId; + let testSnapshotId; + + beforeEach(async () => { + const snapshot = await timeMachine.takeSnapshot(); + testSnapshotId = snapshot["result"]; + }); + + afterEach(async () => { + await timeMachine.revertToSnapshot(testSnapshotId); + }); + + before(async () => { + const snapshot = await timeMachine.takeSnapshot(); + subSuiteSnapshotId = snapshot["result"]; + }); + + after(async () => { + await timeMachine.revertToSnapshot(subSuiteSnapshotId); + }); + + before("Fund pools with stables", async () => { + // fund each APY pool with corresponding stablecoin + await acquireToken( + WHALE_POOLS["DAI"], + daiPool, + daiToken, + "5000000", + deployer + ); + await acquireToken( + WHALE_POOLS["USDC"], + usdcPool, + usdcToken, + "5000000", + deployer + ); + await acquireToken( + WHALE_POOLS["USDT"], + usdtPool, + usdtToken, + "5000000", + deployer + ); + }); + + describe("_fundLpAccount", () => { + it("Revert on missing LP Safe address", async () => { + await addressRegistry.deleteAddress(bytes32("lpAccount")); + await expect(mApt.testFundLpAccount([], [])).to.be.revertedWith( + "Missing address" + ); + }); + + it("Skip on zero amount", async () => { + const mAptSupply = await mApt.totalSupply(); + const poolBalance = await usdcToken.balanceOf(usdcPool.address); + + await mApt.testFundLpAccount([usdcPool.address], [0]); + + // should be no mAPT minted and no change in pool's USDC balance + expect(await mApt.totalSupply()).to.equal(mAptSupply); + expect(await usdcToken.balanceOf(usdcPool.address)).to.equal( + poolBalance + ); + }); + + it("First funding updates balances and registers asset allocations (single pool)", async () => { + // pre-conditions + expect(await daiToken.balanceOf(lpAccount.address)).to.equal(0); + expect(await mApt.totalSupply()).to.equal(0); + + /***********************************************/ + /* Test all balances are updated appropriately */ + /***********************************************/ + const daiPoolBalance = await daiToken.balanceOf(daiPool.address); + + const daiPoolMintAmount = await getMintAmount(daiPool, daiAmount); + + await mApt.testFundLpAccount([daiPool.address], [daiAmount]); + + const strategyDaiBalance = await daiToken.balanceOf(lpAccount.address); + + // Check underlyer amounts transferred correctly + expect(strategyDaiBalance).to.equal(daiAmount); + + expect(await daiToken.balanceOf(daiPool.address)).to.equal( + daiPoolBalance.sub(daiAmount) + ); + + // Check proper mAPT amounts minted + expect(await mApt.balanceOf(daiPool.address)).to.equal( + daiPoolMintAmount + ); + + /*************************************************************/ + /* Check pool manager registered asset allocations correctly */ + /*************************************************************/ + + const erc20AllocationAddress = await tvlManager.getAssetAllocation( + "erc20Allocation" + ); + const expectedDaiId = await tvlManager.testEncodeAssetAllocationId( + erc20AllocationAddress, + 0 + ); + const registeredIds = await tvlManager.getAssetAllocationIds(); + expect(registeredIds.length).to.equal(1); + expect(registeredIds[0]).to.equal(expectedDaiId); + + const registeredDaiSymbol = await tvlManager.symbolOf(registeredIds[0]); + expect(registeredDaiSymbol).to.equal("DAI"); + + const registeredDaiDecimals = await tvlManager.decimalsOf( + registeredIds[0] + ); + expect(registeredDaiDecimals).to.equal(18); + + const registeredStratDaiBal = await tvlManager.balanceOf( + registeredIds[0] + ); + expect(registeredStratDaiBal).equal(strategyDaiBalance); + }); + + it("First funding updates balances and registers asset allocations (multiple pools)", async () => { + // pre-conditions + expect(await daiToken.balanceOf(lpAccount.address)).to.equal(0); + expect(await usdcToken.balanceOf(lpAccount.address)).to.equal(0); + expect(await usdtToken.balanceOf(lpAccount.address)).to.equal(0); + expect(await mApt.totalSupply()).to.equal(0); + + /***********************************************/ + /* Test all balances are updated appropriately */ + /***********************************************/ + const daiPoolBalance = await daiToken.balanceOf(daiPool.address); + const usdcPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const usdtPoolBalance = await usdtToken.balanceOf(usdtPool.address); + + const daiPoolMintAmount = await getMintAmount(daiPool, daiAmount); + const usdcPoolMintAmount = await getMintAmount(usdcPool, usdcAmount); + const usdtPoolMintAmount = await getMintAmount(usdtPool, usdtAmount); + + await mApt.testFundLpAccount( + [daiPool.address, usdcPool.address, usdtPool.address], + [daiAmount, usdcAmount, usdtAmount] + ); + + const strategyDaiBalance = await daiToken.balanceOf(lpAccount.address); + const strategyUsdcBalance = await usdcToken.balanceOf( + lpAccount.address + ); + const strategyUsdtBalance = await usdtToken.balanceOf( + lpAccount.address + ); + + // Check underlyer amounts transferred correctly + expect(strategyDaiBalance).to.equal(daiAmount); + expect(strategyUsdcBalance).to.equal(usdcAmount); + expect(strategyUsdtBalance).to.equal(usdtAmount); + + expect(await daiToken.balanceOf(daiPool.address)).to.equal( + daiPoolBalance.sub(daiAmount) + ); + expect(await usdcToken.balanceOf(usdcPool.address)).to.equal( + usdcPoolBalance.sub(usdcAmount) + ); + expect(await usdtToken.balanceOf(usdtPool.address)).to.equal( + usdtPoolBalance.sub(usdtAmount) + ); + + // Check proper mAPT amounts minted + expect(await mApt.balanceOf(daiPool.address)).to.equal( + daiPoolMintAmount + ); + expect(await mApt.balanceOf(usdcPool.address)).to.equal( + usdcPoolMintAmount + ); + expect(await mApt.balanceOf(usdtPool.address)).to.equal( + usdtPoolMintAmount + ); + + /*************************************************************/ + /* Check pool manager registered asset allocations correctly */ + /*************************************************************/ + + const erc20AllocationAddress = await tvlManager.getAssetAllocation( + "erc20Allocation" + ); + const expectedDaiId = await tvlManager.testEncodeAssetAllocationId( + erc20AllocationAddress, + 0 + ); + const expectedUsdcId = await tvlManager.testEncodeAssetAllocationId( + erc20AllocationAddress, + 1 + ); + const expectedUsdtId = await tvlManager.testEncodeAssetAllocationId( + erc20AllocationAddress, + 2 + ); + const registeredIds = await tvlManager.getAssetAllocationIds(); + expect(registeredIds.length).to.equal(3); + expect(registeredIds[0]).to.equal(expectedDaiId); + expect(registeredIds[1]).to.equal(expectedUsdcId); + expect(registeredIds[2]).to.equal(expectedUsdtId); + + const registeredDaiSymbol = await tvlManager.symbolOf(registeredIds[0]); + const registeredUsdcSymbol = await tvlManager.symbolOf( + registeredIds[1] + ); + const registeredUsdtSymbol = await tvlManager.symbolOf( + registeredIds[2] + ); + expect(registeredDaiSymbol).to.equal("DAI"); + expect(registeredUsdcSymbol).to.equal("USDC"); + expect(registeredUsdtSymbol).to.equal("USDT"); + + const registeredDaiDecimals = await tvlManager.decimalsOf( + registeredIds[0] + ); + const registeredUsdcDecimals = await tvlManager.decimalsOf( + registeredIds[1] + ); + const registeredUsdtDecimals = await tvlManager.decimalsOf( + registeredIds[2] + ); + expect(registeredDaiDecimals).to.equal(18); + expect(registeredUsdcDecimals).to.equal(6); + expect(registeredUsdtDecimals).to.equal(6); + + const registeredStratDaiBal = await tvlManager.balanceOf( + registeredIds[0] + ); + const registeredStratUsdcBal = await tvlManager.balanceOf( + registeredIds[1] + ); + const registeredStratUsdtBal = await tvlManager.balanceOf( + registeredIds[2] + ); + expect(registeredStratDaiBal).equal(strategyDaiBalance); + expect(registeredStratUsdcBal).equal(strategyUsdcBalance); + expect(registeredStratUsdtBal).equal(strategyUsdtBalance); + }); + + it("Second funding updates balances (single pool)", async () => { + // pre-conditions + await mApt.testFundLpAccount([daiPool.address], [daiAmount]); + expect(await daiToken.balanceOf(lpAccount.address)).to.be.gt(0); + expect(await mApt.totalSupply()).to.be.gt(0); + + // adjust the TVL appropriately, as there is no Chainlink to update it + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); // needed to get value + const tvl = await daiPool.getValueFromUnderlyerAmount(daiAmount); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 100); + + /***********************************************/ + /* Test all balances are updated appropriately */ + /***********************************************/ + const prevPoolBalance = await daiToken.balanceOf(daiPool.address); + const prevStrategyBalance = await daiToken.balanceOf(lpAccount.address); + const prevMaptBalance = await mApt.balanceOf(daiPool.address); + + const transferAmount = daiAmount.mul(3); + const mintAmount = await getMintAmount(daiPool, transferAmount); + + await mApt.testFundLpAccount([daiPool.address], [transferAmount]); + + const newPoolBalance = await daiToken.balanceOf(daiPool.address); + const newStrategyBalance = await daiToken.balanceOf(lpAccount.address); + const newMaptBalance = await mApt.balanceOf(daiPool.address); + + // Check underlyer amounts transferred correctly + expect(prevPoolBalance.sub(newPoolBalance)).to.equal(transferAmount); + expect(newStrategyBalance.sub(prevStrategyBalance)).to.equal( + transferAmount + ); + + // Check proper mAPT amounts minted + expect(newMaptBalance.sub(prevMaptBalance)).to.equal(mintAmount); + }); + + it("Second funding updates balances (multiple pools)", async () => { + // pre-conditions + await mApt.testFundLpAccount( + [daiPool.address, usdcPool.address, usdtPool.address], + [daiAmount, usdcAmount, usdtAmount] + ); + expect(await daiToken.balanceOf(lpAccount.address)).to.be.gt(0); + expect(await usdcToken.balanceOf(lpAccount.address)).to.be.gt(0); + expect(await usdtToken.balanceOf(lpAccount.address)).to.be.gt(0); + expect(await mApt.totalSupply()).to.be.gt(0); + + // adjust the TVL appropriately, as there is no Chainlink to update it + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); // needed to get value + const daiValue = await daiPool.getValueFromUnderlyerAmount(daiAmount); + const usdcValue = await usdcPool.getValueFromUnderlyerAmount( + usdcAmount + ); + const usdtValue = await usdtPool.getValueFromUnderlyerAmount( + usdtAmount + ); + const tvl = daiValue.add(usdcValue).add(usdtValue); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 100); + + /***********************************************/ + /* Test all balances are updated appropriately */ + /***********************************************/ + // DAI + const prevDaiPoolBalance = await daiToken.balanceOf(daiPool.address); + const prevSafeDaiBalance = await daiToken.balanceOf(lpAccount.address); + const prevDaiPoolMaptBalance = await mApt.balanceOf(daiPool.address); + // USDC + const prevUsdcPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const prevSafeUsdcBalance = await usdcToken.balanceOf( + lpAccount.address + ); + const prevUsdcPoolMaptBalance = await mApt.balanceOf(usdcPool.address); + // Tether + const prevUsdtPoolBalance = await usdtToken.balanceOf(usdtPool.address); + const prevSafeUsdtBalance = await usdtToken.balanceOf( + lpAccount.address + ); + const prevUsdtPoolMaptBalance = await mApt.balanceOf(usdtPool.address); + + const daiTransferAmount = daiAmount.mul(3); + const usdcTransferAmount = usdcAmount.mul(2).div(3); + const usdtTransferAmount = usdtAmount.div(2); + + const daiPoolMintAmount = await getMintAmount( + daiPool, + daiTransferAmount + ); + const usdcPoolMintAmount = await getMintAmount( + usdcPool, + usdcTransferAmount + ); + const usdtPoolMintAmount = await getMintAmount( + usdtPool, + usdtTransferAmount + ); + + await mApt.testFundLpAccount( + [daiPool.address, usdcPool.address, usdtPool.address], + [daiTransferAmount, usdcTransferAmount, usdtTransferAmount] + ); + + const newDaiPoolBalance = await daiToken.balanceOf(daiPool.address); + const newSafeDaiBalance = await daiToken.balanceOf(lpAccount.address); + const newDaiPoolMaptBalance = await mApt.balanceOf(daiPool.address); + + const newUsdcPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const newSafeUsdcBalance = await usdcToken.balanceOf(lpAccount.address); + const newUsdcPoolMaptBalance = await mApt.balanceOf(usdcPool.address); + + const newUsdtPoolBalance = await usdtToken.balanceOf(usdtPool.address); + const newSafeUsdtBalance = await usdtToken.balanceOf(lpAccount.address); + const newUsdtPoolMaptBalance = await mApt.balanceOf(usdtPool.address); + + // Check underlyer amounts transferred correctly + expect(prevDaiPoolBalance.sub(newDaiPoolBalance)).to.equal( + daiTransferAmount + ); + expect(newSafeDaiBalance.sub(prevSafeDaiBalance)).to.equal( + daiTransferAmount + ); + expect(prevUsdcPoolBalance.sub(newUsdcPoolBalance)).to.equal( + usdcTransferAmount + ); + expect(newSafeUsdcBalance.sub(prevSafeUsdcBalance)).to.equal( + usdcTransferAmount + ); + expect(prevUsdtPoolBalance.sub(newUsdtPoolBalance)).to.equal( + usdtTransferAmount + ); + expect(newSafeUsdtBalance.sub(prevSafeUsdtBalance)).to.equal( + usdtTransferAmount + ); + + // Check proper mAPT amounts minted + expect(newDaiPoolMaptBalance.sub(prevDaiPoolMaptBalance)).to.equal( + daiPoolMintAmount + ); + expect(newUsdcPoolMaptBalance.sub(prevUsdcPoolMaptBalance)).to.equal( + usdcPoolMintAmount + ); + expect(newUsdtPoolMaptBalance.sub(prevUsdtPoolMaptBalance)).to.equal( + usdtPoolMintAmount + ); + }); + }); + + describe("_withdrawFromLpAccount", () => { + it("Withdrawal updates balances correctly (single pool)", async () => { + const transferAmount = tokenAmountToBigNumber("10", 18); + await mApt.testFundLpAccount([daiPool.address], [transferAmount]); + + // adjust the TVL appropriately, as there is no Chainlink to update it + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); // needed to get value + const tvl = await daiPool.getValueFromUnderlyerAmount(transferAmount); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 100); + + const prevSafeBalance = await daiToken.balanceOf(lpAccount.address); + const prevPoolBalance = await daiToken.balanceOf(daiPool.address); + const prevMaptBalance = await mApt.balanceOf(daiPool.address); + + const burnAmount = await getMintAmount(daiPool, transferAmount); + + await mApt.testWithdrawFromLpAccount( + [daiPool.address], + [transferAmount] + ); + + const newSafeBalance = await daiToken.balanceOf(lpAccount.address); + const newPoolBalance = await daiToken.balanceOf(daiPool.address); + expect(prevSafeBalance.sub(newSafeBalance)).to.equal(transferAmount); + expect(newPoolBalance.sub(prevPoolBalance)).to.equal(transferAmount); + + const allowedDeviation = 2; + + const newMaptBalance = await mApt.balanceOf(daiPool.address); + const expectedMaptBalance = prevMaptBalance.sub(burnAmount); + expect(newMaptBalance.sub(expectedMaptBalance).abs()).lt( + allowedDeviation + ); + }); + + it("Withdrawal updates balances correctly (multiple pools)", async () => { + const daiTransferAmount = tokenAmountToBigNumber("10", 18); + const usdcTransferAmount = tokenAmountToBigNumber("25", 6); + const usdtTransferAmount = tokenAmountToBigNumber("8", 6); + await mApt.testFundLpAccount( + [daiPool.address, usdcPool.address, usdtPool.address], + [daiTransferAmount, usdcTransferAmount, usdtTransferAmount] + ); + + // adjust the TVL appropriately, as there is no Chainlink to update it + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); // needed to get value + const daiValue = await daiPool.getValueFromUnderlyerAmount( + daiTransferAmount + ); + const usdcValue = await usdcPool.getValueFromUnderlyerAmount( + usdcTransferAmount + ); + const usdtValue = await usdtPool.getValueFromUnderlyerAmount( + usdtTransferAmount + ); + const tvl = daiValue.add(usdcValue).add(usdtValue); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 100); + + // DAI + const prevSafeDaiBalance = await daiToken.balanceOf(lpAccount.address); + const prevDaiPoolBalance = await daiToken.balanceOf(daiPool.address); + const prevDaiMaptBalance = await mApt.balanceOf(daiPool.address); + // USDC + const prevSafeUsdcBalance = await usdcToken.balanceOf( + lpAccount.address + ); + const prevUsdcPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const prevUsdcMaptBalance = await mApt.balanceOf(usdcPool.address); + // USDT + const prevSafeUsdtBalance = await usdtToken.balanceOf( + lpAccount.address + ); + const prevUsdtPoolBalance = await usdtToken.balanceOf(usdtPool.address); + const prevUsdtMaptBalance = await mApt.balanceOf(usdtPool.address); + + const daiPoolBurnAmount = await getMintAmount( + daiPool, + daiTransferAmount + ); + const usdcPoolBurnAmount = await getMintAmount( + usdcPool, + usdcTransferAmount + ); + const usdtPoolBurnAmount = await getMintAmount( + usdtPool, + usdtTransferAmount + ); + + await mApt.testWithdrawFromLpAccount( + [daiPool.address, usdcPool.address, usdtPool.address], + [daiTransferAmount, usdcTransferAmount, usdtTransferAmount] + ); + + /****************************/ + /* check underlyer balances */ + /****************************/ + + // DAI + const newSafeDaiBalance = await daiToken.balanceOf(lpAccount.address); + const newDaiPoolBalance = await daiToken.balanceOf(daiPool.address); + expect(prevSafeDaiBalance.sub(newSafeDaiBalance)).to.equal( + daiTransferAmount + ); + expect(newDaiPoolBalance.sub(prevDaiPoolBalance)).to.equal( + daiTransferAmount + ); + // USDC + const newSafeUsdcBalance = await usdcToken.balanceOf(lpAccount.address); + const newUsdcPoolBalance = await usdcToken.balanceOf(usdcPool.address); + expect(prevSafeUsdcBalance.sub(newSafeUsdcBalance)).to.equal( + usdcTransferAmount + ); + expect(newUsdcPoolBalance.sub(prevUsdcPoolBalance)).to.equal( + usdcTransferAmount + ); + // USDT + const newSafeUsdtBalance = await daiToken.balanceOf(lpAccount.address); + const newUsdtPoolBalance = await usdtToken.balanceOf(usdtPool.address); + expect(prevSafeUsdtBalance.sub(newSafeUsdtBalance)).to.equal( + usdtTransferAmount + ); + expect(newUsdtPoolBalance.sub(prevUsdtPoolBalance)).to.equal( + usdtTransferAmount + ); + + /***********************/ + /* check mAPT balances */ + /***********************/ + + const allowedDeviation = 2; + // DAI + const newDaiMaptBalance = await mApt.balanceOf(daiPool.address); + const expectedDaiMaptBalance = + prevDaiMaptBalance.sub(daiPoolBurnAmount); + expect(newDaiMaptBalance.sub(expectedDaiMaptBalance).abs()).lt( + allowedDeviation + ); + // USDC + const newUsdcMaptBalance = await mApt.balanceOf(usdcPool.address); + const expectedUsdcMaptBalance = + prevUsdcMaptBalance.sub(usdcPoolBurnAmount); + expect(newUsdcMaptBalance.sub(expectedUsdcMaptBalance).abs()).lt( + allowedDeviation + ); + // USDT + const newUsdtMaptBalance = await mApt.balanceOf(usdtPool.address); + const expectedUsdtMaptBalance = + prevUsdtMaptBalance.sub(usdtPoolBurnAmount); + expect(newUsdtMaptBalance.sub(expectedUsdtMaptBalance).abs()).lt( + allowedDeviation + ); + }); + + it("Full withdrawal reverts if TVL not updated", async () => { + let totalTransferred = tokenAmountToBigNumber(0, 18); + let transferAmount = daiAmount.div(2); + await mApt.testFundLpAccount([daiPool.address], [transferAmount]); + totalTransferred = totalTransferred.add(transferAmount); + + // adjust the tvl appropriately, as there is no chainlink to update it + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); // needed to get value + let tvl = await daiPool.getValueFromUnderlyerAmount(transferAmount); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 100); + + transferAmount = daiAmount.div(3); + await mApt.testFundLpAccount([daiPool.address], [transferAmount]); + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); + totalTransferred = totalTransferred.add(transferAmount); + + await expect( + mApt.testWithdrawFromLpAccount([daiPool.address], [totalTransferred]) + ).to.be.revertedWith("ERC20: burn amount exceeds balance"); + }); + + it("Full withdrawal works if TVL updated", async () => { + expect(await mApt.balanceOf(daiPool.address)).to.equal(0); + const poolBalance = await daiToken.balanceOf(daiPool.address); + + let totalTransferred = tokenAmountToBigNumber(0, 18); + let transferAmount = daiAmount.div(2); + await mApt.testFundLpAccount([daiPool.address], [transferAmount]); + totalTransferred = totalTransferred.add(transferAmount); + + // adjust the tvl appropriately, as there is no chainlink to update it + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); // needed to get value + let tvl = await daiPool.getValueFromUnderlyerAmount(totalTransferred); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 100); + + transferAmount = daiAmount.div(3); + await mApt.testFundLpAccount([daiPool.address], [transferAmount]); + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); + totalTransferred = totalTransferred.add(transferAmount); + + // adjust the tvl appropriately, as there is no chainlink to update it + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); // needed to get value + tvl = await daiPool.getValueFromUnderlyerAmount(totalTransferred); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 100); + + await mApt.testWithdrawFromLpAccount( + [daiPool.address], + [totalTransferred] + ); + + expect(await mApt.balanceOf(daiPool.address)).to.equal(0); + expect(await daiToken.balanceOf(daiPool.address)).to.equal(poolBalance); + }); + }); + }); + + describe("Funding scenarios", () => { + // CAUTION: some of the scenarios here rely on the "it" steps + // proceeding in sequence, using previous state. + // + // So we only revert to snapshot at the this level and leave + // it up to each "describe" below to revert or not at the + // individual test level. + let subSuiteSnapshotId; + + before(async () => { + const snapshot = await timeMachine.takeSnapshot(); + subSuiteSnapshotId = snapshot["result"]; + }); + + after(async () => { + await timeMachine.revertToSnapshot(subSuiteSnapshotId); + }); + + /* + * @param pool + * @param underlyerAmount amount being transferred to LP Account. + * Uses the same sign convention as `pool.getReserveTopUpValue`. + */ + async function updateTvlAfterTransfer(pool, underlyerAmount) { + underlyerAmount = underlyerAmount.mul(-1); + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); + + const underlyerPrice = await pool.getUnderlyerPrice(); + const underlyerAddress = await pool.underlyer(); + + const underlyer = await ethers.getContractAt( + "IDetailedERC20", + underlyerAddress + ); + const decimals = await underlyer.decimals(); + + const underlyerUsdValue = convertToUsdValue( + underlyerAmount, + underlyerPrice, + decimals + ); + + await updateTvl(underlyerUsdValue); + } + + function convertToUsdValue(tokenWeiAmount, tokenUsdPrice, decimals) { + return tokenWeiAmount + .mul(tokenUsdPrice) + .div(BigNumber.from(10).pow(decimals)); + } + + async function updateTvl(usdValue) { + const newTvl = (await oracleAdapter.getTvl()).add(usdValue); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(newTvl, 50); + } + + describe("Initial funding of LP Account", () => { + let testSnapshotId; + + beforeEach(async () => { + const snapshot = await timeMachine.takeSnapshot(); + testSnapshotId = snapshot["result"]; + }); + + afterEach(async () => { + await timeMachine.revertToSnapshot(testSnapshotId); + }); + + beforeEach("Deposit into pools", async () => { + for (const [pool, underlyer] of _.zip(pools, underlyers)) { + const depositAmount = tokenAmountToBigNumber( + "105", + await underlyer.decimals() + ); + await underlyer.approve(pool.address, depositAmount); + await pool.addLiquidity(depositAmount); + + expect(await underlyer.balanceOf(pool.address)).to.equal( + depositAmount + ); + expect(await underlyer.balanceOf(lpAccount.address)).to.be.zero; + } + }); + + it("Remaining pool balance should be reserve percentage (one pool)", async () => { + const oldPoolBalance = await usdcToken.balanceOf(usdcPool.address); + + await mApt.fundLpAccount([usdcPoolId]); + + const lpAccountBalance = await usdcToken.balanceOf(lpAccount.address); + const newPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const reservePercentage = await usdcPool.reservePercentage(); + + const expectedAmount = lpAccountBalance.mul(reservePercentage).div(100); + expect(newPoolBalance).to.equal(expectedAmount); + + expect(newPoolBalance.add(lpAccountBalance)).to.equal(oldPoolBalance); + }); + + it("Remaining pool balance should be reserve percentage (multiple pools)", async () => { + const oldDaiPoolBalance = await daiToken.balanceOf(daiPool.address); + const oldUsdcPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const oldTetherPoolBalance = await usdtToken.balanceOf( + usdtPool.address + ); + + await mApt.fundLpAccount([daiPoolId, usdcPoolId, tetherPoolId]); + + const newDaiPoolBalance = await daiToken.balanceOf(daiPool.address); + const newUsdcPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const newTetherPoolBalance = await usdtToken.balanceOf( + usdtPool.address + ); + const reservePercentage = await usdcPool.reservePercentage(); + + let expectedAmount = (await daiToken.balanceOf(lpAccount.address)) + .mul(reservePercentage) + .div(100); + expect(newDaiPoolBalance).to.equal(expectedAmount); + + expectedAmount = (await usdcToken.balanceOf(lpAccount.address)) + .mul(reservePercentage) + .div(100); + expect(newUsdcPoolBalance).to.equal(expectedAmount); + + expectedAmount = (await usdtToken.balanceOf(lpAccount.address)) + .mul(reservePercentage) + .div(100); + expect(newTetherPoolBalance).to.equal(expectedAmount); + + let totalBalance = (await daiToken.balanceOf(lpAccount.address)).add( + newDaiPoolBalance + ); + expect(totalBalance).to.equal(oldDaiPoolBalance); + + totalBalance = (await usdcToken.balanceOf(lpAccount.address)).add( + newUsdcPoolBalance + ); + expect(totalBalance).to.equal(oldUsdcPoolBalance); + + totalBalance = (await usdtToken.balanceOf(lpAccount.address)).add( + newTetherPoolBalance + ); + expect(totalBalance).to.equal(oldTetherPoolBalance); + }); + }); + + describe("Top-up pools", () => { + let snapshotId; + + const deployedTokens = 15000; + let depositTokens; + + let reservePercentage; + let feePercentage; + // convenient to use this than always changing the + // percentage redeemed + const redeemPercentage = BigNumber.from(1); + + before(async () => { + const snapshot = await timeMachine.takeSnapshot(); + snapshotId = snapshot["result"]; + }); + + after(async () => { + await timeMachine.revertToSnapshot(snapshotId); + }); + + async function setTvlToLpAccountValue() { + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); + + const startLpDaiBalance = await daiToken.balanceOf(lpAccount.address); + const daiUsdValue = await daiPool.getValueFromUnderlyerAmount( + startLpDaiBalance + ); + const startLpUsdcBalance = await usdcToken.balanceOf(lpAccount.address); + const usdcUsdValue = await usdcPool.getValueFromUnderlyerAmount( + startLpUsdcBalance + ); + const startLpUsdtBalance = await usdtToken.balanceOf(lpAccount.address); + const usdtUsdValue = await usdtPool.getValueFromUnderlyerAmount( + startLpUsdtBalance + ); + const totalUsdValue = daiUsdValue.add(usdcUsdValue).add(usdtUsdValue); + await oracleAdapter + .connect(emergencySafe) + .emergencySetTvl(totalUsdValue, 50); + } + + it("Seed LP Account with funds", async () => { + for (const [id, pool, underlyer] of _.zip(ids, pools, underlyers)) { + // FIXME: the test setup assumes each pool will have the same + // fee and reserve percentages + feePercentage = await pool.feePercentage(); + reservePercentage = await pool.reservePercentage(); + + depositTokens = reservePercentage + .add(100) + .mul(deployedTokens) + .div(100) + .toString(); + + const decimals = await underlyer.decimals(); + const depositAmount = tokenAmountToBigNumber(depositTokens, decimals); + await underlyer.approve(pool.address, depositAmount); + await pool.addLiquidity(depositAmount); + + await mApt.fundLpAccount([id]); + + const deployedAmount = tokenAmountToBigNumber( + deployedTokens, + decimals + ); + expect(await underlyer.balanceOf(lpAccount.address)).to.equal( + deployedAmount + ); + + await updateTvlAfterTransfer(pool, deployedAmount.mul(-1)); + } + }); + + it("Can redeem less than reserve amount after funding LP Account", async () => { + const aptBalance = await usdcPool.balanceOf(deployer.address); + const poolBalance = await usdcToken.balanceOf(usdcPool.address); + const redeemAmount = aptBalance.mul(redeemPercentage).div(100); + await expect(usdcPool.redeem(redeemAmount)).to.not.reverted; + + const newPoolBalance = await usdcToken.balanceOf(usdcPool.address); + const expectedWithdrawalAmount = tokenAmountToBigNumber( + depositTokens, + 6 + ) + .mul(redeemAmount) + .div(aptBalance); + const expectedWithdrawalAmountAfterFee = expectedWithdrawalAmount + .mul(BigNumber.from(100).sub(feePercentage)) + .div(100); + const poolBalanceDelta = poolBalance.sub(newPoolBalance); + expect(poolBalanceDelta).to.equal(expectedWithdrawalAmountAfterFee); + }); + + it("Should top-up pool to reserve percentage", async () => { + const transferAmount = await usdcPool.getReserveTopUpValue(); + + await expect(mApt.withdrawFromLpAccount([usdcPoolId])).to.not.be + .reverted; + + await updateTvlAfterTransfer(usdcPool, transferAmount); + + const lpAccountBalance = await usdcToken.balanceOf(lpAccount.address); + const expectedBalance = lpAccountBalance + .mul(reservePercentage) + .div(100); + + const poolBalance = await usdcToken.balanceOf(usdcPool.address); + expect(poolBalance).to.equal(expectedBalance); + }); + + it("Can't redeem more than available reserve", async () => { + const aptBalance = await usdcPool.balanceOf(deployer.address); + const unredeemableAptAmount = aptBalance + .mul(reservePercentage.add(1)) + .div(100); + await expect(usdcPool.redeem(unredeemableAptAmount)).to.be.reverted; + }); + + it("Can add liquidity and redeem after top-up", async () => { + const decimals = await usdcToken.decimals(); + const depositAmount = tokenAmountToBigNumber("1500", decimals); + + const prevAptBalance = await usdcPool.balanceOf(deployer.address); + + await usdcToken.approve(usdcPool.address, depositAmount); + await usdcPool.addLiquidity(depositAmount); + + const newAptBalance = await usdcPool.balanceOf(deployer.address); + + // In [1]: ((15000 * 1.05) * 0.99) / ((15000 * 1.05) * 0.99 + 1500) + // Out[1]: 0.9122422114962703 + expect(prevAptBalance.mul(100).div(newAptBalance)).to.equal(91); + + const prevUnderlyerBalance = await usdcToken.balanceOf( + deployer.address + ); + + // should be allowed to redeem this amount + expect(redeemPercentage).to.be.lt(reservePercentage); + const redeemableAptBalance = newAptBalance + .mul(redeemPercentage) + .div(100); + const originalUsdcBalance = tokenAmountToBigNumber( + depositTokens, + decimals + ); + const redeemedUsdcAmount = originalUsdcBalance + .mul(redeemPercentage) + .div(100); + const redeemedUsdcAfterFee = redeemedUsdcAmount + .mul(BigNumber.from(100).sub(reservePercentage)) + .div(100); + const usdcBalanceAfterRedeem = + originalUsdcBalance.sub(redeemedUsdcAfterFee); + const expectedUnderlyerAmount = usdcBalanceAfterRedeem + .add(depositAmount) + .mul(redeemPercentage) + .div(100); + const expectedUnderlyerAmountAfterFee = expectedUnderlyerAmount + .mul(95) + .div(100); + await expect(usdcPool.redeem(redeemableAptBalance)).to.not.be.reverted; + + const newUnderlyerBalance = await usdcToken.balanceOf(deployer.address); + const underlyerAmount = newUnderlyerBalance.sub(prevUnderlyerBalance); + // allow a few wei deviation + expect( + underlyerAmount.sub(expectedUnderlyerAmountAfterFee).abs() + ).to.be.lt(3); + }); + + it("Increase in TVL should increase value of APT holdings", async () => { + // increase TVL by 10 percent + const newTvl = (await oracleAdapter.getTvl()).mul(110).div(100); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(newTvl, 50); + + const poolBalance = await usdcToken.balanceOf(usdcPool.address); + const lpAccountBalance = await usdcToken.balanceOf(lpAccount.address); + const lpAccountBalanceWithYield = lpAccountBalance.mul(110).div(100); + + const expectedUnderlyerAmount = poolBalance.add( + lpAccountBalanceWithYield + ); + + const aptBalance = await usdcPool.balanceOf(deployer.address); + expect(await usdcPool.totalSupply()).to.equal(aptBalance); + const underlyerAmount = await usdcPool.getUnderlyerAmount(aptBalance); + // allow a few wei deviation + expect(underlyerAmount.sub(expectedUnderlyerAmount).abs()).to.be.lt(3); + }); + + it("Top-up again after TVL increase", async () => { + const lpAccountBalance = await usdcToken.balanceOf(lpAccount.address); + + const transferAmount = await usdcPool.getReserveTopUpValue(); + // Because of the amount of liquidity we added since the last top-up, + // this is now negative. + expect(transferAmount).to.be.lt(0); + await expect(mApt.fundLpAccount([usdcPoolId])).to.not.be.reverted; + + await updateTvlAfterTransfer(usdcPool, transferAmount); + + // need to adjust also by the 10% yield + const lpAccountBalanceWithYield = lpAccountBalance.mul(110).div(100); + const expectedPoolBalance = lpAccountBalanceWithYield + .add(transferAmount.mul(-1)) + .mul(reservePercentage) + .div(100); + + const poolBalance = await usdcToken.balanceOf(usdcPool.address); + // allow a few wei deviation + expect(poolBalance.sub(expectedPoolBalance).abs()).to.be.lt(3); + }); + + it("Can withdraw to pool when top-up is more than available", async () => { + // Increase TVL so that the top-up amount is much larger than + // the LP Account balance + const prevTvl = await oracleAdapter.getTvl(); + const tvl = prevTvl.mul(1000); + await oracleAdapter.connect(emergencySafe).emergencySetTvl(tvl, 50); + + const [usdcAvailableAmount] = await mApt.getLpAccountBalances([ + usdcPoolId, + ]); + console.debug("Available amount (USDC): %s", usdcAvailableAmount); + const [, rebalanceAmounts] = await mApt.getRebalanceAmounts([ + usdcPoolId, + ]); + console.debug("Rebalance amount (USDC): %s", rebalanceAmounts[0]); + expect(usdcAvailableAmount).to.be.lt(rebalanceAmounts[0]); + + const poolBalance = await usdcToken.balanceOf(usdcPool.address); + await mApt.withdrawFromLpAccount([usdcPoolId]); + expect(await usdcToken.balanceOf(usdcPool.address)).to.equal( + poolBalance.add(usdcAvailableAmount) + ); + + await setTvlToLpAccountValue(); + }); + + it("Can withdraw the full TVL by setting high reserve pool size", async () => { + // Reset TVL to the actual USD value of LP Account balances to + // undo previous TVL manipulations. + await setTvlToLpAccountValue(); + + const startLpDaiBalance = await daiToken.balanceOf(lpAccount.address); + const startLpUsdcBalance = await usdcToken.balanceOf(lpAccount.address); + const startLpUsdtBalance = await usdtToken.balanceOf(lpAccount.address); + + const amount = "1500"; + + const daiDecimals = 18; + const daiDeposit = tokenAmountToBigNumber(amount, daiDecimals); + await daiToken.approve(daiPool.address, daiDeposit); + await daiPool.addLiquidity(daiDeposit); + + const usdcDecimals = 6; + const usdcDeposit = tokenAmountToBigNumber(amount, usdcDecimals); + await usdcToken.approve(usdcPool.address, usdcDeposit); + await usdcPool.addLiquidity(usdcDeposit); + + const usdtDecimals = 6; + const usdtDeposit = tokenAmountToBigNumber(amount, usdtDecimals); + await usdtToken.approve(usdtPool.address, usdtDeposit); + await usdtPool.addLiquidity(usdtDeposit); + + const poolIds = [daiPoolId, usdcPoolId, tetherPoolId]; + + let [, [daiTopUp, usdcTopUp, usdtTopUp]] = + await mApt.getRebalanceAmounts(poolIds); + // check that fund will move capital from pools to LP Account + expect(daiTopUp).to.be.lt(0); + expect(usdcTopUp).to.be.lt(0); + expect(usdtTopUp).to.be.lt(0); + + await mApt.fundLpAccount(poolIds); + + await updateTvlAfterTransfer(daiPool, daiTopUp); + await updateTvlAfterTransfer(usdcPool, usdcTopUp); + await updateTvlAfterTransfer(usdtPool, usdtTopUp); + + const prevLpDaiBalance = await daiToken.balanceOf(lpAccount.address); + expect(prevLpDaiBalance.sub(startLpDaiBalance)).to.equal( + daiTopUp.abs() + ); + + const prevLpUsdcBalance = await usdcToken.balanceOf(lpAccount.address); + expect(prevLpUsdcBalance.sub(startLpUsdcBalance)).to.equal( + usdcTopUp.abs() + ); + + const prevLpUsdtBalance = await usdtToken.balanceOf(lpAccount.address); + expect(prevLpUsdtBalance.sub(startLpUsdtBalance)).to.equal( + usdtTopUp.abs() + ); + + const reservePoolSize = ethers.BigNumber.from("1000000000000000000"); + await daiPool.connect(adminSafe).setReservePercentage(reservePoolSize); + await usdcPool.connect(adminSafe).setReservePercentage(reservePoolSize); + await usdtPool.connect(adminSafe).setReservePercentage(reservePoolSize); + + [, [daiTopUp, usdcTopUp, usdtTopUp]] = await mApt.getRebalanceAmounts( + poolIds + ); + // check that fund will move capital from LP Account to pools + expect(daiTopUp).to.be.gt(0); + expect(usdcTopUp).to.be.gt(0); + expect(usdtTopUp).to.be.gt(0); + console.debug("DAI topup: %s", daiTopUp); + console.debug("DAI balance: %s", prevLpDaiBalance); + console.debug("USDC topup: %s", usdcTopUp); + console.debug("USDC balance: %s", prevLpUsdcBalance); + console.debug("Tether topup: %s", usdtTopUp); + console.debug("Tether balance: %s", prevLpUsdtBalance); + + await oracleAdapter.connect(emergencySafe).emergencyUnlock(); + + // Swap all stables to DAI and top-up DAI pool. + // + // A prior version of `withdrawFromLpAccount` used to + // revert if the available DAI balance for the LP account + // was less than the top-up amount. + // + // Since the revert no longer happens, we need to do the + // swaps to ensure we do the full top-up. + await lpAccount + .connect(lpSafe) + .swapWith3Pool(1, 0, prevLpUsdcBalance, 0); + await lpAccount + .connect(lpSafe) + .swapWith3Pool(2, 0, prevLpUsdtBalance, 0); + await setTvlToLpAccountValue(); + await mApt.withdrawFromLpAccount([daiPoolId]); + await setTvlToLpAccountValue(); + + // Swap DAI to USDC and top-up USDC pool. + const currentDaiBalance = await daiToken.balanceOf(lpAccount.address); + await lpAccount + .connect(lpSafe) + .swapWith3Pool(0, 1, currentDaiBalance, 0); + await setTvlToLpAccountValue(); + await mApt.withdrawFromLpAccount([usdcPoolId]); + await setTvlToLpAccountValue(); + + // Swap USDC to Tether and top-up Tether pool. + const currentUsdcBalance = await usdcToken.balanceOf(lpAccount.address); + await lpAccount + .connect(lpSafe) + .swapWith3Pool(1, 2, currentUsdcBalance, 0); + await setTvlToLpAccountValue(); + await mApt.withdrawFromLpAccount([tetherPoolId]); + await setTvlToLpAccountValue(); + + const newLpDaiBalance = await daiToken.balanceOf(lpAccount.address); + expect(newLpDaiBalance).to.be.lte( + tokenAmountToBigNumber("0.0000001", daiDecimals) + ); + + const newLpUsdcBalance = await usdcToken.balanceOf(lpAccount.address); + expect(newLpUsdcBalance).to.be.lte( + tokenAmountToBigNumber("0.000001", usdcDecimals) + ); + + const newLpUsdtBalance = await usdtToken.balanceOf(lpAccount.address); + expect(newLpUsdtBalance).to.be.lte( + tokenAmountToBigNumber("0.000001", usdtDecimals) + ); + }); + }); + }); +}); diff --git a/test-unit/IndexToken.js b/test-unit/IndexToken.js index bd7bc6d1..e83b9a71 100644 --- a/test-unit/IndexToken.js +++ b/test-unit/IndexToken.js @@ -10,12 +10,11 @@ const { ZERO_ADDRESS, FAKE_ADDRESS, tokenAmountToBigNumber, - impersonateAccount, + bytes32, } = require("../utils/helpers"); const IDetailedERC20 = artifacts.require("IDetailedERC20"); const AddressRegistry = artifacts.require("IAddressRegistryV2"); -const MetaPoolToken = artifacts.require("MetaPoolToken"); const OracleAdapter = artifacts.require("OracleAdapter"); describe("Contract: IndexToken", () => { @@ -23,7 +22,7 @@ describe("Contract: IndexToken", () => { let deployer; let adminSafe; let emergencySafe; - let mApt; + let lpAccountFunder; let lpAccount; let lpSafe; let randomUser; @@ -33,10 +32,9 @@ describe("Contract: IndexToken", () => { // mocks let assetMock; let addressRegistryMock; - let mAptMock; let oracleAdapterMock; - // pool + // vault let proxyAdmin; let indexToken; let logic; @@ -57,6 +55,7 @@ describe("Contract: IndexToken", () => { [ deployer, lpAccount, + lpAccountFunder, adminSafe, emergencySafe, lpSafe, @@ -76,8 +75,9 @@ describe("Contract: IndexToken", () => { AddressRegistry.abi ); - mAptMock = await deployMockContract(deployer, MetaPoolToken.abi); - await addressRegistryMock.mock.mAptAddress.returns(mAptMock.address); + await addressRegistryMock.mock.getAddress + .withArgs(bytes32("lpAccountFunder")) + .returns(lpAccountFunder.address); oracleAdapterMock = await deployMockContract(deployer, OracleAdapter.abi); await addressRegistryMock.mock.oracleAdapterAddress.returns( @@ -91,8 +91,6 @@ describe("Contract: IndexToken", () => { emergencySafe.address ); - mApt = await impersonateAccount(mAptMock.address, 10); - const IndexToken = await ethers.getContractFactory("TestIndexToken"); logic = await IndexToken.deploy(); await logic.deployed(); @@ -148,11 +146,12 @@ describe("Contract: IndexToken", () => { .true; }); - it("Contract role given to mAPT", async () => { + it("Contract role given to LP Account Funder", async () => { const CONTRACT_ROLE = await indexToken.CONTRACT_ROLE(); const memberCount = await indexToken.getRoleMemberCount(CONTRACT_ROLE); expect(memberCount).to.equal(1); - expect(await indexToken.hasRole(CONTRACT_ROLE, mApt.address)).to.be.true; + expect(await indexToken.hasRole(CONTRACT_ROLE, lpAccountFunder.address)) + .to.be.true; }); it("Emergency role given to Emergency Safe", async () => { @@ -250,8 +249,8 @@ describe("Contract: IndexToken", () => { }); }); - describe("Lock pool", () => { - it("Emergency Safe can lock and unlock pool", async () => { + describe("Lock vault", () => { + it("Emergency Safe can lock and unlock vault", async () => { await expect(indexToken.connect(emergencySafe).emergencyLock()).to.emit( indexToken, "Paused" @@ -274,7 +273,7 @@ describe("Contract: IndexToken", () => { ).to.be.revertedWith("NOT_EMERGENCY_ROLE"); }); - it("Revert when calling deposit on locked pool", async () => { + it("Revert when calling deposit on locked vault", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( @@ -282,7 +281,7 @@ describe("Contract: IndexToken", () => { ).to.revertedWith("Pausable: paused"); }); - it("Revert when calling mint on locked pool", async () => { + it("Revert when calling mint on locked vault", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( @@ -290,7 +289,7 @@ describe("Contract: IndexToken", () => { ).to.revertedWith("Pausable: paused"); }); - it("Revert when calling redeem on locked pool", async () => { + it("Revert when calling redeem on locked vault", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( @@ -300,7 +299,7 @@ describe("Contract: IndexToken", () => { ).to.revertedWith("Pausable: paused"); }); - it("Revert when calling withdraw on locked pool", async () => { + it("Revert when calling withdraw on locked vault", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( @@ -310,23 +309,23 @@ describe("Contract: IndexToken", () => { ).to.revertedWith("Pausable: paused"); }); - it("Revert when calling transferToLpAccount on locked pool from mAPT", async () => { + it("Revert when calling transferToLpAccount on locked vault from LP Account Funder", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( - indexToken.connect(mApt).transferToLpAccount(100) + indexToken.connect(lpAccountFunder).transferToLpAccount(100) ).to.revertedWith("Pausable: paused"); }); }); - describe("Transfer to LP Safe", () => { + describe("Transfer to LP Account", () => { before(async () => { await assetMock.mock.transfer.returns(true); }); - it("mAPT can call transferToLpAccount", async () => { - await expect(indexToken.connect(mApt).transferToLpAccount(100)).to.not.be - .reverted; + it("LP Account Funder can call transferToLpAccount", async () => { + await expect(indexToken.connect(lpAccountFunder).transferToLpAccount(100)) + .to.not.be.reverted; }); it("Revert when unpermissioned account calls transferToLpAccount", async () => { @@ -403,7 +402,12 @@ describe("Contract: IndexToken", () => { }); }); - describe("_getPoolAssetValue", () => { + describe("_getVaultAssetValue", () => { + beforeEach(async () => { + // create non-zero totalSupply so calls pass to oracle adapter + await indexToken.testMint(deployer.address, 1); + }); + it("Returns correct value regardless of deployed value", async () => { const decimals = 1; await assetMock.mock.decimals.returns(decimals); @@ -417,42 +421,46 @@ describe("Contract: IndexToken", () => { const expectedValue = balance.mul(price).div(10 ** decimals); // force zero deployed value - await mAptMock.mock.getDeployedValue.returns(0); + await oracleAdapterMock.mock.getTvl.returns(0); expect(await indexToken.testGetDeployedValue()).to.equal(0); - expect(await indexToken.testGetPoolAssetValue()).to.equal(expectedValue); + expect(await indexToken.testGetVaultAssetValue()).to.equal(expectedValue); // force non-zero deployed value - await mAptMock.mock.getDeployedValue.returns(1234); + await oracleAdapterMock.mock.getTvl.returns(1234); expect(await indexToken.testGetDeployedValue()).to.be.gt(0); - expect(await indexToken.testGetPoolAssetValue()).to.equal(expectedValue); + expect(await indexToken.testGetVaultAssetValue()).to.equal(expectedValue); }); }); describe("_getDeployedValue", () => { - it("Delegates properly to mAPT contract", async () => { - await mAptMock.mock.getDeployedValue - .withArgs(indexToken.address) - .returns(0); + beforeEach(async () => { + // create non-zero totalSupply so calls pass to oracle adapter + await indexToken.testMint(deployer.address, 1); + }); + + it("Delegates properly to Oracle Adapter", async () => { + await oracleAdapterMock.mock.getTvl.returns(0); expect(await indexToken.testGetDeployedValue()).to.equal(0); const deployedValue = tokenAmountToBigNumber(12345); - await mAptMock.mock.getDeployedValue - .withArgs(indexToken.address) - .returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); expect(await indexToken.testGetDeployedValue()).to.equal(deployedValue); }); it("Reverts with same reason when mAPT reverts", async () => { - await mAptMock.mock.getDeployedValue - .withArgs(indexToken.address) - .revertsWithReason("SOMETHING_WRONG"); + await oracleAdapterMock.mock.getTvl.revertsWithReason("SOMETHING_WRONG"); await expect(indexToken.testGetDeployedValue()).to.be.revertedWith( "SOMETHING_WRONG" ); }); }); - describe("getPoolTotalValue", () => { + describe("getVaultTotalValue", () => { + beforeEach(async () => { + // create non-zero totalSupply so calls pass to oracle adapter + await indexToken.testMint(deployer.address, 1); + }); + it("Returns correct value", async () => { const decimals = 1; await assetMock.mock.decimals.returns(decimals); @@ -460,7 +468,7 @@ describe("Contract: IndexToken", () => { await assetMock.mock.balanceOf.returns(assetBalance); const deployedValue = tokenAmountToBigNumber(1234); - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); const price = 2; await oracleAdapterMock.mock.getAssetPrice.returns(price); @@ -468,7 +476,7 @@ describe("Contract: IndexToken", () => { // asset ETH value: 75 * 2 / 10^1 = 15 const assetValue = assetBalance.mul(price).div(10 ** decimals); const expectedValue = assetValue.add(deployedValue); - expect(await indexToken.getPoolTotalValue()).to.equal(expectedValue); + expect(await indexToken.getVaultTotalValue()).to.equal(expectedValue); }); }); @@ -497,25 +505,25 @@ describe("Contract: IndexToken", () => { const aptAmount = tokenAmountToBigNumber(10); // zero deployed value - await mAptMock.mock.getDeployedValue.returns(0); - let poolTotalValue = await indexToken.getPoolTotalValue(); - let expectedValue = poolTotalValue.mul(aptAmount).div(aptSupply); + await oracleAdapterMock.mock.getTvl.returns(0); + let vaultTotalValue = await indexToken.getVaultTotalValue(); + let expectedValue = vaultTotalValue.mul(aptAmount).div(aptSupply); expect(await indexToken.getUsdValue(aptAmount)).to.equal(expectedValue); // non-zero deployed value const deployedValue = tokenAmountToBigNumber(1234); - await mAptMock.mock.getDeployedValue.returns(deployedValue); - poolTotalValue = await indexToken.getPoolTotalValue(); - expectedValue = poolTotalValue.mul(aptAmount).div(aptSupply); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); + vaultTotalValue = await indexToken.getVaultTotalValue(); + expectedValue = vaultTotalValue.mul(aptAmount).div(aptSupply); expect(await indexToken.getUsdValue(aptAmount)).to.equal(expectedValue); }); }); describe("getReserveTopUpValue", () => { - it("Returns 0 when pool has zero total value", async () => { - // set pool total ETH value to 0 + it("Returns 0 when vault has zero total value", async () => { + // set vault total ETH value to 0 await oracleAdapterMock.mock.getAssetPrice.returns(1); - await mAptMock.mock.getDeployedValue.returns(0); + await oracleAdapterMock.mock.getTvl.returns(0); await assetMock.mock.balanceOf.returns(0); await assetMock.mock.decimals.returns(6); @@ -524,13 +532,13 @@ describe("Contract: IndexToken", () => { it("Returns correctly calculated value when zero deployed value", async () => { await oracleAdapterMock.mock.getAssetPrice.returns(1); - await mAptMock.mock.getDeployedValue.returns(0); - // set positive pool asset ETH value, + await oracleAdapterMock.mock.getTvl.returns(0); + // set positive vault asset ETH value, // which should result in negative reserve top-up const decimals = 6; await assetMock.mock.decimals.returns(decimals); - const poolBalance = tokenAmountToBigNumber(105e10, decimals); - await assetMock.mock.balanceOf.returns(poolBalance); + const vaultBalance = tokenAmountToBigNumber(105e10, decimals); + await assetMock.mock.balanceOf.returns(vaultBalance); const aptSupply = tokenAmountToBigNumber(10000); await indexToken.testMint(deployer.address, aptSupply); @@ -543,7 +551,7 @@ describe("Contract: IndexToken", () => { // is what we are targeting const reservePercentage = await indexToken.reservePercentage(); const targetValue = topUpAmount.mul(-1).mul(reservePercentage).div(100); - expect(poolBalance.add(topUpAmount)).to.equal(targetValue); + expect(vaultBalance.add(topUpAmount)).to.equal(targetValue); }); it("Returns reservePercentage of post deployed value when zero balance", async () => { @@ -557,12 +565,12 @@ describe("Contract: IndexToken", () => { await indexToken.testMint(deployer.address, aptSupply); const deployedValue = tokenAmountToBigNumber(1000); - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); const topUpAmount = await indexToken.getReserveTopUpValue(); const topUpValue = topUpAmount.mul(price).div(10 ** decimals); - // assuming we unwind the top-up value from the pool's deployed + // assuming we unwind the top-up value from the vault's deployed // capital, the reserve percentage of resulting deployed value // is what we are targetting const reservePercentage = await indexToken.reservePercentage(); @@ -577,23 +585,23 @@ describe("Contract: IndexToken", () => { const price = 1; await oracleAdapterMock.mock.getAssetPrice.returns(price); const decimals = 6; - const poolBalance = tokenAmountToBigNumber(1e10, decimals); - await assetMock.mock.balanceOf.returns(poolBalance); + const vaultBalance = tokenAmountToBigNumber(1e10, decimals); + await assetMock.mock.balanceOf.returns(vaultBalance); await assetMock.mock.decimals.returns(decimals); const aptSupply = tokenAmountToBigNumber(10000); await indexToken.testMint(deployer.address, aptSupply); const deployedValue = tokenAmountToBigNumber(500); - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); - const poolAssetValue = await indexToken.testGetPoolAssetValue(); + const vaultAssetValue = await indexToken.testGetVaultAssetValue(); const topUpAmount = await indexToken.getReserveTopUpValue(); expect(topUpAmount).to.be.gt(0); const topUpValue = topUpAmount.mul(price).div(10 ** decimals); - // assuming we unwind the top-up value from the pool's deployed + // assuming we unwind the top-up value from the vault's deployed // capital, the reserve percentage of resulting deployed value // is what we are targeting const reservePercentage = await indexToken.reservePercentage(); @@ -601,30 +609,30 @@ describe("Contract: IndexToken", () => { .sub(topUpValue) .mul(reservePercentage) .div(100); - expect(poolAssetValue.add(topUpValue)).to.equal(targetValue); + expect(vaultAssetValue.add(topUpValue)).to.equal(targetValue); }); it("Returns correctly calculated value when top-up is negative", async () => { const price = 1; await oracleAdapterMock.mock.getAssetPrice.returns(price); const decimals = 6; - const poolBalance = tokenAmountToBigNumber(2.05e18, decimals); - await assetMock.mock.balanceOf.returns(poolBalance); + const vaultBalance = tokenAmountToBigNumber(2.05e18, decimals); + await assetMock.mock.balanceOf.returns(vaultBalance); await assetMock.mock.decimals.returns(decimals); const aptSupply = tokenAmountToBigNumber(10000); await indexToken.testMint(deployer.address, aptSupply); const deployedValue = tokenAmountToBigNumber(20); - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); - const poolAssetValue = await indexToken.testGetPoolAssetValue(); + const vaultAssetValue = await indexToken.testGetVaultAssetValue(); const topUpAmount = await indexToken.getReserveTopUpValue(); expect(topUpAmount).to.be.lt(0); const topUpValue = topUpAmount.mul(price).div(10 ** decimals); - // assuming we deploy the top-up (abs) value to the pool's deployed + // assuming we deploy the top-up (abs) value to the vault's deployed // capital, the reserve percentage of resulting deployed value // is what we are targeting const reservePercentage = await indexToken.reservePercentage(); @@ -632,13 +640,13 @@ describe("Contract: IndexToken", () => { .sub(topUpValue) .mul(reservePercentage) .div(100); - expect(poolAssetValue.add(topUpValue)).to.equal(targetValue); + expect(vaultAssetValue.add(topUpValue)).to.equal(targetValue); }); }); describe("convertToShares", () => { beforeEach(async () => { - await mAptMock.mock.getDeployedValue.returns(0); + await oracleAdapterMock.mock.getTvl.returns(0); }); it("Uses 1:1 token ratio with zero total supply", async () => { @@ -658,14 +666,14 @@ describe("Contract: IndexToken", () => { expectedShareAmount ); - // result doesn't depend on pool's asset balance + // result doesn't depend on vault's asset balance await assetMock.mock.balanceOf.withArgs(indexToken.address).returns(0); expect(await indexToken.convertToShares(depositAmount)).to.equal( expectedShareAmount ); - // result doesn't depend on pool's deployed value - await mAptMock.mock.getDeployedValue.returns(10000000); + // result doesn't depend on vault's deployed value + await oracleAdapterMock.mock.getTvl.returns(10000000); expect(await indexToken.convertToShares(depositAmount)).to.equal( expectedShareAmount ); @@ -676,16 +684,16 @@ describe("Contract: IndexToken", () => { const aptTotalSupply = tokenAmountToBigNumber("900", "18"); const depositAmount = tokenAmountToBigNumber("1000", decimals); - const poolBalance = tokenAmountToBigNumber("9999", decimals); + const vaultBalance = tokenAmountToBigNumber("9999", decimals); await oracleAdapterMock.mock.getAssetPrice.returns(1); - await assetMock.mock.balanceOf.returns(poolBalance); + await assetMock.mock.balanceOf.returns(vaultBalance); await assetMock.mock.decimals.returns(decimals); await indexToken.testMint(indexToken.address, aptTotalSupply); const expectedMintAmount = aptTotalSupply .mul(depositAmount) - .div(poolBalance); + .div(vaultBalance); expect(await indexToken.convertToShares(depositAmount)).to.equal( expectedMintAmount ); @@ -696,26 +704,24 @@ describe("Contract: IndexToken", () => { const aptTotalSupply = tokenAmountToBigNumber("900", "18"); const depositAmount = tokenAmountToBigNumber("1000", decimals); - const poolAssetBalance = tokenAmountToBigNumber("9999", decimals); + const vaultAssetBalance = tokenAmountToBigNumber("9999", decimals); const price = 1; await oracleAdapterMock.mock.getAssetPrice.returns(price); - await assetMock.mock.balanceOf.returns(poolAssetBalance); + await assetMock.mock.balanceOf.returns(vaultAssetBalance); await assetMock.mock.decimals.returns(decimals); - await mAptMock.mock.balanceOf.returns(tokenAmountToBigNumber(10)); - await mAptMock.mock.totalSupply.returns(tokenAmountToBigNumber(1000)); - await mAptMock.mock.getDeployedValue.returns( + await oracleAdapterMock.mock.getTvl.returns( tokenAmountToBigNumber(10000000) ); await indexToken.testMint(indexToken.address, aptTotalSupply); const depositValue = depositAmount.mul(price).div(10 ** decimals); - const poolTotalValue = await indexToken.getPoolTotalValue(); + const vaultTotalValue = await indexToken.getVaultTotalValue(); const expectedMintAmount = aptTotalSupply .mul(depositValue) - .div(poolTotalValue); + .div(vaultTotalValue); expect(await indexToken.convertToShares(depositAmount)).to.equal( expectedMintAmount ); @@ -724,7 +730,7 @@ describe("Contract: IndexToken", () => { describe("convertToAssets", () => { beforeEach(async () => { - await mAptMock.mock.getDeployedValue.returns(0); + await oracleAdapterMock.mock.getTvl.returns(0); }); it("Convert 1:1 on zero total supply", async () => { @@ -881,7 +887,7 @@ describe("Contract: IndexToken", () => { beforeEach(async () => { // These get rollbacked due to snapshotting. // Just enough mocking to get `deposit` to not revert. - await mAptMock.mock.getDeployedValue.returns(0); + await oracleAdapterMock.mock.getTvl.returns(0); await oracleAdapterMock.mock.getAssetPrice.returns(1); await assetMock.mock.decimals.returns(6); await assetMock.mock.allowance.returns(1); @@ -974,7 +980,7 @@ describe("Contract: IndexToken", () => { /* Test with range of deployed TVL values. Using 0 as deployed value forces old code paths without mAPT since - the pool's total ETH value comes purely from its asset + the vault's total ETH value comes purely from its asset holdings. */ const deployedValues = [ @@ -986,7 +992,7 @@ describe("Contract: IndexToken", () => { describe(` deployed value: ${deployedValue}`, () => { const decimals = 6; const depositAmount = tokenAmountToBigNumber(1, decimals); - const poolBalance = tokenAmountToBigNumber(1000, decimals); + const vaultBalance = tokenAmountToBigNumber(1000, decimals); // use EVM snapshots for test isolation let snapshotId; @@ -995,7 +1001,7 @@ describe("Contract: IndexToken", () => { const snapshot = await timeMachine.takeSnapshot(); snapshotId = snapshot["result"]; - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); const price = 1; await oracleAdapterMock.mock.getAssetPrice.returns(price); @@ -1004,7 +1010,7 @@ describe("Contract: IndexToken", () => { await assetMock.mock.allowance.returns(depositAmount); await assetMock.mock.balanceOf .withArgs(indexToken.address) - .returns(poolBalance); + .returns(vaultBalance); await assetMock.mock.transferFrom.returns(true); }); @@ -1029,11 +1035,11 @@ describe("Contract: IndexToken", () => { depositAmount ); - // mock the asset transfer to the pool, so we can - // check deposit event has the post-deposit pool ETH value + // mock the asset transfer to the vault, so we can + // check deposit event has the post-deposit vault ETH value await assetMock.mock.balanceOf .withArgs(indexToken.address) - .returns(poolBalance.add(depositAmount)); + .returns(vaultBalance.add(depositAmount)); const depositPromise = indexToken .connect(randomUser) @@ -1059,7 +1065,7 @@ describe("Contract: IndexToken", () => { * * expect("transferFrom") * .to.be.calledOnContract(assetMock) - * .withArgs(randomUser.address, poolToken.address, depositAmount); + * .withArgs(randomUser.address, vaultToken.address, depositAmount); * * Instead, we have to do some hacky revert-check logic. */ @@ -1117,7 +1123,7 @@ describe("Contract: IndexToken", () => { ).to.be.revertedWith("NOT_EMERGENCY_ROLE"); }); - it("Revert deposit when pool is locked", async () => { + it("Revert deposit when vault is locked", async () => { await indexToken.connect(emergencySafe).emergencyLockDeposit(); await expect( @@ -1146,7 +1152,7 @@ describe("Contract: IndexToken", () => { beforeEach(async () => { // These get rollbacked due to snapshotting. // Just enough mocking to get `mint` to not revert. - await mAptMock.mock.getDeployedValue.returns(0); + await oracleAdapterMock.mock.getTvl.returns(0); await oracleAdapterMock.mock.getAssetPrice.returns(1); await assetMock.mock.decimals.returns(6); await assetMock.mock.allowance.returns(2); // account for rounding up in previewMint @@ -1186,7 +1192,7 @@ describe("Contract: IndexToken", () => { /* Test with range of deployed TVL values. Using 0 as deployed value forces old code paths without mAPT since - the pool's total ETH value comes purely from its asset + the vault's total ETH value comes purely from its asset holdings. */ const deployedValues = [ @@ -1199,7 +1205,7 @@ describe("Contract: IndexToken", () => { const decimals = 6; const mintAmount = tokenAmountToBigNumber(1); let depositAmount; - const poolBalance = tokenAmountToBigNumber(1000, decimals); + const vaultBalance = tokenAmountToBigNumber(1000, decimals); // use EVM snapshots for test isolation let snapshotId; @@ -1208,7 +1214,7 @@ describe("Contract: IndexToken", () => { const snapshot = await timeMachine.takeSnapshot(); snapshotId = snapshot["result"]; - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); const price = 1; await oracleAdapterMock.mock.getAssetPrice.returns(price); @@ -1216,7 +1222,7 @@ describe("Contract: IndexToken", () => { await assetMock.mock.decimals.returns(decimals); await assetMock.mock.balanceOf .withArgs(indexToken.address) - .returns(poolBalance); + .returns(vaultBalance); await assetMock.mock.transferFrom.returns(true); depositAmount = await indexToken.previewMint(mintAmount); @@ -1258,7 +1264,7 @@ describe("Contract: IndexToken", () => { * * expect("transferFrom") * .to.be.calledOnContract(assetMock) - * .withArgs(randomUser.address, poolToken.address, depositAmount); + * .withArgs(randomUser.address, indexToken.address, depositAmount); * * Instead, we have to do some hacky revert-check logic. */ @@ -1309,7 +1315,7 @@ describe("Contract: IndexToken", () => { /* Test with range of deployed TVL values. Using 0 as deployed value forces old code paths without mAPT since - the pool's total ETH value comes purely from its asset + the vault's total ETH value comes purely from its asset holdings. */ const deployedValues = [ @@ -1320,7 +1326,7 @@ describe("Contract: IndexToken", () => { deployedValues.forEach(function (deployedValue) { describe(` deployed value: ${deployedValue}`, () => { const decimals = 6; - const poolBalance = tokenAmountToBigNumber(1000, decimals); + const vaultBalance = tokenAmountToBigNumber(1000, decimals); const aptSupply = tokenAmountToBigNumber(1000000); let reserveAptAmount; let aptAmount; @@ -1332,21 +1338,21 @@ describe("Contract: IndexToken", () => { const snapshot = await timeMachine.takeSnapshot(); snapshotId = snapshot["result"]; - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); const price = 1; await oracleAdapterMock.mock.getAssetPrice.returns(price); await assetMock.mock.decimals.returns(decimals); - await assetMock.mock.allowance.returns(poolBalance); + await assetMock.mock.allowance.returns(vaultBalance); await assetMock.mock.balanceOf .withArgs(indexToken.address) - .returns(poolBalance); + .returns(vaultBalance); await assetMock.mock.transfer.returns(true); - // Mint APT supply to go along with pool's total ETH value. + // Mint APT supply to go along with vault's total ETH value. await indexToken.testMint(deployer.address, aptSupply); - reserveAptAmount = await indexToken.convertToShares(poolBalance); + reserveAptAmount = await indexToken.convertToShares(vaultBalance); await indexToken .connect(deployer) .transfer(randomUser.address, reserveAptAmount); @@ -1501,7 +1507,7 @@ describe("Contract: IndexToken", () => { ).to.be.revertedWith("NOT_EMERGENCY_ROLE"); }); - it("Revert redeem when pool is locked", async () => { + it("Revert redeem when vault is locked", async () => { await indexToken.connect(emergencySafe).emergencyLockRedeem(); await expect( @@ -1523,7 +1529,7 @@ describe("Contract: IndexToken", () => { /* Test with range of deployed TVL values. Using 0 as deployed value forces old code paths without mAPT since - the pool's total ETH value comes purely from its asset + the vault's total ETH value comes purely from its asset holdings. */ const deployedValues = [ @@ -1534,7 +1540,7 @@ describe("Contract: IndexToken", () => { deployedValues.forEach(function (deployedValue) { describe(` deployed value: ${deployedValue}`, () => { const decimals = 6; - const poolBalance = tokenAmountToBigNumber(1000, decimals); + const vaultBalance = tokenAmountToBigNumber(1000, decimals); const aptSupply = tokenAmountToBigNumber(1000000); let reserveAptAmount; let aptAmount; @@ -1547,21 +1553,21 @@ describe("Contract: IndexToken", () => { const snapshot = await timeMachine.takeSnapshot(); snapshotId = snapshot["result"]; - await mAptMock.mock.getDeployedValue.returns(deployedValue); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); const price = 1; await oracleAdapterMock.mock.getAssetPrice.returns(price); await assetMock.mock.decimals.returns(decimals); - await assetMock.mock.allowance.returns(poolBalance); + await assetMock.mock.allowance.returns(vaultBalance); await assetMock.mock.balanceOf .withArgs(indexToken.address) - .returns(poolBalance); + .returns(vaultBalance); await assetMock.mock.transfer.returns(true); - // Mint APT supply to go along with pool's total ETH value. + // Mint APT supply to go along with vault's total ETH value. await indexToken.testMint(deployer.address, aptSupply); - reserveAptAmount = await indexToken.convertToShares(poolBalance); + reserveAptAmount = await indexToken.convertToShares(vaultBalance); await indexToken .connect(deployer) .transfer(randomUser.address, reserveAptAmount); @@ -1697,7 +1703,7 @@ describe("Contract: IndexToken", () => { indexToken .connect(randomUser) .withdraw( - poolBalance.add(1), + vaultBalance.add(1), receiver.address, randomUser.address ) @@ -1707,7 +1713,7 @@ describe("Contract: IndexToken", () => { }); describe("Locking", () => { - it("Revert withdraw when pool is locked", async () => { + it("Revert withdraw when vault is locked", async () => { await indexToken.connect(emergencySafe).emergencyLockRedeem(); await expect( diff --git a/test-unit/LpAccountFunder.js b/test-unit/LpAccountFunder.js new file mode 100644 index 00000000..592fd050 --- /dev/null +++ b/test-unit/LpAccountFunder.js @@ -0,0 +1,399 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); +const { ethers, artifacts, waffle } = hre; +const timeMachine = require("ganache-time-traveler"); +const { AddressZero: ZERO_ADDRESS } = ethers.constants; +const { + FAKE_ADDRESS, + tokenAmountToBigNumber, + bytes32, + deepEqual, +} = require("../utils/helpers"); +const { deployMockContract } = waffle; +const OracleAdapter = artifacts.readArtifactSync("OracleAdapter"); +const IDetailedERC20 = artifacts.readArtifactSync("IDetailedERC20"); + +describe("Contract: LpAccountFunder", () => { + // signers + let deployer; + let emergencySafe; + let adminSafe; + let lpSafe; + let randomUser; + + // deployed contracts + let lpAccountFunder; + + // mocks + let indexToken; + let lpAccount; + let oracleAdapter; + let addressRegistry; + let erc20Allocation; + + // use EVM snapshots for test isolation + let testSnapshotId; + let suiteSnapshotId; + + beforeEach(async () => { + const snapshot = await timeMachine.takeSnapshot(); + testSnapshotId = snapshot["result"]; + }); + + afterEach(async () => { + await timeMachine.revertToSnapshot(testSnapshotId); + }); + + before(async () => { + const snapshot = await timeMachine.takeSnapshot(); + suiteSnapshotId = snapshot["result"]; + }); + + after(async () => { + // In particular, we need to reset the Mainnet accounts, otherwise + // this will cause leakage into other test suites. Doing a `beforeEach` + // instead is viable but makes tests noticeably slower. + await timeMachine.revertToSnapshot(suiteSnapshotId); + }); + + before("Get signers", async () => { + [deployer, emergencySafe, adminSafe, lpSafe, randomUser] = + await ethers.getSigners(); + }); + + before("Setup address registry", async () => { + addressRegistry = await deployMockContract( + deployer, + artifacts.readArtifactSync("AddressRegistryV2").abi + ); + }); + + before("Register Safes", async () => { + await addressRegistry.mock.lpSafeAddress.returns(lpSafe.address); + await addressRegistry.mock.getAddress + .withArgs(bytes32("lpSafe")) + .returns(lpSafe.address); + + await addressRegistry.mock.emergencySafeAddress.returns( + emergencySafe.address + ); + await addressRegistry.mock.getAddress + .withArgs(bytes32("emergencySafe")) + .returns(emergencySafe.address); + + await addressRegistry.mock.adminSafeAddress.returns(adminSafe.address); + await addressRegistry.mock.getAddress + .withArgs(bytes32("adminSafe")) + .returns(adminSafe.address); + }); + + before("Mock dependencies", async () => { + oracleAdapter = await deployMockContract(deployer, OracleAdapter.abi); + await addressRegistry.mock.oracleAdapterAddress.returns( + oracleAdapter.address + ); + + // funding or withdrawing will lock the oracle adapter + await oracleAdapter.mock.lock.returns(); + + lpAccount = await deployMockContract( + deployer, + artifacts.readArtifactSync("ILpAccount").abi + ); + await addressRegistry.mock.lpAccountAddress.returns(lpAccount.address); + + erc20Allocation = await deployMockContract( + deployer, + artifacts.require("IErc20Allocation").abi + ); + await erc20Allocation.mock["isErc20TokenRegistered(address)"].returns(true); + + const tvlManager = await deployMockContract( + deployer, + artifacts.readArtifactSync("IAssetAllocationRegistry").abi + ); + await tvlManager.mock.getAssetAllocation + .withArgs("erc20Allocation") + .returns(erc20Allocation.address); + await addressRegistry.mock.getAddress + .withArgs(bytes32("tvlManager")) + .returns(tvlManager.address); + + indexToken = await deployMockContract( + deployer, + artifacts.readArtifactSync("IndexToken").abi + ); + }); + + before("Deploy LpAccountFunder", async () => { + const LpAccountFunder = await ethers.getContractFactory( + "TestLpAccountFunder" + ); + lpAccountFunder = await LpAccountFunder.deploy( + addressRegistry.address, + indexToken.address + ); + await lpAccountFunder.deployed(); + }); + + describe("Defaults", () => { + it("Default admin role given to Emergency Safe", async () => { + const DEFAULT_ADMIN_ROLE = await lpAccountFunder.DEFAULT_ADMIN_ROLE(); + const memberCount = await lpAccountFunder.getRoleMemberCount( + DEFAULT_ADMIN_ROLE + ); + expect(memberCount).to.equal(1); + expect( + await lpAccountFunder.hasRole(DEFAULT_ADMIN_ROLE, emergencySafe.address) + ).to.be.true; + }); + + it("LP role given to LP Safe", async () => { + const LP_ROLE = await lpAccountFunder.LP_ROLE(); + const memberCount = await lpAccountFunder.getRoleMemberCount(LP_ROLE); + expect(memberCount).to.equal(1); + expect(await lpAccountFunder.hasRole(LP_ROLE, lpSafe.address)).to.be.true; + }); + + it("Emergency role given to Emergency Safe", async () => { + const EMERGENCY_ROLE = await lpAccountFunder.EMERGENCY_ROLE(); + const memberCount = await lpAccountFunder.getRoleMemberCount( + EMERGENCY_ROLE + ); + expect(memberCount).to.equal(1); + expect( + await lpAccountFunder.hasRole(EMERGENCY_ROLE, emergencySafe.address) + ).to.be.true; + }); + + it("Address Registry set correctly", async () => { + expect(await lpAccountFunder.addressRegistry()).to.equal( + addressRegistry.address + ); + }); + + it("Index Token set correctly", async () => { + expect(await lpAccountFunder.indexToken()).to.equal(indexToken.address); + }); + }); + + describe("emergencySetAddressRegistry", () => { + it("Emergency Safe can set to valid address", async () => { + const contractAddress = (await deployMockContract(deployer, [])).address; + await lpAccountFunder + .connect(emergencySafe) + .emergencySetAddressRegistry(contractAddress); + expect(await lpAccountFunder.addressRegistry()).to.equal(contractAddress); + }); + + it("Revert when unpermissioned attempts to set", async () => { + const contractAddress = (await deployMockContract(deployer, [])).address; + await expect( + lpAccountFunder + .connect(randomUser) + .emergencySetAddressRegistry(contractAddress) + ).to.be.revertedWith("NOT_EMERGENCY_ROLE"); + }); + + it("Cannot set to non-contract address", async () => { + await expect( + lpAccountFunder + .connect(emergencySafe) + .emergencySetAddressRegistry(FAKE_ADDRESS) + ).to.be.revertedWith("INVALID_ADDRESS"); + }); + }); + + describe("fund and withdraw", () => { + let asset; + + before("Setup mocks", async () => { + await indexToken.mock.transferToLpAccount.returns(); + await indexToken.mock.getAssetPrice.returns( + tokenAmountToBigNumber("0.998", 8) + ); + + asset = await deployMockContract(deployer, IDetailedERC20.abi); + await indexToken.mock.asset.returns(asset.address); + + await asset.mock.decimals.returns(18); + + await lpAccount.mock.transferToPool.returns(); + + await oracleAdapter.mock.getTvl.returns( + tokenAmountToBigNumber("12345678", 8) + ); + }); + + describe("_registerPoolUnderlyer", () => { + beforeEach("Setup mocks", async () => { + await asset.mock.symbol.returns("3CRV"); + }); + + it("Unregistered asset get registered", async () => { + // set asset as unregistered in ERC20 registry + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(asset.address) + .returns(false); + + // revert on registration + await erc20Allocation.mock["registerErc20Token(address)"].returns(); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(asset.address) + .revertsWithReason("TEST_REGISTER_ASSET"); + + // expect revert since register function should be called + await expect( + lpAccountFunder.testRegisterPoolUnderlyer() + ).to.be.revertedWith("TEST_REGISTER_ASSET"); + }); + + it("Registered asset is skipped", async () => { + // set asset as registered in ERC20 registry + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(asset.address) + .returns(true); + + // revert on registration + await erc20Allocation.mock["registerErc20Token(address)"].returns(); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(asset.address) + .revertsWithReason("TEST_SKIP_REGISTER"); + + // should not revert since asset is already registered + await expect(lpAccountFunder.testRegisterPoolUnderlyer()).to.not.be + .reverted; + }); + }); + + describe("fundLpAccount", () => { + before(async () => { + await indexToken.mock.getReserveTopUpValue.returns(0); + }); + + it("LP Safe can call", async () => { + await expect(lpAccountFunder.connect(lpSafe).fundLpAccount()).to.not.be + .reverted; + }); + + it("Unpermissioned cannot call", async () => { + await expect( + lpAccountFunder.connect(randomUser).fundLpAccount() + ).to.be.revertedWith("NOT_LP_ROLE"); + }); + + it("Revert on unregistered LP Account address", async () => { + await addressRegistry.mock.lpAccountAddress.returns(ZERO_ADDRESS); + await expect( + lpAccountFunder.connect(lpSafe).fundLpAccount() + ).to.be.revertedWith("INVALID_LP_ACCOUNT"); + }); + + it("Locks Oracle Adapter", async () => { + await oracleAdapter.mock.lock.revertsWithReason("TEST_LOCK"); + + await expect( + lpAccountFunder.connect(lpSafe).fundLpAccount() + ).to.be.revertedWith("TEST_LOCK"); + }); + }); + + describe("withdrawFromLpAccount", () => { + before(async () => { + await indexToken.mock.getReserveTopUpValue.returns(0); + await asset.mock.balanceOf.returns(0); + }); + + it("LP Safe can call", async () => { + await expect(lpAccountFunder.connect(lpSafe).withdrawFromLpAccount()).to + .not.be.reverted; + }); + + it("Unpermissioned cannot call", async () => { + await expect( + lpAccountFunder.connect(randomUser).withdrawFromLpAccount() + ).to.be.revertedWith("NOT_LP_ROLE"); + }); + + it("Locks Oracle Adapter", async () => { + await oracleAdapter.mock.lock.revertsWithReason("TEST_LOCK"); + + await expect( + lpAccountFunder.connect(lpSafe).withdrawFromLpAccount() + ).to.be.revertedWith("TEST_LOCK"); + }); + }); + + describe("getRebalanceAmount", () => { + it("Delegates to vault function", async () => { + const vaultRebalanceAmount = tokenAmountToBigNumber("1234888", "18"); + await indexToken.mock.getReserveTopUpValue.returns( + vaultRebalanceAmount + ); + + const result = await lpAccountFunder.getRebalanceAmount(); + expect(result).to.equal(vaultRebalanceAmount); + }); + }); + + describe("getLpAccountBalance", () => { + it("Return array of available stablecoin balances of LP Account", async () => { + const availableAmount = tokenAmountToBigNumber("15325", "18"); + await asset.mock.balanceOf + .withArgs(lpAccount.address) + .returns(availableAmount); + + const result = await lpAccountFunder.getLpAccountBalance(); + expect(result).to.equal(availableAmount); + }); + }); + + describe("_getFundAmount", () => { + it("Replaces negatives with positives, positives with zeros", async () => { + let amount = tokenAmountToBigNumber("159"); + let expectedResult = tokenAmountToBigNumber("0"); + let result = await lpAccountFunder.testGetFundAmount(amount); + deepEqual(expectedResult, result); + + amount = tokenAmountToBigNumber("-159"); + expectedResult = tokenAmountToBigNumber("159"); + result = await lpAccountFunder.testGetFundAmount(amount); + deepEqual(expectedResult, result); + }); + }); + + describe("_calculateAmountToWithdraw", () => { + it("Replaces negatives with zeros", async () => { + let topupAmount = tokenAmountToBigNumber("159"); + let availableAmount = topupAmount; + let expectedResult = topupAmount; + let result = await lpAccountFunder.testCalculateAmountToWithdraw( + topupAmount, + availableAmount + ); + + deepEqual(expectedResult, result); + + topupAmount = tokenAmountToBigNumber("-11"); + expectedResult = tokenAmountToBigNumber("0"); + availableAmount = expectedResult; + result = await lpAccountFunder.testCalculateAmountToWithdraw( + topupAmount, + availableAmount + ); + deepEqual(expectedResult, result); + }); + + it("Uses minimum of topup and available amounts", async () => { + let topupAmount = tokenAmountToBigNumber("159"); + let availableAmount = tokenAmountToBigNumber("122334"); + let expectedResult = topupAmount; + let result = await lpAccountFunder.testCalculateAmountToWithdraw( + topupAmount, + availableAmount + ); + deepEqual(expectedResult, result); + }); + }); + }); +});