From fc16ad7930f3ab3e58d37300967f54f0afc0b2ce Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Thu, 28 Jul 2022 16:19:35 -0400 Subject: [PATCH 01/18] Replace call to mAPT with one to oracle adapter --- contracts/index/IndexToken.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/index/IndexToken.sol b/contracts/index/IndexToken.sol index 008abea0..4ef72435 100644 --- a/contracts/index/IndexToken.sol +++ b/contracts/index/IndexToken.sol @@ -692,12 +692,12 @@ 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 + * @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)); + IOracleAdapter oracleAdapter = + IOracleAdapter(addressRegistry.oracleAdapterAddress()); + return oracleAdapter.getTvl(); } function _previewRedeem(uint256 shareAmount, bool arbFee) From 558dcdb5aeb50c49b6f935b701898a898a637a23 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Fri, 29 Jul 2022 17:56:20 -0400 Subject: [PATCH 02/18] Replace mAPT with lpAccountFunder address --- contracts/index/IndexToken.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/index/IndexToken.sol b/contracts/index/IndexToken.sol index 4ef72435..21be83b1 100644 --- a/contracts/index/IndexToken.sol +++ b/contracts/index/IndexToken.sol @@ -125,7 +125,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; From d96f4c998a6a3afccbd4a41c9f125e43e74cd049 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Fri, 29 Jul 2022 18:23:31 -0400 Subject: [PATCH 03/18] Update unit tests --- test-unit/IndexToken.js | 87 ++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/test-unit/IndexToken.js b/test-unit/IndexToken.js index bd7bc6d1..77fe1c8f 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,7 +32,6 @@ describe("Contract: IndexToken", () => { // mocks let assetMock; let addressRegistryMock; - let mAptMock; let oracleAdapterMock; // pool @@ -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 () => { @@ -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 pool 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 () => { @@ -417,35 +416,29 @@ 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); // 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); }); }); describe("_getDeployedValue", () => { - it("Delegates properly to mAPT contract", async () => { - await mAptMock.mock.getDeployedValue - .withArgs(indexToken.address) - .returns(0); + 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" ); @@ -460,7 +453,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); @@ -497,14 +490,14 @@ describe("Contract: IndexToken", () => { const aptAmount = tokenAmountToBigNumber(10); // zero deployed value - await mAptMock.mock.getDeployedValue.returns(0); + await oracleAdapterMock.mock.getTvl.returns(0); let poolTotalValue = await indexToken.getPoolTotalValue(); let expectedValue = poolTotalValue.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); + await oracleAdapterMock.mock.getTvl.returns(deployedValue); poolTotalValue = await indexToken.getPoolTotalValue(); expectedValue = poolTotalValue.mul(aptAmount).div(aptSupply); expect(await indexToken.getUsdValue(aptAmount)).to.equal(expectedValue); @@ -515,7 +508,7 @@ describe("Contract: IndexToken", () => { it("Returns 0 when pool has zero total value", async () => { // set pool 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,7 +517,7 @@ 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); + await oracleAdapterMock.mock.getTvl.returns(0); // set positive pool asset ETH value, // which should result in negative reserve top-up const decimals = 6; @@ -557,7 +550,7 @@ 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); @@ -585,7 +578,7 @@ describe("Contract: IndexToken", () => { 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 topUpAmount = await indexToken.getReserveTopUpValue(); @@ -616,7 +609,7 @@ describe("Contract: IndexToken", () => { 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 topUpAmount = await indexToken.getReserveTopUpValue(); @@ -638,7 +631,7 @@ describe("Contract: IndexToken", () => { 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 () => { @@ -665,7 +658,7 @@ describe("Contract: IndexToken", () => { ); // result doesn't depend on pool's deployed value - await mAptMock.mock.getDeployedValue.returns(10000000); + await oracleAdapterMock.mock.getTvl.returns(10000000); expect(await indexToken.convertToShares(depositAmount)).to.equal( expectedShareAmount ); @@ -703,9 +696,7 @@ describe("Contract: IndexToken", () => { await assetMock.mock.balanceOf.returns(poolAssetBalance); 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) ); @@ -724,7 +715,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 +872,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); @@ -995,7 +986,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); @@ -1146,7 +1137,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 @@ -1208,7 +1199,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); @@ -1332,7 +1323,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); @@ -1547,7 +1538,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); From a03f552ba5b6d2d9de15c38ba4b0fef55f6965e3 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Mon, 1 Aug 2022 10:56:18 -0400 Subject: [PATCH 04/18] Start int test cleanup --- test-integration/IndexToken.js | 70 ++++++++++++++-------------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/test-integration/IndexToken.js b/test-integration/IndexToken.js index cd0f6a5b..560ccf1c 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,7 +21,12 @@ const link = (amount) => tokenAmountToBigNumber(amount, "18"); console.debugging = false; /* ************************ */ -describe("Contract: IndexToken", () => { + const vaultAssetSymbol = "USDC"; + const vaultAssetAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + // use usdc agg for now + const vaultAggAddress = "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"; + +describe.only("Contract: IndexToken", () => { let deployer; let oracle; let adminSafe; @@ -30,6 +35,14 @@ describe("Contract: IndexToken", () => { let anotherUser; let receiver; + + let tvlAgg; + let asset; + let oracleAdapter; + let lpAccountFunder; + let addressRegistry; + let indexToken; + before(async () => { [ deployer, @@ -39,6 +52,7 @@ describe("Contract: IndexToken", () => { randomUser, anotherUser, receiver, + lpAccountFunder, ] = await ethers.getSigners(); }); @@ -64,19 +78,9 @@ 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 +159,18 @@ 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(); + await addressRegistry.registerAddress(bytes32("lpAccountFunder"), lpAccountFunder.address); - const mAptInitData = MetaPoolToken.interface.encodeFunctionData( - "initialize(address)", - [addressRegistry.address] - ); - const mAptProxy = await TransparentUpgradeableProxy.deploy( - mAptLogic.address, - proxyAdmin.address, - mAptInitData - ); - 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 +198,7 @@ describe("Contract: IndexToken", () => { indexToken = await IndexToken.attach(proxy.address); await acquireToken( - WHALE_POOLS[symbol], + WHALE_POOLS[vaultAssetSymbol], randomUser.address, asset, "1000000", @@ -334,14 +326,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); - + describe.only("Transfer to LP Account", () => { + it("LP Account Funder can call transferToLpAccount", async () => { await indexToken.connect(randomUser).deposit(100, receiver.address); - await expect(indexToken.connect(mAptSigner).transferToLpAccount(100)).to + await expect(indexToken.connect(lpAccountFunder).transferToLpAccount(100)).to .not.be.reverted; }); @@ -1067,7 +1055,7 @@ describe("Contract: IndexToken", () => { ); // seed pool with stablecoin await acquireToken( - WHALE_POOLS[symbol], + WHALE_POOLS[vaultAssetSymbol], indexToken.address, asset, "12000000", // 12 MM @@ -1103,7 +1091,7 @@ describe("Contract: IndexToken", () => { ); // seed pool with stablecoin await acquireToken( - WHALE_POOLS[symbol], + WHALE_POOLS[vaultAssetSymbol], indexToken.address, asset, "12000000", // 12 MM From 4cb702f28080f8059bbba7ff5b2e6dbc34588c4b Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Mon, 1 Aug 2022 10:56:53 -0400 Subject: [PATCH 05/18] Fix deployed value tests --- test-integration/IndexToken.js | 48 +++++++++------------------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/test-integration/IndexToken.js b/test-integration/IndexToken.js index 560ccf1c..2d5d4801 100644 --- a/test-integration/IndexToken.js +++ b/test-integration/IndexToken.js @@ -21,10 +21,10 @@ const link = (amount) => tokenAmountToBigNumber(amount, "18"); console.debugging = false; /* ************************ */ - const vaultAssetSymbol = "USDC"; - const vaultAssetAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - // use usdc agg for now - const vaultAggAddress = "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"; +const vaultAssetSymbol = "USDC"; +const vaultAssetAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +// use usdc agg for now +const vaultAggAddress = "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"; describe.only("Contract: IndexToken", () => { let deployer; @@ -35,7 +35,6 @@ describe.only("Contract: IndexToken", () => { let anotherUser; let receiver; - let tvlAgg; let asset; let oracleAdapter; @@ -78,7 +77,6 @@ describe.only("Contract: IndexToken", () => { await timeMachine.revertToSnapshot(suiteSnapshotId); }); - before("Setup", async () => { asset = await ethers.getContractAt("IDetailedERC20", vaultAssetAddress); @@ -159,10 +157,13 @@ describe.only("Contract: IndexToken", () => { const proxyAdmin = await ProxyAdmin.deploy(); await proxyAdmin.deployed(); - await addressRegistry.registerAddress(bytes32("lpAccountFunder"), lpAccountFunder.address); + await addressRegistry.registerAddress( + bytes32("lpAccountFunder"), + lpAccountFunder.address + ); // dummy address needed for oracle adapter deploy - const mAptAddress = await generateContractAddress(deployer) + const mAptAddress = await generateContractAddress(deployer); await addressRegistry.registerAddress(bytes32("mApt"), mAptAddress); const OracleAdapter = await ethers.getContractFactory("OracleAdapter"); @@ -326,11 +327,11 @@ describe.only("Contract: IndexToken", () => { }); }); - describe.only("Transfer to LP Account", () => { + describe("Transfer to LP Account", () => { it("LP Account Funder can call transferToLpAccount", async () => { await indexToken.connect(randomUser).deposit(100, receiver.address); - await expect(indexToken.connect(lpAccountFunder).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 () => { @@ -461,8 +462,6 @@ describe.only("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); @@ -476,7 +475,6 @@ describe.only("Contract: IndexToken", () => { /* these get rollbacked after each test due to snapshotting */ // default to giving entire deployed value to the pool - await mApt.testMint(indexToken.address, mAptSupply); await updateTvlAgg(deployedValue); await oracleAdapter.connect(emergencySafe).emergencyUnlock(); }); @@ -575,28 +573,6 @@ describe.only("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 () => { From 3389a57c6af690d8ce57c85924d360aa196639d2 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Mon, 1 Aug 2022 11:02:00 -0400 Subject: [PATCH 06/18] Add check to retain old functionality --- contracts/index/IndexToken.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/index/IndexToken.sol b/contracts/index/IndexToken.sol index 21be83b1..3fc7915d 100644 --- a/contracts/index/IndexToken.sol +++ b/contracts/index/IndexToken.sol @@ -698,6 +698,8 @@ contract IndexToken is * @return The USD value. USD prices have 8 decimals. */ function _getDeployedValue() internal view returns (uint256) { + if (totalSupply() == 0) return 0; + IOracleAdapter oracleAdapter = IOracleAdapter(addressRegistry.oracleAdapterAddress()); return oracleAdapter.getTvl(); From 1ccffef9206e7be74033a5e1b61fcadb6480ff7d Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Mon, 1 Aug 2022 15:49:52 -0400 Subject: [PATCH 07/18] Fix redeem unlock test --- test-integration/IndexToken.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test-integration/IndexToken.js b/test-integration/IndexToken.js index 2d5d4801..2efff807 100644 --- a/test-integration/IndexToken.js +++ b/test-integration/IndexToken.js @@ -380,6 +380,7 @@ describe.only("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) From ea0a7d78f583d5b82e86c6976ac191021240f329 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Wed, 3 Aug 2022 13:13:16 -0400 Subject: [PATCH 08/18] Rename pool to vault --- .../index/{IFeePool.sol => IFeeVault.sol} | 4 +- .../{ILockingPool.sol => ILockingVault.sol} | 4 +- contracts/index/IReservePool.sol | 6 +- contracts/index/IReserveVault.sol | 39 ++++++++++++ contracts/index/Imports.sol | 6 +- contracts/index/IndexToken.sol | 62 +++++++++---------- contracts/index/TestIndexToken.sol | 4 +- 7 files changed, 81 insertions(+), 44 deletions(-) rename contracts/index/{IFeePool.sol => IFeeVault.sol} (96%) rename contracts/index/{ILockingPool.sol => ILockingVault.sol} (90%) create mode 100644 contracts/index/IReserveVault.sol 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/IReservePool.sol index 3829375b..02c29354 100644 --- a/contracts/index/IReservePool.sol +++ b/contracts/index/IReservePool.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/IReserveVault.sol b/contracts/index/IReserveVault.sol new file mode 100644 index 00000000..02c29354 --- /dev/null +++ b/contracts/index/IReserveVault.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity 0.6.11; + +/** + * @notice For vaults that keep a separate reserve of tokens + */ +interface IReserveVault { + /** + * @notice Log when the percent held in reserve is changed + * @param reservePercentage The new percent held in reserve + */ + event ReservePercentageChanged(uint256 reservePercentage); + + /** + * @notice Set a new percent of tokens to hold in reserve + * @param reservePercentage_ The new percent + */ + function setReservePercentage(uint256 reservePercentage_) external; + + /** + * @notice Transfer an amount of tokens to the LP Account + * @dev This should only be callable by the `LpAccountFunder` + * @param amount The amount of tokens + */ + function transferToLpAccount(uint256 amount) external; + + /** + * @notice Get the amount of tokens missing from the reserve + * @dev A negative amount indicates extra tokens not needed for the reserve + * @return The amount of missing tokens + */ + function getReserveTopUpValue() external view returns (int256); + + /** + * @notice Get the current percentage of tokens held in reserve + * @return The percent + */ + function reservePercentage() external view returns (uint256); +} 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 3fc7915d..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; /* ------------------------------- */ @@ -238,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, @@ -404,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) { @@ -532,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); } @@ -561,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 @@ -584,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( @@ -608,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( @@ -617,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); @@ -632,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 * @@ -664,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), @@ -682,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)) @@ -693,8 +691,8 @@ 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 + * @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) { 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) From 11956f6594402151849d63c9484f58685eb9ac87 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Wed, 3 Aug 2022 13:26:18 -0400 Subject: [PATCH 09/18] Update tests --- test-integration/IndexToken.js | 66 +++++++-------- test-unit/IndexToken.js | 142 ++++++++++++++++----------------- 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/test-integration/IndexToken.js b/test-integration/IndexToken.js index 2efff807..630dba67 100644 --- a/test-integration/IndexToken.js +++ b/test-integration/IndexToken.js @@ -26,7 +26,7 @@ const vaultAssetAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // use usdc agg for now const vaultAggAddress = "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"; -describe.only("Contract: IndexToken", () => { +describe("Contract: IndexToken", () => { let deployer; let oracle; let adminSafe; @@ -238,8 +238,8 @@ describe.only("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" @@ -262,7 +262,7 @@ describe.only("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( @@ -276,7 +276,7 @@ describe.only("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( @@ -310,7 +310,7 @@ describe.only("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( @@ -365,7 +365,7 @@ describe.only("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( @@ -403,19 +403,19 @@ describe.only("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", @@ -430,16 +430,16 @@ describe.only("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 () => { @@ -475,7 +475,7 @@ describe.only("Contract: IndexToken", () => { beforeEach(async () => { /* these get rollbacked after each test due to snapshotting */ - // default to giving entire deployed value to the pool + // default to giving entire deployed value to the vault await updateTvlAgg(deployedValue); await oracleAdapter.connect(emergencySafe).emergencyUnlock(); }); @@ -509,9 +509,9 @@ describe.only("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)); }); @@ -544,12 +544,12 @@ describe.only("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 ); @@ -565,7 +565,7 @@ describe.only("Contract: IndexToken", () => { expectedAssetValue = await indexToken.getValueFromAssetAmount( assetBalance ); - expect(await indexToken.testGetPoolAssetValue()).to.equal( + expect(await indexToken.testGetVaultAssetValue()).to.equal( expectedAssetValue ); }); @@ -592,8 +592,8 @@ describe.only("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(); @@ -603,7 +603,7 @@ describe.only("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 ); }); @@ -771,7 +771,7 @@ describe.only("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) @@ -805,8 +805,8 @@ describe.only("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 */ @@ -920,7 +920,7 @@ describe.only("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) @@ -942,8 +942,8 @@ describe.only("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 */ @@ -1030,7 +1030,7 @@ describe.only("Contract: IndexToken", () => { deployer.address, tokenAmountToBigNumber("100000") ); - // seed pool with stablecoin + // seed vault with stablecoin await acquireToken( WHALE_POOLS[vaultAssetSymbol], indexToken.address, diff --git a/test-unit/IndexToken.js b/test-unit/IndexToken.js index 77fe1c8f..39a5c467 100644 --- a/test-unit/IndexToken.js +++ b/test-unit/IndexToken.js @@ -17,7 +17,7 @@ const IDetailedERC20 = artifacts.require("IDetailedERC20"); const AddressRegistry = artifacts.require("IAddressRegistryV2"); const OracleAdapter = artifacts.require("OracleAdapter"); -describe("Contract: IndexToken", () => { +describe.only("Contract: IndexToken", () => { // signers let deployer; let adminSafe; @@ -34,7 +34,7 @@ describe("Contract: IndexToken", () => { let addressRegistryMock; let oracleAdapterMock; - // pool + // vault let proxyAdmin; let indexToken; let logic; @@ -249,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" @@ -273,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( @@ -281,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( @@ -289,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( @@ -299,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( @@ -309,7 +309,7 @@ describe("Contract: IndexToken", () => { ).to.revertedWith("Pausable: paused"); }); - it("Revert when calling transferToLpAccount on locked pool from LP Account Funder", async () => { + it("Revert when calling transferToLpAccount on locked vault from LP Account Funder", async () => { await indexToken.connect(emergencySafe).emergencyLock(); await expect( @@ -402,7 +402,7 @@ describe("Contract: IndexToken", () => { }); }); - describe("_getPoolAssetValue", () => { + describe("_getVaultAssetValue", () => { it("Returns correct value regardless of deployed value", async () => { const decimals = 1; await assetMock.mock.decimals.returns(decimals); @@ -418,12 +418,12 @@ describe("Contract: IndexToken", () => { // force zero deployed value 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 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); }); }); @@ -445,7 +445,7 @@ describe("Contract: IndexToken", () => { }); }); - describe("getPoolTotalValue", () => { + describe("getVaultTotalValue", () => { it("Returns correct value", async () => { const decimals = 1; await assetMock.mock.decimals.returns(decimals); @@ -461,7 +461,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); }); }); @@ -491,22 +491,22 @@ describe("Contract: IndexToken", () => { // zero deployed value await oracleAdapterMock.mock.getTvl.returns(0); - let poolTotalValue = await indexToken.getPoolTotalValue(); - let expectedValue = poolTotalValue.mul(aptAmount).div(aptSupply); + 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 oracleAdapterMock.mock.getTvl.returns(deployedValue); - poolTotalValue = await indexToken.getPoolTotalValue(); - expectedValue = poolTotalValue.mul(aptAmount).div(aptSupply); + 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 oracleAdapterMock.mock.getTvl.returns(0); await assetMock.mock.balanceOf.returns(0); @@ -518,12 +518,12 @@ describe("Contract: IndexToken", () => { it("Returns correctly calculated value when zero deployed value", async () => { await oracleAdapterMock.mock.getAssetPrice.returns(1); await oracleAdapterMock.mock.getTvl.returns(0); - // set positive pool asset ETH value, + // 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); @@ -536,7 +536,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 () => { @@ -555,7 +555,7 @@ describe("Contract: IndexToken", () => { 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(); @@ -570,8 +570,8 @@ 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); @@ -580,13 +580,13 @@ describe("Contract: IndexToken", () => { const deployedValue = tokenAmountToBigNumber(500); 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(); @@ -594,15 +594,15 @@ 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); @@ -611,13 +611,13 @@ describe("Contract: IndexToken", () => { const deployedValue = tokenAmountToBigNumber(20); 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(); @@ -625,7 +625,7 @@ describe("Contract: IndexToken", () => { .sub(topUpValue) .mul(reservePercentage) .div(100); - expect(poolAssetValue.add(topUpValue)).to.equal(targetValue); + expect(vaultAssetValue.add(topUpValue)).to.equal(targetValue); }); }); @@ -651,13 +651,13 @@ 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 + // result doesn't depend on vault's deployed value await oracleAdapterMock.mock.getTvl.returns(10000000); expect(await indexToken.convertToShares(depositAmount)).to.equal( expectedShareAmount @@ -669,16 +669,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 ); @@ -689,11 +689,11 @@ 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 oracleAdapterMock.mock.getTvl.returns( @@ -703,10 +703,10 @@ describe("Contract: IndexToken", () => { 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 ); @@ -965,7 +965,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 = [ @@ -977,7 +977,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 +995,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); }); @@ -1020,11 +1020,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) @@ -1050,7 +1050,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. */ @@ -1108,7 +1108,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( @@ -1177,7 +1177,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 = [ @@ -1190,7 +1190,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; @@ -1207,7 +1207,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); @@ -1249,7 +1249,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. */ @@ -1300,7 +1300,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 = [ @@ -1311,7 +1311,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; @@ -1329,15 +1329,15 @@ describe("Contract: IndexToken", () => { 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); @@ -1492,7 +1492,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( @@ -1514,7 +1514,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 = [ @@ -1525,7 +1525,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; @@ -1544,15 +1544,15 @@ describe("Contract: IndexToken", () => { 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); @@ -1688,7 +1688,7 @@ describe("Contract: IndexToken", () => { indexToken .connect(randomUser) .withdraw( - poolBalance.add(1), + vaultBalance.add(1), receiver.address, randomUser.address ) @@ -1698,7 +1698,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( From e2ef2b84e505bd4dbdc8b01f9587b039ca6b9e18 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Wed, 3 Aug 2022 14:12:10 -0400 Subject: [PATCH 10/18] Fix unit tests from earlier change Having `_getDeployedValue` return 0 when zero supply botches some of the calc unit tests, which used to mock out mAPT. --- test-unit/IndexToken.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test-unit/IndexToken.js b/test-unit/IndexToken.js index 39a5c467..e83b9a71 100644 --- a/test-unit/IndexToken.js +++ b/test-unit/IndexToken.js @@ -17,7 +17,7 @@ const IDetailedERC20 = artifacts.require("IDetailedERC20"); const AddressRegistry = artifacts.require("IAddressRegistryV2"); const OracleAdapter = artifacts.require("OracleAdapter"); -describe.only("Contract: IndexToken", () => { +describe("Contract: IndexToken", () => { // signers let deployer; let adminSafe; @@ -403,6 +403,11 @@ describe.only("Contract: IndexToken", () => { }); 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); @@ -428,6 +433,11 @@ describe.only("Contract: IndexToken", () => { }); describe("_getDeployedValue", () => { + 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); @@ -446,6 +456,11 @@ describe.only("Contract: IndexToken", () => { }); 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); From a8ba500d0a85cc3bec1c9770f1a14f7811af4301 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Mon, 1 Aug 2022 17:07:26 -0400 Subject: [PATCH 11/18] Start LP Account Funder --- contracts/index/LpAccountFunder.sol | 313 ++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 contracts/index/LpAccountFunder.sol diff --git a/contracts/index/LpAccountFunder.sol b/contracts/index/LpAccountFunder.sol new file mode 100644 index 00000000..b614a1aa --- /dev/null +++ b/contracts/index/LpAccountFunder.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.6.11; +pragma experimental ABIEncoderV2; + +import {IDetailedERC20} from "contracts/common/Imports.sol"; +import {SafeERC20} from "contracts/libraries/Imports.sol"; +import { + Initializable, + ERC20UpgradeSafe, + ReentrancyGuardUpgradeSafe, + PausableUpgradeSafe, + AccessControlUpgradeSafe, + Address as AddressUpgradeSafe, + SafeMath as SafeMathUpgradeSafe, + SignedSafeMath as SignedSafeMathUpgradeSafe +} from "contracts/proxy/Imports.sol"; +import {ILpAccount} from "contracts/lpaccount/Imports.sol"; +import {IAddressRegistryV2} from "contracts/registry/Imports.sol"; +import {ILockingOracle} from "contracts/oracle/Imports.sol"; +import {IReservePool} from "contracts/pool/Imports.sol"; +import { + IErc20Allocation, + IAssetAllocationRegistry, + Erc20AllocationConstants +} from "contracts/tvl/Imports.sol"; + +import {ILpAccountFunder} from "./ILpAccountFunder.sol"; + +/** + * @notice This contract has hybrid functionality: + * + * - It acts as a token that tracks the capital that has been pulled + * ("deployed") from APY Finance pools (PoolToken contracts) + * + * - It is permissioned to transfer funds between the pools and the + * LP Account contract. + * + * @dev When MetaPoolToken pulls capital from the pools to the LP Account, it + * will mint mAPT for each pool. Conversely, when MetaPoolToken withdraws funds + * from the LP Account to the pools, it will burn mAPT for each pool. + * + * The ratio of each pool's mAPT balance to the total mAPT supply determines + * the amount of the TVL dedicated to the pool. + * + * + * DEPLOY CAPITAL TO YIELD FARMING STRATEGIES + * Mints appropriate mAPT amount to track share of deployed TVL owned by a pool. + * + * +-------------+ MetaPoolToken.fundLpAccount +-----------+ + * | |------------------------------>| | + * | PoolTokenV2 | MetaPoolToken.mint | LpAccount | + * | |<------------------------------| | + * +-------------+ +-----------+ + * + * + * WITHDRAW CAPITAL FROM YIELD FARMING STRATEGIES + * Uses mAPT to calculate the amount of capital returned to the PoolToken. + * + * +-------------+ MetaPoolToken.withdrawFromLpAccount +-----------+ + * | |<--------------------------------------| | + * | PoolTokenV2 | MetaPoolToken.burn | LpAccount | + * | |-------------------------------------->| | + * +-------------+ +-----------+ + */ +contract LpAccountFunder is ReentrancyGuard { + using Address for address; + using SafeMath for uint256; + using SignedSafeMath for int256; + using SafeERC20 for IDetailedERC20; + + /* ------------------------------- */ + /* impl-specific storage variables */ + /* ------------------------------- */ + /** @notice used to protect mint and burn function */ + IAddressRegistryV2 public addressRegistry; + + /* ------------------------------- */ + + event AddressRegistryChanged(address); + + /** + * @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_) public { + _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 override nonReentrant onlyLpRole { + (IReservePool[] memory pools, int256[] memory amounts) = + getRebalanceAmounts(poolIds); + + uint256[] memory fundAmounts = _getFundAmounts(amounts); + + _fundLpAccount(pools, fundAmounts); + + emit FundLpAccount(poolIds, fundAmounts); + } + + function withdrawFromLpAccount(bytes32[] calldata poolIds) + external + override + nonReentrant + onlyLpRole + { + (IReservePool[] memory pools, int256[] memory topupAmounts) = + getRebalanceAmounts(poolIds); + + uint256[] memory lpAccountBalances = getLpAccountBalances(poolIds); + uint256[] memory withdrawAmounts = + _calculateAmountsToWithdraw(topupAmounts, lpAccountBalances); + + _withdrawFromLpAccount(pools, withdrawAmounts); + emit WithdrawFromLpAccount(poolIds, withdrawAmounts); + } + + /** + * @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 An array of rebalance amounts + */ + function getRebalanceAmount() public view returns (int256 rebalanceAmount) { + rebalanceAmount = IReservePool(indexToken).getReserveTopUpValue(); + } + + function getLpAccountBalance() + public + view + returns (uint256 lpAccountBalance) + { + IReservePool pool = IReservePool(indexToken); + IDetailedERC20 underlyer = IDetailedERC20(pool.underlyer()); + + address lpAccountAddress = addressRegistry.lpAccountAddress(); + lpAccountBalance = underlyer.balanceOf(lpAccountAddress); + } + + function _setAddressRegistry(address addressRegistry_) internal { + require(addressRegistry_.isContract(), "INVALID_ADDRESS"); + addressRegistry = IAddressRegistryV2(addressRegistry_); + emit AddressRegistryChanged(addressRegistry_); + } + + function _fundLpAccount(uint256 amount) internal { + address lpAccountAddress = addressRegistry.lpAccountAddress(); + require(lpAccountAddress != address(0), "INVALID_LP_ACCOUNT"); // defensive check -- should never happen + + IReservePool(indexToik).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 + * @param pools list of pool amounts whose pool underlyers will be registered + */ + function _registerPoolUnderlyers(IReservePool[] memory pools) internal { + IAssetAllocationRegistry tvlManager = + IAssetAllocationRegistry(addressRegistry.getAddress("tvlManager")); + IErc20Allocation erc20Allocation = + IErc20Allocation( + address( + tvlManager.getAssetAllocation(Erc20AllocationConstants.NAME) + ) + ); + + for (uint256 i = 0; i < pools.length; i++) { + IDetailedERC20 underlyer = + IDetailedERC20(address(pools[i].underlyer())); + + if (!erc20Allocation.isErc20TokenRegistered(underlyer)) { + erc20Allocation.registerErc20Token(underlyer); + } + } + } + + function _getOracleAdapter() internal view returns (ILockingOracle) { + address oracleAdapterAddress = addressRegistry.oracleAdapterAddress(); + return ILockingOracle(oracleAdapterAddress); + } + + function _calculateDeltas( + IReservePool[] memory pools, + uint256[] memory amounts + ) internal view returns (uint256[] memory) { + require(pools.length == amounts.length, "LENGTHS_MUST_MATCH"); + uint256[] memory deltas = new uint256[](pools.length); + + for (uint256 i = 0; i < pools.length; i++) { + IReservePool pool = pools[i]; + uint256 amount = amounts[i]; + + IDetailedERC20 underlyer = pool.underlyer(); + uint256 tokenPrice = pool.getUnderlyerPrice(); + uint8 decimals = underlyer.decimals(); + + deltas[i] = _calculateDelta(amount, tokenPrice, decimals); + } + + return deltas; + } + + /** + * @notice Calculate mAPT amount for given pool's underlyer amount. + * @param amount Pool underlyer amount to be converted + * @param tokenPrice Pool underlyer's USD price (in wei) per underlyer token + * @param decimals Pool underlyer's number of decimals + * @dev Price parameter is in units of wei per token ("big" unit), since + * attempting to express wei per token bit ("small" unit) will be + * fractional, requiring fixed-point representation. This means we need + * to also pass in the underlyer's number of decimals to do the appropriate + * multiplication in the calculation. + * @dev amount of APT minted should be in same ratio to APT supply + * as deposit value is to pool's total value, i.e.: + * + * mint amount / total supply + * = deposit value / pool total value + * + * For denominators, pre or post-deposit amounts can be used. + * The important thing is they are consistent, i.e. both pre-deposit + * or both post-deposit. + */ + function _calculateDelta( + uint256 amount, + uint256 tokenPrice, + uint8 decimals + ) internal view returns (uint256) { + uint256 value = amount.mul(tokenPrice).div(10**uint256(decimals)); + uint256 totalValue = _getTvl(); + uint256 totalSupply = totalSupply(); + + if (totalValue == 0 || totalSupply == 0) { + return value.mul(DEFAULT_MAPT_TO_UNDERLYER_FACTOR); + } + + return value.mul(totalSupply).div(totalValue); + } + + function _getFundAmounts(int256[] memory amounts) + internal + pure + returns (uint256[] memory) + { + uint256[] memory fundAmounts = new uint256[](amounts.length); + + for (uint256 i = 0; i < amounts.length; i++) { + int256 amount = amounts[i]; + + fundAmounts[i] = amount < 0 ? uint256(-amount) : 0; + } + + return fundAmounts; + } + + /** + * @dev Calculate amounts used for topup, taking into + * account the available LP Account balances. + */ + function _calculateAmountsToWithdraw( + int256[] memory topupAmounts, + uint256[] memory lpAccountBalances + ) internal pure returns (uint256[] memory) { + uint256[] memory withdrawAmounts = new uint256[](topupAmounts.length); + for (uint256 i = 0; i < topupAmounts.length; i++) { + int256 topupAmount = topupAmounts[i]; + + uint256 withdrawAmount = topupAmount > 0 ? uint256(topupAmount) : 0; + uint256 lpAccountBalance = lpAccountBalances[i]; + withdrawAmounts[i] = withdrawAmount > lpAccountBalance + ? lpAccountBalance + : withdrawAmount; + } + + return withdrawAmounts; + } +} From e95332d23fc7c69e14260c997ba6deaab5511bd8 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Mon, 1 Aug 2022 18:17:58 -0400 Subject: [PATCH 12/18] Refactor, cleanup, and fix compiler errors --- contracts/index/LpAccountFunder.sol | 227 ++++++++-------------------- 1 file changed, 60 insertions(+), 167 deletions(-) diff --git a/contracts/index/LpAccountFunder.sol b/contracts/index/LpAccountFunder.sol index b614a1aa..f71a1f55 100644 --- a/contracts/index/LpAccountFunder.sol +++ b/contracts/index/LpAccountFunder.sol @@ -2,18 +2,17 @@ pragma solidity 0.6.11; pragma experimental ABIEncoderV2; -import {IDetailedERC20} from "contracts/common/Imports.sol"; -import {SafeERC20} from "contracts/libraries/Imports.sol"; import { - Initializable, - ERC20UpgradeSafe, - ReentrancyGuardUpgradeSafe, - PausableUpgradeSafe, - AccessControlUpgradeSafe, - Address as AddressUpgradeSafe, - SafeMath as SafeMathUpgradeSafe, - SignedSafeMath as SignedSafeMathUpgradeSafe -} from "contracts/proxy/Imports.sol"; + 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"; @@ -24,59 +23,29 @@ import { Erc20AllocationConstants } from "contracts/tvl/Imports.sol"; -import {ILpAccountFunder} from "./ILpAccountFunder.sol"; - /** - * @notice This contract has hybrid functionality: - * - * - It acts as a token that tracks the capital that has been pulled - * ("deployed") from APY Finance pools (PoolToken contracts) - * - * - It is permissioned to transfer funds between the pools and the - * LP Account contract. - * - * @dev When MetaPoolToken pulls capital from the pools to the LP Account, it - * will mint mAPT for each pool. Conversely, when MetaPoolToken withdraws funds - * from the LP Account to the pools, it will burn mAPT for each pool. - * - * The ratio of each pool's mAPT balance to the total mAPT supply determines - * the amount of the TVL dedicated to the pool. - * - * - * DEPLOY CAPITAL TO YIELD FARMING STRATEGIES - * Mints appropriate mAPT amount to track share of deployed TVL owned by a pool. - * - * +-------------+ MetaPoolToken.fundLpAccount +-----------+ - * | |------------------------------>| | - * | PoolTokenV2 | MetaPoolToken.mint | LpAccount | - * | |<------------------------------| | - * +-------------+ +-----------+ - * - * - * WITHDRAW CAPITAL FROM YIELD FARMING STRATEGIES - * Uses mAPT to calculate the amount of capital returned to the PoolToken. - * - * +-------------+ MetaPoolToken.withdrawFromLpAccount +-----------+ - * | |<--------------------------------------| | - * | PoolTokenV2 | MetaPoolToken.burn | LpAccount | - * | |-------------------------------------->| | - * +-------------+ +-----------+ + * @notice This contract is permissioned to transfer funds between the vault + * and the LP Account contract. */ -contract LpAccountFunder is ReentrancyGuard { +contract LpAccountFunder is + AccessControl, + ReentrancyGuard, + Erc20AllocationConstants +{ using Address for address; using SafeMath for uint256; using SignedSafeMath for int256; using SafeERC20 for IDetailedERC20; - /* ------------------------------- */ - /* impl-specific storage variables */ - /* ------------------------------- */ - /** @notice used to protect mint and burn function */ 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 @@ -90,7 +59,8 @@ contract LpAccountFunder is ReentrancyGuard { * repeatedly. It should be called during the deployment so that * it cannot be called by someone else later. */ - constructor(address addressRegistry_) public { + constructor(address addressRegistry_, address indexToken_) public { + _setIndexToken(indexToken_); _setAddressRegistry(addressRegistry_); _setupRole(DEFAULT_ADMIN_ROLE, addressRegistry.emergencySafeAddress()); _setupRole(LP_ROLE, addressRegistry.lpSafeAddress()); @@ -109,39 +79,32 @@ contract LpAccountFunder is ReentrancyGuard { _setAddressRegistry(addressRegistry_); } - function fundLpAccount() external override nonReentrant onlyLpRole { - (IReservePool[] memory pools, int256[] memory amounts) = - getRebalanceAmounts(poolIds); + function fundLpAccount() external nonReentrant onlyLpRole { + int256 amount = getRebalanceAmount(); + uint256 fundAmount = _getFundAmount(amount); - uint256[] memory fundAmounts = _getFundAmounts(amounts); + _fundLpAccount(fundAmount); + _registerPoolUnderlyers(); - _fundLpAccount(pools, fundAmounts); - - emit FundLpAccount(poolIds, fundAmounts); + emit FundLpAccount(fundAmount); } - function withdrawFromLpAccount(bytes32[] calldata poolIds) - external - override - nonReentrant - onlyLpRole - { - (IReservePool[] memory pools, int256[] memory topupAmounts) = - getRebalanceAmounts(poolIds); + function withdrawFromLpAccount() external nonReentrant onlyLpRole { + int256 topupAmount = getRebalanceAmount(); - uint256[] memory lpAccountBalances = getLpAccountBalances(poolIds); - uint256[] memory withdrawAmounts = - _calculateAmountsToWithdraw(topupAmounts, lpAccountBalances); + uint256 lpAccountBalance = getLpAccountBalance(); + uint256 withdrawAmount = + _calculateAmountToWithdraw(topupAmount, lpAccountBalance); - _withdrawFromLpAccount(pools, withdrawAmounts); - emit WithdrawFromLpAccount(poolIds, withdrawAmounts); + _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 An array of rebalance amounts + * @return rebalanceAmount */ function getRebalanceAmount() public view returns (int256 rebalanceAmount) { rebalanceAmount = IReservePool(indexToken).getReserveTopUpValue(); @@ -165,11 +128,17 @@ contract LpAccountFunder is ReentrancyGuard { 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 - IReservePool(indexToik).transferToLpAccount(amount); + IReservePool(indexToken).transferToLpAccount(amount); ILockingOracle oracleAdapter = _getOracleAdapter(); oracleAdapter.lock(); @@ -189,9 +158,8 @@ contract LpAccountFunder is ReentrancyGuard { /** * @notice Register an asset allocation for the account with each pool underlyer - * @param pools list of pool amounts whose pool underlyers will be registered */ - function _registerPoolUnderlyers(IReservePool[] memory pools) internal { + function _registerPoolUnderlyers() internal { IAssetAllocationRegistry tvlManager = IAssetAllocationRegistry(addressRegistry.getAddress("tvlManager")); IErc20Allocation erc20Allocation = @@ -201,13 +169,11 @@ contract LpAccountFunder is ReentrancyGuard { ) ); - for (uint256 i = 0; i < pools.length; i++) { - IDetailedERC20 underlyer = - IDetailedERC20(address(pools[i].underlyer())); + IReservePool pool = IReservePool(indexToken); + IDetailedERC20 underlyer = IDetailedERC20(pool.underlyer()); - if (!erc20Allocation.isErc20TokenRegistered(underlyer)) { - erc20Allocation.registerErc20Token(underlyer); - } + if (!erc20Allocation.isErc20TokenRegistered(underlyer)) { + erc20Allocation.registerErc20Token(underlyer); } } @@ -216,98 +182,25 @@ contract LpAccountFunder is ReentrancyGuard { return ILockingOracle(oracleAdapterAddress); } - function _calculateDeltas( - IReservePool[] memory pools, - uint256[] memory amounts - ) internal view returns (uint256[] memory) { - require(pools.length == amounts.length, "LENGTHS_MUST_MATCH"); - uint256[] memory deltas = new uint256[](pools.length); - - for (uint256 i = 0; i < pools.length; i++) { - IReservePool pool = pools[i]; - uint256 amount = amounts[i]; - - IDetailedERC20 underlyer = pool.underlyer(); - uint256 tokenPrice = pool.getUnderlyerPrice(); - uint8 decimals = underlyer.decimals(); - - deltas[i] = _calculateDelta(amount, tokenPrice, decimals); - } - - return deltas; - } - - /** - * @notice Calculate mAPT amount for given pool's underlyer amount. - * @param amount Pool underlyer amount to be converted - * @param tokenPrice Pool underlyer's USD price (in wei) per underlyer token - * @param decimals Pool underlyer's number of decimals - * @dev Price parameter is in units of wei per token ("big" unit), since - * attempting to express wei per token bit ("small" unit) will be - * fractional, requiring fixed-point representation. This means we need - * to also pass in the underlyer's number of decimals to do the appropriate - * multiplication in the calculation. - * @dev amount of APT minted should be in same ratio to APT supply - * as deposit value is to pool's total value, i.e.: - * - * mint amount / total supply - * = deposit value / pool total value - * - * For denominators, pre or post-deposit amounts can be used. - * The important thing is they are consistent, i.e. both pre-deposit - * or both post-deposit. - */ - function _calculateDelta( - uint256 amount, - uint256 tokenPrice, - uint8 decimals - ) internal view returns (uint256) { - uint256 value = amount.mul(tokenPrice).div(10**uint256(decimals)); - uint256 totalValue = _getTvl(); - uint256 totalSupply = totalSupply(); - - if (totalValue == 0 || totalSupply == 0) { - return value.mul(DEFAULT_MAPT_TO_UNDERLYER_FACTOR); - } - - return value.mul(totalSupply).div(totalValue); - } - - function _getFundAmounts(int256[] memory amounts) + function _getFundAmount(int256 amount) internal pure - returns (uint256[] memory) + returns (uint256 fundAmount) { - uint256[] memory fundAmounts = new uint256[](amounts.length); - - for (uint256 i = 0; i < amounts.length; i++) { - int256 amount = amounts[i]; - - fundAmounts[i] = amount < 0 ? uint256(-amount) : 0; - } - - return fundAmounts; + fundAmount = amount < 0 ? uint256(-amount) : 0; } /** * @dev Calculate amounts used for topup, taking into * account the available LP Account balances. */ - function _calculateAmountsToWithdraw( - int256[] memory topupAmounts, - uint256[] memory lpAccountBalances - ) internal pure returns (uint256[] memory) { - uint256[] memory withdrawAmounts = new uint256[](topupAmounts.length); - for (uint256 i = 0; i < topupAmounts.length; i++) { - int256 topupAmount = topupAmounts[i]; - - uint256 withdrawAmount = topupAmount > 0 ? uint256(topupAmount) : 0; - uint256 lpAccountBalance = lpAccountBalances[i]; - withdrawAmounts[i] = withdrawAmount > lpAccountBalance - ? lpAccountBalance - : withdrawAmount; - } - - return withdrawAmounts; + function _calculateAmountToWithdraw( + int256 topupAmount, + uint256 lpAccountBalance + ) internal pure returns (uint256 withdrawAmount) { + withdrawAmount = topupAmount > 0 ? uint256(topupAmount) : 0; + withdrawAmount = withdrawAmount > lpAccountBalance + ? lpAccountBalance + : withdrawAmount; } } From 54b1f8ec5fa2810a3171b725f7b4dc578e42c4e3 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Wed, 3 Aug 2022 15:14:26 -0400 Subject: [PATCH 13/18] Start unit tests --- contracts/index/LpAccountFunder.sol | 4 +- test-unit/LpAccountFunder.js | 1058 +++++++++++++++++++++++++++ 2 files changed, 1060 insertions(+), 2 deletions(-) create mode 100644 test-unit/LpAccountFunder.js diff --git a/contracts/index/LpAccountFunder.sol b/contracts/index/LpAccountFunder.sol index f71a1f55..20ff184b 100644 --- a/contracts/index/LpAccountFunder.sol +++ b/contracts/index/LpAccountFunder.sol @@ -84,7 +84,7 @@ contract LpAccountFunder is uint256 fundAmount = _getFundAmount(amount); _fundLpAccount(fundAmount); - _registerPoolUnderlyers(); + _registerPoolUnderlyer(); emit FundLpAccount(fundAmount); } @@ -159,7 +159,7 @@ contract LpAccountFunder is /** * @notice Register an asset allocation for the account with each pool underlyer */ - function _registerPoolUnderlyers() internal { + function _registerPoolUnderlyer() internal { IAssetAllocationRegistry tvlManager = IAssetAllocationRegistry(addressRegistry.getAddress("tvlManager")); IErc20Allocation erc20Allocation = diff --git a/test-unit/LpAccountFunder.js b/test-unit/LpAccountFunder.js new file mode 100644 index 00000000..948c04ba --- /dev/null +++ b/test-unit/LpAccountFunder.js @@ -0,0 +1,1058 @@ +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, + ANOTHER_FAKE_ADDRESS, + tokenAmountToBigNumber, + bytes32, + deepEqual, +} = require("../utils/helpers"); +const { deployMockContract } = waffle; +const OracleAdapter = artifacts.readArtifactSync("OracleAdapter"); +const PoolTokenV2 = artifacts.readArtifactSync("PoolTokenV2"); +const IDetailedERC20 = artifacts.readArtifactSync("IDetailedERC20"); + +const usdc = (amount) => tokenAmountToBigNumber(amount, "6"); +const ether = (amount) => tokenAmountToBigNumber(amount, "18"); + +describe.only("Contract: LpAccountFunder", () => { + // signers + let deployer; + let emergencySafe; + let lpSafe; + let lpAccount; + let randomUser; + let anotherUser; + + // deployed contracts + let lpAccountFunder; + + // mocks + let adminSafe; + let oracleAdapter; + let addressRegistry; + let erc20Allocation; + let indexToken; + + // 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("Setup address registry", async () => { + [deployer] = await ethers.getSigners(); + + addressRegistry = await deployMockContract( + deployer, + artifacts.readArtifactSync("AddressRegistryV2").abi + ); + }); + + before("Register Safes", async () => { + [, emergencySafe, adminSafe, lpSafe] = await ethers.getSigners(); + + 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 () => { + [, , , , randomUser, anotherUser] = await ethers.getSigners(); + + oracleAdapter = await deployMockContract(deployer, OracleAdapter.abi); + await addressRegistry.mock.oracleAdapterAddress.returns( + oracleAdapter.address + ); + + // allows mAPT to mint and burn + 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("LpAccountFunder"); + 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 + ); + }); + }); + + 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("_mintAndTransfer", () => { + it("No minting or transfers for zero mint amount", async () => { + const pool = await deployMockContract(deployer, PoolTokenV2.abi); + await pool.mock.transferToLpAccount.reverts(); + + const mintAmount = 0; + const transferAmount = 100; + + const prevTotalSupply = await lpAccountFunder.totalSupply(); + await expect( + lpAccountFunder.testMintAndTransfer( + pool.address, + mintAmount, + transferAmount + ) + ).to.not.be.reverted; + expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); + }); + + it("Transfer if there is minting", async () => { + const pool = await deployMockContract(deployer, PoolTokenV2.abi); + + const mintAmount = tokenAmountToBigNumber( + 10, + await lpAccountFunder.decimals() + ); + const transferAmount = 100; + + // check pool's transfer funciton gets called + await pool.mock.transferToLpAccount.revertsWithReason( + "TRANSFER_TO_LP_SAFE" + ); + await expect( + lpAccountFunder.testMintAndTransfer( + pool.address, + mintAmount, + transferAmount + ) + ).to.be.revertedWith("TRANSFER_TO_LP_SAFE"); + + const expectedSupply = (await lpAccountFunder.totalSupply()).add( + mintAmount + ); + // reset pool mock to check if supply changes as expected + await pool.mock.transferToLpAccount.returns(); + await lpAccountFunder.testMintAndTransfer( + pool.address, + mintAmount, + transferAmount + ); + expect(await lpAccountFunder.totalSupply()).to.equal(expectedSupply); + }); + + it("No minting if transfer reverts", async () => { + const pool = await deployMockContract(deployer, PoolTokenV2.abi); + await pool.mock.transferToLpAccount.revertsWithReason("TRANSFER_FAILED"); + + const mintAmount = tokenAmountToBigNumber( + 10, + await lpAccountFunder.decimals() + ); + const transferAmount = 100; + + const prevTotalSupply = await lpAccountFunder.totalSupply(); + await expect( + lpAccountFunder.testMintAndTransfer( + pool.address, + mintAmount, + transferAmount + ) + ).to.be.revertedWith("TRANSFER_FAILED"); + expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); + }); + }); + + describe("_burnAndTransfer", () => { + it("No burning or transfers for zero burn amount", async () => { + const pool = await deployMockContract(deployer, PoolTokenV2.abi); + await pool.mock.underlyer.reverts(); + + const burnAmount = 0; + const transferAmount = 100; + + const prevTotalSupply = await lpAccountFunder.totalSupply(); + await expect( + lpAccountFunder.testBurnAndTransfer( + pool.address, + lpSafe.address, + burnAmount, + transferAmount + ) + ).to.not.be.reverted; + expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); + }); + + it("Transfer if there is burning", async () => { + const pool = await deployMockContract(deployer, PoolTokenV2.abi); + + const burnAmount = tokenAmountToBigNumber( + 10, + await lpAccountFunder.decimals() + ); + const transferAmount = 100; + + await lpAccountFunder.testMint(pool.address, burnAmount); + + // check lpAccount's transfer function gets called + await lpAccount.mock.transferToPool.revertsWithReason( + "CALLED_LPACCOUNT_TRANSFER" + ); + await expect( + lpAccountFunder.testBurnAndTransfer( + pool.address, + lpAccount.address, + burnAmount, + transferAmount + ) + ).to.be.revertedWith("CALLED_LPACCOUNT_TRANSFER"); + + const expectedSupply = (await lpAccountFunder.totalSupply()).sub( + burnAmount + ); + // reset lpAccount mock to check if supply changes as expected + await lpAccount.mock.transferToPool.returns(); + await lpAccountFunder.testBurnAndTransfer( + pool.address, + lpAccount.address, + burnAmount, + transferAmount + ); + expect(await lpAccountFunder.totalSupply()).to.equal(expectedSupply); + }); + + it("No burning if transfer reverts", async () => { + const pool = await deployMockContract(deployer, PoolTokenV2.abi); + await lpAccount.mock.transferToPool.revertsWithReason( + "LPACCOUNT_TRANSFER_FAILED" + ); + + const burnAmount = tokenAmountToBigNumber( + 10, + await lpAccountFunder.decimals() + ); + const transferAmount = 100; + + await lpAccountFunder.testMint(pool.address, burnAmount); + + const prevTotalSupply = await lpAccountFunder.totalSupply(); + await expect( + lpAccountFunder.testBurnAndTransfer( + pool.address, + lpAccount.address, + burnAmount, + transferAmount + ) + ).to.be.revertedWith("LPACCOUNT_TRANSFER_FAILED"); + expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); + }); + }); + + describe("Multiple mints and burns", () => { + let pool; + let underlyer; + + before("Setup mocks", async () => { + pool = await deployMockContract(deployer, PoolTokenV2.abi); + await pool.mock.transferToLpAccount.returns(); + await pool.mock.getUnderlyerPrice.returns( + tokenAmountToBigNumber("0.998", 8) + ); + + underlyer = await deployMockContract(deployer, IDetailedERC20.abi); + await pool.mock.underlyer.returns(underlyer.address); + + await underlyer.mock.decimals.returns(6); + + await lpAccount.mock.transferToPool.returns(); + + await oracleAdapter.mock.getTvl.returns( + tokenAmountToBigNumber("12345678", 8) + ); + }); + + describe("_multipleMintAndTransfer", () => { + it("Mints calculated amount", async () => { + const price = await pool.getUnderlyerPrice(); + const decimals = await underlyer.decimals(); + const transferAmount = tokenAmountToBigNumber("1988", decimals); + const expectedMintAmount = await lpAccountFunder.testCalculateDelta( + transferAmount, + price, + decimals + ); + const prevBalance = await lpAccountFunder.balanceOf(pool.address); + const expectedBalance = prevBalance.add(expectedMintAmount); + + await lpAccountFunder.testMultipleMintAndTransfer( + [pool.address], + [transferAmount] + ); + expect(await lpAccountFunder.balanceOf(pool.address)).to.equal( + expectedBalance + ); + }); + + it("Locks after minting", async () => { + const transferAmount = 100; + + await oracleAdapter.mock.lock.revertsWithReason("ORACLE_LOCKED"); + await expect( + lpAccountFunder.testMultipleMintAndTransfer( + [pool.address], + [transferAmount] + ) + ).to.be.revertedWith("ORACLE_LOCKED"); + }); + }); + + describe("_multipleBurnAndTransfer", () => { + it("Burns calculated amount", async () => { + // make supply non-zero so burn calc will use proper share logic, + // not the default multiplier. + await lpAccountFunder.testMint( + pool.address, + tokenAmountToBigNumber("1105") + ); + + const price = await pool.getUnderlyerPrice(); + const decimals = await underlyer.decimals(); + const transferAmount = tokenAmountToBigNumber("1988", decimals); + const expectedBurnAmount = await lpAccountFunder.testCalculateDelta( + transferAmount, + price, + decimals + ); + + const prevBalance = await lpAccountFunder.balanceOf(pool.address); + const expectedBalance = prevBalance.sub(expectedBurnAmount); + + await lpAccountFunder.testMultipleBurnAndTransfer( + [pool.address], + [transferAmount] + ); + expect(await lpAccountFunder.balanceOf(pool.address)).to.equal( + expectedBalance + ); + }); + + it("Locks after burning", async () => { + // make supply non-zero so burn calc will use proper share logic, + // not the default multiplier. + await lpAccountFunder.testMint( + pool.address, + tokenAmountToBigNumber("1105") + ); + + const decimals = await underlyer.decimals(); + const transferAmount = tokenAmountToBigNumber("100", decimals); + + await oracleAdapter.mock.lock.revertsWithReason("ORACLE_LOCKED"); + await expect( + lpAccountFunder.testMultipleBurnAndTransfer( + [pool.address], + [transferAmount] + ) + ).to.be.revertedWith("ORACLE_LOCKED"); + }); + }); + }); + + describe("Calculations", () => { + describe("getDeployedValue", () => { + it("Return 0 if zero mAPT supply", async () => { + expect(await lpAccountFunder.totalSupply()).to.equal("0"); + expect(await lpAccountFunder.getDeployedValue(FAKE_ADDRESS)).to.equal( + "0" + ); + }); + + it("Return 0 if zero mAPT balance", async () => { + await lpAccountFunder.testMint( + FAKE_ADDRESS, + tokenAmountToBigNumber(1000) + ); + expect( + await lpAccountFunder.getDeployedValue(ANOTHER_FAKE_ADDRESS) + ).to.equal(0); + }); + + it("Returns calculated value for non-zero mAPT balance", async () => { + const tvl = ether("502300"); + const balance = tokenAmountToBigNumber("1000"); + const anotherBalance = tokenAmountToBigNumber("12345"); + const totalSupply = balance.add(anotherBalance); + + await oracleAdapter.mock.getTvl.returns(tvl); + await lpAccountFunder.testMint(FAKE_ADDRESS, balance); + await lpAccountFunder.testMint(ANOTHER_FAKE_ADDRESS, anotherBalance); + + const expectedValue = tvl.mul(balance).div(totalSupply); + expect(await lpAccountFunder.getDeployedValue(FAKE_ADDRESS)).to.equal( + expectedValue + ); + }); + }); + + describe("_calculateDelta", () => { + it("Calculate mint amount with zero deployed TVL", async () => { + const usdcEthPrice = tokenAmountToBigNumber("1602950450000000"); + let usdcAmount = usdc(107); + let usdcValue = usdcEthPrice.mul(usdcAmount).div(usdc(1)); + await oracleAdapter.mock.getTvl.returns(0); + + await lpAccountFunder.testMint( + anotherUser.address, + tokenAmountToBigNumber(100) + ); + + const mintAmount = await lpAccountFunder.testCalculateDelta( + usdcAmount, + usdcEthPrice, + "6" + ); + const expectedMintAmount = usdcValue.mul( + await lpAccountFunder.DEFAULT_MAPT_TO_UNDERLYER_FACTOR() + ); + expect(mintAmount).to.be.equal(expectedMintAmount); + }); + + it("Calculate mint amount with zero total supply", async () => { + const usdcEthPrice = tokenAmountToBigNumber("1602950450000000"); + let usdcAmount = usdc(107); + let usdcValue = usdcEthPrice.mul(usdcAmount).div(usdc(1)); + await oracleAdapter.mock.getTvl.returns(1); + + const mintAmount = await lpAccountFunder.testCalculateDelta( + usdcAmount, + usdcEthPrice, + "6" + ); + const expectedMintAmount = usdcValue.mul( + await lpAccountFunder.DEFAULT_MAPT_TO_UNDERLYER_FACTOR() + ); + expect(mintAmount).to.be.equal(expectedMintAmount); + }); + + it("Calculate mint amount with non-zero total supply", async () => { + const usdcEthPrice = tokenAmountToBigNumber("1602950450000000"); + let usdcAmount = usdc(107); + let tvl = usdcEthPrice.mul(usdcAmount).div(usdc(1)); + await oracleAdapter.mock.getTvl.returns(tvl); + + const totalSupply = tokenAmountToBigNumber(21); + await lpAccountFunder.testMint(anotherUser.address, totalSupply); + + let mintAmount = await lpAccountFunder.testCalculateDelta( + usdcAmount, + usdcEthPrice, + "6" + ); + expect(mintAmount).to.be.equal(totalSupply); + + tvl = usdcEthPrice.mul(usdcAmount.mul(2)).div(usdc(1)); + await oracleAdapter.mock.getTvl.returns(tvl); + const expectedMintAmount = totalSupply.div(2); + mintAmount = await lpAccountFunder.testCalculateDelta( + usdcAmount, + usdcEthPrice, + "6" + ); + expect(mintAmount).to.be.equal(expectedMintAmount); + }); + }); + + describe("_calculateDeltas", () => { + let pools; + const underlyerPrice = tokenAmountToBigNumber("1.015", 8); + + before("Mock pools and underlyers", async () => { + const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); + await daiPool.mock.getUnderlyerPrice.returns(underlyerPrice); + const daiToken = await deployMockContract(deployer, IDetailedERC20.abi); + await daiPool.mock.underlyer.returns(daiToken.address); + await daiToken.mock.decimals.returns(18); + + const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); + await usdcPool.mock.getUnderlyerPrice.returns(underlyerPrice); + const usdcToken = await deployMockContract( + deployer, + IDetailedERC20.abi + ); + await usdcPool.mock.underlyer.returns(usdcToken.address); + await usdcToken.mock.decimals.returns(6); + + const usdtPool = await deployMockContract(deployer, PoolTokenV2.abi); + await usdtPool.mock.getUnderlyerPrice.returns(underlyerPrice); + const usdtToken = await deployMockContract( + deployer, + IDetailedERC20.abi + ); + await usdtPool.mock.underlyer.returns(usdtToken.address); + await usdtToken.mock.decimals.returns(6); + + pools = [daiPool.address, usdcPool.address, usdtPool.address]; + }); + + before("Set TVL", async () => { + const tvl = tokenAmountToBigNumber("502300", 8); + await oracleAdapter.mock.getTvl.returns(tvl); + }); + + it("Revert if array lengths do not match", async () => { + const amounts = new Array(pools.length - 1).fill( + tokenAmountToBigNumber("1", "18") + ); + + await expect( + lpAccountFunder.testCalculateDeltas(pools, amounts) + ).to.be.revertedWith("LENGTHS_MUST_MATCH"); + }); + + it("Return an empty array when given empty arrays", async () => { + const result = await lpAccountFunder.testCalculateDeltas([], []); + expect(result).to.deep.equal([]); + }); + + it("Returns expected amounts from _calculateDelta", async () => { + const amounts = [ + tokenAmountToBigNumber(384, 18), // DAI + tokenAmountToBigNumber(9899, 6), // Tether + ]; + const expectedAmounts = [ + await lpAccountFunder.testCalculateDelta( + amounts[0], + underlyerPrice, + 18 + ), + await lpAccountFunder.testCalculateDelta( + amounts[1], + underlyerPrice, + 6 + ), + ]; + + const result = await lpAccountFunder.testCalculateDeltas( + [pools[0], pools[2]], + amounts + ); + expect(result[0]).to.equal(expectedAmounts[0]); + expect(result[1]).to.equal(expectedAmounts[1]); + expect(result).to.deep.equal(expectedAmounts); + }); + + it("Get zero mint amount for zero transfer", async () => { + const amounts = [0, tokenAmountToBigNumber(347, 6), 0]; + const result = await lpAccountFunder.testCalculateDeltas( + pools, + amounts + ); + + const expectedAmount = await lpAccountFunder.testCalculateDelta( + amounts[1], + underlyerPrice, + 6 + ); + + expect(result[0]).to.equal(0); + expect(result[1]).to.be.equal(expectedAmount); + expect(result[2]).to.equal(0); + }); + }); + }); + + describe("getTvl", () => { + it("Call delegates to oracle adapter's getTvl", async () => { + const usdTvl = tokenAmountToBigNumber("25100123.87654321", "8"); + await oracleAdapter.mock.getTvl.returns(usdTvl); + expect(await lpAccountFunder.testGetTvl()).to.equal(usdTvl); + }); + + it("getTvl reverts with same reason as oracle adapter", async () => { + await oracleAdapter.mock.getTvl.revertsWithReason("SOMETHING_WRONG"); + await expect(lpAccountFunder.testGetTvl()).to.be.revertedWith( + "SOMETHING_WRONG" + ); + }); + }); + + describe("_registerPoolUnderlyers", () => { + let daiPool; + let daiToken; + let usdcPool; + let usdcToken; + + beforeEach("Setup mocks", async () => { + daiPool = await deployMockContract(deployer, PoolTokenV2.abi); + daiToken = await deployMockContract(deployer, IDetailedERC20.abi); + await daiPool.mock.underlyer.returns(daiToken.address); + await daiToken.mock.decimals.returns(18); + await daiToken.mock.symbol.returns("DAI"); + + usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); + usdcToken = await deployMockContract(deployer, IDetailedERC20.abi); + await usdcPool.mock.underlyer.returns(usdcToken.address); + await usdcToken.mock.decimals.returns(6); + await usdcToken.mock.symbol.returns("USDC"); + }); + + it("Unregistered underlyers get registered", async () => { + // set DAI as unregistered in ERC20 registry + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(daiToken.address) + .returns(false); + + // revert on registration for DAI but not others + await erc20Allocation.mock["registerErc20Token(address)"].returns(); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(daiToken.address) + .revertsWithReason("REGISTERED_DAI"); + + // expect revert since register function should be called + await expect( + lpAccountFunder.testRegisterPoolUnderlyers([daiPool.address]) + ).to.be.revertedWith("REGISTERED_DAI"); + }); + + it("Registered underlyers are skipped", async () => { + // set DAI as registered while USDC is not + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(daiToken.address) + .returns(true); + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(usdcToken.address) + .returns(false); + + // revert on registration for DAI or USDC + await erc20Allocation.mock["registerErc20Token(address)"].returns(); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(usdcToken.address) + .revertsWithReason("REGISTERED_USDC"); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(daiToken.address) + .revertsWithReason("REGISTERED_DAI"); + + // should not revert since DAI should not be registered + await expect( + lpAccountFunder.testRegisterPoolUnderlyers([daiPool.address]) + ).to.not.be.reverted; + + // should revert for USDC registration + await expect( + lpAccountFunder.testRegisterPoolUnderlyers([ + daiPool.address, + usdcPool.address, + ]) + ).to.be.revertedWith("REGISTERED_USDC"); + }); + }); + + describe("fundLpAccount", () => { + it("LP Safe can call", async () => { + // await expect(lpAccountFunder.connect(lpSafe).fundLpAccount([])).to.not.be.reverted; + await lpAccountFunder.connect(lpSafe).fundLpAccount([]); + }); + + 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"); + }); + }); + + describe("withdrawFromLpAccount", () => { + 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("Revert on unregistered LP Account address", async () => { + await addressRegistry.mock.lpAccountAddress.returns(ZERO_ADDRESS); + await expect( + lpAccountFunder.connect(lpSafe).withdrawFromLpAccount([]) + ).to.be.revertedWith("INVALID_LP_ACCOUNT"); + }); + }); + + describe("getRebalanceAmounts", () => { + it("Return pair of empty arrays when give an empty array", async () => { + const result = await lpAccountFunder.getRebalanceAmounts([]); + expect(result).to.deep.equal([[], []]); + }); + + it("Return array of top-up PoolAmounts from specified pools", async () => { + const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); + const daiRebalanceAmount = tokenAmountToBigNumber("1234888", "18"); + await daiPool.mock.getReserveTopUpValue.returns(daiRebalanceAmount); + await addressRegistry.mock.getAddress + .withArgs(bytes32("daiPool")) + .returns(daiPool.address); + + const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); + const usdcRebalanceAmount = tokenAmountToBigNumber("459999", "6"); + await usdcPool.mock.getReserveTopUpValue.returns(usdcRebalanceAmount); + await addressRegistry.mock.getAddress + .withArgs(bytes32("usdcPool")) + .returns(usdcPool.address); + + const result = await lpAccountFunder.getRebalanceAmounts([ + bytes32("daiPool"), + bytes32("usdcPool"), + ]); + deepEqual(result, [ + [daiPool.address, usdcPool.address], + [daiRebalanceAmount, usdcRebalanceAmount], + ]); + }); + }); + + describe("getLpAccountBalances", () => { + it("Return empty array when given an empty array", async () => { + const result = await lpAccountFunder.getLpAccountBalances([]); + expect(result).to.deep.equal([]); + }); + + it("Return array of available stablecoin balances of LP Account", async () => { + const daiToken = await deployMockContract(deployer, IDetailedERC20.abi); + const daiAvailableAmount = tokenAmountToBigNumber("15325", "18"); + await daiToken.mock.balanceOf + .withArgs(lpAccount.address) + .returns(daiAvailableAmount); + + const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); + await daiPool.mock.underlyer.returns(daiToken.address); + await addressRegistry.mock.getAddress + .withArgs(bytes32("daiPool")) + .returns(daiPool.address); + + const usdcToken = await deployMockContract(deployer, IDetailedERC20.abi); + const usdcAvailableAmount = tokenAmountToBigNumber("110200", "6"); + await usdcToken.mock.balanceOf + .withArgs(lpAccount.address) + .returns(usdcAvailableAmount); + + const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); + await usdcPool.mock.underlyer.returns(usdcToken.address); + await addressRegistry.mock.getAddress + .withArgs(bytes32("usdcPool")) + .returns(usdcPool.address); + + const result = await lpAccountFunder.getLpAccountBalances([ + bytes32("daiPool"), + bytes32("usdcPool"), + ]); + deepEqual(result, [daiAvailableAmount, usdcAvailableAmount]); + }); + }); + + describe("_getFundAmounts", () => { + it("Returns empty array given empty array", async () => { + const result = await lpAccountFunder.testGetFundAmounts([]); + expect(result).to.be.empty; + }); + + it("Replaces negatives with positives, positives with zeros", async () => { + let amounts = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("1777"), + tokenAmountToBigNumber("11"), + tokenAmountToBigNumber("122334"), + ]; + let expectedResult = [ + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + ]; + let result = await lpAccountFunder.testGetFundAmounts(amounts); + deepEqual(expectedResult, result); + + amounts = [ + tokenAmountToBigNumber("-159"), + tokenAmountToBigNumber("-1777"), + tokenAmountToBigNumber("-11"), + ]; + expectedResult = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("1777"), + tokenAmountToBigNumber("11"), + ]; + result = await lpAccountFunder.testGetFundAmounts(amounts); + deepEqual(expectedResult, result); + + amounts = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("-1777"), + tokenAmountToBigNumber("-11"), + tokenAmountToBigNumber("122334"), + tokenAmountToBigNumber("0"), + ]; + expectedResult = [ + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("1777"), + tokenAmountToBigNumber("11"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + ]; + result = await lpAccountFunder.testGetFundAmounts(amounts); + deepEqual(expectedResult, result); + }); + }); + + describe("_calculateAmountsToWithdraw", () => { + it("Returns empty array given empty array", async () => { + const result = await lpAccountFunder.testCalculateAmountsToWithdraw( + [], + [] + ); + expect(result).to.be.empty; + }); + + it("Replaces negatives with zeros", async () => { + let topupAmounts = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("1777"), + tokenAmountToBigNumber("11"), + tokenAmountToBigNumber("122334"), + ]; + let availableAmounts = topupAmounts; + let expectedResult = topupAmounts; + let result = await lpAccountFunder.testCalculateAmountsToWithdraw( + topupAmounts, + availableAmounts + ); + + deepEqual(expectedResult, result); + + topupAmounts = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("-1777"), + tokenAmountToBigNumber("-11"), + tokenAmountToBigNumber("122334"), + tokenAmountToBigNumber("0"), + ]; + expectedResult = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("122334"), + tokenAmountToBigNumber("0"), + ]; + availableAmounts = expectedResult; + result = await lpAccountFunder.testCalculateAmountsToWithdraw( + topupAmounts, + availableAmounts + ); + deepEqual(expectedResult, result); + }); + + it("Uses minimum of topup and available amounts", async () => { + let topupAmounts = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("1777"), + tokenAmountToBigNumber("11"), + tokenAmountToBigNumber("122334"), + ]; + let availableAmounts = [ + tokenAmountToBigNumber("122334"), + tokenAmountToBigNumber("122334"), + tokenAmountToBigNumber("122334"), + tokenAmountToBigNumber("122334"), + ]; + let expectedResult = topupAmounts; + let result = await lpAccountFunder.testCalculateAmountsToWithdraw( + topupAmounts, + availableAmounts + ); + deepEqual(expectedResult, result); + + topupAmounts = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("1777"), + tokenAmountToBigNumber("11"), + tokenAmountToBigNumber("122334"), + ]; + availableAmounts = [ + tokenAmountToBigNumber("1000"), + tokenAmountToBigNumber("1000"), + tokenAmountToBigNumber("1000"), + tokenAmountToBigNumber("1000"), + ]; + expectedResult = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("1000"), + tokenAmountToBigNumber("11"), + tokenAmountToBigNumber("1000"), + ]; + result = await lpAccountFunder.testCalculateAmountsToWithdraw( + topupAmounts, + availableAmounts + ); + deepEqual(expectedResult, result); + + topupAmounts = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("-1777"), + tokenAmountToBigNumber("-11"), + tokenAmountToBigNumber("122334"), + tokenAmountToBigNumber("0"), + ]; + availableAmounts = [ + tokenAmountToBigNumber("1000"), + tokenAmountToBigNumber("1"), + tokenAmountToBigNumber("100"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("10000"), + tokenAmountToBigNumber("10"), + ]; + expectedResult = [ + tokenAmountToBigNumber("159"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("0"), + tokenAmountToBigNumber("10000"), + tokenAmountToBigNumber("0"), + ]; + result = await lpAccountFunder.testCalculateAmountsToWithdraw( + topupAmounts, + availableAmounts + ); + deepEqual(expectedResult, result); + }); + }); +}); From dfc9873af679ee41243f071037b338270829465d Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Thu, 4 Aug 2022 17:07:06 -0400 Subject: [PATCH 14/18] Create test contract --- contracts/index/TestLpAccountFunder.sol | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 contracts/index/TestLpAccountFunder.sol 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); + } +} From 7cb88e55bae877df8eab1710620367d40f45fea2 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Thu, 4 Aug 2022 18:00:39 -0400 Subject: [PATCH 15/18] Fixup tests --- test-unit/LpAccountFunder.js | 954 +++++++---------------------------- 1 file changed, 173 insertions(+), 781 deletions(-) diff --git a/test-unit/LpAccountFunder.js b/test-unit/LpAccountFunder.js index 948c04ba..eec661fe 100644 --- a/test-unit/LpAccountFunder.js +++ b/test-unit/LpAccountFunder.js @@ -5,7 +5,6 @@ const timeMachine = require("ganache-time-traveler"); const { AddressZero: ZERO_ADDRESS } = ethers.constants; const { FAKE_ADDRESS, - ANOTHER_FAKE_ADDRESS, tokenAmountToBigNumber, bytes32, deepEqual, @@ -15,9 +14,6 @@ const OracleAdapter = artifacts.readArtifactSync("OracleAdapter"); const PoolTokenV2 = artifacts.readArtifactSync("PoolTokenV2"); const IDetailedERC20 = artifacts.readArtifactSync("IDetailedERC20"); -const usdc = (amount) => tokenAmountToBigNumber(amount, "6"); -const ether = (amount) => tokenAmountToBigNumber(amount, "18"); - describe.only("Contract: LpAccountFunder", () => { // signers let deployer; @@ -25,7 +21,6 @@ describe.only("Contract: LpAccountFunder", () => { let lpSafe; let lpAccount; let randomUser; - let anotherUser; // deployed contracts let lpAccountFunder; @@ -93,7 +88,7 @@ describe.only("Contract: LpAccountFunder", () => { }); before("Mock dependencies", async () => { - [, , , , randomUser, anotherUser] = await ethers.getSigners(); + [, , , , randomUser] = await ethers.getSigners(); oracleAdapter = await deployMockContract(deployer, OracleAdapter.abi); await addressRegistry.mock.oracleAdapterAddress.returns( @@ -133,7 +128,9 @@ describe.only("Contract: LpAccountFunder", () => { }); before("Deploy LpAccountFunder", async () => { - const LpAccountFunder = await ethers.getContractFactory("LpAccountFunder"); + const LpAccountFunder = await ethers.getContractFactory( + "TestLpAccountFunder" + ); lpAccountFunder = await LpAccountFunder.deploy( addressRegistry.address, indexToken.address @@ -176,6 +173,10 @@ describe.only("Contract: LpAccountFunder", () => { addressRegistry.address ); }); + + it("Index Token set correctly", async () => { + expect(await lpAccountFunder.indexToken()).to.equal(indexToken.address); + }); }); describe("emergencySetAddressRegistry", () => { @@ -205,166 +206,6 @@ describe.only("Contract: LpAccountFunder", () => { }); }); - describe("_mintAndTransfer", () => { - it("No minting or transfers for zero mint amount", async () => { - const pool = await deployMockContract(deployer, PoolTokenV2.abi); - await pool.mock.transferToLpAccount.reverts(); - - const mintAmount = 0; - const transferAmount = 100; - - const prevTotalSupply = await lpAccountFunder.totalSupply(); - await expect( - lpAccountFunder.testMintAndTransfer( - pool.address, - mintAmount, - transferAmount - ) - ).to.not.be.reverted; - expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); - }); - - it("Transfer if there is minting", async () => { - const pool = await deployMockContract(deployer, PoolTokenV2.abi); - - const mintAmount = tokenAmountToBigNumber( - 10, - await lpAccountFunder.decimals() - ); - const transferAmount = 100; - - // check pool's transfer funciton gets called - await pool.mock.transferToLpAccount.revertsWithReason( - "TRANSFER_TO_LP_SAFE" - ); - await expect( - lpAccountFunder.testMintAndTransfer( - pool.address, - mintAmount, - transferAmount - ) - ).to.be.revertedWith("TRANSFER_TO_LP_SAFE"); - - const expectedSupply = (await lpAccountFunder.totalSupply()).add( - mintAmount - ); - // reset pool mock to check if supply changes as expected - await pool.mock.transferToLpAccount.returns(); - await lpAccountFunder.testMintAndTransfer( - pool.address, - mintAmount, - transferAmount - ); - expect(await lpAccountFunder.totalSupply()).to.equal(expectedSupply); - }); - - it("No minting if transfer reverts", async () => { - const pool = await deployMockContract(deployer, PoolTokenV2.abi); - await pool.mock.transferToLpAccount.revertsWithReason("TRANSFER_FAILED"); - - const mintAmount = tokenAmountToBigNumber( - 10, - await lpAccountFunder.decimals() - ); - const transferAmount = 100; - - const prevTotalSupply = await lpAccountFunder.totalSupply(); - await expect( - lpAccountFunder.testMintAndTransfer( - pool.address, - mintAmount, - transferAmount - ) - ).to.be.revertedWith("TRANSFER_FAILED"); - expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); - }); - }); - - describe("_burnAndTransfer", () => { - it("No burning or transfers for zero burn amount", async () => { - const pool = await deployMockContract(deployer, PoolTokenV2.abi); - await pool.mock.underlyer.reverts(); - - const burnAmount = 0; - const transferAmount = 100; - - const prevTotalSupply = await lpAccountFunder.totalSupply(); - await expect( - lpAccountFunder.testBurnAndTransfer( - pool.address, - lpSafe.address, - burnAmount, - transferAmount - ) - ).to.not.be.reverted; - expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); - }); - - it("Transfer if there is burning", async () => { - const pool = await deployMockContract(deployer, PoolTokenV2.abi); - - const burnAmount = tokenAmountToBigNumber( - 10, - await lpAccountFunder.decimals() - ); - const transferAmount = 100; - - await lpAccountFunder.testMint(pool.address, burnAmount); - - // check lpAccount's transfer function gets called - await lpAccount.mock.transferToPool.revertsWithReason( - "CALLED_LPACCOUNT_TRANSFER" - ); - await expect( - lpAccountFunder.testBurnAndTransfer( - pool.address, - lpAccount.address, - burnAmount, - transferAmount - ) - ).to.be.revertedWith("CALLED_LPACCOUNT_TRANSFER"); - - const expectedSupply = (await lpAccountFunder.totalSupply()).sub( - burnAmount - ); - // reset lpAccount mock to check if supply changes as expected - await lpAccount.mock.transferToPool.returns(); - await lpAccountFunder.testBurnAndTransfer( - pool.address, - lpAccount.address, - burnAmount, - transferAmount - ); - expect(await lpAccountFunder.totalSupply()).to.equal(expectedSupply); - }); - - it("No burning if transfer reverts", async () => { - const pool = await deployMockContract(deployer, PoolTokenV2.abi); - await lpAccount.mock.transferToPool.revertsWithReason( - "LPACCOUNT_TRANSFER_FAILED" - ); - - const burnAmount = tokenAmountToBigNumber( - 10, - await lpAccountFunder.decimals() - ); - const transferAmount = 100; - - await lpAccountFunder.testMint(pool.address, burnAmount); - - const prevTotalSupply = await lpAccountFunder.totalSupply(); - await expect( - lpAccountFunder.testBurnAndTransfer( - pool.address, - lpAccount.address, - burnAmount, - transferAmount - ) - ).to.be.revertedWith("LPACCOUNT_TRANSFER_FAILED"); - expect(await lpAccountFunder.totalSupply()).to.equal(prevTotalSupply); - }); - }); - describe("Multiple mints and burns", () => { let pool; let underlyer; @@ -388,671 +229,222 @@ describe.only("Contract: LpAccountFunder", () => { ); }); - describe("_multipleMintAndTransfer", () => { - it("Mints calculated amount", async () => { - const price = await pool.getUnderlyerPrice(); - const decimals = await underlyer.decimals(); - const transferAmount = tokenAmountToBigNumber("1988", decimals); - const expectedMintAmount = await lpAccountFunder.testCalculateDelta( - transferAmount, - price, - decimals - ); - const prevBalance = await lpAccountFunder.balanceOf(pool.address); - const expectedBalance = prevBalance.add(expectedMintAmount); - - await lpAccountFunder.testMultipleMintAndTransfer( - [pool.address], - [transferAmount] - ); - expect(await lpAccountFunder.balanceOf(pool.address)).to.equal( - expectedBalance - ); - }); + describe("_registerPoolUnderlyer", () => { + let daiPool; + let daiToken; + let usdcPool; + let usdcToken; - it("Locks after minting", async () => { - const transferAmount = 100; + beforeEach("Setup mocks", async () => { + daiPool = await deployMockContract(deployer, PoolTokenV2.abi); + daiToken = await deployMockContract(deployer, IDetailedERC20.abi); + await daiPool.mock.underlyer.returns(daiToken.address); + await daiToken.mock.decimals.returns(18); + await daiToken.mock.symbol.returns("DAI"); - await oracleAdapter.mock.lock.revertsWithReason("ORACLE_LOCKED"); - await expect( - lpAccountFunder.testMultipleMintAndTransfer( - [pool.address], - [transferAmount] - ) - ).to.be.revertedWith("ORACLE_LOCKED"); + usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); + usdcToken = await deployMockContract(deployer, IDetailedERC20.abi); + await usdcPool.mock.underlyer.returns(usdcToken.address); + await usdcToken.mock.decimals.returns(6); + await usdcToken.mock.symbol.returns("USDC"); }); - }); - describe("_multipleBurnAndTransfer", () => { - it("Burns calculated amount", async () => { - // make supply non-zero so burn calc will use proper share logic, - // not the default multiplier. - await lpAccountFunder.testMint( - pool.address, - tokenAmountToBigNumber("1105") - ); + it("Unregistered underlyers get registered", async () => { + // set DAI as unregistered in ERC20 registry + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(daiToken.address) + .returns(false); - const price = await pool.getUnderlyerPrice(); - const decimals = await underlyer.decimals(); - const transferAmount = tokenAmountToBigNumber("1988", decimals); - const expectedBurnAmount = await lpAccountFunder.testCalculateDelta( - transferAmount, - price, - decimals - ); + // revert on registration for DAI but not others + await erc20Allocation.mock["registerErc20Token(address)"].returns(); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(daiToken.address) + .revertsWithReason("REGISTERED_DAI"); - const prevBalance = await lpAccountFunder.balanceOf(pool.address); - const expectedBalance = prevBalance.sub(expectedBurnAmount); - - await lpAccountFunder.testMultipleBurnAndTransfer( - [pool.address], - [transferAmount] - ); - expect(await lpAccountFunder.balanceOf(pool.address)).to.equal( - expectedBalance - ); + // expect revert since register function should be called + await expect( + lpAccountFunder.testRegisterPoolUnderlyer() + ).to.be.revertedWith("REGISTERED_DAI"); }); - it("Locks after burning", async () => { - // make supply non-zero so burn calc will use proper share logic, - // not the default multiplier. - await lpAccountFunder.testMint( - pool.address, - tokenAmountToBigNumber("1105") - ); - - const decimals = await underlyer.decimals(); - const transferAmount = tokenAmountToBigNumber("100", decimals); - - await oracleAdapter.mock.lock.revertsWithReason("ORACLE_LOCKED"); + it("Registered underlyers are skipped", async () => { + // set DAI as registered while USDC is not + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(daiToken.address) + .returns(true); + await erc20Allocation.mock["isErc20TokenRegistered(address)"] + .withArgs(usdcToken.address) + .returns(false); + + // revert on registration for DAI or USDC + await erc20Allocation.mock["registerErc20Token(address)"].returns(); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(usdcToken.address) + .revertsWithReason("REGISTERED_USDC"); + await erc20Allocation.mock["registerErc20Token(address)"] + .withArgs(daiToken.address) + .revertsWithReason("REGISTERED_DAI"); + + // should not revert since DAI should not be registered + await expect(lpAccountFunder.testRegisterPoolUnderlyer()).to.not.be + .reverted; + + // should revert for USDC registration await expect( - lpAccountFunder.testMultipleBurnAndTransfer( - [pool.address], - [transferAmount] - ) - ).to.be.revertedWith("ORACLE_LOCKED"); + lpAccountFunder.testRegisterPoolUnderlyer() + ).to.be.revertedWith("REGISTERED_USDC"); }); }); - }); - describe("Calculations", () => { - describe("getDeployedValue", () => { - it("Return 0 if zero mAPT supply", async () => { - expect(await lpAccountFunder.totalSupply()).to.equal("0"); - expect(await lpAccountFunder.getDeployedValue(FAKE_ADDRESS)).to.equal( - "0" - ); + describe("fundLpAccount", () => { + it("LP Safe can call", async () => { + await expect(lpAccountFunder.connect(lpSafe).fundLpAccount()).to.not.be + .reverted; }); - it("Return 0 if zero mAPT balance", async () => { - await lpAccountFunder.testMint( - FAKE_ADDRESS, - tokenAmountToBigNumber(1000) - ); - expect( - await lpAccountFunder.getDeployedValue(ANOTHER_FAKE_ADDRESS) - ).to.equal(0); + it("Unpermissioned cannot call", async () => { + await expect( + lpAccountFunder.connect(randomUser).fundLpAccount() + ).to.be.revertedWith("NOT_LP_ROLE"); }); - it("Returns calculated value for non-zero mAPT balance", async () => { - const tvl = ether("502300"); - const balance = tokenAmountToBigNumber("1000"); - const anotherBalance = tokenAmountToBigNumber("12345"); - const totalSupply = balance.add(anotherBalance); - - await oracleAdapter.mock.getTvl.returns(tvl); - await lpAccountFunder.testMint(FAKE_ADDRESS, balance); - await lpAccountFunder.testMint(ANOTHER_FAKE_ADDRESS, anotherBalance); - - const expectedValue = tvl.mul(balance).div(totalSupply); - expect(await lpAccountFunder.getDeployedValue(FAKE_ADDRESS)).to.equal( - expectedValue - ); + 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"); }); }); - describe("_calculateDelta", () => { - it("Calculate mint amount with zero deployed TVL", async () => { - const usdcEthPrice = tokenAmountToBigNumber("1602950450000000"); - let usdcAmount = usdc(107); - let usdcValue = usdcEthPrice.mul(usdcAmount).div(usdc(1)); - await oracleAdapter.mock.getTvl.returns(0); - - await lpAccountFunder.testMint( - anotherUser.address, - tokenAmountToBigNumber(100) - ); - - const mintAmount = await lpAccountFunder.testCalculateDelta( - usdcAmount, - usdcEthPrice, - "6" - ); - const expectedMintAmount = usdcValue.mul( - await lpAccountFunder.DEFAULT_MAPT_TO_UNDERLYER_FACTOR() - ); - expect(mintAmount).to.be.equal(expectedMintAmount); + describe("withdrawFromLpAccount", () => { + it("LP Safe can call", async () => { + await expect(lpAccountFunder.connect(lpSafe).withdrawFromLpAccount()).to + .not.be.reverted; }); - it("Calculate mint amount with zero total supply", async () => { - const usdcEthPrice = tokenAmountToBigNumber("1602950450000000"); - let usdcAmount = usdc(107); - let usdcValue = usdcEthPrice.mul(usdcAmount).div(usdc(1)); - await oracleAdapter.mock.getTvl.returns(1); + it("Unpermissioned cannot call", async () => { + await expect( + lpAccountFunder.connect(randomUser).withdrawFromLpAccount() + ).to.be.revertedWith("NOT_LP_ROLE"); + }); - const mintAmount = await lpAccountFunder.testCalculateDelta( - usdcAmount, - usdcEthPrice, - "6" - ); - const expectedMintAmount = usdcValue.mul( - await lpAccountFunder.DEFAULT_MAPT_TO_UNDERLYER_FACTOR() - ); - expect(mintAmount).to.be.equal(expectedMintAmount); + it("Revert on unregistered LP Account address", async () => { + await addressRegistry.mock.lpAccountAddress.returns(ZERO_ADDRESS); + await expect( + lpAccountFunder.connect(lpSafe).withdrawFromLpAccount() + ).to.be.revertedWith("INVALID_LP_ACCOUNT"); }); + }); - it("Calculate mint amount with non-zero total supply", async () => { - const usdcEthPrice = tokenAmountToBigNumber("1602950450000000"); - let usdcAmount = usdc(107); - let tvl = usdcEthPrice.mul(usdcAmount).div(usdc(1)); - await oracleAdapter.mock.getTvl.returns(tvl); + describe("getRebalanceAmount", () => { + it("Return pair of empty arrays when give an empty array", async () => { + const result = await lpAccountFunder.getRebalanceAmount(); + expect(result).to.deep.equal([[], []]); + }); - const totalSupply = tokenAmountToBigNumber(21); - await lpAccountFunder.testMint(anotherUser.address, totalSupply); + it("Return array of top-up PoolAmounts from specified pools", async () => { + const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); + const daiRebalanceAmount = tokenAmountToBigNumber("1234888", "18"); + await daiPool.mock.getReserveTopUpValue.returns(daiRebalanceAmount); + await addressRegistry.mock.getAddress + .withArgs(bytes32("daiPool")) + .returns(daiPool.address); - let mintAmount = await lpAccountFunder.testCalculateDelta( - usdcAmount, - usdcEthPrice, - "6" - ); - expect(mintAmount).to.be.equal(totalSupply); - - tvl = usdcEthPrice.mul(usdcAmount.mul(2)).div(usdc(1)); - await oracleAdapter.mock.getTvl.returns(tvl); - const expectedMintAmount = totalSupply.div(2); - mintAmount = await lpAccountFunder.testCalculateDelta( - usdcAmount, - usdcEthPrice, - "6" - ); - expect(mintAmount).to.be.equal(expectedMintAmount); + const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); + const usdcRebalanceAmount = tokenAmountToBigNumber("459999", "6"); + await usdcPool.mock.getReserveTopUpValue.returns(usdcRebalanceAmount); + await addressRegistry.mock.getAddress + .withArgs(bytes32("usdcPool")) + .returns(usdcPool.address); + + const result = await lpAccountFunder.getRebalanceAmount(); + deepEqual(result, [ + [daiPool.address, usdcPool.address], + [daiRebalanceAmount, usdcRebalanceAmount], + ]); }); }); - describe("_calculateDeltas", () => { - let pools; - const underlyerPrice = tokenAmountToBigNumber("1.015", 8); + describe("getLpAccountBalance", () => { + it("Return array of available stablecoin balances of LP Account", async () => { + const daiToken = await deployMockContract(deployer, IDetailedERC20.abi); + const daiAvailableAmount = tokenAmountToBigNumber("15325", "18"); + await daiToken.mock.balanceOf + .withArgs(lpAccount.address) + .returns(daiAvailableAmount); - before("Mock pools and underlyers", async () => { const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); - await daiPool.mock.getUnderlyerPrice.returns(underlyerPrice); - const daiToken = await deployMockContract(deployer, IDetailedERC20.abi); await daiPool.mock.underlyer.returns(daiToken.address); - await daiToken.mock.decimals.returns(18); + await addressRegistry.mock.getAddress + .withArgs(bytes32("daiPool")) + .returns(daiPool.address); - const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); - await usdcPool.mock.getUnderlyerPrice.returns(underlyerPrice); const usdcToken = await deployMockContract( deployer, IDetailedERC20.abi ); - await usdcPool.mock.underlyer.returns(usdcToken.address); - await usdcToken.mock.decimals.returns(6); + const usdcAvailableAmount = tokenAmountToBigNumber("110200", "6"); + await usdcToken.mock.balanceOf + .withArgs(lpAccount.address) + .returns(usdcAvailableAmount); - const usdtPool = await deployMockContract(deployer, PoolTokenV2.abi); - await usdtPool.mock.getUnderlyerPrice.returns(underlyerPrice); - const usdtToken = await deployMockContract( - deployer, - IDetailedERC20.abi - ); - await usdtPool.mock.underlyer.returns(usdtToken.address); - await usdtToken.mock.decimals.returns(6); + const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); + await usdcPool.mock.underlyer.returns(usdcToken.address); + await addressRegistry.mock.getAddress + .withArgs(bytes32("usdcPool")) + .returns(usdcPool.address); - pools = [daiPool.address, usdcPool.address, usdtPool.address]; + const result = await lpAccountFunder.getLpAccountBalance(); + deepEqual(result, [daiAvailableAmount, usdcAvailableAmount]); }); + }); + + 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); - before("Set TVL", async () => { - const tvl = tokenAmountToBigNumber("502300", 8); - await oracleAdapter.mock.getTvl.returns(tvl); + amount = tokenAmountToBigNumber("-159"); + expectedResult = tokenAmountToBigNumber("159"); + result = await lpAccountFunder.testGetFundAmount(amount); + deepEqual(expectedResult, result); }); + }); - it("Revert if array lengths do not match", async () => { - const amounts = new Array(pools.length - 1).fill( - tokenAmountToBigNumber("1", "18") + 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 ); - await expect( - lpAccountFunder.testCalculateDeltas(pools, amounts) - ).to.be.revertedWith("LENGTHS_MUST_MATCH"); - }); + deepEqual(expectedResult, result); - it("Return an empty array when given empty arrays", async () => { - const result = await lpAccountFunder.testCalculateDeltas([], []); - expect(result).to.deep.equal([]); - }); - - it("Returns expected amounts from _calculateDelta", async () => { - const amounts = [ - tokenAmountToBigNumber(384, 18), // DAI - tokenAmountToBigNumber(9899, 6), // Tether - ]; - const expectedAmounts = [ - await lpAccountFunder.testCalculateDelta( - amounts[0], - underlyerPrice, - 18 - ), - await lpAccountFunder.testCalculateDelta( - amounts[1], - underlyerPrice, - 6 - ), - ]; - - const result = await lpAccountFunder.testCalculateDeltas( - [pools[0], pools[2]], - amounts + topupAmount = tokenAmountToBigNumber("-11"); + expectedResult = tokenAmountToBigNumber("0"); + availableAmount = expectedResult; + result = await lpAccountFunder.testCalculateAmountToWithdraw( + topupAmount, + availableAmount ); - expect(result[0]).to.equal(expectedAmounts[0]); - expect(result[1]).to.equal(expectedAmounts[1]); - expect(result).to.deep.equal(expectedAmounts); + deepEqual(expectedResult, result); }); - it("Get zero mint amount for zero transfer", async () => { - const amounts = [0, tokenAmountToBigNumber(347, 6), 0]; - const result = await lpAccountFunder.testCalculateDeltas( - pools, - amounts - ); - - const expectedAmount = await lpAccountFunder.testCalculateDelta( - amounts[1], - underlyerPrice, - 6 + 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 ); - - expect(result[0]).to.equal(0); - expect(result[1]).to.be.equal(expectedAmount); - expect(result[2]).to.equal(0); + deepEqual(expectedResult, result); }); }); }); - - describe("getTvl", () => { - it("Call delegates to oracle adapter's getTvl", async () => { - const usdTvl = tokenAmountToBigNumber("25100123.87654321", "8"); - await oracleAdapter.mock.getTvl.returns(usdTvl); - expect(await lpAccountFunder.testGetTvl()).to.equal(usdTvl); - }); - - it("getTvl reverts with same reason as oracle adapter", async () => { - await oracleAdapter.mock.getTvl.revertsWithReason("SOMETHING_WRONG"); - await expect(lpAccountFunder.testGetTvl()).to.be.revertedWith( - "SOMETHING_WRONG" - ); - }); - }); - - describe("_registerPoolUnderlyers", () => { - let daiPool; - let daiToken; - let usdcPool; - let usdcToken; - - beforeEach("Setup mocks", async () => { - daiPool = await deployMockContract(deployer, PoolTokenV2.abi); - daiToken = await deployMockContract(deployer, IDetailedERC20.abi); - await daiPool.mock.underlyer.returns(daiToken.address); - await daiToken.mock.decimals.returns(18); - await daiToken.mock.symbol.returns("DAI"); - - usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); - usdcToken = await deployMockContract(deployer, IDetailedERC20.abi); - await usdcPool.mock.underlyer.returns(usdcToken.address); - await usdcToken.mock.decimals.returns(6); - await usdcToken.mock.symbol.returns("USDC"); - }); - - it("Unregistered underlyers get registered", async () => { - // set DAI as unregistered in ERC20 registry - await erc20Allocation.mock["isErc20TokenRegistered(address)"] - .withArgs(daiToken.address) - .returns(false); - - // revert on registration for DAI but not others - await erc20Allocation.mock["registerErc20Token(address)"].returns(); - await erc20Allocation.mock["registerErc20Token(address)"] - .withArgs(daiToken.address) - .revertsWithReason("REGISTERED_DAI"); - - // expect revert since register function should be called - await expect( - lpAccountFunder.testRegisterPoolUnderlyers([daiPool.address]) - ).to.be.revertedWith("REGISTERED_DAI"); - }); - - it("Registered underlyers are skipped", async () => { - // set DAI as registered while USDC is not - await erc20Allocation.mock["isErc20TokenRegistered(address)"] - .withArgs(daiToken.address) - .returns(true); - await erc20Allocation.mock["isErc20TokenRegistered(address)"] - .withArgs(usdcToken.address) - .returns(false); - - // revert on registration for DAI or USDC - await erc20Allocation.mock["registerErc20Token(address)"].returns(); - await erc20Allocation.mock["registerErc20Token(address)"] - .withArgs(usdcToken.address) - .revertsWithReason("REGISTERED_USDC"); - await erc20Allocation.mock["registerErc20Token(address)"] - .withArgs(daiToken.address) - .revertsWithReason("REGISTERED_DAI"); - - // should not revert since DAI should not be registered - await expect( - lpAccountFunder.testRegisterPoolUnderlyers([daiPool.address]) - ).to.not.be.reverted; - - // should revert for USDC registration - await expect( - lpAccountFunder.testRegisterPoolUnderlyers([ - daiPool.address, - usdcPool.address, - ]) - ).to.be.revertedWith("REGISTERED_USDC"); - }); - }); - - describe("fundLpAccount", () => { - it("LP Safe can call", async () => { - // await expect(lpAccountFunder.connect(lpSafe).fundLpAccount([])).to.not.be.reverted; - await lpAccountFunder.connect(lpSafe).fundLpAccount([]); - }); - - 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"); - }); - }); - - describe("withdrawFromLpAccount", () => { - 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("Revert on unregistered LP Account address", async () => { - await addressRegistry.mock.lpAccountAddress.returns(ZERO_ADDRESS); - await expect( - lpAccountFunder.connect(lpSafe).withdrawFromLpAccount([]) - ).to.be.revertedWith("INVALID_LP_ACCOUNT"); - }); - }); - - describe("getRebalanceAmounts", () => { - it("Return pair of empty arrays when give an empty array", async () => { - const result = await lpAccountFunder.getRebalanceAmounts([]); - expect(result).to.deep.equal([[], []]); - }); - - it("Return array of top-up PoolAmounts from specified pools", async () => { - const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); - const daiRebalanceAmount = tokenAmountToBigNumber("1234888", "18"); - await daiPool.mock.getReserveTopUpValue.returns(daiRebalanceAmount); - await addressRegistry.mock.getAddress - .withArgs(bytes32("daiPool")) - .returns(daiPool.address); - - const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); - const usdcRebalanceAmount = tokenAmountToBigNumber("459999", "6"); - await usdcPool.mock.getReserveTopUpValue.returns(usdcRebalanceAmount); - await addressRegistry.mock.getAddress - .withArgs(bytes32("usdcPool")) - .returns(usdcPool.address); - - const result = await lpAccountFunder.getRebalanceAmounts([ - bytes32("daiPool"), - bytes32("usdcPool"), - ]); - deepEqual(result, [ - [daiPool.address, usdcPool.address], - [daiRebalanceAmount, usdcRebalanceAmount], - ]); - }); - }); - - describe("getLpAccountBalances", () => { - it("Return empty array when given an empty array", async () => { - const result = await lpAccountFunder.getLpAccountBalances([]); - expect(result).to.deep.equal([]); - }); - - it("Return array of available stablecoin balances of LP Account", async () => { - const daiToken = await deployMockContract(deployer, IDetailedERC20.abi); - const daiAvailableAmount = tokenAmountToBigNumber("15325", "18"); - await daiToken.mock.balanceOf - .withArgs(lpAccount.address) - .returns(daiAvailableAmount); - - const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); - await daiPool.mock.underlyer.returns(daiToken.address); - await addressRegistry.mock.getAddress - .withArgs(bytes32("daiPool")) - .returns(daiPool.address); - - const usdcToken = await deployMockContract(deployer, IDetailedERC20.abi); - const usdcAvailableAmount = tokenAmountToBigNumber("110200", "6"); - await usdcToken.mock.balanceOf - .withArgs(lpAccount.address) - .returns(usdcAvailableAmount); - - const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); - await usdcPool.mock.underlyer.returns(usdcToken.address); - await addressRegistry.mock.getAddress - .withArgs(bytes32("usdcPool")) - .returns(usdcPool.address); - - const result = await lpAccountFunder.getLpAccountBalances([ - bytes32("daiPool"), - bytes32("usdcPool"), - ]); - deepEqual(result, [daiAvailableAmount, usdcAvailableAmount]); - }); - }); - - describe("_getFundAmounts", () => { - it("Returns empty array given empty array", async () => { - const result = await lpAccountFunder.testGetFundAmounts([]); - expect(result).to.be.empty; - }); - - it("Replaces negatives with positives, positives with zeros", async () => { - let amounts = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("1777"), - tokenAmountToBigNumber("11"), - tokenAmountToBigNumber("122334"), - ]; - let expectedResult = [ - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - ]; - let result = await lpAccountFunder.testGetFundAmounts(amounts); - deepEqual(expectedResult, result); - - amounts = [ - tokenAmountToBigNumber("-159"), - tokenAmountToBigNumber("-1777"), - tokenAmountToBigNumber("-11"), - ]; - expectedResult = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("1777"), - tokenAmountToBigNumber("11"), - ]; - result = await lpAccountFunder.testGetFundAmounts(amounts); - deepEqual(expectedResult, result); - - amounts = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("-1777"), - tokenAmountToBigNumber("-11"), - tokenAmountToBigNumber("122334"), - tokenAmountToBigNumber("0"), - ]; - expectedResult = [ - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("1777"), - tokenAmountToBigNumber("11"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - ]; - result = await lpAccountFunder.testGetFundAmounts(amounts); - deepEqual(expectedResult, result); - }); - }); - - describe("_calculateAmountsToWithdraw", () => { - it("Returns empty array given empty array", async () => { - const result = await lpAccountFunder.testCalculateAmountsToWithdraw( - [], - [] - ); - expect(result).to.be.empty; - }); - - it("Replaces negatives with zeros", async () => { - let topupAmounts = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("1777"), - tokenAmountToBigNumber("11"), - tokenAmountToBigNumber("122334"), - ]; - let availableAmounts = topupAmounts; - let expectedResult = topupAmounts; - let result = await lpAccountFunder.testCalculateAmountsToWithdraw( - topupAmounts, - availableAmounts - ); - - deepEqual(expectedResult, result); - - topupAmounts = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("-1777"), - tokenAmountToBigNumber("-11"), - tokenAmountToBigNumber("122334"), - tokenAmountToBigNumber("0"), - ]; - expectedResult = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("122334"), - tokenAmountToBigNumber("0"), - ]; - availableAmounts = expectedResult; - result = await lpAccountFunder.testCalculateAmountsToWithdraw( - topupAmounts, - availableAmounts - ); - deepEqual(expectedResult, result); - }); - - it("Uses minimum of topup and available amounts", async () => { - let topupAmounts = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("1777"), - tokenAmountToBigNumber("11"), - tokenAmountToBigNumber("122334"), - ]; - let availableAmounts = [ - tokenAmountToBigNumber("122334"), - tokenAmountToBigNumber("122334"), - tokenAmountToBigNumber("122334"), - tokenAmountToBigNumber("122334"), - ]; - let expectedResult = topupAmounts; - let result = await lpAccountFunder.testCalculateAmountsToWithdraw( - topupAmounts, - availableAmounts - ); - deepEqual(expectedResult, result); - - topupAmounts = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("1777"), - tokenAmountToBigNumber("11"), - tokenAmountToBigNumber("122334"), - ]; - availableAmounts = [ - tokenAmountToBigNumber("1000"), - tokenAmountToBigNumber("1000"), - tokenAmountToBigNumber("1000"), - tokenAmountToBigNumber("1000"), - ]; - expectedResult = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("1000"), - tokenAmountToBigNumber("11"), - tokenAmountToBigNumber("1000"), - ]; - result = await lpAccountFunder.testCalculateAmountsToWithdraw( - topupAmounts, - availableAmounts - ); - deepEqual(expectedResult, result); - - topupAmounts = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("-1777"), - tokenAmountToBigNumber("-11"), - tokenAmountToBigNumber("122334"), - tokenAmountToBigNumber("0"), - ]; - availableAmounts = [ - tokenAmountToBigNumber("1000"), - tokenAmountToBigNumber("1"), - tokenAmountToBigNumber("100"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("10000"), - tokenAmountToBigNumber("10"), - ]; - expectedResult = [ - tokenAmountToBigNumber("159"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("0"), - tokenAmountToBigNumber("10000"), - tokenAmountToBigNumber("0"), - ]; - result = await lpAccountFunder.testCalculateAmountsToWithdraw( - topupAmounts, - availableAmounts - ); - deepEqual(expectedResult, result); - }); - }); }); From 16ffe0d871ab87c639013ddefc30997b501839b7 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Fri, 5 Aug 2022 10:42:25 -0400 Subject: [PATCH 16/18] Update naming; delete deprecated file --- contracts/index/IReservePool.sol | 39 -------- contracts/index/LpAccountFunder.sol | 20 ++-- test-unit/LpAccountFunder.js | 150 ++++++++-------------------- 3 files changed, 52 insertions(+), 157 deletions(-) delete mode 100644 contracts/index/IReservePool.sol diff --git a/contracts/index/IReservePool.sol b/contracts/index/IReservePool.sol deleted file mode 100644 index 02c29354..00000000 --- a/contracts/index/IReservePool.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: BUSDL-1.1 -pragma solidity 0.6.11; - -/** - * @notice For vaults that keep a separate reserve of tokens - */ -interface IReserveVault { - /** - * @notice Log when the percent held in reserve is changed - * @param reservePercentage The new percent held in reserve - */ - event ReservePercentageChanged(uint256 reservePercentage); - - /** - * @notice Set a new percent of tokens to hold in reserve - * @param reservePercentage_ The new percent - */ - function setReservePercentage(uint256 reservePercentage_) external; - - /** - * @notice Transfer an amount of tokens to the LP Account - * @dev This should only be callable by the `LpAccountFunder` - * @param amount The amount of tokens - */ - function transferToLpAccount(uint256 amount) external; - - /** - * @notice Get the amount of tokens missing from the reserve - * @dev A negative amount indicates extra tokens not needed for the reserve - * @return The amount of missing tokens - */ - function getReserveTopUpValue() external view returns (int256); - - /** - * @notice Get the current percentage of tokens held in reserve - * @return The percent - */ - function reservePercentage() external view returns (uint256); -} diff --git a/contracts/index/LpAccountFunder.sol b/contracts/index/LpAccountFunder.sol index 20ff184b..156ecaf8 100644 --- a/contracts/index/LpAccountFunder.sol +++ b/contracts/index/LpAccountFunder.sol @@ -16,7 +16,7 @@ import { import {ILpAccount} from "contracts/lpaccount/Imports.sol"; import {IAddressRegistryV2} from "contracts/registry/Imports.sol"; import {ILockingOracle} from "contracts/oracle/Imports.sol"; -import {IReservePool} from "contracts/pool/Imports.sol"; +import {IERC4626, IReserveVault} from "contracts/index/Imports.sol"; import { IErc20Allocation, IAssetAllocationRegistry, @@ -107,7 +107,7 @@ contract LpAccountFunder is * @return rebalanceAmount */ function getRebalanceAmount() public view returns (int256 rebalanceAmount) { - rebalanceAmount = IReservePool(indexToken).getReserveTopUpValue(); + rebalanceAmount = IReserveVault(indexToken).getReserveTopUpValue(); } function getLpAccountBalance() @@ -115,11 +115,11 @@ contract LpAccountFunder is view returns (uint256 lpAccountBalance) { - IReservePool pool = IReservePool(indexToken); - IDetailedERC20 underlyer = IDetailedERC20(pool.underlyer()); + IERC4626 vault = IERC4626(indexToken); + IDetailedERC20 asset = IDetailedERC20(vault.asset()); address lpAccountAddress = addressRegistry.lpAccountAddress(); - lpAccountBalance = underlyer.balanceOf(lpAccountAddress); + lpAccountBalance = asset.balanceOf(lpAccountAddress); } function _setAddressRegistry(address addressRegistry_) internal { @@ -138,7 +138,7 @@ contract LpAccountFunder is address lpAccountAddress = addressRegistry.lpAccountAddress(); require(lpAccountAddress != address(0), "INVALID_LP_ACCOUNT"); // defensive check -- should never happen - IReservePool(indexToken).transferToLpAccount(amount); + IReserveVault(indexToken).transferToLpAccount(amount); ILockingOracle oracleAdapter = _getOracleAdapter(); oracleAdapter.lock(); @@ -169,11 +169,11 @@ contract LpAccountFunder is ) ); - IReservePool pool = IReservePool(indexToken); - IDetailedERC20 underlyer = IDetailedERC20(pool.underlyer()); + IERC4626 vault = IERC4626(indexToken); + IDetailedERC20 asset = IDetailedERC20(vault.asset()); - if (!erc20Allocation.isErc20TokenRegistered(underlyer)) { - erc20Allocation.registerErc20Token(underlyer); + if (!erc20Allocation.isErc20TokenRegistered(asset)) { + erc20Allocation.registerErc20Token(asset); } } diff --git a/test-unit/LpAccountFunder.js b/test-unit/LpAccountFunder.js index eec661fe..a9d39fda 100644 --- a/test-unit/LpAccountFunder.js +++ b/test-unit/LpAccountFunder.js @@ -11,10 +11,9 @@ const { } = require("../utils/helpers"); const { deployMockContract } = waffle; const OracleAdapter = artifacts.readArtifactSync("OracleAdapter"); -const PoolTokenV2 = artifacts.readArtifactSync("PoolTokenV2"); const IDetailedERC20 = artifacts.readArtifactSync("IDetailedERC20"); -describe.only("Contract: LpAccountFunder", () => { +describe("Contract: LpAccountFunder", () => { // signers let deployer; let emergencySafe; @@ -206,21 +205,19 @@ describe.only("Contract: LpAccountFunder", () => { }); }); - describe("Multiple mints and burns", () => { - let pool; - let underlyer; + describe("fund and withdraw", () => { + let asset; before("Setup mocks", async () => { - pool = await deployMockContract(deployer, PoolTokenV2.abi); - await pool.mock.transferToLpAccount.returns(); - await pool.mock.getUnderlyerPrice.returns( + await indexToken.mock.transferToLpAccount.returns(); + await indexToken.mock.getAssetPrice.returns( tokenAmountToBigNumber("0.998", 8) ); - underlyer = await deployMockContract(deployer, IDetailedERC20.abi); - await pool.mock.underlyer.returns(underlyer.address); + asset = await deployMockContract(deployer, IDetailedERC20.abi); + await indexToken.mock.asset.returns(asset.address); - await underlyer.mock.decimals.returns(6); + await asset.mock.decimals.returns(18); await lpAccount.mock.transferToPool.returns(); @@ -230,73 +227,51 @@ describe.only("Contract: LpAccountFunder", () => { }); describe("_registerPoolUnderlyer", () => { - let daiPool; - let daiToken; - let usdcPool; - let usdcToken; - beforeEach("Setup mocks", async () => { - daiPool = await deployMockContract(deployer, PoolTokenV2.abi); - daiToken = await deployMockContract(deployer, IDetailedERC20.abi); - await daiPool.mock.underlyer.returns(daiToken.address); - await daiToken.mock.decimals.returns(18); - await daiToken.mock.symbol.returns("DAI"); - - usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); - usdcToken = await deployMockContract(deployer, IDetailedERC20.abi); - await usdcPool.mock.underlyer.returns(usdcToken.address); - await usdcToken.mock.decimals.returns(6); - await usdcToken.mock.symbol.returns("USDC"); + await asset.mock.symbol.returns("3CRV"); }); - it("Unregistered underlyers get registered", async () => { - // set DAI as unregistered in ERC20 registry + it("Unregistered asset get registered", async () => { + // set asset as unregistered in ERC20 registry await erc20Allocation.mock["isErc20TokenRegistered(address)"] - .withArgs(daiToken.address) + .withArgs(asset.address) .returns(false); - // revert on registration for DAI but not others + // revert on registration await erc20Allocation.mock["registerErc20Token(address)"].returns(); await erc20Allocation.mock["registerErc20Token(address)"] - .withArgs(daiToken.address) - .revertsWithReason("REGISTERED_DAI"); + .withArgs(asset.address) + .revertsWithReason("TEST_REGISTER_ASSET"); // expect revert since register function should be called await expect( lpAccountFunder.testRegisterPoolUnderlyer() - ).to.be.revertedWith("REGISTERED_DAI"); + ).to.be.revertedWith("TEST_REGISTER_ASSET"); }); - it("Registered underlyers are skipped", async () => { - // set DAI as registered while USDC is not + it("Registered asset is skipped", async () => { + // set asset as registered in ERC20 registry await erc20Allocation.mock["isErc20TokenRegistered(address)"] - .withArgs(daiToken.address) + .withArgs(asset.address) .returns(true); - await erc20Allocation.mock["isErc20TokenRegistered(address)"] - .withArgs(usdcToken.address) - .returns(false); - // revert on registration for DAI or USDC + // revert on registration await erc20Allocation.mock["registerErc20Token(address)"].returns(); await erc20Allocation.mock["registerErc20Token(address)"] - .withArgs(usdcToken.address) - .revertsWithReason("REGISTERED_USDC"); - await erc20Allocation.mock["registerErc20Token(address)"] - .withArgs(daiToken.address) - .revertsWithReason("REGISTERED_DAI"); + .withArgs(asset.address) + .revertsWithReason("TEST_SKIP_REGISTER"); - // should not revert since DAI should not be registered + // should not revert since asset is already registered await expect(lpAccountFunder.testRegisterPoolUnderlyer()).to.not.be .reverted; - - // should revert for USDC registration - await expect( - lpAccountFunder.testRegisterPoolUnderlyer() - ).to.be.revertedWith("REGISTERED_USDC"); }); }); 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; @@ -317,6 +292,11 @@ describe.only("Contract: LpAccountFunder", () => { }); 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; @@ -327,75 +307,29 @@ describe.only("Contract: LpAccountFunder", () => { lpAccountFunder.connect(randomUser).withdrawFromLpAccount() ).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).withdrawFromLpAccount() - ).to.be.revertedWith("INVALID_LP_ACCOUNT"); - }); }); describe("getRebalanceAmount", () => { - it("Return pair of empty arrays when give an empty array", async () => { - const result = await lpAccountFunder.getRebalanceAmount(); - expect(result).to.deep.equal([[], []]); - }); - - it("Return array of top-up PoolAmounts from specified pools", async () => { - const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); - const daiRebalanceAmount = tokenAmountToBigNumber("1234888", "18"); - await daiPool.mock.getReserveTopUpValue.returns(daiRebalanceAmount); - await addressRegistry.mock.getAddress - .withArgs(bytes32("daiPool")) - .returns(daiPool.address); - - const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); - const usdcRebalanceAmount = tokenAmountToBigNumber("459999", "6"); - await usdcPool.mock.getReserveTopUpValue.returns(usdcRebalanceAmount); - await addressRegistry.mock.getAddress - .withArgs(bytes32("usdcPool")) - .returns(usdcPool.address); + it("Delegates to vault function", async () => { + const vaultRebalanceAmount = tokenAmountToBigNumber("1234888", "18"); + await indexToken.mock.getReserveTopUpValue.returns( + vaultRebalanceAmount + ); const result = await lpAccountFunder.getRebalanceAmount(); - deepEqual(result, [ - [daiPool.address, usdcPool.address], - [daiRebalanceAmount, usdcRebalanceAmount], - ]); + expect(result).to.equal(vaultRebalanceAmount); }); }); describe("getLpAccountBalance", () => { it("Return array of available stablecoin balances of LP Account", async () => { - const daiToken = await deployMockContract(deployer, IDetailedERC20.abi); - const daiAvailableAmount = tokenAmountToBigNumber("15325", "18"); - await daiToken.mock.balanceOf - .withArgs(lpAccount.address) - .returns(daiAvailableAmount); - - const daiPool = await deployMockContract(deployer, PoolTokenV2.abi); - await daiPool.mock.underlyer.returns(daiToken.address); - await addressRegistry.mock.getAddress - .withArgs(bytes32("daiPool")) - .returns(daiPool.address); - - const usdcToken = await deployMockContract( - deployer, - IDetailedERC20.abi - ); - const usdcAvailableAmount = tokenAmountToBigNumber("110200", "6"); - await usdcToken.mock.balanceOf + const availableAmount = tokenAmountToBigNumber("15325", "18"); + await asset.mock.balanceOf .withArgs(lpAccount.address) - .returns(usdcAvailableAmount); - - const usdcPool = await deployMockContract(deployer, PoolTokenV2.abi); - await usdcPool.mock.underlyer.returns(usdcToken.address); - await addressRegistry.mock.getAddress - .withArgs(bytes32("usdcPool")) - .returns(usdcPool.address); + .returns(availableAmount); const result = await lpAccountFunder.getLpAccountBalance(); - deepEqual(result, [daiAvailableAmount, usdcAvailableAmount]); + expect(result).to.equal(availableAmount); }); }); From aa91064c74c0057f045a0c2025199acf565ceffd Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Fri, 5 Aug 2022 11:44:39 -0400 Subject: [PATCH 17/18] Test locking; some cleanup --- test-unit/LpAccountFunder.js | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/test-unit/LpAccountFunder.js b/test-unit/LpAccountFunder.js index a9d39fda..592fd050 100644 --- a/test-unit/LpAccountFunder.js +++ b/test-unit/LpAccountFunder.js @@ -17,19 +17,19 @@ describe("Contract: LpAccountFunder", () => { // signers let deployer; let emergencySafe; + let adminSafe; let lpSafe; - let lpAccount; let randomUser; // deployed contracts let lpAccountFunder; // mocks - let adminSafe; + let indexToken; + let lpAccount; let oracleAdapter; let addressRegistry; let erc20Allocation; - let indexToken; // use EVM snapshots for test isolation let testSnapshotId; @@ -56,9 +56,12 @@ describe("Contract: LpAccountFunder", () => { await timeMachine.revertToSnapshot(suiteSnapshotId); }); - before("Setup address registry", async () => { - [deployer] = await ethers.getSigners(); + 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 @@ -66,8 +69,6 @@ describe("Contract: LpAccountFunder", () => { }); before("Register Safes", async () => { - [, emergencySafe, adminSafe, lpSafe] = await ethers.getSigners(); - await addressRegistry.mock.lpSafeAddress.returns(lpSafe.address); await addressRegistry.mock.getAddress .withArgs(bytes32("lpSafe")) @@ -87,14 +88,12 @@ describe("Contract: LpAccountFunder", () => { }); before("Mock dependencies", async () => { - [, , , , randomUser] = await ethers.getSigners(); - oracleAdapter = await deployMockContract(deployer, OracleAdapter.abi); await addressRegistry.mock.oracleAdapterAddress.returns( oracleAdapter.address ); - // allows mAPT to mint and burn + // funding or withdrawing will lock the oracle adapter await oracleAdapter.mock.lock.returns(); lpAccount = await deployMockContract( @@ -289,6 +288,14 @@ describe("Contract: LpAccountFunder", () => { 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", () => { @@ -307,6 +314,14 @@ describe("Contract: LpAccountFunder", () => { 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", () => { From 46de84f00ab9991929cab266ef77898cf91d1522 Mon Sep 17 00:00:00 2001 From: Chan-Ho Suh Date: Tue, 9 Aug 2022 10:19:13 -0400 Subject: [PATCH 18/18] temp --- test-integration/LpAccountFunder.js | 1547 +++++++++++++++++++++++++++ 1 file changed, 1547 insertions(+) create mode 100644 test-integration/LpAccountFunder.js 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) + ); + }); + }); + }); +});