From da78372158b5f6ce0644f759368dc1338f294b1e Mon Sep 17 00:00:00 2001 From: Nenad Misic Date: Tue, 15 Aug 2023 10:29:54 +0200 Subject: [PATCH] Get older tests up-to-date (#266) * Remove DummyERC20 use from core tests * Clean redundant awaits in AccountsStrategy * Update Router wrong Promise arg * Rename GasFwd > user to accounts * Fix GasFwd tests * Set Accounts as default signer in GasFwd tests > deployGasFwdAsProxy * Fix wrong await expects * Refactor some tests in AccountsCreateEndowment * Fix deploy error tests * Update 'unauthorized' error tests * Fix IndexFund tests * Wrap all IndexFund txs in wait * Move 'wait' to test/utils * Wait all unwaited txs * Fix Vaults tests * Fix AccountsSwapRouter tests * Add missing hre param to packActionData in Router & AccountsStrategy * Update deployer/proxyAdmin in .env.template * Remove temp_task * Remove hardhat/console.sol from AccountsSwapRouter * Move IndexFund errors to IIndexFund * Update `AccountsStrategy` tests and address issues found in the contract (#267) * Use getChainId in AccountsStrategy * Make AccountsStrategy 'upon strategyInvest reverts when' tests pass with fake contracts * Make AccountsStrategy 'upon strategyInvest and calls the local router' tests pass with fake contracts * Make AccountsStrategy 'upon strategyInvest and calls axelar GMP' tests pass with fake contracts * Make AccountsStrategy 'upon strategyRedeem reverts when' tests pass with fake contracts * Add missing EndowmentRedeemed emissions in strategyRedeem * Make AccountsStrategy 'upon strategyRedeem and calls the local router' tests pass with fake contracts * Make AccountsStrategy 'upon strategyRedeem and calls axelar GMP' tests pass with fake contracts * Move state/facet declaration to root describe * Move ACCOUNT_ID up * Move common fake contracts to root describe * Set type of investRequest to AccountMessages.InvestRequestStruct * Refactor strategy approval state * Use 'const' where applicable * Refactor registrar fakes * Check WITHDRAW_ONLY flow in strategyRedeem * Remove redundant token.transfer.returns(true) * Add missing types * Move lock/liq/gas fee consts up * Move all common fake contracts up * Move 'and calls the local router' outside of 'reverts when' * Make upon strategyRedeemAll work with 'reverts when' and 'and calls the local router' * Rename all constants to upper case * Make upon strategyRedeemAll work with 'and calls axelar GMP' * Add new test case when more than enough balance to pay gas * Reorg. imports * Move 'token.approve.returns' mock to beforeEach * Add missing test cases related to amnt of gas covered by GasFwd * Fix lock. bal calc. in _payForGasWithAccountBalance * Add new test case for _payForGasWithAccountBalance * Move balance setup up * Add revert cases for strategyInvest _payForGasWithAccountBalance * Move strategyInvest > _payForGasWithAccountBalance revert tests to 'and calls axelar GMP' section * Remove redundant check in reverting test for strategyRedeem * Add missing test cases for strategyRedeem * Fix/add test cases for strategyRedeemAll * Add new ext. call tests * Move gasFwd, registrar, router and token fakes init to beforeEach * Add test case for strategyRedeemAll > approvalState == WITHDRAW_ONLY * Remove async func from describes * Reorganize callback tests * Pass 0 tokens to _payForGasWithAccountBalance on redeem funcs * Refactor liqGas calculation Co-authored-by: katzman <84420280+stevieraykatz@users.noreply.github.com> * Use liq. percentage to calculate gas to pay from account bal * Revert "Use liq. percentage to calculate gas to pay from account bal" This reverts commit 8e53c23ef0fb41d9cebdaf478ce07beed0c999bc. * Extract investAmt * Add gasPercentFromLiq param to _payForGasWithAccountBalance * Add missing require at least one of locked/liquid is passed * Move comments * Add missing 'Must X at least one of locked/liquid' tests * Use ACCOUNT_ID instead of hardcoded 1s * Add valid value check for gasPercentFromLiq in _payForGasWithAccountBalance * Revert "Add valid value check for gasPercentFromLiq in _payForGasWithAccountBalance" This reverts commit f7e1a0a33314cd9b6ba6b830ac78ef07a22bc9c1. * Fix tests * Add more data to InsufficientFundsForGas error * Add more test cases for _execute * Fix wrongful expect awaits into await expects * Fix 'into _execute' tests * Fix all tests * Add missing types to redeemRequests * Fix typo in InsufficientFundsForGas * Fix comments in _payForGasWithAccountBalance * Add missing emit EndowmentRedeemed in _axelarCallbackWithToken * Rename actionStatus to vaultStatus * Rename gasPercentFromLiq to gasRateFromLiq_withPrecision * Use BIG_NUMBA_BASIS instead of PERCENT_BASIS * Add fractional calculation tests for strategy[Invest/Redeem] * Remove extra fields from InsufficientFundsForGas * Merge all strategyRedeem > 'makes all the correct external calls and pays for part of gas fee' tests into one * Fix strategyRedeemAll percentage, changed basis into BIG_NUMBA_BASIS * Add more strategyRedeemAll tests * Add custom descriptions * Add new strategyInvest test cases * Update EndowmentInvested and EndowmentRedeemed events to include endowId * Add missing expects and events to tests * Move state updates in strategyInvest before if/else * Emit custom error on zero amounts set to invest/redeem * Fix _payForGasWithAccountBalance, no longer accepts lock/liq amts * Fix tests * Move ZeroAmount to interface --------- Co-authored-by: katzman <84420280+stevieraykatz@users.noreply.github.com> * Remove redundant state.config assignment in IndexFund.initialize --------- Co-authored-by: katzman <84420280+stevieraykatz@users.noreply.github.com> --- .env.template | 8 +- .../core/accounts/facets/AccountsStrategy.sol | 108 +- .../accounts/facets/AccountsSwapRouter.sol | 1 - .../accounts/interfaces/IAccountsEvents.sol | 4 +- .../accounts/interfaces/IAccountsStrategy.sol | 3 +- contracts/core/index-fund/IIndexFund.sol | 8 +- contracts/core/index-fund/IndexFund.sol | 35 +- contracts/core/vault/APVault_V1.sol | 3 +- tasks/helpers/deploy/deployDummyERC20.ts | 9 +- test/core/IndexFund.ts | 513 ++-- test/core/accounts/AccountsAllowance.ts | 60 +- test/core/accounts/AccountsCreateEndowment.ts | 35 +- test/core/accounts/AccountsDeployContract.ts | 20 +- .../AccountsDepositWithdrawEndowments.ts | 186 +- test/core/accounts/AccountsDonationMatch.ts | 40 +- test/core/accounts/AccountsGasManager.ts | 27 +- test/core/accounts/AccountsQueryEndowments.ts | 18 +- test/core/accounts/AccountsStrategy.ts | 2621 ++++++++++------- test/core/accounts/AccountsSwapRouter.ts | 79 +- test/core/accounts/AccountsUpdate.ts | 24 +- ...countsUpdateEndowmentSettingsController.ts | 54 +- .../core/accounts/AccountsUpdateEndowments.ts | 54 +- .../AccountsUpdateStatusEndowments.ts | 28 +- test/core/gasFwd/GasFwd.ts | 100 +- test/core/registrar/LocalRegistrar.ts | 55 +- test/core/router/Router.ts | 115 +- test/core/vault/Vault.ts | 224 +- test/halo/Halo.ts | 1 + test/integrations/flux/FluxStrategy.ts | 122 +- test/utils/Registrar.ts | 14 +- test/utils/helpers/RouterHelpers.ts | 12 +- test/utils/helpers/accounts/defaults.ts | 92 +- test/utils/index.ts | 1 + test/utils/wait.ts | 7 + 34 files changed, 2546 insertions(+), 2135 deletions(-) create mode 100644 test/utils/wait.ts diff --git a/.env.template b/.env.template index aabfc732e..63a09c2fb 100644 --- a/.env.template +++ b/.env.template @@ -22,10 +22,10 @@ GANACHE_RPC_URL="http://127.0.0.1:8545" GANACHE_PRIVATE_KEY="put your key here plz" ## Account private keys -DEPLOYER_KEY="0x036803cfb0810e2d3e9a8773179e47a7747155d17974a619f6cd130be0590556" # Dummy wallet for CI purposes replace with own -DEPLOYER_ADDRESS="0xd14f192084c1cdd017caacb1d9109f0507fc1c46" # dummy wallet for ci purposes replace with own -PROXY_ADMIN_KEY="0x036803cfb0810e2d3e9a8773179e47a7747155d17974a619f6cd130be0590556" # Dummy wallet for CI purposes replace with own -PROXY_ADMIN_ADDRESS="0xd14f192084c1cdd017caacb1d9109f0507fc1c46" # dummy wallet for ci purposes replace with own +DEPLOYER_KEY="0x13fd300b4664e8329b02d9ce1e1b779879c3dc99854bf596b9bac22872786b24" # Dummy wallet for CI purposes replace with own +DEPLOYER_ADDRESS="0xc9c192A1ef0BbEd747883F17B834CFD5F505a920" # dummy wallet for ci purposes replace with own +PROXY_ADMIN_KEY="0x6b2e3df50e192eacea6f44884555d5b1000ddec6d8124d2e9df2fd81468d26ce" # Dummy wallet for CI purposes replace with own +PROXY_ADMIN_ADDRESS="0x3584aC1A3353B400F4791788Fa52325054203DC3" # dummy wallet for ci purposes replace with own AP_TEAM_1_KEY="0x036803cfb0810e2d3e9a8773179e47a7747155d17974a619f6cd130be0590556" # Dummy wallet for CI purposes replace with own AP_TEAM_1_ADDRESS="0xd14f192084c1cdd017caacb1d9109f0507fc1c46" # dummy wallet for ci purposes replace with own AP_TEAM_2_KEY="0x02490597040bcc85ebe1a32fb86947a7e8438336546dc2b40d0073155928afc0" # Dummy wallet for CI purposes replace with own diff --git a/contracts/core/accounts/facets/AccountsStrategy.sol b/contracts/core/accounts/facets/AccountsStrategy.sol index c2f472abc..3973c2e1b 100644 --- a/contracts/core/accounts/facets/AccountsStrategy.sol +++ b/contracts/core/accounts/facets/AccountsStrategy.sol @@ -33,6 +33,9 @@ contract AccountsStrategy is { using SafeERC20 for IERC20; + uint256 constant FIFTY_PERCENT_BIG_NUMBA_RATE = + (50 * LibAccounts.BIG_NUMBA_BASIS) / LibAccounts.PERCENT_BASIS; + /** * @notice This function that allows users to deposit into a yield strategy using tokens from their locked or liquid account in an endowment. * @dev Allows the owner of an endowment to invest tokens into specified yield vaults. @@ -45,6 +48,10 @@ contract AccountsStrategy is AccountStorage.State storage state = LibAccounts.diamondStorage(); AccountStorage.Endowment storage tempEndowment = state.ENDOWMENTS[id]; + if (investRequest.lockAmt == 0 && investRequest.liquidAmt == 0) { + revert ZeroAmount(); + } + // check if the msg sender is either the owner or their delegate address and // that they have the power to manage the investments for an account balance if (investRequest.lockAmt > 0) { @@ -98,9 +105,16 @@ contract AccountsStrategy is "Token not approved" ); + uint256 investAmt = investRequest.lockAmt + investRequest.liquidAmt; + uint32[] memory accts = new uint32[](1); accts[0] = id; + state.STATES[id].balances.locked[tokenAddress] -= investRequest.lockAmt; + state.STATES[id].balances.liquid[tokenAddress] -= investRequest.liquidAmt; + state.STATES[id].activeStrategies[investRequest.strategy] = true; + emit EndowmentInvested(id); + // Strategy exists on the local network if (Validator.compareStrings(state.config.networkName, stratParams.network)) { IVault.VaultActionData memory payload = IVault.VaultActionData({ @@ -115,24 +129,16 @@ contract AccountsStrategy is }); bytes memory packedPayload = RouterLib.packCallData(payload); - IERC20(tokenAddress).safeTransfer( - thisNetwork.router, - (investRequest.lockAmt + investRequest.liquidAmt) - ); + IERC20(tokenAddress).safeTransfer(thisNetwork.router, investAmt); IVault.VaultActionData memory response = IRouter(thisNetwork.router).executeWithTokenLocal( state.config.networkName, AddressToString.toString(address(this)), packedPayload, investRequest.token, - (investRequest.lockAmt + investRequest.liquidAmt) + investAmt ); - if (response.status == IVault.VaultActionStatus.SUCCESS) { - state.STATES[id].balances.locked[tokenAddress] -= investRequest.lockAmt; - state.STATES[id].balances.liquid[tokenAddress] -= investRequest.liquidAmt; - state.STATES[id].activeStrategies[investRequest.strategy] = true; - emit EndowmentInvested(response.status); - } else { + if (response.status != IVault.VaultActionStatus.SUCCESS) { revert InvestFailed(response.status); } } @@ -159,8 +165,7 @@ contract AccountsStrategy is _payForGasWithAccountBalance( id, tokenAddress, - investRequest.lockAmt, - investRequest.liquidAmt, + (investRequest.liquidAmt * LibAccounts.BIG_NUMBA_BASIS) / investAmt, (investRequest.gasFee - gasFwdGas) ); } @@ -171,25 +176,19 @@ contract AccountsStrategy is AddressToString.toString(network.router), packedPayload, investRequest.token, - (investRequest.lockAmt + investRequest.liquidAmt), + investAmt, tokenAddress, investRequest.gasFee, state.ENDOWMENTS[id].gasFwd ); - IERC20(tokenAddress).safeApprove( - thisNetwork.axelarGateway, - (investRequest.lockAmt + investRequest.liquidAmt) - ); + IERC20(tokenAddress).safeApprove(thisNetwork.axelarGateway, investAmt); IAxelarGateway(thisNetwork.axelarGateway).callContractWithToken( stratParams.network, AddressToString.toString(network.router), packedPayload, investRequest.token, - (investRequest.lockAmt + investRequest.liquidAmt) + investAmt ); - state.STATES[id].balances.locked[tokenAddress] -= investRequest.lockAmt; - state.STATES[id].balances.liquid[tokenAddress] -= investRequest.liquidAmt; - state.STATES[id].activeStrategies[investRequest.strategy] = true; } } @@ -204,6 +203,10 @@ contract AccountsStrategy is AccountStorage.State storage state = LibAccounts.diamondStorage(); AccountStorage.Endowment storage tempEndowment = state.ENDOWMENTS[id]; + if (redeemRequest.lockAmt == 0 && redeemRequest.liquidAmt == 0) { + revert ZeroAmount(); + } + // check if the msg sender is either the owner or their delegate address and // that they have the power to manage the investments for an account balance if (redeemRequest.lockAmt > 0) { @@ -264,10 +267,12 @@ contract AccountsStrategy is if (response.status == IVault.VaultActionStatus.SUCCESS) { state.STATES[id].balances.locked[tokenAddress] += response.lockAmt; state.STATES[id].balances.liquid[tokenAddress] += response.liqAmt; + emit EndowmentRedeemed(id, response.status); } else if (response.status == IVault.VaultActionStatus.POSITION_EXITED) { state.STATES[id].balances.locked[tokenAddress] += response.lockAmt; state.STATES[id].balances.liquid[tokenAddress] += response.liqAmt; state.STATES[id].activeStrategies[redeemRequest.strategy] = false; + emit EndowmentRedeemed(id, response.status); } else { revert RedeemFailed(response.status); } @@ -292,11 +297,12 @@ contract AccountsStrategy is redeemRequest.gasFee ); if (gasFwdGas < redeemRequest.gasFee) { + uint256 gasRateFromLiq_withPrecision = (redeemRequest.liquidAmt * + LibAccounts.BIG_NUMBA_BASIS) / (redeemRequest.liquidAmt + redeemRequest.lockAmt); _payForGasWithAccountBalance( id, tokenAddress, - redeemRequest.lockAmt, - redeemRequest.liquidAmt, + gasRateFromLiq_withPrecision, (redeemRequest.gasFee - gasFwdGas) ); } @@ -329,10 +335,10 @@ contract AccountsStrategy is AccountStorage.State storage state = LibAccounts.diamondStorage(); AccountStorage.Endowment storage tempEndowment = state.ENDOWMENTS[id]; - require( - redeemAllRequest.redeemLiquid || redeemAllRequest.redeemLocked, - "Must redeem at least one of Locked/Liquid" - ); + if (!redeemAllRequest.redeemLiquid && !redeemAllRequest.redeemLocked) { + revert ZeroAmount(); + } + if (redeemAllRequest.redeemLocked) { require( Validator.canChange( @@ -395,7 +401,7 @@ contract AccountsStrategy is state.STATES[id].balances.locked[tokenAddress] += response.lockAmt; state.STATES[id].balances.liquid[tokenAddress] += response.liqAmt; state.STATES[id].activeStrategies[redeemAllRequest.strategy] = false; - emit EndowmentRedeemed(response.status); + emit EndowmentRedeemed(id, response.status); } else { revert RedeemAllFailed(response.status); } @@ -423,8 +429,7 @@ contract AccountsStrategy is _payForGasWithAccountBalance( id, tokenAddress, - 1, // Split evenly - 1, + FIFTY_PERCENT_BIG_NUMBA_RATE, (redeemAllRequest.gasFee - gasFwdGas) ); } @@ -461,6 +466,7 @@ contract AccountsStrategy is ) { state.STATES[id].balances.locked[response.token] += response.lockAmt; state.STATES[id].balances.liquid[response.token] += response.liqAmt; + emit EndowmentRedeemed(id, response.status); return true; } // Redeem/RedeemAll Cases @@ -474,11 +480,13 @@ contract AccountsStrategy is if (response.status == IVault.VaultActionStatus.SUCCESS) { state.STATES[id].balances.locked[response.token] += response.lockAmt; state.STATES[id].balances.liquid[response.token] += response.liqAmt; + emit EndowmentRedeemed(id, response.status); return true; } else if (response.status == IVault.VaultActionStatus.POSITION_EXITED) { state.STATES[id].balances.locked[response.token] += response.lockAmt; state.STATES[id].balances.liquid[response.token] += response.liqAmt; state.STATES[id].activeStrategies[response.strategyId] = false; + emit EndowmentRedeemed(id, response.status); return true; } } else { @@ -563,45 +571,41 @@ contract AccountsStrategy is * We split the gas payment proprotionally between locked and liquid if possible and * use liquid funds for locked gas needs, but not the other way around in the case of a shortage. * Revert if the combined balances of the account cannot cover both the investment request and the gas payment. + * @param id Endowment ID + * @param token Token address + * @param gasRateFromLiq_withPrecision Percentage of gas to pay from liquid portion + * @param gasRemaining Amount of gas to be payed from locked & liquid balances */ function _payForGasWithAccountBalance( uint32 id, address token, - uint256 lockAmt, - uint256 liqAmt, + uint256 gasRateFromLiq_withPrecision, uint256 gasRemaining ) internal { AccountStorage.State storage state = LibAccounts.diamondStorage(); uint256 lockBal = state.STATES[id].balances.locked[token]; uint256 liqBal = state.STATES[id].balances.liquid[token]; - uint256 sendAmt = lockAmt + liqAmt; - // Split gas proportionally between liquid and lock amts - uint256 liqGas = (gasRemaining * ((liqAmt * LibAccounts.BIG_NUMBA_BASIS) / sendAmt)) / - LibAccounts.BIG_NUMBA_BASIS; + uint256 liqGas = (gasRemaining * gasRateFromLiq_withPrecision) / LibAccounts.BIG_NUMBA_BASIS; uint256 lockGas = gasRemaining - liqGas; - uint256 lockNeed = lockGas + lockAmt; - uint256 liqNeed = liqGas + liqAmt; - // Cases: // 1) lockBal and liqBal each cover the respective needs - if ((lockNeed <= lockBal) && (liqNeed <= liqBal)) { + if ((lockGas <= lockBal) && (liqGas <= liqBal)) { state.STATES[id].balances.locked[token] -= lockGas; state.STATES[id].balances.liquid[token] -= liqGas; - } else if ((lockNeed > lockBal) && (liqNeed <= liqBal)) { - // 2) lockBal does not cover lockNeeds, liqBal can cover deficit in addition to liqNeeds - if ((lockNeed - lockBal) <= (liqBal - liqNeed)) { - state.STATES[id].balances.locked[token] = 0; - state.STATES[id].balances.liquid[token] -= (liqGas + (lockNeed - lockBal)); - } - // 3) lockBal does not cover lockNeeds and liqBal cannot cover -> revert - else { + } else if ((lockGas > lockBal) && (liqGas <= liqBal)) { + // 2) lockBal does not cover lockGas, check if liqBal can cover deficit in addition to liqGas + uint256 lockNeedDeficit = lockGas - lockBal; + if (lockNeedDeficit <= (liqBal - liqGas)) { + state.STATES[id].balances.locked[token] -= (lockGas - lockNeedDeficit); + state.STATES[id].balances.liquid[token] -= (liqGas + lockNeedDeficit); + } else { + // 3) lockBal does not cover lockGas and liqBal cannot cover -> revert revert InsufficientFundsForGas(id); } - } - // 4) lockBal covers lockNeeds, liqBal does not cover liqNeeds -> revert - else { + } else { + // 4) lockBal covers lockGas, liqBal does not cover liqGas -> revert revert InsufficientFundsForGas(id); } } diff --git a/contracts/core/accounts/facets/AccountsSwapRouter.sol b/contracts/core/accounts/facets/AccountsSwapRouter.sol index 5cc7ab048..2ffe1a0a8 100644 --- a/contracts/core/accounts/facets/AccountsSwapRouter.sol +++ b/contracts/core/accounts/facets/AccountsSwapRouter.sol @@ -16,7 +16,6 @@ import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; -import "hardhat/console.sol"; uint256 constant ACCEPTABLE_PRICE_DELAY = 300; // 5 minutes, in seconds diff --git a/contracts/core/accounts/interfaces/IAccountsEvents.sol b/contracts/core/accounts/interfaces/IAccountsEvents.sol index 8ff1f3bba..27125f03b 100644 --- a/contracts/core/accounts/interfaces/IAccountsEvents.sol +++ b/contracts/core/accounts/interfaces/IAccountsEvents.sol @@ -46,8 +46,8 @@ interface IAccountsEvents { uint256 amountOut ); event EndowmentSettingUpdated(uint256 endowId, string setting); - event EndowmentInvested(IVault.VaultActionStatus); - event EndowmentRedeemed(IVault.VaultActionStatus); + event EndowmentInvested(uint256 endowId); + event EndowmentRedeemed(uint256 endowId, IVault.VaultActionStatus); event RefundNeeded(IVault.VaultActionData); event UnexpectedTokens(IVault.VaultActionData); } diff --git a/contracts/core/accounts/interfaces/IAccountsStrategy.sol b/contracts/core/accounts/interfaces/IAccountsStrategy.sol index 368a6be3a..f18ed8b92 100644 --- a/contracts/core/accounts/interfaces/IAccountsStrategy.sol +++ b/contracts/core/accounts/interfaces/IAccountsStrategy.sol @@ -8,12 +8,13 @@ import {AccountMessages} from "../message.sol"; * @title AccountsStrategy */ interface IAccountsStrategy { - error InsufficientFundsForGas(uint32); + error InsufficientFundsForGas(uint32 endowId); error InvestFailed(IVault.VaultActionStatus); error RedeemFailed(IVault.VaultActionStatus); error RedeemAllFailed(IVault.VaultActionStatus); error UnexpectedResponse(IVault.VaultActionData); error UnexpectedCaller(IVault.VaultActionData, string, string); + error ZeroAmount(); struct NetworkInfo { uint256 chainId; diff --git a/contracts/core/index-fund/IIndexFund.sol b/contracts/core/index-fund/IIndexFund.sol index 01db8df99..d893b45e3 100644 --- a/contracts/core/index-fund/IIndexFund.sol +++ b/contracts/core/index-fund/IIndexFund.sol @@ -17,9 +17,15 @@ interface IIndexFund { event ActiveFundUpdated(uint256 fundId); event StateUpdated(); + /*//////////////////////////////////////////////// + ERRORS + */ //////////////////////////////////////////////// + error InvalidAddress(string param); + error InvalidToken(); + /*//////////////////////////////////////////////// ENDPOINTS - */ //////////////////////////////////////////////// + */ //////////////////////////////////////////////// struct StateResponse { uint256 activeFund; // index ID of the Active IndexFund diff --git a/contracts/core/index-fund/IndexFund.sol b/contracts/core/index-fund/IndexFund.sol index b24351af4..e3ae14b75 100644 --- a/contracts/core/index-fund/IndexFund.sol +++ b/contracts/core/index-fund/IndexFund.sol @@ -49,25 +49,23 @@ contract IndexFund is IIndexFund, Storage, OwnableUpgradeable, ReentrancyGuard, // active fund rotations can set by either a Time-based or Amoount-based // or neither (wherein both are == 0) - state.config.fundRotation = fundRotation; - if (fundingGoal == 0 || (fundRotation == 0 && fundingGoal > 0)) { - state.config.fundingGoal = fundingGoal; - } else { + if (fundingGoal > 0 && fundRotation > 0) { revert("Invalid Fund Rotation configuration"); } - if (Validator.addressChecker(registrarContract)) { - state.config = IndexFundStorage.Config({ - registrarContract: registrarContract, - fundRotation: fundRotation, - fundingGoal: fundingGoal - }); + if (!Validator.addressChecker(registrarContract)) { + revert InvalidAddress("registrarContract"); } + state.config.registrarContract = registrarContract; + state.config.fundRotation = fundRotation; + state.config.fundingGoal = fundingGoal; + state.activeFund = 0; state.nextFundId = 1; state.roundDonations = 0; state.nextRotationBlock = block.number + state.config.fundRotation; + emit Instantiated(registrarContract, fundRotation, fundingGoal); } @@ -83,19 +81,19 @@ contract IndexFund is IIndexFund, Storage, OwnableUpgradeable, ReentrancyGuard, uint256 fundRotation, uint256 fundingGoal ) external onlyOwner { - if (Validator.addressChecker(registrarContract)) { - state.config.registrarContract = registrarContract; + if (!Validator.addressChecker(registrarContract)) { + revert InvalidAddress("registrarContract"); } // active fund rotations can set by either a Time-based or Amoount-based // or neither (wherein both are == 0) - state.config.fundRotation = fundRotation; - if (fundingGoal == 0 || (fundRotation == 0 && fundingGoal > 0)) { - state.config.fundingGoal = fundingGoal; - } else { + if (fundingGoal > 0 && fundRotation > 0) { revert("Invalid Fund Rotation configuration"); } + state.config.registrarContract = registrarContract; + state.config.fundRotation = fundRotation; + state.config.fundingGoal = fundingGoal; emit ConfigUpdated(registrarContract, fundingGoal, fundRotation); } @@ -249,7 +247,10 @@ contract IndexFund is IIndexFund, Storage, OwnableUpgradeable, ReentrancyGuard, Validator.addressChecker(registrarConfig.accountsContract), "Accounts contract not configured in Registrar" ); - // Require that the incoming token is accpeted + if (token == address(0)) { + revert InvalidToken(); + } + // Require that the incoming token is accepted require(_tokenIsAccepted(token), "Unaccepted Token"); // tokens must be transfered from the sender to this contract diff --git a/contracts/core/vault/APVault_V1.sol b/contracts/core/vault/APVault_V1.sol index bb5baea17..5afef1682 100644 --- a/contracts/core/vault/APVault_V1.sol +++ b/contracts/core/vault/APVault_V1.sol @@ -404,7 +404,8 @@ contract APVault_V1 is IVault, ERC4626AP { //////////////////////////////////////////////////////////////*/ function _isOperator(address _operator) internal view override returns (bool) { - return IRegistrar(vaultConfig.registrar).getVaultOperatorApproved(_operator); + return + IRegistrar(vaultConfig.registrar).getVaultOperatorApproved(_operator) || _isSiblingVault(); } function _isApprovedRouter() internal view override returns (bool) { diff --git a/tasks/helpers/deploy/deployDummyERC20.ts b/tasks/helpers/deploy/deployDummyERC20.ts index 34d35d64b..9f3f1cc01 100644 --- a/tasks/helpers/deploy/deployDummyERC20.ts +++ b/tasks/helpers/deploy/deployDummyERC20.ts @@ -1,9 +1,6 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; -import {DummyERC20__factory, DummyERC20} from "typechain-types"; - -export async function mint(token: DummyERC20, to: string, amt: number) { - await token.mint(to, amt); -} +import {wait} from "test/utils"; +import {DummyERC20__factory} from "typechain-types"; export async function deployDummyERC20( deployer: SignerWithAddress, @@ -18,7 +15,7 @@ export async function deployDummyERC20( if (recipients && amounts) { for (var i in recipients) { - await mint(token, recipients[i], amounts[i]); + await wait(token.mint(recipients[i], amounts[i])); } } diff --git a/test/core/IndexFund.ts b/test/core/IndexFund.ts index e809a1107..3fc9af545 100644 --- a/test/core/IndexFund.ts +++ b/test/core/IndexFund.ts @@ -1,30 +1,31 @@ import {FakeContract, smock} from "@defi-wonderland/smock"; +import {impersonateAccount, setBalance, time} from "@nomicfoundation/hardhat-network-helpers"; +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect} from "chai"; import hre from "hardhat"; -import {BigNumber} from "ethers"; -import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; -import {impersonateAccount, setBalance, time} from "@nomicfoundation/hardhat-network-helpers"; +import {deployFacetAsProxy} from "test/core/accounts/utils/deployTestFacet"; +import {DEFAULT_CHARITY_ENDOWMENT, DEFAULT_REGISTRAR_CONFIG, wait} from "test/utils"; import { AccountsDepositWithdrawEndowments, AccountsDepositWithdrawEndowments__factory, - DummyERC20, - DummyERC20__factory, DummyWMATIC, DummyWMATIC__factory, + IERC20, + IERC20__factory, IndexFund, IndexFund__factory, - ITransparentUpgradeableProxy__factory, + ProxyContract, Registrar, Registrar__factory, TestFacetProxyContract, } from "typechain-types"; -import {DEFAULT_CHARITY_ENDOWMENT, DEFAULT_REGISTRAR_CONFIG} from "test/utils"; -import {getSigners} from "utils"; -import {deployFacetAsProxy} from "test/core/accounts/utils/deployTestFacet"; import {RegistrarStorage} from "typechain-types/contracts/core/registrar/Registrar"; +import {getSigners} from "utils"; describe("IndexFund", function () { - const {ethers, upgrades} = hre; + const {ethers} = hre; + + const MAX_ENDOWMENT_MEMBERS = 10; let owner: SignerWithAddress; let proxyAdmin: SignerWithAddress; @@ -32,30 +33,27 @@ describe("IndexFund", function () { let registrar: FakeContract; let wmatic: FakeContract; - let token: FakeContract; + let token: FakeContract; - let facet: AccountsDepositWithdrawEndowments; + let accountsDepositWithdrawEndowments: AccountsDepositWithdrawEndowments; let state: TestFacetProxyContract; + let facet: IndexFund; async function deployIndexFundAsProxy( + registrarContract = registrar.address, fundRotation: number = 0, // no block-based rotation fundingGoal: number = 10000 - ): Promise { - if (!registrar) { - registrar = await smock.fake(new Registrar__factory()); - } - + ): Promise { const IndexFundFactory = new IndexFund__factory(owner); const IndexFundImpl = await IndexFundFactory.deploy(); await IndexFundImpl.deployed(); const ProxyContract = await ethers.getContractFactory("ProxyContract"); const IndexFundInitData = IndexFundImpl.interface.encodeFunctionData("initialize", [ - registrar.address, + registrarContract, fundRotation, fundingGoal, ]); - const IndexFundProxy = await ProxyContract.deploy( IndexFundImpl.address, proxyAdmin.address, @@ -63,28 +61,7 @@ describe("IndexFund", function () { ); await IndexFundProxy.deployed(); - // registrar config has the accounts & index fund contract addresses set - const registrarConfig: RegistrarStorage.ConfigStruct = { - ...DEFAULT_REGISTRAR_CONFIG, - wMaticAddress: wmatic.address, - accountsContract: facet.address, - indexFundContract: IndexFundProxy.address, - treasury: owner.address, - splitToLiquid: {defaultSplit: 50, max: 100, min: 0}, - }; - registrar.queryConfig.returns(registrarConfig); - registrar.isTokenAccepted.whenCalledWith(token.address).returns(true); - - return IndexFund__factory.connect(IndexFundProxy.address, owner); - } - - async function upgradeProxy(signer: SignerWithAddress, proxy: string) { - const IndexFundFactory = new IndexFund__factory(owner); - const IndexFundImpl = await IndexFundFactory.deploy(); - await IndexFundImpl.deployed(); - - const IndexFundProxy = ITransparentUpgradeableProxy__factory.connect(proxy, signer); - IndexFundProxy.upgradeTo(IndexFundImpl.address); + return IndexFundProxy; } before(async function () { @@ -94,313 +71,305 @@ describe("IndexFund", function () { user = signers.apTeam1; registrar = await smock.fake(new Registrar__factory()); - token = await smock.fake(new DummyERC20__factory()); + token = await smock.fake(IERC20__factory.createInterface()); wmatic = await smock.fake(new DummyWMATIC__factory()); token.transferFrom.returns(true); token.transfer.returns(true); token.approve.returns(true); - const registrarConfig: RegistrarStorage.ConfigStruct = { - ...DEFAULT_REGISTRAR_CONFIG, - wMaticAddress: wmatic.address, - splitToLiquid: {defaultSplit: 50, max: 90, min: 10}, - treasury: owner.address, - }; - registrar.queryConfig.returns(registrarConfig); - registrar.isTokenAccepted.whenCalledWith(token.address).returns(true); - // setup the Accounts DepositWithdraw Endowments facet once let Facet = new AccountsDepositWithdrawEndowments__factory(owner); let facetImpl = await Facet.deploy(); state = await deployFacetAsProxy(hre, owner, proxyAdmin, facetImpl.address); - await state.setConfig({ - owner: owner.address, - version: "1", - networkName: "Polygon", - registrarContract: registrar.address, - nextAccountId: 1, - reentrancyGuardLocked: false, - }); - facet = AccountsDepositWithdrawEndowments__factory.connect(state.address, owner); + accountsDepositWithdrawEndowments = AccountsDepositWithdrawEndowments__factory.connect( + state.address, + owner + ); + let nextAccountId = 1; // setup the various Endowments for testing index funds once // #1 - A closed endowment for error checks - await state.setClosingEndowmentState(1, true, { - data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, - enumData: 0, - }); + await wait( + state.setClosingEndowmentState(nextAccountId, true, { + data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, + enumData: 0, + }) + ); // #2 - A non-closing endowment - await state.setClosingEndowmentState(2, false, { - data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, - enumData: 0, - }); + await wait( + state.setClosingEndowmentState(++nextAccountId, false, { + data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, + enumData: 0, + }) + ); // accepts token for deposits - await state.setTokenAccepted(2, token.address, true); + await wait(state.setTokenAccepted(nextAccountId, token.address, true)); // setup endowment with minimum needed for testing let endowment = DEFAULT_CHARITY_ENDOWMENT; - await state.setEndowmentDetails(2, endowment); + await wait(state.setEndowmentDetails(nextAccountId, endowment)); // #3 - A non-closing endowment - await state.setClosingEndowmentState(3, false, { - data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, - enumData: 0, - }); + await wait( + state.setClosingEndowmentState(++nextAccountId, false, { + data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, + enumData: 0, + }) + ); // accepts token & wmatic for deposits - await state.setTokenAccepted(3, token.address, true); - await state.setTokenAccepted(3, wmatic.address, true); + await wait(state.setTokenAccepted(nextAccountId, token.address, true)); + await wait(state.setTokenAccepted(nextAccountId, wmatic.address, true)); // setup endowment with minimum needed for testing - await state.setEndowmentDetails(3, endowment); + await wait(state.setEndowmentDetails(nextAccountId, endowment)); + + await wait( + state.setConfig({ + owner: owner.address, + version: "1", + networkName: "Polygon", + registrarContract: registrar.address, + nextAccountId: nextAccountId + 1, + reentrancyGuardLocked: false, + }) + ); }); - describe("Deploying the contract", function () { - let indexFund: IndexFund; + beforeEach(async () => { + const proxy = await deployIndexFundAsProxy(); + facet = IndexFund__factory.connect(proxy.address, owner); - beforeEach(async function () { - indexFund = await deployIndexFundAsProxy(); - }); + // registrar config has the accounts & index fund contract addresses set + const registrarConfig: RegistrarStorage.ConfigStruct = { + ...DEFAULT_REGISTRAR_CONFIG, + wMaticAddress: wmatic.address, + accountsContract: accountsDepositWithdrawEndowments.address, + indexFundContract: facet.address, + treasury: owner.address, + splitToLiquid: {defaultSplit: 50, max: 100, min: 0}, + }; + registrar.queryConfig.returns(registrarConfig); + registrar.isTokenAccepted.whenCalledWith(token.address).returns(true); + }); + describe("Deploying the contract", function () { it("Deploying the contract as an upgradable proxy", async function () { - expect(indexFund.address); - expect(await upgradeProxy(proxyAdmin, indexFund.address)).to.emit( - indexFund, - "IndexFundInstantiated" - ); - }); + const proxy = await deployIndexFundAsProxy(); + const facet = IndexFund__factory.connect(proxy.address, owner); - it("should have correct starting state values", async function () { - let state = await indexFund.queryState(); - // round donations == 0 - expect(state.roundDonations).to.equal(0); - // active fund ID == 0 - expect(state.activeFund).to.equal(0); + await expect(proxy.deployTransaction).to.emit(facet, "Instantiated"); }); it("reverts if an invalid fund rotation setup is passed", async function () { let rotation = 250; let goal = 5000; - expect(deployIndexFundAsProxy(rotation, goal)).to.be.revertedWith( - "Invalid Registrar address" - ); + + try { + await deployIndexFundAsProxy(registrar.address, rotation, goal); + throw new Error("Should not occur"); + } catch (error: any) { + expect(error.reason).to.contain( + "reverted with reason string 'Invalid Fund Rotation configuration" + ); + } }); - it("accepts rotation and goal as part of initialization", async function () { - let rotation = 0; - let goal = 5000; - indexFund = await deployIndexFundAsProxy(rotation, goal); - let config = await indexFund.queryConfig(); - expect(config.fundRotation).to.equal(rotation); - expect(config.fundingGoal).to.equal(goal); + it("reverts if registrar address passed is invalid", async function () { + try { + await deployIndexFundAsProxy(ethers.constants.AddressZero); + throw new Error("Should not occur"); + } catch (error: any) { + expect(error.reason).to.contain("reverted with custom error 'InvalidAddress"); + } }); - }); - describe("Updating the Config", async function () { - let indexFund: IndexFund; + it("should have correct starting state values", async function () { + let state = await facet.queryState(); + expect(state.roundDonations).to.equal(0); + expect(state.activeFund).to.equal(0); - beforeEach(async function () { - indexFund = await deployIndexFundAsProxy(); + let config = await facet.queryConfig(); + expect(config.fundRotation).to.equal(0); + expect(config.fundingGoal).to.equal(10000); }); + }); + describe("Updating the Config", function () { it("reverts when the message sender is not the owner", async function () { - expect(indexFund.connect(user).updateConfig(registrar.address, 200, 5000)).to.be.revertedWith( - "Unauthorized" + await expect(facet.connect(user).updateConfig(registrar.address, 0, 5000)).to.be.revertedWith( + "Ownable: caller is not the owner" ); }); it("reverts when both rotation-related arguments are non-zero", async function () { - expect(indexFund.updateConfig(registrar.address, 200, 5000)).to.be.revertedWith( + await expect(facet.updateConfig(registrar.address, 200, 5000)).to.be.revertedWith( "Invalid Fund Rotation configuration" ); }); it("reverts if registrar address passed is invalid", async function () { - expect(indexFund.updateConfig(ethers.constants.AddressZero, 200, 5000)).to.be.revertedWith( - "Invalid Registrar address" - ); + await expect( + facet.updateConfig(ethers.constants.AddressZero, 0, 5000) + ).to.be.revertedWithCustomError(facet, "InvalidAddress"); }); it("passes with valid sender and all correct inputs", async function () { // update config with all the correct bits - expect(await indexFund.updateConfig(registrar.address, 0, 5000)).to.emit( - indexFund, - "ConfigUpdated" - ); + await expect(facet.updateConfig(registrar.address, 0, 5000)).to.emit(facet, "ConfigUpdated"); // query the new config and check that updates applied correctly - let newConfig = await indexFund.queryConfig(); + let newConfig = await facet.queryConfig(); expect(newConfig.fundRotation).to.equal(0); expect(newConfig.fundingGoal).to.equal(5000); }); }); - describe("Creating a new Fund", async function () { - let indexFund: IndexFund; - - beforeEach(async function () { - indexFund = await deployIndexFundAsProxy(); - }); - + describe("Creating a new Fund", function () { it("reverts when the message sender is not the owner", async function () { - expect( - indexFund.connect(user).createIndexFund("Test Fund #1", "Test fund", [1, 2], false, 0, 0) - ).to.be.revertedWith("Unauthorized"); + await expect( + facet.connect(user).createIndexFund("Test Fund #1", "Test fund", [1, 2], false, 0, 0) + ).to.be.revertedWith("Ownable: caller is not the owner"); }); it("reverts when no endowment members are passed", async function () { - expect( - indexFund.createIndexFund("Test Fund #1", "Test fund", [], false, 0, 0) + await expect( + facet.createIndexFund("Test Fund #1", "Test fund", [], false, 0, 0) ).to.be.revertedWith("Fund must have one or more endowment members"); }); it("reverts when too many endowment members are passed (> fundMemberLimit)", async function () { - expect( - indexFund.createIndexFund("Test Fund #1", "Test fund", [1, 2, 3], false, 0, 0) + await expect( + facet.createIndexFund( + "Test Fund #1", + "Test fund", + [...Array(MAX_ENDOWMENT_MEMBERS + 1).keys()], + false, + 0, + 0 + ) ).to.be.revertedWith("Fund endowment members exceeds upper limit"); }); it("reverts when the split is greater than 100", async function () { - expect( - indexFund.createIndexFund("Test Fund #1", "Test fund", [1, 2], false, 0, 105) + await expect( + facet.createIndexFund("Test Fund #1", "Test fund", [1, 2], false, 105, 0) ).to.be.revertedWith("Invalid split: must be less or equal to 100"); }); it("reverts when a non-zero expiryTime is passed, less than or equal to current time", async function () { let currTime = await time.latest(); - expect( - indexFund.createIndexFund("Test Fund #1", "Test fund", [1, 2], false, 0, currTime) + await expect( + facet.createIndexFund("Test Fund #1", "Test fund", [1, 2], false, 0, currTime) ).to.be.revertedWith("Invalid expiry time"); }); it("passes when all inputs are correct", async function () { // create a new fund with two Endowment members - expect(await indexFund.createIndexFund("Test Fund #1", "Test fund", [1, 2], true, 0, 0)) - .to.emit(indexFund, "FundCreated") + await expect(facet.createIndexFund("Test Fund #1", "Test fund", [1, 2], true, 0, 0)) + .to.emit(facet, "FundCreated") .withArgs(1); - let activeFund = await indexFund.queryActiveFundDetails(); + let activeFund = await facet.queryActiveFundDetails(); expect(activeFund.id).to.equal(1); // check that the list of rotating funds has a length of 1 - let rotatingFunds = await indexFund.queryRotatingFunds(); + let rotatingFunds = await facet.queryRotatingFunds(); expect(rotatingFunds.length).to.equal(1); // create 1 expired fund let currTime = await time.latest(); - await indexFund.createIndexFund("Test Fund #2", "Test fund", [3], true, 50, currTime + 42069); - time.increase(42069); // move time forward so Fund #2 is @ expiry + await expect( + facet.createIndexFund("Test Fund #2", "Test fund", [3], true, 50, currTime + 42069) + ).to.not.be.reverted; + await time.increase(42069); // move time forward so Fund #2 is @ expiry // check that the list of rotating funds is now increased by 1 fund - let newRotatingFunds = await indexFund.queryRotatingFunds(); + let newRotatingFunds = await facet.queryRotatingFunds(); expect(newRotatingFunds.length).to.equal(rotatingFunds.length + 1); }); }); - describe("Updating an existing Fund's endowment members", async function () { - let indexFund: IndexFund; - - before(async function () { - indexFund = await deployIndexFundAsProxy(); + describe("Updating an existing Fund's endowment members", function () { + beforeEach(async function () { // create 2 funds (1 active and 1 expired) let currTime = await time.latest(); - await indexFund.createIndexFund("Test Fund #1", "Test fund", [2, 3], true, 50, 0); - await indexFund.createIndexFund( - "Test Fund #2", - "Test fund", - [3], - false, - 50, - currTime + 42069 + await wait(facet.createIndexFund("Test Fund #1", "Test fund", [2, 3], true, 50, 0)); + await wait( + facet.createIndexFund("Test Fund #2", "Test fund", [3], false, 50, currTime + 42069) ); - time.increase(42069); // move time forward so Fund #2 is @ expiry + await time.increase(42069); // move time forward so Fund #2 is @ expiry }); it("reverts when the message sender is not the owner", async function () { - expect(indexFund.connect(user).updateFundMembers(1, [1, 2], [])).to.be.revertedWith( - "Unauthorized" + await expect(facet.connect(user).updateFundMembers(1, [1, 2], [])).to.be.revertedWith( + "Ownable: caller is not the owner" ); }); it("reverts when no members are passed", async function () { - expect(indexFund.updateFundMembers(1, [], [])).to.be.revertedWith( - "Must pass at least one endowment member to add to the Fund" + await expect(facet.updateFundMembers(1, [], [])).to.be.revertedWith( + "Must pass at least one endowment member to add to or remove from the Fund" ); }); it("reverts when too many members are passed", async function () { - expect(indexFund.updateFundMembers(1, [1, 2, 3], [])).to.be.revertedWith( - "Fund endowment members exceeds upper limit" - ); + await expect( + facet.updateFundMembers(1, [...Array(MAX_ENDOWMENT_MEMBERS + 1).keys()], []) + ).to.be.revertedWith("Fund endowment members exceeds upper limit"); }); it("reverts when the fund is expired", async function () { - expect(indexFund.updateFundMembers(2, [1, 2], [])).to.be.revertedWith("Fund Expired"); + await expect(facet.updateFundMembers(2, [1, 2], [])).to.be.revertedWith("Fund Expired"); }); it("passes when the fund is not expired and member inputs are valid", async function () { - expect(await indexFund.updateFundMembers(1, [], [3])) - .to.emit(indexFund, "MembersUpdated") + await expect(facet.updateFundMembers(1, [], [3])) + .to.emit(facet, "MembersUpdated") .withArgs(1, [2]); - expect(await indexFund.updateFundMembers(1, [1, 2], [3])) - .to.emit(indexFund, "MembersUpdated") - .withArgs(1, [1, 2]); + await expect(facet.updateFundMembers(1, [1, 2], [3])) + .to.emit(facet, "MembersUpdated") + .withArgs(1, [2, 1]); }); }); - describe("Removing an existing Fund", async function () { - let indexFund: IndexFund; - - before(async function () { - indexFund = await deployIndexFundAsProxy(); + describe("Removing an existing Fund", function () { + beforeEach(async function () { // create 2 funds (1 active and 1 expired) let currTime = await time.latest(); - await indexFund.createIndexFund("Test Fund #1", "Test fund", [2, 3], true, 50, 0); - await indexFund.createIndexFund( - "Test Fund #2", - "Test fund", - [3], - false, - 50, - currTime + 42069 + await wait(facet.createIndexFund("Test Fund #1", "Test fund", [2, 3], true, 50, 0)); + await wait( + facet.createIndexFund("Test Fund #2", "Test fund", [3], false, 50, currTime + 42069) ); - time.increase(42069); // move time forward so Fund #2 is @ expiry + await time.increase(42069); // move time forward so Fund #2 is @ expiry }); it("reverts when the message sender is not the owner", async function () { - expect(indexFund.connect(user).removeIndexFund(1)).to.be.revertedWith("Unauthorized"); + await expect(facet.connect(user).removeIndexFund(1)).to.be.revertedWith( + "Ownable: caller is not the owner" + ); }); it("reverts when the fund is already expired", async function () { - expect(indexFund.removeIndexFund(2)).to.be.revertedWith("Fund Expired"); + await expect(facet.removeIndexFund(2)).to.be.revertedWith("Fund Expired"); }); it("passes when all inputs are correct", async function () { - expect(await indexFund.removeIndexFund(1)) - .to.emit(indexFund, "FundRemoved") - .withArgs(1); + await expect(facet.removeIndexFund(1)).to.emit(facet, "FundRemoved").withArgs(1); }); }); - describe("Removing an endowment from all involved Funds", async function () { - let indexFund: IndexFund; - - before(async function () { - indexFund = await deployIndexFundAsProxy(); + describe("Removing an endowment from all involved Funds", function () { + beforeEach(async function () { // create 2 funds (1 active and 1 expired) let currTime = await time.latest(); - await indexFund.createIndexFund("Test Fund #1", "Test fund", [2, 3], true, 50, 0); - await indexFund.createIndexFund( - "Test Fund #2", - "Test fund", - [3], - false, - 50, - currTime + 42069 + await wait(facet.createIndexFund("Test Fund #1", "Test fund", [2, 3], true, 50, 0)); + await wait( + facet.createIndexFund("Test Fund #2", "Test fund", [3], false, 50, currTime + 42069) ); - time.increase(42069); // move time forward so Fund #2 is @ expiry + await time.increase(42069); // move time forward so Fund #2 is @ expiry }); it("reverts when the message sender is not the accounts contract", async function () { - expect(indexFund.removeMember(1)).to.be.revertedWith("Unauthorized"); + await expect(facet.removeMember(1)).to.be.revertedWith("Unauthorized"); }); it("passes with correct sender", async function () { @@ -411,162 +380,140 @@ describe("IndexFund", function () { const acctSigner = await ethers.getSigner(regConfig.accountsContract); // Endowment #1 should be invloved with two funds - let funds = await indexFund.queryInvolvedFunds(3); + let funds = await facet.queryInvolvedFunds(3); expect(funds.length).to.equal(2); // remove Endowment #1 from all funds - expect(await indexFund.connect(acctSigner).removeMember(3)).to.emit( - indexFund, - "MemberRemoved" - ); + await expect(facet.connect(acctSigner).removeMember(3)).to.emit(facet, "MemberRemoved"); // Endowment #1 should not be invloved with any funds now - funds = await indexFund.queryInvolvedFunds(3); + funds = await facet.queryInvolvedFunds(3); expect(funds.length).to.equal(0); }); }); - describe("When a user deposits tokens to a Fund", async function () { - let indexFund: IndexFund; - - before(async function () { - indexFund = await deployIndexFundAsProxy(); + describe("When a user deposits tokens to a Fund", function () { + beforeEach(async function () { + let currTime = await time.latest(); // create 1 active, non-rotating fund - await indexFund.createIndexFund("Test Fund #1", "Test fund", [2, 3], false, 50, 0); + await wait(facet.createIndexFund("Test Fund #1", "Test fund", [2, 3], false, 50, 0)); // create 1 expired, non-rotating fund - let currTime = await time.latest(); - await indexFund.createIndexFund( - "Test Fund #2", - "Test fund", - [2, 3], - false, - 50, - currTime + 42069 + await wait( + facet.createIndexFund("Test Fund #2", "Test fund", [2, 3], false, 50, currTime + 42069) ); - time.increase(42069); // move time forward so Fund #2 is @ expiry - registrar.isTokenAccepted.whenCalledWith(token.address).returns(true); + await time.increase(42069); // move time forward so Fund #2 is @ expiry }); it("reverts when amount is zero", async function () { - expect(indexFund.depositERC20(1, token.address, 0)).to.be.revertedWith( + await expect(facet.depositERC20(1, token.address, 0)).to.be.revertedWith( "Amount to donate must be greater than zero" ); }); it("reverts if the token isn't accepted", async function () { registrar.isTokenAccepted.whenCalledWith(token.address).returns(false); - await expect(indexFund.depositERC20(1, token.address, 100)).to.be.revertedWith( + await expect(facet.depositERC20(1, token.address, 100)).to.be.revertedWith( "Unaccepted Token" ); - registrar.isTokenAccepted.whenCalledWith(token.address).returns(true); }); it("reverts when fund passed is expired", async function () { - expect(indexFund.depositERC20(2, token.address, 100)).to.be.revertedWith("Expired Fund"); + await expect(facet.depositERC20(2, token.address, 100)).to.be.revertedWith("Expired Fund"); }); it("reverts when invalid token is passed", async function () { - expect(indexFund.depositERC20(1, ethers.constants.AddressZero, 100)).to.be.revertedWith( - "Invalid token" - ); + await expect( + facet.depositERC20(1, ethers.constants.AddressZero, 100) + ).to.be.revertedWithCustomError(facet, "InvalidToken"); }); it("reverts when amount donated, on a per endowment-basis for a fund, would be < min units", async function () { - expect(indexFund.depositERC20(1, token.address, 100)).to.be.revertedWith( + await expect(facet.depositERC20(1, token.address, 100)).to.be.revertedWith( "Amount must be enough to cover the minimum units per endowment for all members of a Fund" ); }); it("reverts when target fund is expired", async function () { - expect(indexFund.depositERC20(2, token.address, 100)).to.be.revertedWith("Fund expired"); + await expect(facet.depositERC20(2, token.address, 100)).to.be.revertedWith("Expired Fund"); }); it("reverts when `0` Fund ID is passed with no rotating funds(empty)", async function () { // should fail with no rotating funds set - expect(indexFund.depositERC20(0, token.address, 100)).to.be.revertedWith( - "Must have rotating funds active to pass a Fund ID of 0" + await expect(facet.depositERC20(0, token.address, 100)).to.be.revertedWith( + "No rotating funds" ); }); it("reverts when `0` Fund ID is passed with no un-expired rotating funds(0 after cleanup)", async function () { // create 1 expired, rotating fund let currTime = await time.latest(); - expect( - await indexFund.createIndexFund( - "Test Fund #3", - "Test fund", - [2, 3], - true, - 50, - currTime + 42069 - ) + await expect( + facet.createIndexFund("Test Fund #3", "Test fund", [2, 3], true, 50, currTime + 42069) ) - .to.emit(indexFund, "FundCreated") + .to.emit(facet, "FundCreated") .withArgs(3); - time.increase(42069); // move time forward so Fund #3 is @ expiry + await time.increase(42069); // move time forward so Fund #3 is @ expiry // check that there is an active fund set - let activeFund = await indexFund.queryActiveFundDetails(); + let activeFund = await facet.queryActiveFundDetails(); expect(activeFund.id).to.equal(3); // should fail when prep clean up process removes the expired fund, leaving 0 funds available - expect(indexFund.depositERC20(0, token.address, 500)).to.be.revertedWith( - "Must have rotating funds active to pass a Fund ID of 0" + await expect(facet.depositERC20(0, token.address, 500)).to.be.revertedWith( + "No rotating funds" ); }); it("passes for a specific fund, amount > min & token is valid", async function () { // create 1 active, rotating fund - expect(await indexFund.createIndexFund("Test Fund #4", "Test fund", [2, 3], true, 50, 0)) - .to.emit(indexFund, "FundCreated") - .withArgs(4); + const fundId = 3; + await expect(facet.createIndexFund("Test Fund #3", "Test fund", [2, 3], true, 50, 0)) + .to.emit(facet, "FundCreated") + .withArgs(fundId); - expect( - await indexFund.depositERC20(4, token.address, 500, { - gasPrice: 100000, - gasLimit: 10000000, - }) - ) - .to.emit(indexFund, "DonationProcessed") - .withArgs(4); + await expect(facet.depositERC20(fundId, token.address, 500)) + .to.emit(facet, "DonationProcessed") + .withArgs(fundId); }); it("passes for an active fund donation(amount-based rotation), amount > min & token is valid", async function () { + // create an active, rotating fund for full rotation testing + const currActiveFund = 3; + await expect(facet.createIndexFund("Test Fund #3", "Test fund", [2], true, 100, 0)) + .to.emit(facet, "FundCreated") + .withArgs(currActiveFund); + // create 1 more active, rotating fund for full rotation testing - expect(await indexFund.createIndexFund("Test Fund #5", "Test fund", [2], true, 100, 0)) - .to.emit(indexFund, "FundCreated") - .withArgs(5); + const nextActiveFund = 4; + await expect(facet.createIndexFund("Test Fund #4", "Test fund", [2], true, 100, 0)) + .to.emit(facet, "FundCreated") + .withArgs(nextActiveFund); - let ifState = await indexFund.queryState(); - expect(ifState.activeFund).to.equal(4); + let ifState = await facet.queryState(); + expect(ifState.activeFund).to.equal(currActiveFund); expect(ifState.roundDonations).to.equal(0); - let ifRotating = await indexFund.queryRotatingFunds(); + let ifRotating = await facet.queryRotatingFunds(); expect(ifRotating.length).to.equal(2); // deposit: should fill whole active fund goal and rotate to next fund for final 1000 - expect( - await indexFund.depositERC20(0, token.address, 11000, { - gasPrice: 100000, - gasLimit: 10000000, - }) - ) - .to.emit(indexFund, "DonationProcessed") + await expect(facet.depositERC20(0, token.address, 11000)) + .to.emit(facet, "DonationProcessed") .withArgs(4); // check all donation metrics reflect expected - ifState = await indexFund.queryState(); - expect(ifState.activeFund).to.equal(5); + ifState = await facet.queryState(); + expect(ifState.activeFund).to.equal(nextActiveFund); expect(ifState.roundDonations).to.equal(1000); // test with a LARGER donation amount for gas-usage and rotation stress-tests - expect( - await indexFund.depositERC20(0, token.address, 1000000, { + await expect( + facet.depositERC20(0, token.address, 1000000, { gasPrice: 100000, gasLimit: 10000000, }) ) - .to.emit(indexFund, "DonationProcessed") - .withArgs(4); + .to.emit(facet, "DonationProcessed") + .withArgs(nextActiveFund); }); }); }); diff --git a/test/core/accounts/AccountsAllowance.ts b/test/core/accounts/AccountsAllowance.ts index 38e90a5da..e1b965915 100644 --- a/test/core/accounts/AccountsAllowance.ts +++ b/test/core/accounts/AccountsAllowance.ts @@ -2,12 +2,12 @@ import {FakeContract, smock} from "@defi-wonderland/smock"; import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect} from "chai"; import hre from "hardhat"; -import {DEFAULT_CHARITY_ENDOWMENT} from "test/utils"; +import {DEFAULT_CHARITY_ENDOWMENT, wait} from "test/utils"; import { AccountsAllowance, AccountsAllowance__factory, - DummyERC20, - DummyERC20__factory, + IERC20, + IERC20__factory, TestFacetProxyContract, } from "typechain-types"; import {genWallet, getSigners} from "utils"; @@ -20,7 +20,7 @@ describe("AccountsAllowance", function () { let proxyAdmin: SignerWithAddress; let user: SignerWithAddress; - let tokenFake: FakeContract; + let tokenFake: FakeContract; before(async function () { const signers = await getSigners(hre); @@ -30,7 +30,7 @@ describe("AccountsAllowance", function () { }); beforeEach(async () => { - tokenFake = await smock.fake(new DummyERC20__factory()); + tokenFake = await smock.fake(IERC20__factory.createInterface()); }); describe("Test cases for `manageAllowances`", async function () { @@ -43,28 +43,32 @@ describe("AccountsAllowance", function () { state = await deployFacetAsProxy(hre, owner, proxyAdmin, facetImpl.address); facet = AccountsAllowance__factory.connect(state.address, owner); // set a non-closing endowment up for testing with (#42) - await state.setClosingEndowmentState(42, false, { - data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, - enumData: 0, - }); + await wait( + state.setClosingEndowmentState(42, false, { + data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, + enumData: 0, + }) + ); // add an accepted token to endowment #42 - await state.setTokenAccepted(42, tokenFake.address, true); + await wait(state.setTokenAccepted(42, tokenFake.address, true)); // set a starting balance for Endowment: 100 qty of tokens in liquid - await state.setEndowmentTokenBalance(42, tokenFake.address, 0, 100); + await wait(state.setEndowmentTokenBalance(42, tokenFake.address, 0, 100)); // setup endowment 42 with minimum needed for testing // Allowlists Beneficiaries set for a user let endowment = DEFAULT_CHARITY_ENDOWMENT; endowment.owner = owner.address; endowment.allowlistedBeneficiaries = [user.address]; endowment.maturityAllowlist = [user.address]; - await state.setEndowmentDetails(42, endowment); + await wait(state.setEndowmentDetails(42, endowment)); }); it("reverts when the endowment is closed", async function () { - await state.setClosingEndowmentState(42, true, { - data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, - enumData: 0, - }); + await wait( + state.setClosingEndowmentState(42, true, { + data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, + enumData: 0, + }) + ); await expect( facet.manageAllowances(42, user.address, tokenFake.address, 10) ).to.be.revertedWith("Endowment is closed"); @@ -95,7 +99,7 @@ describe("AccountsAllowance", function () { it("reverts when there are no adjustments needed (ie. proposed amount == spender balance amount)", async function () { // set allowance for user to 10 tokens of total 10 tokens outstanding - await state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10); + await wait(state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10)); await expect( facet.manageAllowances(42, user.address, tokenFake.address, 10) @@ -123,7 +127,7 @@ describe("AccountsAllowance", function () { it("passes when try to decrease an existing spender's allowance", async function () { // now we allocate some token allowance to the user address to spend from - await state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10); + await wait(state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10)); // set a lower total token allowance for the user, returning the delta to liquid balance expect(await facet.manageAllowances(42, user.address, tokenFake.address, 3)) @@ -149,14 +153,16 @@ describe("AccountsAllowance", function () { state = await deployFacetAsProxy(hre, owner, proxyAdmin, facetImpl.address); facet = AccountsAllowance__factory.connect(state.address, owner); // set a non-closing endowment up for testing with (#42) - await state.setClosingEndowmentState(42, false, { - data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, - enumData: 0, - }); + await wait( + state.setClosingEndowmentState(42, false, { + data: {endowId: 0, fundId: 0, addr: ethers.constants.AddressZero}, + enumData: 0, + }) + ); // add an accepted token to endowment #42 - await state.setTokenAccepted(42, tokenFake.address, true); + await wait(state.setTokenAccepted(42, tokenFake.address, true)); // set a starting balance for Endowment: 100 qty of tokens in liquid - await state.setEndowmentTokenBalance(42, tokenFake.address, 0, 100); + await wait(state.setEndowmentTokenBalance(42, tokenFake.address, 0, 100)); }); it("reverts when try to spend token that is invalid(zero address) or dne in allowances", async function () { @@ -173,7 +179,7 @@ describe("AccountsAllowance", function () { it("reverts when try to spend zero amount of allowance", async function () { // now we allocate some token allowance to the user address to spend from - await state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10); + await wait(state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10)); // try to spend zero allowance await expect(facet.spendAllowance(42, tokenFake.address, 0, user.address)).to.be.revertedWith( @@ -183,7 +189,7 @@ describe("AccountsAllowance", function () { it("reverts when try to spend more allowance than is available for token", async function () { // now we allocate some token allowance to the user address to spend from - await state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10); + await wait(state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10)); // try to spend more allowance than user was allocated await expect( @@ -198,7 +204,7 @@ describe("AccountsAllowance", function () { it("passes when spend less than or equal to the allowance available for token", async function () { // now we allocate some token allowance to the user address to spend from - await state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10); + await wait(state.setTokenAllowance(42, user.address, tokenFake.address, 10, 10)); // mint tokens so that the contract can transfer them to recipient tokenFake.transfer.returns(true); diff --git a/test/core/accounts/AccountsCreateEndowment.ts b/test/core/accounts/AccountsCreateEndowment.ts index 5b61b75af..d365856ed 100644 --- a/test/core/accounts/AccountsCreateEndowment.ts +++ b/test/core/accounts/AccountsCreateEndowment.ts @@ -3,7 +3,7 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect, use} from "chai"; import {BigNumber} from "ethers"; import hre from "hardhat"; -import {DEFAULT_REGISTRAR_CONFIG} from "test/utils"; +import {DEFAULT_REGISTRAR_CONFIG, wait} from "test/utils"; import { AccountsCreateEndowment, AccountsCreateEndowment__factory, @@ -146,14 +146,16 @@ describe("AccountsCreateEndowment", function () { let facetImpl = await Facet.deploy(); state = await deployFacetAsProxy(hre, owner, proxyAdmin, facetImpl.address); - await state.setConfig({ - owner: owner.address, - version: "1", - networkName: "Polygon", - registrarContract: registrarFake.address, - nextAccountId: expectedNextAccountId, - reentrancyGuardLocked: false, - }); + await wait( + state.setConfig({ + owner: owner.address, + version: "1", + networkName: "Polygon", + registrarContract: registrarFake.address, + nextAccountId: expectedNextAccountId, + reentrancyGuardLocked: false, + }) + ); facet = AccountsCreateEndowment__factory.connect(state.address, owner); }); @@ -314,18 +316,11 @@ describe("AccountsCreateEndowment", function () { it("should create a normal endowment if the caller is authorized and input parameters are valid", async () => { const request = {...createEndowmentRequest}; - const tx = await facet.connect(charityApplications).createEndowment(request); - const createEndowmentReceipt = await tx.wait(); - - // Get the endowment ID from the event emitted in the transaction receipt - const event = createEndowmentReceipt.events?.find((e) => e.event === "EndowmentCreated"); - let endowmentId = event?.args?.endowId ? BigNumber.from(event.args.endowId) : undefined; - - // verify endowment was created by checking the emitted event's parameter - expect(endowmentId).to.exist; - endowmentId = endowmentId!; + await expect(facet.connect(charityApplications).createEndowment(request)) + .to.emit(facet, "EndowmentCreated") + .withArgs(expectedNextAccountId, request.endowType); - const result = await state.getEndowmentDetails(endowmentId); + const result = await state.getEndowmentDetails(expectedNextAccountId); expect(result.allowlistedBeneficiaries).to.have.same.members(request.allowlistedBeneficiaries); expect(result.allowlistedContributors).to.have.same.members(request.allowlistedContributors); diff --git a/test/core/accounts/AccountsDeployContract.ts b/test/core/accounts/AccountsDeployContract.ts index 50d1e23b9..84089a9ef 100644 --- a/test/core/accounts/AccountsDeployContract.ts +++ b/test/core/accounts/AccountsDeployContract.ts @@ -3,7 +3,7 @@ import {impersonateAccount, setBalance} from "@nomicfoundation/hardhat-network-h import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect, use} from "chai"; import hre from "hardhat"; -import {DEFAULT_REGISTRAR_CONFIG} from "test/utils"; +import {DEFAULT_REGISTRAR_CONFIG, wait} from "test/utils"; import { AccountsDeployContract, AccountsDeployContract__factory, @@ -66,14 +66,16 @@ describe("AccountsDeployContract", function () { const facetImpl = await Facet.deploy(); state = await deployFacetAsProxy(hre, accOwner, proxyAdmin, facetImpl.address); - await state.setConfig({ - owner: accOwner.address, - version: "1", - networkName: "Polygon", - registrarContract: registrarFake.address, - nextAccountId: 1, - reentrancyGuardLocked: false, - }); + await wait( + state.setConfig({ + owner: accOwner.address, + version: "1", + networkName: "Polygon", + registrarContract: registrarFake.address, + nextAccountId: 1, + reentrancyGuardLocked: false, + }) + ); facet = AccountsDeployContract__factory.connect(state.address, accOwner); }); diff --git a/test/core/accounts/AccountsDepositWithdrawEndowments.ts b/test/core/accounts/AccountsDepositWithdrawEndowments.ts index aeb3816bd..a69d943ec 100644 --- a/test/core/accounts/AccountsDepositWithdrawEndowments.ts +++ b/test/core/accounts/AccountsDepositWithdrawEndowments.ts @@ -3,7 +3,7 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect, use} from "chai"; import {BigNumber} from "ethers"; import hre from "hardhat"; -import {DEFAULT_CHARITY_ENDOWMENT, DEFAULT_REGISTRAR_CONFIG} from "test/utils"; +import {DEFAULT_CHARITY_ENDOWMENT, DEFAULT_REGISTRAR_CONFIG, wait} from "test/utils"; import { AccountsDepositWithdrawEndowments, AccountsDepositWithdrawEndowments__factory, @@ -11,8 +11,8 @@ import { DonationMatchCharity, DonationMatchCharity__factory, DonationMatch__factory, - DummyERC20, - DummyERC20__factory, + IERC20, + IERC20__factory, DummyWMATIC, DummyWMATIC__factory, IAccountsDepositWithdrawEndowments, @@ -62,7 +62,7 @@ describe("AccountsDepositWithdrawEndowments", function () { let donationMatchCharity: FakeContract; let registrarFake: FakeContract; let wmaticFake: FakeContract; - let tokenFake: FakeContract; + let tokenFake: FakeContract; before(async function () { const signers = await getSigners(hre); @@ -105,7 +105,7 @@ describe("AccountsDepositWithdrawEndowments", function () { registrarFake = await smock.fake(new Registrar__factory()); - tokenFake = await smock.fake(new DummyERC20__factory()); + tokenFake = await smock.fake(IERC20__factory.createInterface()); wmaticFake = await smock.fake(new DummyWMATIC__factory()); @@ -123,17 +123,19 @@ describe("AccountsDepositWithdrawEndowments", function () { registrarFake.queryConfig.returns(registrarConfig); registrarFake.isTokenAccepted.whenCalledWith(tokenFake.address).returns(true); - await state.setEndowmentDetails(charityId, charity); - await state.setEndowmentDetails(normalEndowId, normalEndow); - - await state.setConfig({ - owner: accOwner.address, - version: "1", - networkName: "Polygon", - registrarContract: registrarFake.address, - nextAccountId: 3, // 2 endows already added - reentrancyGuardLocked: false, - }); + await wait(state.setEndowmentDetails(charityId, charity)); + await wait(state.setEndowmentDetails(normalEndowId, normalEndow)); + + await wait( + state.setConfig({ + owner: accOwner.address, + version: "1", + networkName: "Polygon", + registrarContract: registrarFake.address, + nextAccountId: 3, // 2 endows already added + reentrancyGuardLocked: false, + }) + ); }); describe("upon depositMatic", async function () { @@ -141,15 +143,17 @@ describe("AccountsDepositWithdrawEndowments", function () { it("reverts if the deposit value is 0 (zero)", async () => { await expect(facet.depositMatic(depositToCharity, {value: 0})).to.be.revertedWith( - "Invalid Amount" + "Amount must be greater than zero" ); }); it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(charityId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(charityId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect(facet.depositMatic(depositToCharity, {value})).to.be.revertedWith( "Endowment is closed" ); @@ -173,10 +177,12 @@ describe("AccountsDepositWithdrawEndowments", function () { }); it("reverts if the deposit fee transfer fails", async () => { - await state.setEndowmentDetails(charityId, { - ...charity, - depositFee: {payoutAddress: genWallet().address, bps: 5}, - }); + await wait( + state.setEndowmentDetails(charityId, { + ...charity, + depositFee: {payoutAddress: genWallet().address, bps: 5}, + }) + ); wmaticFake.transfer.returns(false); @@ -245,7 +251,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(charityId, charityBps); + await wait(state.setEndowmentDetails(charityId, charityBps)); wmaticFake.transfer.returns(true); await expect( @@ -344,7 +350,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToCharity.id, charityBps); + await wait(state.setEndowmentDetails(depositToCharity.id, charityBps)); wmaticFake.transfer.returns(true); const curConfig = await registrarFake.queryConfig(); @@ -426,7 +432,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...normalEndow, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(normalEndowId, normalEndowBps); + await wait(state.setEndowmentDetails(normalEndowId, normalEndowBps)); wmaticFake.transfer.returns(true); await expect( @@ -483,10 +489,12 @@ describe("AccountsDepositWithdrawEndowments", function () { }); it("matches the donation", async () => { - await state.setEndowmentDetails(normalEndowId, { - ...normalEndow, - donationMatchContract: donationMatch.address, - }); + await wait( + state.setEndowmentDetails(normalEndowId, { + ...normalEndow, + donationMatchContract: donationMatch.address, + }) + ); const expectedLockedAmt = BigNumber.from(6000); const expectedLiquidAmt = BigNumber.from(4000); @@ -524,7 +532,7 @@ describe("AccountsDepositWithdrawEndowments", function () { donationMatchContract: donationMatch.address, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps); + await wait(state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps)); wmaticFake.transfer.returns(true); const expectedLockedAmt = BigNumber.from(5994); @@ -601,7 +609,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(charityId, charityBps); + await wait(state.setEndowmentDetails(charityId, charityBps)); wmaticFake.transfer.returns(true); await expect( @@ -700,7 +708,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToCharity.id, charityBps); + await wait(state.setEndowmentDetails(depositToCharity.id, charityBps)); wmaticFake.transfer.returns(true); const curConfig = await registrarFake.queryConfig(); @@ -782,7 +790,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...normalEndow, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(normalEndowId, normalEndowBps); + await wait(state.setEndowmentDetails(normalEndowId, normalEndowBps)); wmaticFake.transfer.returns(true); await expect( @@ -839,10 +847,12 @@ describe("AccountsDepositWithdrawEndowments", function () { }); it("matches the donation", async () => { - await state.setEndowmentDetails(normalEndowId, { - ...normalEndow, - donationMatchContract: donationMatch.address, - }); + await wait( + state.setEndowmentDetails(normalEndowId, { + ...normalEndow, + donationMatchContract: donationMatch.address, + }) + ); const expectedLockedAmt = BigNumber.from(6000); const expectedLiquidAmt = BigNumber.from(4000); @@ -880,7 +890,7 @@ describe("AccountsDepositWithdrawEndowments", function () { donationMatchContract: donationMatch.address, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps); + await wait(state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps)); wmaticFake.transfer.returns(true); const expectedLockedAmt = BigNumber.from(5994); @@ -929,10 +939,12 @@ describe("AccountsDepositWithdrawEndowments", function () { }); it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(charityId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(charityId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect( facet.depositERC20(depositToCharity, tokenFake.address, depositAmt) ).to.be.revertedWith("Endowment is closed"); @@ -966,10 +978,12 @@ describe("AccountsDepositWithdrawEndowments", function () { }); it("reverts if the deposit fee transfer fails", async () => { - await state.setEndowmentDetails(charityId, { - ...charity, - depositFee: {payoutAddress: genWallet().address, bps: 5}, - }); + await wait( + state.setEndowmentDetails(charityId, { + ...charity, + depositFee: {payoutAddress: genWallet().address, bps: 5}, + }) + ); tokenFake.transfer.returns(false); @@ -1031,7 +1045,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(charityId, charityBps); + await wait(state.setEndowmentDetails(charityId, charityBps)); await expect( facet.connect(indexFund).depositERC20( @@ -1121,7 +1135,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToCharity.id, charityBps); + await wait(state.setEndowmentDetails(depositToCharity.id, charityBps)); const curConfig = await registrarFake.queryConfig(); const regConfig: RegistrarStorage.ConfigStruct = { @@ -1198,7 +1212,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...normalEndow, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(normalEndowId, normalEndowBps); + await wait(state.setEndowmentDetails(normalEndowId, normalEndowBps)); await expect( facet.connect(indexFund).depositERC20( @@ -1257,10 +1271,12 @@ describe("AccountsDepositWithdrawEndowments", function () { }); it("matches the donation", async () => { - await state.setEndowmentDetails(normalEndowId, { - ...normalEndow, - donationMatchContract: donationMatch.address, - }); + await wait( + state.setEndowmentDetails(normalEndowId, { + ...normalEndow, + donationMatchContract: donationMatch.address, + }) + ); const expectedLockedAmt = BigNumber.from(6000); const expectedLiquidAmt = BigNumber.from(4000); @@ -1301,7 +1317,7 @@ describe("AccountsDepositWithdrawEndowments", function () { donationMatchContract: donationMatch.address, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps); + await wait(state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps)); const expectedLockedAmt = BigNumber.from(5994); const expectedLiquidAmt = BigNumber.from(3996); @@ -1380,7 +1396,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(charityId, charityBps); + await wait(state.setEndowmentDetails(charityId, charityBps)); await expect( facet.depositERC20( @@ -1466,7 +1482,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToCharity.id, charityBps); + await wait(state.setEndowmentDetails(depositToCharity.id, charityBps)); const curConfig = await registrarFake.queryConfig(); const regConfig: RegistrarStorage.ConfigStruct = { @@ -1541,7 +1557,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...normalEndow, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(normalEndowId, normalEndowBps); + await wait(state.setEndowmentDetails(normalEndowId, normalEndowBps)); await expect( facet.depositERC20( @@ -1596,10 +1612,12 @@ describe("AccountsDepositWithdrawEndowments", function () { }); it("matches the donation", async () => { - await state.setEndowmentDetails(normalEndowId, { - ...normalEndow, - donationMatchContract: donationMatch.address, - }); + await wait( + state.setEndowmentDetails(normalEndowId, { + ...normalEndow, + donationMatchContract: donationMatch.address, + }) + ); const expectedLockedAmt = BigNumber.from(6000); const expectedLiquidAmt = BigNumber.from(4000); @@ -1636,7 +1654,7 @@ describe("AccountsDepositWithdrawEndowments", function () { donationMatchContract: donationMatch.address, depositFee: {payoutAddress: genWallet().address, bps: 10}, }; - await state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps); + await wait(state.setEndowmentDetails(depositToNormalEndow.id, normalEndowBps)); const expectedLockedAmt = BigNumber.from(5994); const expectedLiquidAmt = BigNumber.from(3996); @@ -1696,15 +1714,17 @@ describe("AccountsDepositWithdrawEndowments", function () { }); beforeEach(async () => { - await state.setEndowmentTokenBalance(charityId, tokenFake.address, lockBal, liqBal); - await state.setEndowmentTokenBalance(normalEndowId, tokenFake.address, lockBal, liqBal); + await wait(state.setEndowmentTokenBalance(charityId, tokenFake.address, lockBal, liqBal)); + await wait(state.setEndowmentTokenBalance(normalEndowId, tokenFake.address, lockBal, liqBal)); }); it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(charityId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(charityId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect( facet.withdraw(charityId, VaultType.LIQUID, genWallet().address, 0, [ {addr: tokenFake.address, amnt: 1}, @@ -1759,7 +1779,7 @@ describe("AccountsDepositWithdrawEndowments", function () { maturityTime: 1, maturityAllowlist: [genWallet().address], }; - await state.setEndowmentDetails(charityId, matureCharity); + await wait(state.setEndowmentDetails(charityId, matureCharity)); await expect( facet.withdraw(charityId, VaultType.LIQUID, genWallet().address, 0, [ @@ -1830,10 +1850,12 @@ describe("AccountsDepositWithdrawEndowments", function () { it("reverts if the transfer of all tokens to the ultimate beneficiary endowment fails", async () => { const beneficiaryId = normalEndowId; - await state.setClosingEndowmentState(beneficiaryId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(beneficiaryId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect( facet.withdraw(charityId, VaultType.LIQUID, ethers.constants.AddressZero, beneficiaryId, [ @@ -1883,10 +1905,10 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, allowlistedBeneficiaries: [indexFund.address], }; - await state.setEndowmentDetails(charityId, charityWithAllowlist); + await wait(state.setEndowmentDetails(charityId, charityWithAllowlist)); - const tokenFake2 = await smock.fake(new DummyERC20__factory()); - await state.setEndowmentTokenBalance(charityId, tokenFake2.address, lockBal, liqBal); + const tokenFake2 = await smock.fake(IERC20__factory.createInterface()); + await wait(state.setEndowmentTokenBalance(charityId, tokenFake2.address, lockBal, liqBal)); tokenFake2.transfer.returns(true); @@ -1966,7 +1988,7 @@ describe("AccountsDepositWithdrawEndowments", function () { maturityTime: 1, maturityAllowlist: [indexFund.address], }; - await state.setEndowmentDetails(charityId, matureCharity); + await wait(state.setEndowmentDetails(charityId, matureCharity)); const acctType = VaultType.LOCKED; const beneficiaryAddress = ethers.constants.AddressZero; @@ -2019,7 +2041,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...charity, withdrawFee: {bps: 0, payoutAddress: ethers.constants.AddressZero}, }; - await state.setEndowmentDetails(charityId, charityNoWithFee); + await wait(state.setEndowmentDetails(charityId, charityNoWithFee)); const earlyLockWithFeeSetting: LibAccounts.FeeSettingStruct = { bps: 30, @@ -2068,7 +2090,7 @@ describe("AccountsDepositWithdrawEndowments", function () { ...normalEndow, withdrawFee: {bps: 0, payoutAddress: ethers.constants.AddressZero}, }; - await state.setEndowmentDetails(normalEndowId, normalEndowNoWithFee); + await wait(state.setEndowmentDetails(normalEndowId, normalEndowNoWithFee)); const acctType = VaultType.LOCKED; const beneficiaryAddress = genWallet().address; diff --git a/test/core/accounts/AccountsDonationMatch.ts b/test/core/accounts/AccountsDonationMatch.ts index c752c65bd..016a67144 100644 --- a/test/core/accounts/AccountsDonationMatch.ts +++ b/test/core/accounts/AccountsDonationMatch.ts @@ -3,7 +3,7 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect, use} from "chai"; import {BigNumber} from "ethers"; import hre from "hardhat"; -import {DEFAULT_CHARITY_ENDOWMENT, DEFAULT_REGISTRAR_CONFIG} from "test/utils"; +import {DEFAULT_CHARITY_ENDOWMENT, DEFAULT_REGISTRAR_CONFIG, wait} from "test/utils"; import { AccountsDonationMatch, AccountsDonationMatch__factory, @@ -11,8 +11,8 @@ import { DonationMatchEmitter, DonationMatchEmitter__factory, DonationMatch__factory, - DummyERC20, - DummyERC20__factory, + IERC20, + IERC20__factory, Registrar, Registrar__factory, TestFacetProxyContract, @@ -41,10 +41,10 @@ describe("AccountsDonationMatch", function () { let facet: AccountsDonationMatch; let state: TestFacetProxyContract; - let daoTokenFake: FakeContract; + let daoTokenFake: FakeContract; let donationMatchFake: FakeContract; let donationMatchEmitterFake: FakeContract; - let haloTokenFake: FakeContract; + let haloTokenFake: FakeContract; let registrarFake: FakeContract; before(async function () { @@ -62,12 +62,12 @@ describe("AccountsDonationMatch", function () { facet = AccountsDonationMatch__factory.connect(state.address, endowOwner); - daoTokenFake = await smock.fake(new DummyERC20__factory()); + daoTokenFake = await smock.fake(IERC20__factory.createInterface()); donationMatchFake = await smock.fake(new DonationMatch__factory()); donationMatchEmitterFake = await smock.fake( new DonationMatchEmitter__factory() ); - haloTokenFake = await smock.fake(new DummyERC20__factory()); + haloTokenFake = await smock.fake(IERC20__factory.createInterface()); registrarFake = await smock.fake(new Registrar__factory()); const config: RegistrarStorage.ConfigStruct = { @@ -85,15 +85,17 @@ describe("AccountsDonationMatch", function () { owner: endowOwner.address, daoToken: daoTokenFake.address, }; - await state.setEndowmentDetails(endowId, endowment); - await state.setConfig({ - owner: accOwner.address, - version: "1", - networkName: "Polygon", - registrarContract: registrarFake.address, - nextAccountId: endowId + 1, - reentrancyGuardLocked: false, - }); + await wait(state.setEndowmentDetails(endowId, endowment)); + await wait( + state.setConfig({ + owner: accOwner.address, + version: "1", + networkName: "Polygon", + registrarContract: registrarFake.address, + nextAccountId: endowId + 1, + reentrancyGuardLocked: false, + }) + ); }); describe("upon depositDonationMatchERC20", () => { @@ -154,7 +156,7 @@ describe("AccountsDonationMatch", function () { const amount = 10; const recipient = genWallet().address; - await state.setDaoTokenBalance(endowId, amount); + await wait(state.setDaoTokenBalance(endowId, amount)); daoTokenFake.transfer.whenCalledWith(recipient, amount).returns(true); @@ -169,7 +171,7 @@ describe("AccountsDonationMatch", function () { const amount = 10; const recipient = genWallet().address; - await state.setDaoTokenBalance(endowId, amount + 1); + await wait(state.setDaoTokenBalance(endowId, amount + 1)); daoTokenFake.transfer.whenCalledWith(recipient, amount).returns(true); @@ -207,7 +209,7 @@ describe("AccountsDonationMatch", function () { ...prevEndow, donationMatchContract: genWallet().address, }; - await state.setEndowmentDetails(endowId, endowWithDonMatch); + await wait(state.setEndowmentDetails(endowId, endowWithDonMatch)); await expect(facet.setupDonationMatch(endowId, details)).to.be.revertedWith( "A Donation Match contract already exists for this Endowment" diff --git a/test/core/accounts/AccountsGasManager.ts b/test/core/accounts/AccountsGasManager.ts index 4c0dedd0b..5e8312ba1 100644 --- a/test/core/accounts/AccountsGasManager.ts +++ b/test/core/accounts/AccountsGasManager.ts @@ -7,12 +7,13 @@ import { DEFAULT_CHARITY_ENDOWMENT, DEFAULT_PERMISSIONS_STRUCT, DEFAULT_SETTINGS_STRUCT, + wait, } from "test/utils"; import { AccountsGasManager, AccountsGasManager__factory, - DummyERC20, - DummyERC20__factory, + IERC20, + IERC20__factory, GasFwd, GasFwd__factory, TestFacetProxyContract, @@ -27,7 +28,7 @@ describe("AccountsGasManager", function () { let proxyAdmin: SignerWithAddress; let user: SignerWithAddress; let impl: AccountsGasManager; - let token: FakeContract; + let token: FakeContract; let gasFwd: FakeContract; const ACCOUNT_ID = 1; const BALANCE = 1000; @@ -42,7 +43,7 @@ describe("AccountsGasManager", function () { }); beforeEach(async () => { - token = await smock.fake(new DummyERC20__factory()); + token = await smock.fake(IERC20__factory.createInterface()); gasFwd = await smock.fake(new GasFwd__factory()); token.transfer.returns(true); @@ -62,7 +63,7 @@ describe("AccountsGasManager", function () { ...DEFAULT_CHARITY_ENDOWMENT, gasFwd: gasFwd.address, }; - await state.setEndowmentDetails(ACCOUNT_ID, endowment); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endowment)); }); it("reverts if not called by self", async function () { @@ -95,14 +96,14 @@ describe("AccountsGasManager", function () { ...DEFAULT_ACCOUNTS_CONFIG, owner: owner.address, }; - await state.setConfig(config); + await wait(state.setConfig(config)); let endowment = { ...DEFAULT_CHARITY_ENDOWMENT, owner: user.address, gasFwd: gasFwd.address, }; - await state.setEndowmentDetails(ACCOUNT_ID, endowment); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endowment)); }); it("reverts if not called by admin", async function () { @@ -158,8 +159,8 @@ describe("AccountsGasManager", function () { lockedInvestmentManagement: lockedPerms, }, }; - await state.setEndowmentDetails(ACCOUNT_ID, endowment); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, BALANCE, BALANCE); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endowment)); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, BALANCE, BALANCE)); await expect( facet.connect(user).addGas(ACCOUNT_ID, VaultType.LOCKED, token.address, GAS_COST) @@ -187,8 +188,8 @@ describe("AccountsGasManager", function () { liquidInvestmentManagement: liquidPerms, }, }; - await state.setEndowmentDetails(ACCOUNT_ID, endowment); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, BALANCE, BALANCE); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endowment)); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, BALANCE, BALANCE)); await expect( facet.connect(user).addGas(ACCOUNT_ID, VaultType.LIQUID, token.address, GAS_COST) @@ -205,8 +206,8 @@ describe("AccountsGasManager", function () { owner: user.address, gasFwd: gasFwd.address, }; - await state.setEndowmentDetails(ACCOUNT_ID, endowment); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, BALANCE, BALANCE); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endowment)); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, BALANCE, BALANCE)); await expect( facet.connect(user).addGas(ACCOUNT_ID, VaultType.LIQUID, token.address, GAS_COST) diff --git a/test/core/accounts/AccountsQueryEndowments.ts b/test/core/accounts/AccountsQueryEndowments.ts index ecfc9a5b3..82b371200 100644 --- a/test/core/accounts/AccountsQueryEndowments.ts +++ b/test/core/accounts/AccountsQueryEndowments.ts @@ -2,7 +2,7 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect} from "chai"; import {BigNumber} from "ethers"; import hre from "hardhat"; -import {DEFAULT_ACCOUNTS_CONFIG, DEFAULT_CHARITY_ENDOWMENT} from "test/utils"; +import {DEFAULT_ACCOUNTS_CONFIG, DEFAULT_CHARITY_ENDOWMENT, wait} from "test/utils"; import { AccountsQueryEndowments, AccountsQueryEndowments__factory, @@ -52,8 +52,8 @@ describe("AccountsQueryEndowments", function () { state = await deployFacetAsProxy(hre, owner, proxyAdmin, facetImpl.address); facet = AccountsQueryEndowments__factory.connect(state.address, owner); - await state.setEndowmentDetails(accountId, DEFAULT_CHARITY_ENDOWMENT); - await state.setEndowmentTokenBalance(accountId, tokenAddress, lockedBal, liquidBal); + await wait(state.setEndowmentDetails(accountId, DEFAULT_CHARITY_ENDOWMENT)); + await wait(state.setEndowmentTokenBalance(accountId, tokenAddress, lockedBal, liquidBal)); config = { ...DEFAULT_ACCOUNTS_CONFIG, @@ -61,12 +61,14 @@ describe("AccountsQueryEndowments", function () { nextAccountId: accountId + 1, // endowment was created in previous step }; - await state.setConfig(config); + await wait(state.setConfig(config)); - await state.setClosingEndowmentState( - accountId, - endowState.closingEndowment, - endowState.closingBeneficiary + await wait( + state.setClosingEndowmentState( + accountId, + endowState.closingEndowment, + endowState.closingBeneficiary + ) ); }); diff --git a/test/core/accounts/AccountsStrategy.ts b/test/core/accounts/AccountsStrategy.ts index a5e202f80..ceff219f3 100644 --- a/test/core/accounts/AccountsStrategy.ts +++ b/test/core/accounts/AccountsStrategy.ts @@ -1,9 +1,8 @@ -import {FakeContract, MockContract, smock} from "@defi-wonderland/smock"; +import {FakeContract, smock} from "@defi-wonderland/smock"; import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect, use} from "chai"; +import {BigNumber} from "ethers"; import hre from "hardhat"; - -import {deployDummyGasService} from "tasks/helpers"; import { DEFAULT_ACCOUNTS_CONFIG, DEFAULT_AP_PARAMS, @@ -11,95 +10,186 @@ import { DEFAULT_INVEST_REQUEST, DEFAULT_METHOD_SELECTOR, DEFAULT_NETWORK_INFO, - DEFAULT_PERMISSIONS_STRUCT, DEFAULT_REDEEM_ALL_REQUEST, DEFAULT_REDEEM_REQUEST, - DEFAULT_SETTINGS_STRUCT, DEFAULT_STRATEGY_PARAMS, DEFAULT_STRATEGY_SELECTOR, convertVaultActionStructToArray, - deployDummyERC20, - deployDummyGateway, - deployDummyVault, packActionData, + wait, } from "test/utils"; import { AccountsStrategy, AccountsStrategy__factory, - DummyERC20, - DummyGasService, - DummyGateway, - DummyVault, + ERC20, + ERC20__factory, GasFwd, GasFwd__factory, + IAxelarGasService, + IAxelarGasService__factory, + IAxelarGateway, + IAxelarGateway__factory, + IVault, + IVault__factory, Registrar, Registrar__factory, Router, Router__factory, TestFacetProxyContract, } from "typechain-types"; +import { + AccountMessages, + IVault as IVaultStrategy, +} from "typechain-types/contracts/core/accounts/facets/AccountsStrategy"; +import {LocalRegistrarLib} from "typechain-types/contracts/core/registrar/LocalRegistrar"; import {IAccountsStrategy} from "typechain-types/contracts/core/registrar/Registrar"; import {AccountStorage} from "typechain-types/contracts/test/accounts/TestFacetProxyContract"; -import {StrategyApprovalState, VaultActionStatus, getSigners} from "utils"; +import {StrategyApprovalState, VaultActionStatus, genWallet, getChainId, getSigners} from "utils"; import {deployFacetAsProxy} from "./utils"; use(smock.matchers); describe("AccountsStrategy", function () { const {ethers} = hre; + + const ACCOUNT_ID = 1; + + const INITIAL_LOCK_BAL = 500; + const INITIAL_LIQ_BAL = 500; + const LOCK_AMT = 300; + const LIQ_AMT = 200; + const GAS_FEE = 100; + + const NET_NAME_THIS = "ThisNet"; + const NET_NAME_THAT = "ThatNet"; + let owner: SignerWithAddress; let admin: SignerWithAddress; let user: SignerWithAddress; + + let gasFwd: FakeContract; + let gasService: FakeContract; + let gateway: FakeContract; let registrar: FakeContract; let router: FakeContract; - let vault: DummyVault; + let token: FakeContract; + let vault: FakeContract; + let facetImpl: AccountsStrategy; + let state: TestFacetProxyContract; + let facet: AccountsStrategy; + + let netInfoThis: IAccountsStrategy.NetworkInfoStruct; + let netInfoThat: IAccountsStrategy.NetworkInfoStruct; + + let endowDetails: AccountStorage.EndowmentStruct; before(async function () { const {deployer, proxyAdmin, apTeam1} = await getSigners(hre); owner = deployer; admin = proxyAdmin; user = apTeam1; - vault = await deployDummyVault(owner, { - baseToken: ethers.constants.AddressZero, - yieldToken: ethers.constants.AddressZero, - }); + + gasService = await smock.fake(IAxelarGasService__factory.createInterface()); + gateway = await smock.fake(IAxelarGateway__factory.createInterface()); + vault = await smock.fake(IVault__factory.createInterface()); + + const Facet = new AccountsStrategy__factory(owner); + facetImpl = await Facet.deploy(); + await facetImpl.deployed(); + }); + + beforeEach(async function () { + gasFwd = await smock.fake(new GasFwd__factory()); registrar = await smock.fake(new Registrar__factory()); router = await smock.fake(new Router__factory()); - let Facet = new AccountsStrategy__factory(owner); - facetImpl = await Facet.deploy(); + token = await smock.fake(ERC20__factory.createInterface()); + + state = await deployFacetAsProxy(hre, owner, admin, facetImpl.address); + facet = AccountsStrategy__factory.connect(state.address, owner); + + token.approve.returns(true); + token.symbol.returns("TKN"); + token.transfer.returns(true); + + const config: AccountStorage.ConfigStruct = { + ...DEFAULT_ACCOUNTS_CONFIG, + networkName: NET_NAME_THIS, + registrarContract: registrar.address, + }; + await wait(state.setConfig(config)); + + endowDetails = { + ...DEFAULT_CHARITY_ENDOWMENT, + gasFwd: gasFwd.address, + owner: genWallet().address, + settingsController: { + ...DEFAULT_CHARITY_ENDOWMENT.settingsController, + liquidInvestmentManagement: { + locked: false, + delegate: { + addr: owner.address, + expires: 0, + }, + }, + lockedInvestmentManagement: { + locked: false, + delegate: { + addr: owner.address, + expires: 0, + }, + }, + }, + }; + await wait(state.setEndowmentDetails(ACCOUNT_ID, endowDetails)); + await wait( + state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, INITIAL_LOCK_BAL, INITIAL_LIQ_BAL) + ); + + netInfoThis = { + ...DEFAULT_NETWORK_INFO, + chainId: await getChainId(hre), + axelarGateway: gateway.address, + gasReceiver: gasService.address, + router: router.address, + }; + netInfoThat = { + ...DEFAULT_NETWORK_INFO, + chainId: 42, + router: genWallet().address, + }; + registrar.queryNetworkConnection.whenCalledWith(NET_NAME_THIS).returns(netInfoThis); + registrar.queryNetworkConnection.whenCalledWith(NET_NAME_THAT).returns(netInfoThat); + + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { + ...DEFAULT_STRATEGY_PARAMS, + network: NET_NAME_THIS, + approvalState: StrategyApprovalState.APPROVED, + }; + registrar.getStrategyParamsById.returns(stratParams); + + registrar.isTokenAccepted.returns(true); + + gateway.tokenAddresses.returns(token.address); }); - describe("upon strategyInvest", async function () { - let facet: AccountsStrategy; - let state: TestFacetProxyContract; - let token: DummyERC20; - let gateway: DummyGateway; - let network: IAccountsStrategy.NetworkInfoStruct; - const ACCOUNT_ID = 1; - - before(async function () { - token = await deployDummyERC20(owner); - gateway = await deployDummyGateway(owner); - await gateway.setTestTokenAddress(token.address); - - network = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - }; - await registrar.queryNetworkConnection.returns(network); + describe("upon strategyInvest", function () { + beforeEach(async () => { + await wait( + state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, false) + ); }); - beforeEach(async function () { - state = await deployFacetAsProxy(hre, owner, admin, facetImpl.address); - facet = AccountsStrategy__factory.connect(state.address, owner); - }); + describe("reverts when", function () { + it("neither locked nor liquid funds are set to be invested", async function () { + await expect( + facet.strategyInvest(ACCOUNT_ID, DEFAULT_INVEST_REQUEST) + ).to.be.revertedWithCustomError(facet, "ZeroAmount"); + }); - describe("reverts when", async function () { it("the caller is not approved for locked fund mgmt", async function () { - await state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT); - let investRequest = { + await wait(state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT)); + const investRequest: AccountMessages.InvestRequestStruct = { ...DEFAULT_INVEST_REQUEST, lockAmt: 1, }; @@ -109,8 +199,8 @@ describe("AccountsStrategy", function () { }); it("the caller is not approved for liquid fund mgmt", async function () { - await state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT); - let investRequest = { + await wait(state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT)); + const investRequest: AccountMessages.InvestRequestStruct = { ...DEFAULT_INVEST_REQUEST, liquidAmt: 1, }; @@ -120,43 +210,27 @@ describe("AccountsStrategy", function () { }); it("the strategy is not approved", async function () { - await state.setEndowmentDetails(1, DEFAULT_CHARITY_ENDOWMENT); - let config = { - ...DEFAULT_ACCOUNTS_CONFIG, - registrarContract: registrar.address, + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { + ...DEFAULT_STRATEGY_PARAMS, + network: NET_NAME_THIS, + approvalState: StrategyApprovalState.NOT_APPROVED, + }; + registrar.getStrategyParamsById.returns(stratParams); + + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + liquidAmt: 1, + lockAmt: 1, }; - await state.setConfig(config); - await expect(facet.strategyInvest(ACCOUNT_ID, DEFAULT_INVEST_REQUEST)).to.be.revertedWith( + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)).to.be.revertedWith( "Strategy is not approved" ); }); it("the account locked balance is insufficient", async function () { - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.settingsController.lockedInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); - - let config = { - ...DEFAULT_ACCOUNTS_CONFIG, - registrarContract: registrar.address, - }; - await state.setConfig(config); - - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - approvalState: StrategyApprovalState.APPROVED, - }; - await registrar.getStrategyParamsById.returns(stratParams); - - let investRequest = { + const investRequest: AccountMessages.InvestRequestStruct = { ...DEFAULT_INVEST_REQUEST, - lockAmt: 1, + lockAmt: INITIAL_LOCK_BAL + 1, }; await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)).to.be.revertedWith( "Insufficient Balance" @@ -164,31 +238,9 @@ describe("AccountsStrategy", function () { }); it("the account liquid balance is insufficient", async function () { - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.settingsController.liquidInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); - - let config = { - ...DEFAULT_ACCOUNTS_CONFIG, - registrarContract: registrar.address, - }; - await state.setConfig(config); - - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - approvalState: StrategyApprovalState.APPROVED, - }; - await registrar.getStrategyParamsById.returns(stratParams); - - let investRequest = { + const investRequest: AccountMessages.InvestRequestStruct = { ...DEFAULT_INVEST_REQUEST, - liquidAmt: 1, + liquidAmt: INITIAL_LIQ_BAL + 1, }; await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)).to.be.revertedWith( "Insufficient Balance" @@ -196,294 +248,343 @@ describe("AccountsStrategy", function () { }); it("the token isn't accepted", async function () { - let config = { - ...DEFAULT_ACCOUNTS_CONFIG, - registrarContract: registrar.address, - }; - await state.setConfig(config); + registrar.isTokenAccepted.returns(false); - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - approvalState: StrategyApprovalState.APPROVED, + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + liquidAmt: 1, }; - await registrar.getStrategyParamsById.returns(stratParams); - - await expect(facet.strategyInvest(ACCOUNT_ID, DEFAULT_INVEST_REQUEST)).to.be.revertedWith( + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)).to.be.revertedWith( "Token not approved" ); }); }); - describe("and calls the local router", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - - before(async function () { - const network = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - router: router.address, - axelarGateway: gateway.address, - }; - await registrar.queryNetworkConnection.returns(network); - await registrar.isTokenAccepted.returns(true); - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - network: "ThisNet", - approvalState: StrategyApprovalState.APPROVED, - }; - await registrar.getStrategyParamsById.returns(stratParams); - }); - - beforeEach(async function () { - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.settingsController.liquidInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.settingsController.lockedInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); - - token.mint(facet.address, 1000); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, 500, 500); - - const config = { - ...DEFAULT_ACCOUNTS_CONFIG, - networkName: "ThisNet", - gateway: gateway.address, - registrarContract: registrar.address, + describe("and calls the local router", function () { + it("and the response is SUCCESS", async function () { + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + token: token.address, }; - await state.setConfig(config); - }); - it("and the response is SUCCESS", async function () { - router.executeWithTokenLocal.returns({ + const vaultActionData: IVaultStrategy.VaultActionDataStruct = { destinationChain: "", strategyId: DEFAULT_STRATEGY_SELECTOR, selector: DEFAULT_METHOD_SELECTOR, accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, + token: investRequest.token, + lockAmt: investRequest.lockAmt, + liqAmt: investRequest.liquidAmt, status: VaultActionStatus.SUCCESS, - }); - - let investRequest = { - ...DEFAULT_INVEST_REQUEST, - lockAmt: LOCK_AMT, - liquidAmt: LIQ_AMT, }; + router.executeWithTokenLocal.returns(vaultActionData); - expect(await facet.strategyInvest(ACCOUNT_ID, investRequest)) + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)) .to.emit(facet, "EndowmentInvested") - .withArgs(VaultActionStatus.SUCCESS); + .withArgs(ACCOUNT_ID); + + expect(token.transfer).to.have.been.calledWith( + netInfoThis.router, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)) + ); + + const payload = packActionData( + { + destinationChain: NET_NAME_THIS, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: investRequest.token, + lockAmt: investRequest.lockAmt, + liqAmt: investRequest.liquidAmt, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + expect(router.executeWithTokenLocal).to.have.been.calledWith( + NET_NAME_THIS, + facet.address.toLowerCase(), + payload, + investRequest.token, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)) + ); - let routerBal = await token.balanceOf(router.address); - expect(routerBal).to.equal(LOCK_AMT + LIQ_AMT); const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(500 - LOCK_AMT); - expect(liqBal).to.equal(500 - LIQ_AMT); - let strategyActive = await state.getActiveStrategyEndowmentState( + expect(lockBal).to.equal(INITIAL_LOCK_BAL - LOCK_AMT); + expect(liqBal).to.equal(INITIAL_LIQ_BAL - LIQ_AMT); + const strategyActive = await state.getActiveStrategyEndowmentState( ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR ); expect(strategyActive).to.be.true; }); - it("and the response is anything other than SUCCESS", async function () { - await router.executeWithTokenLocal.returns({ - destinationChain: "", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: DEFAULT_METHOD_SELECTOR, - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.FAIL_TOKENS_FALLBACK, - }); + [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.POSITION_EXITED, + VaultActionStatus.UNPROCESSED, + ].forEach((vaultStatus) => { + it(`reverts when the response is ${VaultActionStatus[vaultStatus]}`, async function () { + const vaultActionData: IVaultStrategy.VaultActionDataStruct = { + destinationChain: "", + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: DEFAULT_METHOD_SELECTOR, + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: vaultStatus, + }; + router.executeWithTokenLocal.returns(vaultActionData); - let investRequest = { - ...DEFAULT_INVEST_REQUEST, - lockAmt: LOCK_AMT, - liquidAmt: LIQ_AMT, - }; - await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)) - .to.be.revertedWithCustomError(facet, "InvestFailed") - .withArgs(VaultActionStatus.FAIL_TOKENS_FALLBACK); + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + }; - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(500); - expect(liqBal).to.equal(500); - let strategyActive = await state.getActiveStrategyEndowmentState( - ACCOUNT_ID, - DEFAULT_STRATEGY_SELECTOR - ); - expect(!strategyActive); + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)) + .to.be.revertedWithCustomError(facet, "InvestFailed") + .withArgs(vaultStatus); + }); }); }); - describe("and calls axelar GMP", async function () { - let gasReceiver: DummyGasService; - let gasFwd: MockContract; - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const INITIAL_LOCK_BAL = 500; - const INITIAL_LIQ_BAL = 500; - const GAS_FEE = 100; - - before(async function () { - gasReceiver = await deployDummyGasService(owner); - const gasFwdFactory = await smock.mock("GasFwd"); - gasFwd = await gasFwdFactory.deploy(); - - const thisNet = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - gasReceiver: gasReceiver.address, - }; - const thatNet = { - ...DEFAULT_NETWORK_INFO, - chainId: 42, - router: router.address, - }; - await registrar.queryNetworkConnection.whenCalledWith("ThisNet").returns(thisNet); - await registrar.queryNetworkConnection.whenCalledWith("ThatNet").returns(thatNet); - - await registrar.isTokenAccepted.returns(true); - const stratParams = { + describe("and calls axelar GMP", function () { + beforeEach(async function () { + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { ...DEFAULT_STRATEGY_PARAMS, - network: "ThatNet", + network: NET_NAME_THAT, approvalState: StrategyApprovalState.APPROVED, }; - await registrar.getStrategyParamsById.returns(stratParams); + registrar.getStrategyParamsById.returns(stratParams); }); - beforeEach(async function () { - const config = { - ...DEFAULT_ACCOUNTS_CONFIG, - registrarContract: registrar.address, - networkName: "ThisNet", - gateway: gateway.address, - }; - await state.setConfig(config); + [ + {payForGasResult: GAS_FEE, text: "just enough balance"}, + {payForGasResult: GAS_FEE + 1, text: "more than enough balance"}, + ].forEach(({payForGasResult, text}) => { + it(`makes all the correct external calls when GasFwd has ${text} to pay for gas`, async function () { + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + gasFee: GAS_FEE, + }; - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.settingsController.liquidInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.settingsController.lockedInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.gasFwd = gasFwd.address; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); + gasFwd.payForGas.returns(payForGasResult); + + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)) + .to.emit(facet, "EndowmentInvested") + .withArgs(ACCOUNT_ID); + + expect(gasFwd.payForGas).to.have.been.calledWith(token.address, investRequest.gasFee); + expect(token.approve).to.have.been.calledWith(gasService.address, investRequest.gasFee); + + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + expect(gasService.payGasForContractCallWithToken).to.have.been.calledWith( + facet.address, + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), // AddressToString.toString produces only lowercase constters + payload, + investRequest.token, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)), + token.address, + investRequest.gasFee, + gasFwd.address + ); + expect(token.approve).to.have.been.calledWith( + gateway.address, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)) + ); + expect(gateway.callContractWithToken).to.have.been.calledWith( + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload, + investRequest.token, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)) + ); - await token.mint(facet.address, INITIAL_LIQ_BAL + INITIAL_LOCK_BAL); - await token.mint(gasFwd.address, GAS_FEE); - await state.setEndowmentTokenBalance( - ACCOUNT_ID, - token.address, - INITIAL_LOCK_BAL, - INITIAL_LIQ_BAL - ); - await gasFwd.setVariable("accounts", facet.address); + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(INITIAL_LOCK_BAL - LOCK_AMT); + expect(liqBal).to.equal(INITIAL_LIQ_BAL - LIQ_AMT); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + }); }); - it("makes all the correct external calls", async function () { - let investRequest = { - ...DEFAULT_INVEST_REQUEST, + [ + { lockAmt: LOCK_AMT, - liquidAmt: LIQ_AMT, + liqAmt: LIQ_AMT, gasFee: GAS_FEE, - }; - - let payload = packActionData({ - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, + gasFwdGas: GAS_FEE - 1, + expectedLockBal: INITIAL_LOCK_BAL - LOCK_AMT - 1, + expectedLiqBal: INITIAL_LIQ_BAL - LIQ_AMT, + description: + "paying out of locked balance takes precedence when gas fee is too small to split", + }, + { lockAmt: LOCK_AMT, liqAmt: LIQ_AMT, - status: VaultActionStatus.UNPROCESSED, - }); + gasFee: 200, + gasFwdGas: 0, + expectedLockBal: INITIAL_LOCK_BAL - LOCK_AMT - 120, + expectedLiqBal: INITIAL_LIQ_BAL - LIQ_AMT - 80, + description: "both liquid and locked balances cover their respective gas fee portions", + }, + { + lockAmt: 43, + liqAmt: 55, + gasFee: 400, + gasFwdGas: 79, + expectedLockBal: INITIAL_LOCK_BAL - 184, + expectedLiqBal: INITIAL_LIQ_BAL - 235, + description: "fractional percentages case", + }, + { + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + gasFee: 1000, + gasFwdGas: 600, + expectedLockBal: 0, + expectedLiqBal: 100, + description: "liquid covers part of locked portion of the gas fee", + }, + ].forEach((caseData) => { + it(`makes all the correct external calls and pays for part of the gas fee: ${caseData.description}`, async function () { + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: caseData.lockAmt, + liquidAmt: caseData.liqAmt, + gasFee: caseData.gasFee, + }; - expect(await facet.strategyInvest(ACCOUNT_ID, investRequest)) - .to.emit(gasReceiver, "GasPaidWithToken") - .withArgs( + gasFwd.payForGas.returns(caseData.gasFwdGas); + + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)) + .to.emit(facet, "EndowmentInvested") + .withArgs(ACCOUNT_ID); + + expect(gasFwd.payForGas).to.have.been.calledWith(token.address, investRequest.gasFee); + expect(token.approve).to.have.been.calledWith(gasService.address, investRequest.gasFee); + + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: caseData.lockAmt, + liqAmt: caseData.liqAmt, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + expect(gasService.payGasForContractCallWithToken).to.have.been.calledWith( facet.address, - "ThatNet", - router.address, + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), // AddressToString.toString produces only lowercase constters payload, - "TKN", - LOCK_AMT + LIQ_AMT, + investRequest.token, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)), token.address, - GAS_FEE, + investRequest.gasFee, gasFwd.address - ) - .to.emit(gateway, "ContractCallWtihToken") - .withArgs("ThatNet", router.address, payload); + ); + expect(token.approve).to.have.been.calledWith( + gateway.address, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)) + ); + expect(gateway.callContractWithToken).to.have.been.calledWith( + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload, + investRequest.token, + BigNumber.from(investRequest.liquidAmt).add(BigNumber.from(investRequest.lockAmt)) + ); - let gasReceiverApproved = await token.allowance(facet.address, gasReceiver.address); - expect(gasReceiverApproved).to.equal(GAS_FEE); - let gatewayApproved = await token.allowance(facet.address, gateway.address); - expect(gatewayApproved).to.equal(LOCK_AMT + LIQ_AMT); - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(INITIAL_LOCK_BAL - LOCK_AMT); - expect(liqBal).to.equal(INITIAL_LIQ_BAL - LIQ_AMT); - expect(await state.getActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR)) - .to.be.true; + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(caseData.expectedLockBal); + expect(liqBal).to.equal(caseData.expectedLiqBal); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + }); + }); + + describe("but reverts because", () => { + it("neither locked nor liquid balances can cover their respective gas fees", async () => { + const hugeGasFee = INITIAL_LOCK_BAL + INITIAL_LIQ_BAL; + + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + gasFee: hugeGasFee, + }; + + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)) + .to.be.revertedWithCustomError(facet, "InsufficientFundsForGas") + .withArgs(ACCOUNT_ID); + }); + + it("liquid balances can't cover locked gas deficit", async () => { + const hugeGasFee = INITIAL_LOCK_BAL + 100; + + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + gasFee: hugeGasFee, + }; + + await expect(facet.strategyInvest(ACCOUNT_ID, investRequest)) + .to.be.revertedWithCustomError(facet, "InsufficientFundsForGas") + .withArgs(ACCOUNT_ID); + }); }); }); }); - describe("upon strategyRedeem", async function () { - let facet: AccountsStrategy; - let state: TestFacetProxyContract; - let token: DummyERC20; - let gateway: DummyGateway; - let network: IAccountsStrategy.NetworkInfoStruct; - const ACCOUNT_ID = 1; - - before(async function () { - token = await deployDummyERC20(owner); - gateway = await deployDummyGateway(owner); - await gateway.setTestTokenAddress(token.address); - - network = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - }; - await registrar.queryNetworkConnection.returns(network); + describe("upon strategyRedeem", function () { + beforeEach(async () => { + await wait( + state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, true) + ); }); - beforeEach(async function () { - state = await deployFacetAsProxy(hre, owner, admin, facetImpl.address); - facet = AccountsStrategy__factory.connect(state.address, owner); - }); + describe("reverts when", function () { + it("neither locked nor liquid funds are set to be redeemed", async function () { + await expect( + facet.strategyRedeem(ACCOUNT_ID, DEFAULT_REDEEM_REQUEST) + ).to.be.revertedWithCustomError(facet, "ZeroAmount"); + }); - describe("reverts when", async function () { it("the caller is not approved for locked fund mgmt", async function () { - await state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT); - let redeemRequest = { + await wait(state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT)); + const redeemRequest: AccountMessages.RedeemRequestStruct = { ...DEFAULT_REDEEM_REQUEST, lockAmt: 1, }; @@ -493,8 +594,8 @@ describe("AccountsStrategy", function () { }); it("the caller is not approved for liquid fund mgmt", async function () { - await state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT); - let redeemRequest = { + await wait(state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT)); + const redeemRequest: AccountMessages.RedeemRequestStruct = { ...DEFAULT_REDEEM_REQUEST, liquidAmt: 1, }; @@ -504,307 +605,342 @@ describe("AccountsStrategy", function () { }); it("the strategy is not approved", async function () { - let stratParams = { + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { ...DEFAULT_STRATEGY_PARAMS, - network: "ThisNet", + network: NET_NAME_THIS, approvalState: StrategyApprovalState.NOT_APPROVED, }; - await registrar.getStrategyParamsById.returns(stratParams); - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.owner = owner.address; - await state.setEndowmentDetails(1, endowDetails); - let config = DEFAULT_ACCOUNTS_CONFIG; - config.registrarContract = registrar.address; - await state.setConfig(config); - await expect(facet.strategyRedeem(ACCOUNT_ID, DEFAULT_REDEEM_REQUEST)).to.be.revertedWith( + registrar.getStrategyParamsById.returns(stratParams); + + const redeemRequest: AccountMessages.RedeemRequestStruct = { + ...DEFAULT_REDEEM_REQUEST, + lockAmt: 1, + }; + await expect(facet.strategyRedeem(ACCOUNT_ID, redeemRequest)).to.be.revertedWith( "Strategy is not approved" ); }); }); - describe("and calls the local router", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const redeemRequest = { + describe("and calls the local router", function () { + const redeemRequest: AccountMessages.RedeemRequestStruct = { ...DEFAULT_REDEEM_REQUEST, lockAmt: LOCK_AMT, liquidAmt: LIQ_AMT, }; - before(async function () { - const network = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - router: router.address, - axelarGateway: gateway.address, - }; - registrar.queryNetworkConnection.whenCalledWith("ThisNet").returns(network); - - registrar.isTokenAccepted.returns(true); - - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - network: "ThisNet", + [ + { + vaultStatus: VaultActionStatus.POSITION_EXITED, approvalState: StrategyApprovalState.APPROVED, - }; - registrar.getStrategyParamsById.returns(stratParams); - }); - - beforeEach(async function () { - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.settingsController.liquidInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.settingsController.lockedInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); + }, + { + vaultStatus: VaultActionStatus.POSITION_EXITED, + approvalState: StrategyApprovalState.WITHDRAW_ONLY, + }, + { + vaultStatus: VaultActionStatus.SUCCESS, + approvalState: StrategyApprovalState.APPROVED, + }, + { + vaultStatus: VaultActionStatus.SUCCESS, + approvalState: StrategyApprovalState.WITHDRAW_ONLY, + }, + ].forEach(({approvalState, vaultStatus}) => { + it(`and the response is ${VaultActionStatus[vaultStatus]} with approval state: ${StrategyApprovalState[approvalState]}`, async function () { + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { + ...DEFAULT_STRATEGY_PARAMS, + network: NET_NAME_THIS, + approvalState, + }; + registrar.getStrategyParamsById.returns(stratParams); - token.mint(facet.address, LOCK_AMT + LIQ_AMT); - await state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, true); + const vaultActionData: IVaultStrategy.VaultActionDataStruct = { + destinationChain: "", + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: DEFAULT_METHOD_SELECTOR, + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: vaultStatus, + }; + router.executeLocal.returns(vaultActionData); - const config = { - ...DEFAULT_ACCOUNTS_CONFIG, - networkName: "ThisNet", - gateway: gateway.address, - registrarContract: registrar.address, - }; - await state.setConfig(config); - }); + await expect(facet.strategyRedeem(ACCOUNT_ID, redeemRequest)) + .to.emit(facet, "EndowmentRedeemed") + .withArgs(ACCOUNT_ID, vaultStatus); + + const payload = packActionData( + { + destinationChain: NET_NAME_THIS, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("redeem"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + expect(router.executeLocal).to.have.been.calledWith( + NET_NAME_THIS, + facet.address.toLowerCase(), + payload + ); - it("and the response is SUCCESS", async function () { - await router.executeLocal.returns({ - destinationChain: "", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: DEFAULT_METHOD_SELECTOR, - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.SUCCESS, + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(INITIAL_LOCK_BAL + LOCK_AMT); + expect(liqBal).to.equal(INITIAL_LIQ_BAL + LIQ_AMT); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.equal(vaultStatus === VaultActionStatus.SUCCESS); }); - - expect(await facet.strategyRedeem(ACCOUNT_ID, redeemRequest)) - .to.emit(facet, "EndowmentRedeemed") - .withArgs(VaultActionStatus.SUCCESS); - - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - let strategyActive = await state.getActiveStrategyEndowmentState( - ACCOUNT_ID, - DEFAULT_STRATEGY_SELECTOR - ); - expect(strategyActive).to.be.true; }); - it("and the response is POSITION_EXITED", async function () { - await router.executeLocal.returns({ - destinationChain: "", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: DEFAULT_METHOD_SELECTOR, - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.POSITION_EXITED, - }); - - expect(await facet.strategyRedeem(ACCOUNT_ID, redeemRequest)) - .to.emit(facet, "EndowmentRedeemed") - .withArgs(VaultActionStatus.POSITION_EXITED); - - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - let strategyActive = await state.getActiveStrategyEndowmentState( - ACCOUNT_ID, - DEFAULT_STRATEGY_SELECTOR - ); - expect(strategyActive).to.be.false; - }); + [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.UNPROCESSED, + ].forEach((vaultStatus) => { + it(`reverts when the response is: ${VaultActionStatus[vaultStatus]}`, async function () { + const vaultActionData: IVaultStrategy.VaultActionDataStruct = { + destinationChain: "", + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: DEFAULT_METHOD_SELECTOR, + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: vaultStatus, + }; + router.executeLocal.returns(vaultActionData); - it("and the response is anything else", async function () { - await router.executeLocal.returns({ - destinationChain: "", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: DEFAULT_METHOD_SELECTOR, - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.FAIL_TOKENS_FALLBACK, + await expect(facet.strategyRedeem(ACCOUNT_ID, redeemRequest)) + .to.be.revertedWithCustomError(facet, "RedeemFailed") + .withArgs(vaultStatus); }); - await expect(facet.strategyRedeem(ACCOUNT_ID, redeemRequest)) - .to.be.revertedWithCustomError(facet, "RedeemFailed") - .withArgs(VaultActionStatus.FAIL_TOKENS_FALLBACK); - - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(0); - expect(liqBal).to.equal(0); - let strategyActive = await state.getActiveStrategyEndowmentState( - ACCOUNT_ID, - DEFAULT_STRATEGY_SELECTOR - ); - expect(strategyActive); }); }); - describe("and calls axelar GMP", async function () { - let gasReceiver: DummyGasService; - let gasFwd: MockContract; - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const INITIAL_LOCK_BAL = 0; - const INITIAL_LIQ_BAL = 0; - const GAS_FEE = 100; - - before(async function () { - gasReceiver = await deployDummyGasService(owner); - const gasFwdFactory = await smock.mock("GasFwd"); - gasFwd = await gasFwdFactory.deploy(); - - const thisNet = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - gasReceiver: gasReceiver.address, - }; - const thatNet = { - ...DEFAULT_NETWORK_INFO, - chainId: 42, - router: router.address, - }; - await registrar.queryNetworkConnection.whenCalledWith("ThisNet").returns(thisNet); - await registrar.queryNetworkConnection.whenCalledWith("ThatNet").returns(thatNet); - - await registrar.isTokenAccepted.returns(true); - const stratParams = { + describe("and calls axelar GMP", function () { + beforeEach(async function () { + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { ...DEFAULT_STRATEGY_PARAMS, - network: "ThatNet", + network: NET_NAME_THAT, approvalState: StrategyApprovalState.APPROVED, }; - await registrar.getStrategyParamsById.returns(stratParams); + registrar.getStrategyParamsById.returns(stratParams); }); - beforeEach(async function () { - const config = { - ...DEFAULT_ACCOUNTS_CONFIG, - registrarContract: registrar.address, - networkName: "ThisNet", - }; - await state.setConfig(config); + [ + {payForGasResult: GAS_FEE, text: "just enough balance"}, + {payForGasResult: GAS_FEE + 1, text: "more than enough balance"}, + ].forEach(({payForGasResult, text}) => { + it(`makes all the correct external calls when GasFwd has ${text} to pay for gas`, async function () { + const redeemRequest: AccountMessages.RedeemRequestStruct = { + ...DEFAULT_REDEEM_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + gasFee: GAS_FEE, + }; - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.settingsController.liquidInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.settingsController.lockedInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.gasFwd = gasFwd.address; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("redeem"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + + gasFwd.payForGas.returns(payForGasResult); + + await expect(facet.strategyRedeem(ACCOUNT_ID, redeemRequest)).to.not.be.reverted; - await token.mint(gasFwd.address, GAS_FEE); - await gasFwd.setVariable("accounts", facet.address); + expect(gasFwd.payForGas).to.have.been.calledWith(token.address, redeemRequest.gasFee); + expect(token.approve).to.have.been.calledWith(gasService.address, redeemRequest.gasFee); + expect(gasService.payGasForContractCall).to.have.been.calledWith( + facet.address, + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload, + token.address, + redeemRequest.gasFee, + endowDetails.owner + ); + expect(gateway.callContract).to.have.been.calledWith( + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload + ); + + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(INITIAL_LOCK_BAL); + expect(liqBal).to.equal(INITIAL_LIQ_BAL); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + }); }); - it("makes all the correct external calls", async function () { - let redeemRequest = { - ...DEFAULT_REDEEM_REQUEST, + [ + { lockAmt: LOCK_AMT, - liquidAmt: LIQ_AMT, + liqAmt: LIQ_AMT, gasFee: GAS_FEE, - }; - - let payload = packActionData({ - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("redeem"), - accountIds: [ACCOUNT_ID], - token: token.address, + gasFwdGas: GAS_FEE - 1, + expectedLockBal: INITIAL_LOCK_BAL - 1, + expectedLiqBal: INITIAL_LIQ_BAL, + description: + "paying out of locked balance takes precedence when gas fee is too small to split", + }, + { lockAmt: LOCK_AMT, liqAmt: LIQ_AMT, - status: VaultActionStatus.UNPROCESSED, - }); + gasFee: 400, + gasFwdGas: 0, + expectedLockBal: INITIAL_LOCK_BAL - 240, + expectedLiqBal: INITIAL_LIQ_BAL - 160, + description: "both liquid and locked balances cover their respective gas fee portions", + }, + { + lockAmt: 43, + liqAmt: 55, + gasFee: 400, + gasFwdGas: 79, + expectedLockBal: INITIAL_LOCK_BAL - 141, + expectedLiqBal: INITIAL_LIQ_BAL - 180, + description: "fractional percentages case", + }, + { + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + gasFee: 1000, + gasFwdGas: 100, + expectedLockBal: 0, + expectedLiqBal: 100, + description: "liquid covers part of locked portion of the gas fee", + }, + ].forEach((caseData) => { + it(`makes all the correct external calls and pays for part of ${ + caseData.gasFee + } gas fee (total ${caseData.gasFee - caseData.gasFwdGas}): ${ + caseData.description + }`, async function () { + const redeemRequest: AccountMessages.RedeemRequestStruct = { + ...DEFAULT_REDEEM_REQUEST, + lockAmt: caseData.lockAmt, + liquidAmt: caseData.liqAmt, + gasFee: caseData.gasFee, + }; + + gasFwd.payForGas.returns(caseData.gasFwdGas); + + await expect(facet.strategyRedeem(ACCOUNT_ID, redeemRequest)).to.not.be.reverted; + + expect(gasFwd.payForGas).to.have.been.calledWith(token.address, redeemRequest.gasFee); + expect(token.approve).to.have.been.calledWith(gasService.address, redeemRequest.gasFee); - expect(await facet.strategyRedeem(ACCOUNT_ID, redeemRequest)) - .to.emit(gasReceiver, "GasPaid") - .withArgs( + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("redeem"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: caseData.lockAmt, + liqAmt: caseData.liqAmt, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + expect(gasService.payGasForContractCall).to.have.been.calledWith( facet.address, - "ThatNet", - router.address, + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), payload, token.address, - GAS_FEE, - gasFwd.address - ) - .to.emit(gateway, "ContractCall") - .withArgs("ThatNet", router.address, payload); + redeemRequest.gasFee, + endowDetails.owner + ); + expect(gateway.callContract).to.have.been.calledWith( + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload + ); - let gasReceiverApproved = await token.allowance(facet.address, gasReceiver.address); - expect(gasReceiverApproved).to.equal(GAS_FEE); + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(caseData.expectedLockBal); + expect(liqBal).to.equal(caseData.expectedLiqBal); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + }); }); - }); - }); - describe("upon strategyRedeemAll", async function () { - let facet: AccountsStrategy; - let state: TestFacetProxyContract; - let token: DummyERC20; - let gateway: DummyGateway; - const ACCOUNT_ID = 1; - - before(async function () { - token = await deployDummyERC20(owner); - gateway = await deployDummyGateway(owner); - await gateway.setTestTokenAddress(token.address); - - const network = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - }; - registrar.queryNetworkConnection.returns(network); - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - network: "ThisNet", - approvalState: StrategyApprovalState.NOT_APPROVED, - }; - registrar.getStrategyParamsById.returns(stratParams); + describe("but reverts because", () => { + it("neither locked nor liquid balances can cover their respective gas fees", async () => { + const hugeGasFee = (INITIAL_LOCK_BAL + INITIAL_LIQ_BAL) * 2; + + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + gasFee: hugeGasFee, + }; + + await expect(facet.strategyRedeem(ACCOUNT_ID, investRequest)) + .to.be.revertedWithCustomError(facet, "InsufficientFundsForGas") + .withArgs(ACCOUNT_ID); + }); + + it("liquid balances can't cover locked gas deficit", async () => { + const hugeGasFee = INITIAL_LOCK_BAL + INITIAL_LIQ_BAL + 100; + + const investRequest: AccountMessages.InvestRequestStruct = { + ...DEFAULT_INVEST_REQUEST, + lockAmt: LOCK_AMT, + liquidAmt: LIQ_AMT, + gasFee: hugeGasFee, + }; + + await expect(facet.strategyRedeem(ACCOUNT_ID, investRequest)) + .to.be.revertedWithCustomError(facet, "InsufficientFundsForGas") + .withArgs(ACCOUNT_ID); + }); + }); }); + }); - beforeEach(async function () { - state = await deployFacetAsProxy(hre, owner, admin, facetImpl.address); - facet = AccountsStrategy__factory.connect(state.address, owner); + describe("upon strategyRedeemAll", function () { + beforeEach(async () => { + await wait( + state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, true) + ); }); - describe("reverts when", async function () { - it("the caller is not approved for locked nor liquid fund mgmt", async function () { - await state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT); + describe("reverts when", function () { + it("neither locked nor liquid funds are set to be redeemed", async function () { await expect( - facet.connect(user).strategyRedeemAll(ACCOUNT_ID, DEFAULT_REDEEM_ALL_REQUEST) - ).to.be.revertedWith("Must redeem at least one of Locked/Liquid"); + facet.strategyRedeemAll(ACCOUNT_ID, DEFAULT_REDEEM_ALL_REQUEST) + ).to.be.revertedWithCustomError(facet, "ZeroAmount"); }); it("the caller is not approved for locked fund mgmt", async function () { - await state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT); - let redeemAllRequest = { + await wait(state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT)); + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { ...DEFAULT_REDEEM_ALL_REQUEST, redeemLocked: true, }; @@ -813,8 +949,8 @@ describe("AccountsStrategy", function () { ).to.be.revertedWith("Unauthorized"); }); it("the caller is not approved for liquid fund mgmt", async function () { - await state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT); - let redeemAllRequest = { + await wait(state.setEndowmentDetails(ACCOUNT_ID, DEFAULT_CHARITY_ENDOWMENT)); + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { ...DEFAULT_REDEEM_ALL_REQUEST, redeemLiquid: true, }; @@ -824,32 +960,14 @@ describe("AccountsStrategy", function () { }); it("the strategy is not approved", async function () { - let endowDetails: AccountStorage.EndowmentStruct = { - ...DEFAULT_CHARITY_ENDOWMENT, - owner: owner.address, - settingsController: { - ...DEFAULT_SETTINGS_STRUCT, - lockedInvestmentManagement: { - ...DEFAULT_PERMISSIONS_STRUCT, - delegate: { - expires: 0, - addr: user.address, - }, - }, - liquidInvestmentManagement: { - ...DEFAULT_PERMISSIONS_STRUCT, - delegate: { - expires: 0, - addr: user.address, - }, - }, - }, + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { + ...DEFAULT_STRATEGY_PARAMS, + network: NET_NAME_THIS, + approvalState: StrategyApprovalState.NOT_APPROVED, }; - await state.setEndowmentDetails(1, endowDetails); - let config = DEFAULT_ACCOUNTS_CONFIG; - config.registrarContract = registrar.address; - await state.setConfig(config); - let redeemAllRequest = { + registrar.getStrategyParamsById.returns(stratParams); + + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { ...DEFAULT_REDEEM_ALL_REQUEST, redeemLiquid: true, }; @@ -857,97 +975,83 @@ describe("AccountsStrategy", function () { "Strategy is not approved" ); }); + }); - describe("and calls the local router", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - let redeemAllRequest = { - ...DEFAULT_REDEEM_ALL_REQUEST, - redeemLocked: true, - redeemLiquid: true, - }; - - before(async function () { - const network = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - router: router.address, - }; - registrar.queryNetworkConnection.whenCalledWith("ThisNet").returns(network); - registrar.isTokenAccepted.returns(true); - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - network: "ThisNet", - approvalState: StrategyApprovalState.APPROVED, - }; - registrar.getStrategyParamsById.returns(stratParams); - }); + describe("and calls the local router", function () { + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { + ...DEFAULT_REDEEM_ALL_REQUEST, + redeemLocked: true, + redeemLiquid: true, + }; - beforeEach(async function () { - const config = { - ...DEFAULT_ACCOUNTS_CONFIG, - networkName: "ThisNet", - gateway: gateway.address, - registrarContract: registrar.address, - }; - await state.setConfig(config); - - let endowDetails: AccountStorage.EndowmentStruct = { - ...DEFAULT_CHARITY_ENDOWMENT, - owner: owner.address, - settingsController: { - ...DEFAULT_SETTINGS_STRUCT, - lockedInvestmentManagement: { - ...DEFAULT_PERMISSIONS_STRUCT, - delegate: { - expires: 0, - addr: user.address, - }, + [StrategyApprovalState.APPROVED, StrategyApprovalState.WITHDRAW_ONLY].forEach( + (approvalState) => { + it(`redeems when the response is POSITION_EXITED and approval state is: ${StrategyApprovalState[approvalState]}`, async function () { + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { + ...DEFAULT_STRATEGY_PARAMS, + network: NET_NAME_THIS, + approvalState, + }; + registrar.getStrategyParamsById.returns(stratParams); + + const vaultActionData: IVaultStrategy.VaultActionDataStruct = { + destinationChain: "", + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: DEFAULT_METHOD_SELECTOR, + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: VaultActionStatus.POSITION_EXITED, + }; + router.executeLocal.returns(vaultActionData); + + const payload = packActionData( + { + destinationChain: NET_NAME_THIS, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("redeemAll"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, }, - liquidInvestmentManagement: { - ...DEFAULT_PERMISSIONS_STRUCT, - delegate: { - expires: 0, - addr: user.address, - }, - }, - }, - }; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); - - await token.mint(facet.address, LOCK_AMT + LIQ_AMT); - await state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, true); - }); - - it("and the response is POSITION_EXITED", async function () { - await router.executeLocal.returns({ - destinationChain: "", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: DEFAULT_METHOD_SELECTOR, - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.POSITION_EXITED, + hre + ); + + await expect(facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)) + .to.emit(facet, "EndowmentRedeemed") + .withArgs(ACCOUNT_ID, VaultActionStatus.POSITION_EXITED); + + expect(router.executeLocal).to.have.been.calledWith( + NET_NAME_THIS, + facet.address.toLowerCase(), + payload + ); + + const [lockBal, liqBal] = await state.getEndowmentTokenBalance( + ACCOUNT_ID, + token.address + ); + expect(lockBal).to.equal(INITIAL_LOCK_BAL + LOCK_AMT); + expect(liqBal).to.equal(INITIAL_LIQ_BAL + LIQ_AMT); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.false; }); + } + ); - expect(await facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)) - .to.emit(facet, "EndowmentRedeemed") - .withArgs(VaultActionStatus.POSITION_EXITED); - - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - let strategyActive = await state.getActiveStrategyEndowmentState( - ACCOUNT_ID, - DEFAULT_STRATEGY_SELECTOR - ); - expect(strategyActive).to.be.false; - }); - - it("and the response is anything else", async function () { - await router.executeLocal.returns({ + [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.SUCCESS, + ].forEach((vaultActionStatus) => { + it(`reverts when response is ${VaultActionStatus[vaultActionStatus]}`, async function () { + const vaultActionData: IVaultStrategy.VaultActionDataStruct = { destinationChain: "", strategyId: DEFAULT_STRATEGY_SELECTOR, selector: DEFAULT_METHOD_SELECTOR, @@ -955,494 +1059,753 @@ describe("AccountsStrategy", function () { token: token.address, lockAmt: LOCK_AMT, liqAmt: LIQ_AMT, - status: VaultActionStatus.FAIL_TOKENS_FALLBACK, - }); + status: vaultActionStatus, + }; + router.executeLocal.returns(vaultActionData); + await expect(facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)) .to.be.revertedWithCustomError(facet, "RedeemAllFailed") - .withArgs(VaultActionStatus.FAIL_TOKENS_FALLBACK); + .withArgs(vaultActionStatus); const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(0); - expect(liqBal).to.equal(0); - let strategyActive = await state.getActiveStrategyEndowmentState( + expect(lockBal).to.equal(INITIAL_LOCK_BAL); + expect(liqBal).to.equal(INITIAL_LIQ_BAL); + const strategyActive = await state.getActiveStrategyEndowmentState( ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR ); - expect(strategyActive); + expect(strategyActive).to.be.true; }); }); }); - describe("and calls axelar GMP", async function () { - let gasReceiver: DummyGasService; - let gasFwd: MockContract; - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const GAS_FEE = 100; - - before(async function () { - gasReceiver = await deployDummyGasService(owner); - const gasFwdFactory = await smock.mock("GasFwd"); - gasFwd = await gasFwdFactory.deploy(); - - const thisNet = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - gasReceiver: gasReceiver.address, - }; - const thatNet = { - ...DEFAULT_NETWORK_INFO, - chainId: 42, - router: router.address, - }; - registrar.queryNetworkConnection.whenCalledWith("ThisNet").returns(thisNet); - registrar.queryNetworkConnection.whenCalledWith("ThatNet").returns(thatNet); - - registrar.isTokenAccepted.returns(true); - const stratParams = { + describe("and calls axelar GMP", function () { + beforeEach(async function () { + const stratParams: LocalRegistrarLib.StrategyParamsStruct = { ...DEFAULT_STRATEGY_PARAMS, - network: "ThatNet", + network: NET_NAME_THAT, approvalState: StrategyApprovalState.APPROVED, }; registrar.getStrategyParamsById.returns(stratParams); }); - beforeEach(async function () { - const config = { - ...DEFAULT_ACCOUNTS_CONFIG, - registrarContract: registrar.address, - networkName: "ThisNet", - }; - await state.setConfig(config); + [ + {payForGasResult: GAS_FEE, text: "just enough balance"}, + {payForGasResult: GAS_FEE + 1, text: "more than enough balance"}, + ].forEach(({payForGasResult, text}) => { + it(`makes all the correct external calls when GasFwd has ${text} to pay for gas`, async function () { + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { + ...DEFAULT_REDEEM_ALL_REQUEST, + redeemLocked: true, + redeemLiquid: true, + gasFee: GAS_FEE, + }; - let endowDetails = DEFAULT_CHARITY_ENDOWMENT; - endowDetails.settingsController.liquidInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.settingsController.lockedInvestmentManagement = { - locked: false, - delegate: { - addr: owner.address, - expires: 0, - }, - }; - endowDetails.gasFwd = gasFwd.address; - await state.setEndowmentDetails(ACCOUNT_ID, endowDetails); + gasFwd.payForGas.returns(payForGasResult); + + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("redeemAll"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + + await expect(facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)).to.not.be.reverted; - await token.mint(gasFwd.address, GAS_FEE); - await gasFwd.setVariable("accounts", facet.address); + expect(gasFwd.payForGas).to.have.been.calledWith(token.address, redeemAllRequest.gasFee); + expect(token.approve).to.have.been.calledWith(netInfoThis.gasReceiver, GAS_FEE); + expect(gasService.payGasForContractCall).to.have.been.calledWith( + facet.address, + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload, + token.address, + GAS_FEE, + endowDetails.owner + ); + expect(gateway.callContract).to.have.been.calledWith( + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload + ); + + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(INITIAL_LOCK_BAL); + expect(liqBal).to.equal(INITIAL_LIQ_BAL); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + }); }); - it("makes all the correct external calls", async function () { - let redeemAllRequest = { - ...DEFAULT_REDEEM_ALL_REQUEST, + [ + { redeemLocked: true, redeemLiquid: true, gasFee: GAS_FEE, - }; + gasFwdGas: GAS_FEE - 1, + expectedLockBal: INITIAL_LOCK_BAL - 1, + expectedLiqBal: INITIAL_LIQ_BAL, + description: + "paying out of locked balance takes precedence when gas fee is too small to split", + }, + { + redeemLocked: true, + redeemLiquid: true, + gasFee: 400, + gasFwdGas: 0, + expectedLockBal: INITIAL_LOCK_BAL - 200, + expectedLiqBal: INITIAL_LIQ_BAL - 200, + description: "both liquid and locked balances cover their respective gas fee portions", + }, + { + redeemLocked: true, + redeemLiquid: true, + gasFee: 400, + gasFwdGas: 79, + expectedLockBal: INITIAL_LOCK_BAL - 161, + expectedLiqBal: INITIAL_LIQ_BAL - 160, + description: "fractional percentages case", + }, + { + redeemLocked: true, + redeemLiquid: true, + gasFee: 1000, + gasFwdGas: 100, + prevLockBal: 400, + prevLiqBal: 600, + expectedLockBal: 0, + expectedLiqBal: 100, + description: "liquid covers part of locked portion of the gas fee", + }, + { + redeemLocked: true, + redeemLiquid: false, + gasFee: GAS_FEE, + gasFwdGas: GAS_FEE - 1, + expectedLockBal: INITIAL_LOCK_BAL - 1, + expectedLiqBal: INITIAL_LIQ_BAL, + description: + "paying out of locked balance takes precedence when gas fee is too small to split", + }, + { + redeemLocked: true, + redeemLiquid: false, + gasFee: 400, + gasFwdGas: 0, + expectedLockBal: INITIAL_LOCK_BAL - 200, + expectedLiqBal: INITIAL_LIQ_BAL - 200, + description: "both liquid and locked balances cover their respective gas fee portions", + }, + { + redeemLocked: true, + redeemLiquid: false, + gasFee: 400, + gasFwdGas: 79, + expectedLockBal: INITIAL_LOCK_BAL - 161, + expectedLiqBal: INITIAL_LIQ_BAL - 160, + description: "fractional percentages case", + }, + { + redeemLocked: true, + redeemLiquid: false, + gasFee: 1000, + gasFwdGas: 100, + prevLockBal: 400, + prevLiqBal: 600, + expectedLockBal: 0, + expectedLiqBal: 100, + description: "liquid covers part of locked portion of the gas fee", + }, + { + redeemLocked: false, + redeemLiquid: true, + gasFee: GAS_FEE, + gasFwdGas: GAS_FEE - 1, + expectedLockBal: INITIAL_LOCK_BAL - 1, + expectedLiqBal: INITIAL_LIQ_BAL, + description: + "paying out of locked balance takes precedence when gas fee is too small to split", + }, + { + redeemLocked: false, + redeemLiquid: true, + gasFee: 400, + gasFwdGas: 0, + expectedLockBal: INITIAL_LOCK_BAL - 200, + expectedLiqBal: INITIAL_LIQ_BAL - 200, + description: "both liquid and locked balances cover their respective gas fee portions", + }, + { + redeemLocked: false, + redeemLiquid: true, + gasFee: 400, + gasFwdGas: 79, + expectedLockBal: INITIAL_LOCK_BAL - 161, + expectedLiqBal: INITIAL_LIQ_BAL - 160, + description: "fractional percentages case", + }, + { + redeemLocked: false, + redeemLiquid: true, + gasFee: 1000, + gasFwdGas: 100, + prevLockBal: 400, + prevLiqBal: 600, + expectedLockBal: 0, + expectedLiqBal: 100, + description: "liquid covers part of locked portion of the gas fee", + }, + ].forEach((caseData) => { + it(`redeeming ${ + caseData.redeemLiquid && caseData.redeemLocked + ? "locked & liquid" + : caseData.redeemLiquid + ? "liquid" + : "locked" + } - makes all the correct external calls and pays for part of the gas fee: ${ + caseData.description + }`, async function () { + if (caseData.prevLockBal) { + await wait( + state.setEndowmentTokenBalance( + ACCOUNT_ID, + token.address, + caseData.prevLockBal, + caseData.prevLiqBal + ) + ); + } + + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { + ...DEFAULT_REDEEM_ALL_REQUEST, + redeemLocked: caseData.redeemLocked, + redeemLiquid: caseData.redeemLiquid, + gasFee: caseData.gasFee, + }; + + gasFwd.payForGas.returns(caseData.gasFwdGas); + + await expect(facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)).to.not.be.reverted; + + expect(gasFwd.payForGas).to.have.been.calledWith(token.address, redeemAllRequest.gasFee); + expect(token.approve).to.have.been.calledWith( + netInfoThis.gasReceiver, + redeemAllRequest.gasFee + ); + + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("redeemAll"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: redeemAllRequest.redeemLocked ? 1 : 0, + liqAmt: redeemAllRequest.redeemLiquid ? 1 : 0, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + expect(gasService.payGasForContractCall).to.have.been.calledWith( + facet.address, + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload, + token.address, + redeemAllRequest.gasFee, + endowDetails.owner + ); + expect(gateway.callContract).to.have.been.calledWith( + NET_NAME_THAT, + netInfoThat.router.toLowerCase(), + payload + ); + + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(caseData.expectedLockBal); + expect(liqBal).to.equal(caseData.expectedLiqBal); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + }); + }); + + describe("but reverts because", () => { + it("neither locked nor liquid balances can cover their respective gas fees", async () => { + const hugeGasFee = (INITIAL_LOCK_BAL + INITIAL_LIQ_BAL) * 2; + + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { + ...DEFAULT_REDEEM_ALL_REQUEST, + redeemLocked: true, + redeemLiquid: true, + gasFee: hugeGasFee, + }; + + await expect(facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)) + .to.be.revertedWithCustomError(facet, "InsufficientFundsForGas") + .withArgs(ACCOUNT_ID); + }); + + it("liquid balances can't cover locked gas deficit", async () => { + const hugeGasFee = INITIAL_LOCK_BAL + INITIAL_LIQ_BAL + 100; + + const redeemAllRequest: AccountMessages.RedeemAllRequestStruct = { + ...DEFAULT_REDEEM_ALL_REQUEST, + redeemLocked: true, + redeemLiquid: true, + gasFee: hugeGasFee, + }; + + await expect(facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)) + .to.be.revertedWithCustomError(facet, "InsufficientFundsForGas") + .withArgs(ACCOUNT_ID); + }); + }); + }); + }); + + describe("upon axelar callback", function () { + beforeEach(async () => { + gateway.validateContractCall.returns(true); + gateway.validateContractCallAndMint.returns(true); + + await wait( + state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, true) + ); + }); + + describe("into _execute", () => { + it("reverts if the call was not approved by Axelar gateway", async function () { + gateway.validateContractCall.returns(false); + + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); - let payload = packActionData({ - destinationChain: "ThatNet", + await expect( + facet.execute( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + router.address, + payload + ) + ).to.be.revertedWithCustomError(facet, "NotApprovedByGateway"); + }); + + it("reverts if the call didn't originate from the expected chain", async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("redeemAll"), + selector: vault.interface.getSighash("deposit"), accountIds: [ACCOUNT_ID], token: token.address, lockAmt: 1, liqAmt: 1, status: VaultActionStatus.UNPROCESSED, - }); + }; + const payload = packActionData(action, hre); + const returnedAction = convertVaultActionStructToArray(action); + const unexpectedChain = "NotThis"; - expect(await facet.strategyRedeemAll(ACCOUNT_ID, redeemAllRequest)) - .to.emit(gasReceiver, "GasPaid") - .withArgs( - facet.address, - "ThatNet", + await expect( + facet.execute( + ethers.utils.formatBytes32String("true"), + unexpectedChain, router.address, - payload, - token.address, - GAS_FEE, - gasFwd.address + payload ) - .to.emit(gateway, "ContractCall") - .withArgs("ThatNet", router.address, payload); + ) + .to.be.revertedWithCustomError(facet, "UnexpectedCaller") + .withArgs(returnedAction, unexpectedChain, router.address); + }); - let gasReceiverApproved = await token.allowance(facet.address, gasReceiver.address); - expect(gasReceiverApproved).to.equal(GAS_FEE); + it("reverts if the call didn't originate from the chain's router", async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, + }; + const payload = packActionData(action, hre); + const returnedAction = convertVaultActionStructToArray(action); + const notRouter = genWallet().address; + + await expect( + facet.execute(ethers.utils.formatBytes32String("true"), NET_NAME_THIS, notRouter, payload) + ) + .to.be.revertedWithCustomError(facet, "UnexpectedCaller") + .withArgs(returnedAction, NET_NAME_THIS, notRouter); }); - }); - }); - describe("upon axelar callback", async function () { - let facet: AccountsStrategy; - let state: TestFacetProxyContract; - let token: DummyERC20; - let gateway: DummyGateway; - const ACCOUNT_ID = 1; - - before(async function () { - token = await deployDummyERC20(owner); - gateway = await deployDummyGateway(owner); - await gateway.setTestTokenAddress(token.address); - - const thisNet = { - ...DEFAULT_NETWORK_INFO, - chainId: (await ethers.provider.getNetwork()).chainId, - axelarGateway: gateway.address, - }; - registrar.queryNetworkConnection.whenCalledWith("ThisNet").returns(thisNet); + it("successfully handles status == FAIL_TOKENS_FALLBACK", async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.FAIL_TOKENS_FALLBACK, + }; + const payload = packActionData(action, hre); + // const returnedAction = convertVaultActionStructToArray(action); - let stratParams = { - ...DEFAULT_STRATEGY_PARAMS, - network: "ThatNet", - approvalState: StrategyApprovalState.NOT_APPROVED, - }; - registrar.getStrategyParamsById.returns(stratParams); - }); + await expect( + facet.execute( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + router.address, + payload + ) + ).to.emit(facet, "RefundNeeded"); + // .withArgs(returnedAction); + // `chai` currently can't deep compare nested arrays in `.withArgs(...)` + // when checking emitted events + // see open issue https://github.com/NomicFoundation/hardhat/issues/3833 + }); - beforeEach(async function () { - state = await deployFacetAsProxy(hre, owner, admin, facetImpl.address); - facet = AccountsStrategy__factory.connect(state.address, owner); - const config = { - ...DEFAULT_ACCOUNTS_CONFIG, - networkName: "ThisNet", - registrarContract: registrar.address, - }; - await state.setConfig(config); + [ + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.POSITION_EXITED, + VaultActionStatus.SUCCESS, + VaultActionStatus.UNPROCESSED, + ].forEach((vaultActionStatus) => { + it(`reverts for response status: ${VaultActionStatus[vaultActionStatus]}`, async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: vaultActionStatus, + }; + const payload = packActionData(action, hre); + const returnedAction = convertVaultActionStructToArray(action); + await expect( + facet.execute( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + router.address, + payload + ) + ) + .to.be.revertedWithCustomError(facet, "UnexpectedResponse") + .withArgs(returnedAction); + }); + }); }); - it("reverts in _execute if the call didn't originate from the expected chain", async function () { - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: 1, - liqAmt: 1, - status: VaultActionStatus.UNPROCESSED, - }; - const payload = packActionData(action); - const returnedAction = convertVaultActionStructToArray(action); - await expect( - facet.execute(ethers.utils.formatBytes32String("true"), "NotNet", owner.address, payload) - ) - .to.be.revertedWithCustomError(facet, "UnexpectedCaller") - .withArgs(returnedAction, "NotNet", owner.address); - }); + describe("into _executeWithToken", () => { + it("reverts if the call was not approved by Axelar gateway", async function () { + gateway.validateContractCallAndMint.returns(false); - it("reverts in _executeWithToken if the call didn't originate from the expected chain", async function () { - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: 1, - liqAmt: 1, - status: VaultActionStatus.UNPROCESSED, - }; - const payload = packActionData(action); - const returnedAction = convertVaultActionStructToArray(action); - await expect( - facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "NotNet", - owner.address, - payload, - "TKN", - 1 - ) - ) - .to.be.revertedWithCustomError(facet, "UnexpectedCaller") - .withArgs(returnedAction, "NotNet", owner.address); - }); + const payload = packActionData( + { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, + }, + hre + ); + await expect( + facet.executeWithToken( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + owner.address, + payload, + await token.symbol(), + 1 + ) + ).to.be.revertedWithCustomError(facet, "NotApprovedByGateway"); + }); - it("reverts in _execute if the call didn't originate from the chain's router", async function () { - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: 1, - liqAmt: 1, - status: VaultActionStatus.UNPROCESSED, - }; - const payload = packActionData(action); - const returnedAction = convertVaultActionStructToArray(action); - await expect( - facet.execute(ethers.utils.formatBytes32String("true"), "ThatNet", owner.address, payload) - ) - .to.be.revertedWithCustomError(facet, "UnexpectedCaller") - .withArgs(returnedAction, "ThatNet", owner.address); - }); + it("reverts if the call didn't originate from the expected chain", async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, + }; + const payload = packActionData(action, hre); + const returnedAction = convertVaultActionStructToArray(action); + const unexpectedChain = "NotThis"; - it("reverts in _executeWithToken if the call didn't originate from the expected chain", async function () { - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: 1, - liqAmt: 1, - status: VaultActionStatus.UNPROCESSED, - }; - const payload = packActionData(action); - const returnedAction = convertVaultActionStructToArray(action); - await expect( - facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "ThatNet", - owner.address, - payload, - "TKN", - 1 + await expect( + facet.executeWithToken( + ethers.utils.formatBytes32String("true"), + unexpectedChain, + router.address, + payload, + await token.symbol(), + 1 + ) ) - ) - .to.be.revertedWithCustomError(facet, "UnexpectedCaller") - .withArgs(returnedAction, "ThatNet", owner.address); - }); + .to.be.revertedWithCustomError(facet, "UnexpectedCaller") + .withArgs(returnedAction, unexpectedChain, router.address); + }); - it("_execute successfully handles status == FAIL_TOKENS_FALLBACK", async function () { - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: 1, - liqAmt: 1, - status: VaultActionStatus.FAIL_TOKENS_FALLBACK, - }; - const payload = packActionData(action); - const returnedAction = convertVaultActionStructToArray(action); - expect( - await facet.execute( - ethers.utils.formatBytes32String("true"), - "ThatNet", - router.address, - payload + it("reverts if the call didn't originate from the expected chain", async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, + }; + const payload = packActionData(action, hre); + const returnedAction = convertVaultActionStructToArray(action); + await expect( + facet.executeWithToken( + ethers.utils.formatBytes32String("true"), + NET_NAME_THAT, + owner.address, + payload, + await token.symbol(), + 1 + ) ) - ) - .to.emit(facet, "RefundNeeded") - .withArgs(returnedAction); - }); + .to.be.revertedWithCustomError(facet, "UnexpectedCaller") + .withArgs(returnedAction, NET_NAME_THAT, owner.address); + }); - it("_execute reverts for any other status", async function () { - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: 1, - liqAmt: 1, - status: VaultActionStatus.UNPROCESSED, - }; - const payload = packActionData(action); - const returnedAction = convertVaultActionStructToArray(action); - await expect( - facet.execute(ethers.utils.formatBytes32String("true"), "ThatNet", router.address, payload) - ) - .to.be.revertedWithCustomError(facet, "UnexpectedResponse") - .withArgs(returnedAction); - }); + it("reverts if the call didn't originate from the chain's router", async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: 1, + liqAmt: 1, + status: VaultActionStatus.UNPROCESSED, + }; + const payload = packActionData(action, hre); + const returnedAction = convertVaultActionStructToArray(action); + const notRouter = genWallet().address; - it("_executeWithToken: deposit && FAIL_TOKENS_RETURNED", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.FAIL_TOKENS_RETURNED, - }; - const payload = packActionData(action); - await facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "ThatNet", - router.address, - payload, - "TKN", - 1 - ); - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - }); + await expect( + facet.executeWithToken( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + notRouter, + payload, + await token.symbol(), + 1 + ) + ) + .to.be.revertedWithCustomError(facet, "UnexpectedCaller") + .withArgs(returnedAction, NET_NAME_THIS, notRouter); + }); - it("_executeWithToken: redeem && SUCCESS", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("redeem"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.SUCCESS, - }; - const payload = packActionData(action); - await facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "ThatNet", - router.address, - payload, - "TKN", - 1 - ); - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - }); + it("succeeds: deposit && FAIL_TOKENS_RETURNED", async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash("deposit"), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: VaultActionStatus.FAIL_TOKENS_RETURNED, + }; + const payload = packActionData(action, hre); + await expect( + facet.executeWithToken( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + router.address, + payload, + await token.symbol(), + 1 + ) + ).to.not.be.reverted; + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(INITIAL_LOCK_BAL + LOCK_AMT); + expect(liqBal).to.equal(INITIAL_LIQ_BAL + LIQ_AMT); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + }); - it("_executeWithToken: redeemAll && SUCCESS", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("redeemAll"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.SUCCESS, - }; - const payload = packActionData(action); - await facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "ThatNet", - router.address, - payload, - "TKN", - 1 - ); - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - }); + const cases: {vaultFunction: keyof IVault["functions"]; vaultStatus: VaultActionStatus}[] = [ + {vaultFunction: "redeem", vaultStatus: VaultActionStatus.SUCCESS}, + {vaultFunction: "redeem", vaultStatus: VaultActionStatus.POSITION_EXITED}, + {vaultFunction: "redeemAll", vaultStatus: VaultActionStatus.SUCCESS}, + {vaultFunction: "redeemAll", vaultStatus: VaultActionStatus.POSITION_EXITED}, + ]; + cases.forEach(({vaultFunction, vaultStatus}) => { + it(`succeeds: ${vaultFunction} && ${VaultActionStatus[vaultStatus]}`, async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash(vaultFunction), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: vaultStatus, + }; + const payload = packActionData(action, hre); + + await expect( + facet.executeWithToken( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + router.address, + payload, + await token.symbol(), + 1 + ) + ) + .to.emit(facet, "EndowmentRedeemed") + .withArgs(ACCOUNT_ID, vaultStatus); - it("_executeWithToken: redeem && POSITION_EXITED", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("redeem"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.POSITION_EXITED, - }; - const payload = packActionData(action); - await state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, true); - - await facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "ThatNet", - router.address, - payload, - "TKN", - 1 - ); - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - let strategyActive = await state.getActiveStrategyEndowmentState( - ACCOUNT_ID, - DEFAULT_STRATEGY_SELECTOR - ); - expect(strategyActive).to.be.false; + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(INITIAL_LOCK_BAL + LOCK_AMT); + expect(liqBal).to.equal(INITIAL_LIQ_BAL + LIQ_AMT); + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.equal(vaultStatus !== VaultActionStatus.POSITION_EXITED); + }); + }); }); - it("_executeWithToken: redeemAll && POSITION_EXITED", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("redeemAll"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.POSITION_EXITED, - }; - const payload = packActionData(action); - await state.setActiveStrategyEndowmentState(ACCOUNT_ID, DEFAULT_STRATEGY_SELECTOR, true); - - await facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "ThatNet", - router.address, - payload, - "TKN", - 1 - ); - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(LOCK_AMT); - expect(liqBal).to.equal(LIQ_AMT); - let strategyActive = await state.getActiveStrategyEndowmentState( - ACCOUNT_ID, - DEFAULT_STRATEGY_SELECTOR - ); - expect(!strategyActive); - }); + // keys of the map are IVault functions, while their respective values are all + // the VaultActionStatus values for which `_refundFallback` gets triggered + const caseData: {[key in keyof IVault["functions"]]: VaultActionStatus[]} = { + deposit: [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.POSITION_EXITED, + VaultActionStatus.SUCCESS, + VaultActionStatus.UNPROCESSED, + ], + getVaultConfig: [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.POSITION_EXITED, + VaultActionStatus.SUCCESS, + VaultActionStatus.UNPROCESSED, + ], + harvest: [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.POSITION_EXITED, + VaultActionStatus.SUCCESS, + VaultActionStatus.UNPROCESSED, + ], + redeem: [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.UNPROCESSED, + ], + redeemAll: [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.UNPROCESSED, + ], + setVaultConfig: [ + VaultActionStatus.FAIL_TOKENS_FALLBACK, + VaultActionStatus.FAIL_TOKENS_RETURNED, + VaultActionStatus.POSITION_EXITED, + VaultActionStatus.SUCCESS, + VaultActionStatus.UNPROCESSED, + ], + }; + Object.entries(caseData).forEach(([vaultFunction, unmatchedStatuses]) => { + unmatchedStatuses.forEach((vaultStatus) => { + it(`into _refundFallback succeeds: ${vaultFunction} && ${VaultActionStatus[vaultStatus]}`, async function () { + const action: IVaultStrategy.VaultActionDataStruct = { + destinationChain: NET_NAME_THAT, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: vault.interface.getSighash(vaultFunction), + accountIds: [ACCOUNT_ID], + token: token.address, + lockAmt: LOCK_AMT, + liqAmt: LIQ_AMT, + status: vaultStatus, + }; + const payload = packActionData(action, hre); + // const returnedAction = convertVaultActionStructToArray(action); - it("_refundFallback", async function () { - const LOCK_AMT = 300; - const LIQ_AMT = 200; - const action = { - destinationChain: "ThatNet", - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: vault.interface.getSighash("deposit"), - accountIds: [ACCOUNT_ID], - token: token.address, - lockAmt: LOCK_AMT, - liqAmt: LIQ_AMT, - status: VaultActionStatus.UNPROCESSED, - }; - const payload = packActionData(action); - const returnedAction = convertVaultActionStructToArray(action); + const apParams: LocalRegistrarLib.AngelProtocolParamsStruct = { + ...DEFAULT_AP_PARAMS, + refundAddr: user.address, + }; + registrar.getAngelProtocolParams.returns(apParams); + + await expect( + facet.executeWithToken( + ethers.utils.formatBytes32String("true"), + NET_NAME_THIS, + router.address, + payload, + await token.symbol(), + LOCK_AMT + LIQ_AMT + ) + ).to.emit(facet, "RefundNeeded"); + // .withArgs(returnedAction); + // `chai` currently can't deep compare nested arrays in `.withArgs(...)` + // when checking emitted events + // see open issue https://github.com/NomicFoundation/hardhat/issues/3833 - const apParams = { - ...DEFAULT_AP_PARAMS, - refundAddr: user.address, - }; - registrar.getAngelProtocolParams.returns(apParams); - - await token.mint(facet.address, LOCK_AMT + LIQ_AMT); - expect( - await facet.executeWithToken( - ethers.utils.formatBytes32String("true"), - "ThatNet", - router.address, - payload, - "TKN", - LOCK_AMT + LIQ_AMT - ) - ) - .to.emit(facet, "RefundNeeded") - .withArgs(returnedAction); - const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); - expect(lockBal).to.equal(0); - expect(liqBal).to.equal(0); - let userBal = await token.balanceOf(user.address); - expect(userBal).to.equal(LOCK_AMT + LIQ_AMT); + const [lockBal, liqBal] = await state.getEndowmentTokenBalance(ACCOUNT_ID, token.address); + expect(lockBal).to.equal(INITIAL_LOCK_BAL); + expect(liqBal).to.equal(INITIAL_LIQ_BAL); + + const strategyActive = await state.getActiveStrategyEndowmentState( + ACCOUNT_ID, + DEFAULT_STRATEGY_SELECTOR + ); + expect(strategyActive).to.be.true; + + expect(token.transfer).to.have.been.calledWith(apParams.refundAddr, LOCK_AMT + LIQ_AMT); + }); + }); }); }); }); diff --git a/test/core/accounts/AccountsSwapRouter.ts b/test/core/accounts/AccountsSwapRouter.ts index aeee3f84b..3b5ae2945 100644 --- a/test/core/accounts/AccountsSwapRouter.ts +++ b/test/core/accounts/AccountsSwapRouter.ts @@ -8,6 +8,7 @@ import { DEFAULT_CHARITY_ENDOWMENT, DEFAULT_PERMISSIONS_STRUCT, DEFAULT_REGISTRAR_CONFIG, + wait, } from "test/utils"; import { AccountsSwapRouter, @@ -25,6 +26,7 @@ import { } from "typechain-types"; import {VaultType, genWallet, getSigners} from "utils"; import {deployFacetAsProxy} from "./utils"; +import {time} from "@nomicfoundation/hardhat-network-helpers"; use(smock.matchers); @@ -73,7 +75,7 @@ describe("AccountsSwapRouter", function () { ...DEFAULT_ACCOUNTS_CONFIG, registrarContract: registrar.address, }; - await state.setConfig(config); + await wait(state.setConfig(config)); facet = AccountsSwapRouter__factory.connect(state.address, owner); }); @@ -269,7 +271,7 @@ describe("AccountsSwapRouter", function () { addr: user.address, }, }; - await state.setEndowmentDetails(ACCOUNT_ID, endow); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endow)); await expect( facet .connect(user) @@ -292,7 +294,7 @@ describe("AccountsSwapRouter", function () { addr: user.address, }, }; - await state.setEndowmentDetails(ACCOUNT_ID, endow); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endow)); await expect( facet @@ -312,10 +314,10 @@ describe("AccountsSwapRouter", function () { ...DEFAULT_CHARITY_ENDOWMENT, owner: owner.address, }; - await state.setEndowmentDetails(ACCOUNT_ID, endow); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endow)); registrar.queryTokenPriceFeed.returns(ethers.constants.AddressZero); let token = genWallet().address; - await state.setEndowmentTokenBalance(ACCOUNT_ID, token, 100, 100); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token, 100, 100)); await expect( facet.swapToken(ACCOUNT_ID, VaultType.LIQUID, token, 1, genWallet().address, 1) @@ -337,9 +339,9 @@ describe("AccountsSwapRouter", function () { ...DEFAULT_CHARITY_ENDOWMENT, owner: owner.address, }; - await state.setEndowmentDetails(ACCOUNT_ID, endow); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endow)); registrar.queryTokenPriceFeed.returns(chainlink.address); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, 100, 100); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token.address, 100, 100)); await expect( facet.swapToken(ACCOUNT_ID, VaultType.LIQUID, token.address, 1, genWallet().address, 1) @@ -366,13 +368,13 @@ describe("AccountsSwapRouter", function () { }; token1 = await deployDummyERC20(owner); token2 = await deployDummyERC20(owner); - await state.setEndowmentDetails(ACCOUNT_ID, endow); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token1.address, AMT1, 0); + await wait(state.setEndowmentDetails(ACCOUNT_ID, endow)); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token1.address, AMT1, 0)); }); describe("revert cases", async function () { beforeEach(async function () { - await token1.mint(facet.address, AMT1); + await wait(token1.mint(facet.address, AMT1)); }); it("reverts if the price feed response is invalid", async function () { const ANSWER = 0; @@ -384,7 +386,8 @@ describe("AccountsSwapRouter", function () { it("reverts if the factory cant find a pool", async function () { const ANSWER = 1; - chainlink.latestRoundData.returns([0, ANSWER, 0, 0, 0]); + let currTime = await time.latest(); + chainlink.latestRoundData.returns([0, ANSWER, 0, currTime, 0]); await expect( facet.swapToken(ACCOUNT_ID, VaultType.LOCKED, token1.address, AMT1, token2.address, 1) ).to.be.revertedWith("No pool found to swap"); @@ -392,9 +395,10 @@ describe("AccountsSwapRouter", function () { it("reverts if output is less than expected", async function () { const ANSWER = 1; - chainlink.latestRoundData.returns([0, ANSWER, 0, 0, 0]); - await uniswapFactory.setPool(genWallet().address); - await uniswapRouter.setOutputValue(0); + let currTime = await time.latest(); + chainlink.latestRoundData.returns([0, ANSWER, 0, currTime, 0]); + await wait(uniswapFactory.setPool(genWallet().address)); + await wait(uniswapRouter.setOutputValue(0)); await expect( facet.swapToken(ACCOUNT_ID, VaultType.LOCKED, token1.address, AMT1, token2.address, 1) ).to.be.revertedWith("Output funds less than the minimum output"); @@ -406,28 +410,22 @@ describe("AccountsSwapRouter", function () { const AMT2 = 1000; beforeEach(async function () { const ANSWER = 1; - chainlink.latestRoundData.returns([0, ANSWER, 0, 0, 0]); - await uniswapFactory.setPool(genWallet().address); - await uniswapRouter.setOutputValue(AMT2); + let currTime = await time.latest(); + chainlink.latestRoundData.returns([0, ANSWER, 0, currTime, 0]); + await wait(uniswapFactory.setPool(genWallet().address)); + await wait(uniswapRouter.setOutputValue(AMT2)); }); it("swaps and updates the locked balance successfully", async function () { - await token1.mint(facet.address, AMT1); - await token2.mint(uniswapRouter.address, AMT2); - await uniswapRouter.setOutputValue(AMT2); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token1.address, AMT1, 0); - expect( - await facet.swapToken( - ACCOUNT_ID, - VaultType.LOCKED, - token1.address, - AMT1, - token2.address, - 1 - ) + await wait(token1.mint(facet.address, AMT1)); + await wait(token2.mint(uniswapRouter.address, AMT2)); + await wait(uniswapRouter.setOutputValue(AMT2)); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token1.address, AMT1, 0)); + await expect( + facet.swapToken(ACCOUNT_ID, VaultType.LOCKED, token1.address, AMT1, token2.address, 1) ) .to.emit(facet, "TokenSwapped") - .withArgs([ACCOUNT_ID, VaultType.LOCKED, token1.address, AMT1, token2.address, AMT2]); + .withArgs(ACCOUNT_ID, VaultType.LOCKED, token1.address, AMT1, token2.address, AMT2); const [lockBal_token1, liqBal_token1] = await state.getEndowmentTokenBalance( ACCOUNT_ID, @@ -444,21 +442,14 @@ describe("AccountsSwapRouter", function () { }); it("swaps and updates the liquid balance successfully", async function () { - await token1.mint(facet.address, AMT1); - await token2.mint(uniswapRouter.address, AMT2); - await state.setEndowmentTokenBalance(ACCOUNT_ID, token1.address, 0, AMT1); - expect( - await facet.swapToken( - ACCOUNT_ID, - VaultType.LIQUID, - token1.address, - AMT1, - token2.address, - 1 - ) + await wait(token1.mint(facet.address, AMT1)); + await wait(token2.mint(uniswapRouter.address, AMT2)); + await wait(state.setEndowmentTokenBalance(ACCOUNT_ID, token1.address, 0, AMT1)); + await expect( + facet.swapToken(ACCOUNT_ID, VaultType.LIQUID, token1.address, AMT1, token2.address, 1) ) .to.emit(facet, "TokenSwapped") - .withArgs([ACCOUNT_ID, VaultType.LOCKED, token1.address, AMT1, token2.address, AMT2]); + .withArgs(ACCOUNT_ID, VaultType.LIQUID, token1.address, AMT1, token2.address, AMT2); const [lockBal_token1, liqBal_token1] = await state.getEndowmentTokenBalance( ACCOUNT_ID, diff --git a/test/core/accounts/AccountsUpdate.ts b/test/core/accounts/AccountsUpdate.ts index f062a07d9..574f740bc 100644 --- a/test/core/accounts/AccountsUpdate.ts +++ b/test/core/accounts/AccountsUpdate.ts @@ -2,9 +2,9 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect} from "chai"; import hre from "hardhat"; import {AccountsUpdate, AccountsUpdate__factory, TestFacetProxyContract} from "typechain-types"; -import {LibAccounts} from "typechain-types/contracts/core/accounts/facets/AccountsCreateEndowment"; import {getSigners} from "utils"; import {deployFacetAsProxy} from "./utils"; +import {wait} from "test/utils"; describe("AccountsUpdate", function () { const {ethers} = hre; @@ -32,21 +32,23 @@ describe("AccountsUpdate", function () { let facetImpl = await Facet.deploy(); state = await deployFacetAsProxy(hre, owner, proxyAdmin, facetImpl.address); - await state.setConfig({ - owner: owner.address, - version: "1", - networkName: "Polygon", - registrarContract: ethers.constants.AddressZero, - nextAccountId: 1, - reentrancyGuardLocked: false, - }); + await wait( + state.setConfig({ + owner: owner.address, + version: "1", + networkName: "Polygon", + registrarContract: ethers.constants.AddressZero, + nextAccountId: 1, + reentrancyGuardLocked: false, + }) + ); facet = AccountsUpdate__factory.connect(state.address, owner); }); describe("updateOwner", () => { it("should update the owner when called by the current owner", async () => { - expect(await facet.updateOwner(user.address)) + await expect(facet.updateOwner(user.address)) .to.emit(facet, "OwnerUpdated") .withArgs(user.address); @@ -72,7 +74,7 @@ describe("AccountsUpdate", function () { describe("updateConfig", () => { it("should update the config when called by the owner", async () => { - expect(await facet.updateConfig(newRegistrar)).to.emit(facet, "ConfigUpdated"); + await expect(facet.updateConfig(newRegistrar)).to.emit(facet, "ConfigUpdated"); const config = await state.getConfig(); diff --git a/test/core/accounts/AccountsUpdateEndowmentSettingsController.ts b/test/core/accounts/AccountsUpdateEndowmentSettingsController.ts index dbe9a7421..af286a74c 100644 --- a/test/core/accounts/AccountsUpdateEndowmentSettingsController.ts +++ b/test/core/accounts/AccountsUpdateEndowmentSettingsController.ts @@ -2,7 +2,7 @@ import {smock} from "@defi-wonderland/smock"; import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect, use} from "chai"; import hre from "hardhat"; -import {DEFAULT_CHARITY_ENDOWMENT} from "test/utils"; +import {DEFAULT_CHARITY_ENDOWMENT, wait} from "test/utils"; import { AccountsUpdateEndowmentSettingsController, AccountsUpdateEndowmentSettingsController__factory, @@ -57,19 +57,21 @@ describe("AccountsUpdateEndowmentSettingsController", function () { let facetImpl = await Facet.deploy(); state = await deployFacetAsProxy(hre, owner, proxyAdmin, facetImpl.address); - await state.setConfig({ - owner: owner.address, - version: "1", - networkName: "", - registrarContract: ethers.constants.AddressZero, - nextAccountId: 1, - reentrancyGuardLocked: false, - }); + await wait( + state.setConfig({ + owner: owner.address, + version: "1", + networkName: "", + registrarContract: ethers.constants.AddressZero, + nextAccountId: 1, + reentrancyGuardLocked: false, + }) + ); facet = AccountsUpdateEndowmentSettingsController__factory.connect(state.address, endowOwner); - await state.setEndowmentDetails(charityId, oldCharity); - await state.setEndowmentDetails(normalEndowId, oldNormalEndow); + await wait(state.setEndowmentDetails(charityId, oldCharity)); + await wait(state.setEndowmentDetails(normalEndowId, oldNormalEndow)); }); describe("updateEndowmentSettings", () => { @@ -95,10 +97,12 @@ describe("AccountsUpdateEndowmentSettingsController", function () { }); it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(normalEndowId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(normalEndowId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect(facet.updateEndowmentSettings(normalEndowReq)).to.be.revertedWith( "UpdatesAfterClosed" ); @@ -276,10 +280,12 @@ describe("AccountsUpdateEndowmentSettingsController", function () { }; it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(charityReq.id, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(charityReq.id, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect(facet.updateEndowmentController(charityReq)).to.be.revertedWith( "UpdatesAfterClosed" ); @@ -558,10 +564,12 @@ describe("AccountsUpdateEndowmentSettingsController", function () { }); it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(request.id, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(request.id, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect(facet.updateFeeSettings(request)).to.be.revertedWith("UpdatesAfterClosed"); }); diff --git a/test/core/accounts/AccountsUpdateEndowments.ts b/test/core/accounts/AccountsUpdateEndowments.ts index c4415bf02..99c0858a3 100644 --- a/test/core/accounts/AccountsUpdateEndowments.ts +++ b/test/core/accounts/AccountsUpdateEndowments.ts @@ -3,7 +3,7 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {expect, use} from "chai"; import {BigNumberish} from "ethers"; import hre from "hardhat"; -import {DEFAULT_CHARITY_ENDOWMENT} from "test/utils"; +import {DEFAULT_CHARITY_ENDOWMENT, wait} from "test/utils"; import { AccountsUpdateEndowments, AccountsUpdateEndowments__factory, @@ -66,8 +66,8 @@ describe("AccountsUpdateEndowments", function () { facet = AccountsUpdateEndowments__factory.connect(state.address, endowOwner); - await state.setEndowmentDetails(charityId, oldCharity); - await state.setEndowmentDetails(normalEndowId, oldNormalEndow); + await wait(state.setEndowmentDetails(charityId, oldCharity)); + await wait(state.setEndowmentDetails(normalEndowId, oldNormalEndow)); }); describe("updateEndowmentDetails", () => { @@ -110,10 +110,12 @@ describe("AccountsUpdateEndowments", function () { }); it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(normalEndowId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(normalEndowId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect(facet.updateEndowmentDetails(normalEndowReq)).to.be.revertedWith( "UpdatesAfterClosed" ); @@ -387,10 +389,12 @@ describe("AccountsUpdateEndowments", function () { const newDelegateExpiry = 200; it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(normalEndowId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(normalEndowId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect( facet.updateDelegate( normalEndowId, @@ -581,10 +585,12 @@ describe("AccountsUpdateEndowments", function () { }); it("reverts if the endowment is closed", async () => { - await state.setClosingEndowmentState(normalEndowId, true, { - enumData: 0, - data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, - }); + await wait( + state.setClosingEndowmentState(normalEndowId, true, { + enumData: 0, + data: {addr: ethers.constants.AddressZero, endowId: 0, fundId: 0}, + }) + ); await expect( facet.updateAcceptedToken(normalEndowId, tokenAddr, priceFeedAddr, true) ).to.be.revertedWith("UpdatesAfterClosed"); @@ -670,14 +676,16 @@ describe("AccountsUpdateEndowments", function () { const registrarFake = await smock.fake(new Registrar__factory(), { address: genWallet().address, }); - await state.setConfig({ - networkName: "test", - owner: accOwner.address, - version: "1", - registrarContract: registrarFake.address, - nextAccountId: 1, - reentrancyGuardLocked: false, - }); + await wait( + state.setConfig({ + networkName: "test", + owner: accOwner.address, + version: "1", + registrarContract: registrarFake.address, + nextAccountId: 1, + reentrancyGuardLocked: false, + }) + ); return registrarFake; } }); diff --git a/test/core/accounts/AccountsUpdateStatusEndowments.ts b/test/core/accounts/AccountsUpdateStatusEndowments.ts index 3aa262448..62737840f 100644 --- a/test/core/accounts/AccountsUpdateStatusEndowments.ts +++ b/test/core/accounts/AccountsUpdateStatusEndowments.ts @@ -7,11 +7,11 @@ import { DEFAULT_CHARITY_ENDOWMENT, DEFAULT_REGISTRAR_CONFIG, DEFAULT_STRATEGY_SELECTOR, + wait, } from "test/utils"; import { AccountsUpdateStatusEndowments, AccountsUpdateStatusEndowments__factory, - IIndexFund, IndexFund, IndexFund__factory, Registrar, @@ -82,15 +82,17 @@ describe("AccountsUpdateStatusEndowments", function () { registrarFake.queryAllStrategies.returns(strategies); - await state.setEndowmentDetails(accountId, endowment); - await state.setConfig({ - owner: accOwner.address, - version: "1", - networkName: "Polygon", - registrarContract: registrarFake.address, - nextAccountId: accountId + 1, - reentrancyGuardLocked: false, - }); + await wait(state.setEndowmentDetails(accountId, endowment)); + await wait( + state.setConfig({ + owner: accOwner.address, + version: "1", + networkName: "Polygon", + registrarContract: registrarFake.address, + nextAccountId: accountId + 1, + reentrancyGuardLocked: false, + }) + ); facet = AccountsUpdateStatusEndowments__factory.connect(state.address, endowOwner); }); @@ -103,7 +105,7 @@ describe("AccountsUpdateStatusEndowments", function () { }); it("reverts if the endowment is already closed", async () => { - await state.setClosingEndowmentState(accountId, true, beneficiary); + await wait(state.setClosingEndowmentState(accountId, true, beneficiary)); await expect(facet.closeEndowment(accountId, beneficiary)).to.be.revertedWith( "Endowment is closed" ); @@ -120,7 +122,7 @@ describe("AccountsUpdateStatusEndowments", function () { }); it("reverts if the endowment has active strategies", async () => { - await state.setActiveStrategyEndowmentState(accountId, strategies[0], true); + await wait(state.setActiveStrategyEndowmentState(accountId, strategies[0], true)); await expect(facet.closeEndowment(accountId, beneficiary)).to.be.revertedWith( "Not fully exited" ); @@ -184,7 +186,7 @@ describe("AccountsUpdateStatusEndowments", function () { }); it("sets the active state to false for the specified strategy", async function () { - await state.setActiveStrategyEndowmentState(accountId, DEFAULT_STRATEGY_SELECTOR, true); + await wait(state.setActiveStrategyEndowmentState(accountId, DEFAULT_STRATEGY_SELECTOR, true)); await facet .connect(endowOwner) .forceSetStrategyInactive(accountId, DEFAULT_STRATEGY_SELECTOR); diff --git a/test/core/gasFwd/GasFwd.ts b/test/core/gasFwd/GasFwd.ts index 792f39c9d..f6ab69b88 100644 --- a/test/core/gasFwd/GasFwd.ts +++ b/test/core/gasFwd/GasFwd.ts @@ -1,22 +1,32 @@ +import {FakeContract, smock} from "@defi-wonderland/smock"; import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; -import {expect} from "chai"; +import {expect, use} from "chai"; import hre from "hardhat"; +import { + GasFwd, + GasFwd__factory, + IERC20, + IERC20__factory, + ProxyContract__factory, +} from "typechain-types"; import {getSigners} from "utils"; -import {GasFwd__factory, GasFwd, ProxyContract__factory, DummyERC20} from "typechain-types"; -import {deployDummyERC20} from "test/utils"; + +use(smock.matchers); describe("GasFwd", function () { - const {ethers, upgrades} = hre; const BALANCE = 1000; let owner: SignerWithAddress; let admin: SignerWithAddress; - let user: SignerWithAddress; + let accounts: SignerWithAddress; + + let token: FakeContract; + let gasFwd: GasFwd; async function deployGasFwdAsProxy( owner: SignerWithAddress, admin: SignerWithAddress, - accounts?: SignerWithAddress + accounts: SignerWithAddress ): Promise { let GasFwd = new GasFwd__factory(admin); let gasFwdImpl = await GasFwd.deploy(); @@ -27,82 +37,78 @@ describe("GasFwd", function () { let proxyFactory = new ProxyContract__factory(owner); let proxy = await proxyFactory.deploy(gasFwdImpl.address, admin.address, data); await proxy.deployed(); - return GasFwd__factory.connect(proxy.address, owner); + return GasFwd__factory.connect(proxy.address, accounts); } before(async function () { const {deployer, proxyAdmin, apTeam1} = await getSigners(hre); owner = deployer; admin = proxyAdmin; - user = apTeam1; + accounts = apTeam1; }); - describe("upon deployment", async function () { - it("can be deployed as a proxy", async function () { - expect(await deployGasFwdAsProxy(owner, admin)); - }); + beforeEach(async function () { + token = await smock.fake(IERC20__factory.createInterface()); + gasFwd = await deployGasFwdAsProxy(owner, admin, accounts); + token.balanceOf.returns(BALANCE); + token.transfer.returns(true); }); describe("upon payForGas", async function () { - let token: DummyERC20; - let gasFwd: GasFwd; - beforeEach(async function () { - token = await deployDummyERC20(owner); - gasFwd = await deployGasFwdAsProxy(owner, admin, user); - }); it("reverts if called by non-accounts contract", async function () { - await expect(gasFwd.payForGas(token.address, 1)).to.be.revertedWithCustomError( + await expect(gasFwd.connect(owner).payForGas(token.address, 1)).to.be.revertedWithCustomError( gasFwd, "OnlyAccounts" ); }); it("nothing happens when requested amount is 0", async function () { - await token.mint(gasFwd.address, BALANCE); - await expect(gasFwd.connect(user).payForGas(token.address, 0)).to.not.emit(gasFwd, "GasPay"); - expect(await token.balanceOf(user.address)).to.equal(0); - expect(await token.balanceOf(gasFwd.address)).to.equal(BALANCE); + await expect(gasFwd.payForGas(token.address, 0)).to.not.emit(gasFwd, "GasPay"); + expect(token.transfer).to.not.be.called; }); it("nothing happens when current balance is 0", async function () { - await expect(gasFwd.connect(user).payForGas(token.address, 1)).to.not.emit(gasFwd, "GasPay"); - let balance = await token.balanceOf(user.address); - expect(balance).to.equal(0); + token.balanceOf.returns(0); + await expect(gasFwd.payForGas(token.address, 1)).to.not.emit(gasFwd, "GasPay"); + expect(token.transfer).to.not.be.called; }); it("transfers tokens which do not exceed the balance", async function () { - await token.mint(gasFwd.address, BALANCE); - await gasFwd.connect(user).payForGas(token.address, 1); - let balance = await token.balanceOf(user.address); - expect(balance).to.equal(1); + const amount = 1; + await expect(gasFwd.payForGas(token.address, amount)) + .to.emit(gasFwd, "GasPay") + .withArgs(token.address, amount); + expect(token.transfer).to.have.been.calledWith(accounts.address, amount); + }); + it("transfers tokens when amount to transfer is equal to balance", async function () { + await expect(gasFwd.payForGas(token.address, BALANCE)) + .to.emit(gasFwd, "GasPay") + .withArgs(token.address, BALANCE); + expect(token.transfer).to.have.been.calledWith(accounts.address, BALANCE); }); it("transfers tokens when the call exceeds the balance", async function () { - await token.mint(gasFwd.address, BALANCE); - await gasFwd.connect(user).payForGas(token.address, BALANCE + 1); - let balance = await token.balanceOf(user.address); - expect(balance).to.equal(BALANCE); + await expect(gasFwd.payForGas(token.address, BALANCE + 1)) + .to.emit(gasFwd, "GasPay") + .withArgs(token.address, BALANCE); + expect(token.transfer).to.have.been.calledWith(accounts.address, BALANCE); }); }); describe("upon sweep", async function () { - let token: DummyERC20; - let gasFwd: GasFwd; - beforeEach(async function () { - token = await deployDummyERC20(owner); - gasFwd = await deployGasFwdAsProxy(owner, admin, user); - }); it("reverts if called by non-accounts contract", async function () { - await expect(gasFwd.sweep(token.address)).to.be.revertedWithCustomError( + await expect(gasFwd.connect(owner).sweep(token.address)).to.be.revertedWithCustomError( gasFwd, "OnlyAccounts" ); }); it("nothing happens if there's no balance to sweep", async () => { - await expect(gasFwd.connect(user).sweep(token.address)).to.not.emit(gasFwd, "Sweep"); - expect(await token.balanceOf(user.address)).to.equal(0); + token.balanceOf.returns(0); + await expect(gasFwd.sweep(token.address)).to.not.emit(gasFwd, "Sweep"); + expect(token.transfer).to.not.be.called; }); it("transfers the token balance", async function () { - await token.mint(gasFwd.address, BALANCE); - await gasFwd.connect(user).sweep(token.address); - let balance = await token.balanceOf(user.address); - expect(balance).to.equal(BALANCE); + token.transfer.returns(true); + await expect(gasFwd.sweep(token.address)) + .to.emit(gasFwd, "Sweep") + .withArgs(token.address, BALANCE); + expect(token.transfer).to.have.been.calledWith(accounts.address, BALANCE); }); }); }); diff --git a/test/core/registrar/LocalRegistrar.ts b/test/core/registrar/LocalRegistrar.ts index 67a059989..06c8478ee 100644 --- a/test/core/registrar/LocalRegistrar.ts +++ b/test/core/registrar/LocalRegistrar.ts @@ -104,7 +104,7 @@ describe("Local Registrar", function () { it("Should accept and set the values", async function () { let newValues = defaultRebalParams; newValues.rebalanceLiquidProfits = true; - await registrar.setRebalanceParams(newValues); + await expect(registrar.setRebalanceParams(newValues)).to.not.be.reverted; let returnedValues = await registrar.getRebalanceParams(); expect(returnedValues.rebalanceLiquidProfits).to.equal(newValues.rebalanceLiquidProfits); }); @@ -119,7 +119,7 @@ describe("Local Registrar", function () { it("Should accept and set the values", async function () { let newValues = defaultApParams; newValues.routerAddr = user.address; - await registrar.setAngelProtocolParams(newValues); + await expect(registrar.setAngelProtocolParams(newValues)).to.not.be.reverted; let returnedValues = await registrar.getAngelProtocolParams(); expect(returnedValues.routerAddr).to.equal(newValues.routerAddr); }); @@ -135,10 +135,9 @@ describe("Local Registrar", function () { }); it("Should accept and set the values", async function () { - await registrar.setUniswapAddresses( - mockUniswapAddresses.router, - mockUniswapAddresses.factory - ); + await expect( + registrar.setUniswapAddresses(mockUniswapAddresses.router, mockUniswapAddresses.factory) + ).to.not.be.reverted; let newFactoryAddr = await registrar.getUniswapFactoryAddress(); let newRouterAddr = await registrar.getUniswapRouterAddress(); expect(newFactoryAddr).to.equal(mockUniswapAddresses.factory); @@ -156,7 +155,9 @@ describe("Local Registrar", function () { }); it("Should accept and set the new value", async function () { - await registrar.setAccountsContractAddressByChain(originatingChain, accountsContract); + await expect( + registrar.setAccountsContractAddressByChain(originatingChain, accountsContract) + ).to.not.be.reverted; let storedAddressString = await registrar.getAccountsContractAddressByChain( originatingChain ); @@ -175,7 +176,7 @@ describe("Local Registrar", function () { }); it("Should accept and set the new value", async function () { - await registrar.setTokenAccepted(user.address, true); + await expect(registrar.setTokenAccepted(user.address, true)).to.not.be.reverted; let returnedValue = await registrar.isTokenAccepted(user.address); expect(returnedValue).to.be.true; }); @@ -187,7 +188,7 @@ describe("Local Registrar", function () { }); it("Should accept and set the new value", async function () { - await registrar.setGasByToken(user.address, 1); + await expect(registrar.setGasByToken(user.address, 1)).to.not.be.reverted; let returnedValue = await registrar.getGasByToken(user.address); expect(returnedValue.toNumber()).to.equal(1); }); @@ -280,13 +281,15 @@ describe("Local Registrar", function () { }); it("Should accept and set new values", async function () { - await registrar.setStrategyParams( - strategyId, - strategyParams.network, - strategyParams.Locked.vaultAddr, - strategyParams.Liquid.vaultAddr, - strategyParams.approvalState - ); + await expect( + registrar.setStrategyParams( + strategyId, + strategyParams.network, + strategyParams.Locked.vaultAddr, + strategyParams.Liquid.vaultAddr, + strategyParams.approvalState + ) + ).to.not.be.reverted; let returnedValue = await registrar.getStrategyParamsById(strategyId); expect(returnedValue.network).to.equal(strategyParams.network); expect(returnedValue.approvalState).to.equal(strategyParams.approvalState); @@ -297,16 +300,23 @@ describe("Local Registrar", function () { }); it("Should let the owner change the approval state", async function () { - await registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.APPROVED); + await expect(registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.APPROVED)) + .to.not.be.reverted; let returnedValue = await registrar.getStrategyApprovalState(strategyId); expect(returnedValue).to.equal(StrategyApprovalState.APPROVED); - await registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.WITHDRAW_ONLY); + await expect( + registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.WITHDRAW_ONLY) + ).to.not.be.reverted; returnedValue = await registrar.getStrategyApprovalState(strategyId); expect(returnedValue).to.equal(StrategyApprovalState.WITHDRAW_ONLY); - await registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.DEPRECATED); + await expect( + registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.DEPRECATED) + ).to.not.be.reverted; returnedValue = await registrar.getStrategyApprovalState(strategyId); expect(returnedValue).to.equal(StrategyApprovalState.DEPRECATED); - await registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.NOT_APPROVED); + await expect( + registrar.setStrategyApprovalState(strategyId, StrategyApprovalState.NOT_APPROVED) + ).to.not.be.reverted; returnedValue = await registrar.getStrategyApprovalState(strategyId); expect(returnedValue).to.equal(StrategyApprovalState.NOT_APPROVED); }); @@ -319,7 +329,7 @@ describe("Local Registrar", function () { }); it("Should set and get the vault operator approval status", async function () { expect(await registrar.getVaultOperatorApproved(user.address)).to.be.false; - await registrar.setVaultOperatorApproved(user.address, true); + await expect(registrar.setVaultOperatorApproved(user.address, true)).to.not.be.reverted; expect(await registrar.getVaultOperatorApproved(user.address)).to.be.true; }); }); @@ -333,7 +343,8 @@ describe("Local Registrar", function () { ).to.be.reverted; }); it("Should set and get the vault operator approval status", async function () { - await registrar.setFeeSettingsByFeesType(FeeTypes.Harvest, 1, user.address); + await expect(registrar.setFeeSettingsByFeesType(FeeTypes.Harvest, 1, user.address)).to.not + .be.reverted; let afterHarvestFee = await registrar.getFeeSettingsByFeeType(FeeTypes.Harvest); expect(afterHarvestFee.bps).to.equal(1); expect(afterHarvestFee.payoutAddress).to.equal(user.address); diff --git a/test/core/router/Router.ts b/test/core/router/Router.ts index 0204ac3be..e294d244c 100644 --- a/test/core/router/Router.ts +++ b/test/core/router/Router.ts @@ -1,13 +1,12 @@ +import {FakeContract, smock} from "@defi-wonderland/smock"; import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; -import {FakeContract, MockContract, smock} from "@defi-wonderland/smock"; import {expect, use} from "chai"; import hre from "hardhat"; import { - packActionData, - DEFAULT_NETWORK_INFO, DEFAULT_ACTION_DATA, - DEFAULT_STRATEGY_PARAMS, + DEFAULT_NETWORK_INFO, DEFAULT_STRATEGY_SELECTOR, + packActionData, } from "test/utils"; import { DummyERC20, @@ -25,7 +24,7 @@ import { Router__factory, } from "typechain-types"; import {LocalRegistrarLib} from "typechain-types/contracts/core/registrar/LocalRegistrar"; -import {getSigners, StrategyApprovalState, VaultActionStatus} from "utils"; +import {StrategyApprovalState, VaultActionStatus, getSigners} from "utils"; use(smock.matchers); @@ -251,7 +250,7 @@ describe("Router", function () { actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; actionData.accountIds = [1, 2, 3]; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), @@ -272,7 +271,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.token = token.address; actionData.selector = liquidVault.interface.getSighash("redeem"); - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), @@ -293,7 +292,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), @@ -315,7 +314,7 @@ describe("Router", function () { actionData.token = token.address; actionData.liqAmt = 0; actionData.lockAmt = 0; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), @@ -335,7 +334,7 @@ describe("Router", function () { actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; registrar.isTokenAccepted.whenCalledWith(token.address).returns(false); - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), @@ -356,7 +355,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), @@ -378,7 +377,7 @@ describe("Router", function () { actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; actionData.selector = liquidVault.interface.getSighash("redeem"); - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.execute( ethers.utils.formatBytes32String("true"), @@ -448,14 +447,14 @@ describe("Router", function () { actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; actionData.accountIds = [1, 2, 3]; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), originatingChain, accountsContract, packedData, - "TKN", + await token.symbol(), TOTAL_AMT ) ) @@ -468,14 +467,14 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.token = token.address; actionData.selector = liquidVault.interface.getSighash("redeem"); - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( await router.executeWithToken( ethers.utils.formatBytes32String("true"), originatingChain, accountsContract, packedData, - "TKN", + await token.symbol(), TOTAL_AMT ) ) @@ -488,14 +487,14 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), originatingChain, accountsContract, packedData, - "TKN", + await token.symbol(), TOTAL_AMT - 1 ) ) @@ -510,14 +509,14 @@ describe("Router", function () { actionData.token = token.address; actionData.liqAmt = 0; actionData.lockAmt = 0; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), originatingChain, accountsContract, packedData, - "TKN", + await await token.symbol(), 0 ) ) @@ -530,14 +529,14 @@ describe("Router", function () { actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; registrar.isTokenAccepted.whenCalledWith(token.address).returns(false); - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), originatingChain, accountsContract, packedData, - "TKN", + await token.symbol(), TOTAL_AMT ) ) @@ -550,14 +549,14 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.executeWithToken( ethers.utils.formatBytes32String("true"), originatingChain, accountsContract, packedData, - "TKN", + await token.symbol(), TOTAL_AMT ) ) @@ -570,7 +569,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.token = token.address; actionData.selector = liquidVault.interface.getSighash("redeem"); - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); await expect( router.execute( ethers.utils.formatBytes32String("true"), @@ -654,7 +653,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("deposit"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); expect( await router.executeWithToken( ethers.utils.formatBytes32String("true"), @@ -673,7 +672,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("redeem"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); lockedVault.redeem.returns({ token: token.address, amount: LOCK_AMT, @@ -700,7 +699,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("redeemAll"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); lockedVault.redeemAll.returns({ token: token.address, amount: LOCK_AMT, @@ -727,7 +726,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("harvest"); actionData.token = token.address; - let packedData = await packActionData(actionData); + let packedData = await packActionData(actionData, hre); expect( await router.execute( ethers.utils.formatBytes32String("true"), @@ -811,13 +810,13 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.token = token.address; actionData.selector = liquidVault.interface.getSighash("deposit"); - let packedData = packActionData(actionData); + let packedData = packActionData(actionData, hre); await router.executeWithToken( ethers.utils.formatBytes32String("true"), originatingChain, accountsContract, packedData, - "TKN", + await token.symbol(), TOTAL_AMT ); expect(lockedVault.deposit).to.have.been.calledWith(1, token.address, LOCK_AMT); @@ -896,7 +895,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("redeem"); actionData.token = token.address; - let packedData = packActionData(actionData); + let packedData = packActionData(actionData, hre); lockedVault.redeem.returns({ token: token.address, amount: LOCK_AMT, @@ -919,16 +918,19 @@ describe("Router", function () { expect(liquidVault.redeem).to.have.been.calledWith(1, LIQ_AMT); expect(token.approve).to.have.been.calledWith(gateway.address, TOTAL_AMT - GAS_COST); expect(token.approve).to.have.been.calledWith(gasService.address, GAS_COST); - let expectedPayload = packActionData({ - destinationChain: originatingChain, - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: liquidVault.interface.getSighash("redeem"), - accountIds: [1], - token: token.address, - lockAmt: LOCK_AMT - 2, // less weighted gas - liqAmt: LIQ_AMT - 3, // less weighted gas - status: VaultActionStatus.SUCCESS, - }); + let expectedPayload = packActionData( + { + destinationChain: originatingChain, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: liquidVault.interface.getSighash("redeem"), + accountIds: [1], + token: token.address, + lockAmt: LOCK_AMT - 2, // less weighted gas + liqAmt: LIQ_AMT - 3, // less weighted gas + status: VaultActionStatus.SUCCESS, + }, + hre + ); expect(gasService.payGasForContractCallWithToken).to.have.been.calledWith( router.address, originatingChain, @@ -953,7 +955,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.selector = liquidVault.interface.getSighash("redeem"); actionData.token = token.address; - let packedData = packActionData(actionData); + let packedData = packActionData(actionData, hre); lockedVault.redeem.returns({ token: token.address, amount: LOCK_AMT, @@ -1047,7 +1049,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.token = token.address; actionData.selector = liquidVault.interface.getSighash("redeemAll"); - let packedData = packActionData(actionData); + let packedData = packActionData(actionData, hre); lockedVault.redeemAll.returns({ token: token.address, amount: LOCK_AMT, @@ -1070,16 +1072,19 @@ describe("Router", function () { expect(liquidVault.redeemAll).to.have.been.calledWith(1); expect(token.approve).to.have.been.calledWith(gateway.address, TOTAL_AMT - GAS_COST); expect(token.approve).to.have.been.calledWith(gasService.address, GAS_COST); - let expectedPayload = packActionData({ - destinationChain: originatingChain, - strategyId: DEFAULT_STRATEGY_SELECTOR, - selector: liquidVault.interface.getSighash("redeemAll"), - accountIds: [1], - token: token.address, - lockAmt: LOCK_AMT - 2, // less weighted gas - liqAmt: LIQ_AMT - 3, // less weighted gas - status: VaultActionStatus.POSITION_EXITED, - }); + let expectedPayload = packActionData( + { + destinationChain: originatingChain, + strategyId: DEFAULT_STRATEGY_SELECTOR, + selector: liquidVault.interface.getSighash("redeemAll"), + accountIds: [1], + token: token.address, + lockAmt: LOCK_AMT - 2, // less weighted gas + liqAmt: LIQ_AMT - 3, // less weighted gas + status: VaultActionStatus.POSITION_EXITED, + }, + hre + ); expect(gasService.payGasForContractCallWithToken).to.have.been.calledWith( router.address, originatingChain, @@ -1104,7 +1109,7 @@ describe("Router", function () { let actionData = getDefaultActionData(); actionData.token = token.address; actionData.selector = liquidVault.interface.getSighash("redeemAll"); - let packedData = packActionData(actionData); + let packedData = packActionData(actionData, hre); lockedVault.redeemAll.returns({ token: token.address, amount: LOCK_AMT, diff --git a/test/core/vault/Vault.ts b/test/core/vault/Vault.ts index aa4fbf81b..ed339c8b4 100644 --- a/test/core/vault/Vault.ts +++ b/test/core/vault/Vault.ts @@ -1,6 +1,5 @@ import {expect} from "chai"; import {BigNumber} from "ethers"; -import {ethers} from "hardhat"; import hre from "hardhat"; import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; import {StrategyApprovalState, getSigners} from "utils"; @@ -12,6 +11,7 @@ import { DEFAULT_VAULT_NAME, DEFAULT_VAULT_SYMBOL, DEFAULT_NETWORK, + wait, } from "test/utils"; import { APVault_V1, @@ -23,6 +23,8 @@ import { } from "typechain-types"; describe("Vault", function () { + const {ethers} = hre; + let owner: SignerWithAddress; let proxyAdmin: SignerWithAddress; let user: SignerWithAddress; @@ -190,7 +192,7 @@ describe("Vault", function () { admin: owner.address, }); registrar = await deployLocalRegistrarAsProxy(owner, admin); - await registrar.setVaultOperatorApproved(owner.address, true); + await wait(registrar.setVaultOperatorApproved(owner.address, true)); }); beforeEach(async function () { vault = await deployVault({ @@ -204,32 +206,36 @@ describe("Vault", function () { }); it("reverts if the operator isn't approved as an operator, sibling vault, or approved router", async function () { - await registrar.setVaultOperatorApproved(owner.address, false); - await registrar.setStrategyParams( - DEFAULT_STRATEGY_SELECTOR, - DEFAULT_NETWORK, - user.address, - vault.address, - StrategyApprovalState.APPROVED + await wait(registrar.setVaultOperatorApproved(owner.address, false)); + await wait( + registrar.setStrategyParams( + DEFAULT_STRATEGY_SELECTOR, + DEFAULT_NETWORK, + user.address, + vault.address, + StrategyApprovalState.APPROVED + ) + ); + await wait( + registrar.setAngelProtocolParams({ + routerAddr: user.address, + refundAddr: collector.address, + }) ); - await registrar.setAngelProtocolParams({ - routerAddr: user.address, - refundAddr: collector.address, - }); await expect(vault.deposit(0, baseToken.address, 1)).to.be.revertedWithCustomError( vault, "OnlyApproved" ); - await registrar.setVaultOperatorApproved(owner.address, true); + await wait(registrar.setVaultOperatorApproved(owner.address, true)); }); it("reverts if the strategy is paused", async function () { - await strategy.pause(); + await wait(strategy.pause()); await expect(vault.deposit(0, baseToken.address, 1)).to.be.revertedWithCustomError( vault, "OnlyNotPaused" ); - await strategy.unpause(); + await wait(strategy.unpause()); }); it("reverts if the token provided isn't the base token", async function () { @@ -240,20 +246,20 @@ describe("Vault", function () { }); it("reverts if the baseToken approval fails", async function () { - await baseToken.setApproveAllowed(false); - await baseToken.mint(vault.address, 1); + await wait(baseToken.setApproveAllowed(false)); + await wait(baseToken.mint(vault.address, 1)); await expect(vault.deposit(0, baseToken.address, 1)).to.be.revertedWithCustomError( vault, "ApproveFailed" ); - await baseToken.setApproveAllowed(true); + await wait(baseToken.setApproveAllowed(true)); }); it("successfully completes the deposit", async function () { - await baseToken.mint(vault.address, 1); - await yieldToken.mint(strategy.address, 1); - await strategy.setDummyAmt(1); - expect(await vault.deposit(0, baseToken.address, 1)); + await wait(baseToken.mint(vault.address, 1)); + await wait(yieldToken.mint(strategy.address, 1)); + await wait(strategy.setDummyAmt(1)); + await expect(vault.deposit(0, baseToken.address, 1)).to.not.be.reverted; expect(await yieldToken.balanceOf(vault.address)).to.equal(1); expect(await baseToken.balanceOf(strategy.address)).to.equal(1); expect(await vault.balanceOf(0)).to.equal(1); @@ -262,19 +268,19 @@ describe("Vault", function () { }); it("successfully adds to the position after subsequent deposits", async function () { - await baseToken.mint(vault.address, 10); - await yieldToken.mint(strategy.address, 10); - await strategy.setDummyAmt(5); + await wait(baseToken.mint(vault.address, 10)); + await wait(yieldToken.mint(strategy.address, 10)); + await wait(strategy.setDummyAmt(5)); expect(await vault.deposit(0, baseToken.address, 5)); expect(await vault.deposit(0, baseToken.address, 5)); }); it("allows multiple accounts to deposit and tracks them separately", async function () { - await baseToken.mint(vault.address, 1000); - await yieldToken.mint(strategy.address, 1000); - await strategy.setDummyAmt(500); + await wait(baseToken.mint(vault.address, 1000)); + await wait(yieldToken.mint(strategy.address, 1000)); + await wait(strategy.setDummyAmt(500)); await vault.deposit(0, baseToken.address, 500); // Acct. 0 gets 1:1 - await strategy.setDummyAmt(250); // Acct. 1 gets 1:2 + await wait(strategy.setDummyAmt(250)); // Acct. 1 gets 1:2 await vault.deposit(1, baseToken.address, 500); let shares_0 = await vault.balanceOf(0); let shares_1 = await vault.balanceOf(1); @@ -295,8 +301,8 @@ describe("Vault", function () { const PRECISION = BigNumber.from(10).pow(24); before(async function () { registrar = await deployLocalRegistrarAsProxy(owner, admin); - await registrar.setVaultOperatorApproved(owner.address, true); - await registrar.setFeeSettingsByFeesType(0, TAX_RATE, collector.address); // establish tax collector + await wait(registrar.setVaultOperatorApproved(owner.address, true)); + await wait(registrar.setFeeSettingsByFeesType(0, TAX_RATE, collector.address)); // establish tax collector }); beforeEach(async function () { baseToken = await deployDummyERC20(owner); @@ -314,49 +320,49 @@ describe("Vault", function () { strategy: strategy.address, registrar: registrar.address, }); - await baseToken.mint(vault.address, DEPOSIT); - await yieldToken.mint(strategy.address, DEPOSIT * EX_RATE); - await strategy.setDummyAmt(DEPOSIT * EX_RATE); + await wait(baseToken.mint(vault.address, DEPOSIT)); + await wait(yieldToken.mint(strategy.address, DEPOSIT * EX_RATE)); + await wait(strategy.setDummyAmt(DEPOSIT * EX_RATE)); await vault.deposit(0, baseToken.address, DEPOSIT); }); it("reverts if the strategy is paused", async function () { - await strategy.pause(); + await wait(strategy.pause()); await expect(vault.redeem(0, DEPOSIT / 2)).to.be.revertedWithCustomError( vault, "OnlyNotPaused" ); - await strategy.unpause(); + await wait(strategy.unpause()); }); it("reverts if the caller isn't approved", async function () { - await registrar.setVaultOperatorApproved(owner.address, false); + await wait(registrar.setVaultOperatorApproved(owner.address, false)); await expect(vault.redeem(0, DEPOSIT / 2)).to.be.revertedWithCustomError( vault, "OnlyApproved" ); - await registrar.setVaultOperatorApproved(owner.address, true); + await wait(registrar.setVaultOperatorApproved(owner.address, true)); }); it("reverts if the baseToken transfer fails", async function () { - await baseToken.setTransferAllowed(false); + await wait(baseToken.setTransferAllowed(false)); await expect(vault.redeem(0, DEPOSIT / 2)).to.be.revertedWithCustomError( vault, "TransferFailed" ); - await baseToken.setTransferAllowed(true); + await wait(baseToken.setTransferAllowed(true)); }); it("reverts if the baseToken approve fails", async function () { - await baseToken.setApproveAllowed(false); - await strategy.setDummyAmt(DEPOSIT / 2); + await wait(baseToken.setApproveAllowed(false)); + await wait(strategy.setDummyAmt(DEPOSIT / 2)); await expect(vault.redeem(0, DEPOSIT / 2)).to.be.reverted; - await baseToken.setApproveAllowed(true); + await wait(baseToken.setApproveAllowed(true)); }); it("does not tax if the position hasn't earned yield", async function () { let shares = await vault.balanceOf(0); - await strategy.setDummyAmt(DEPOSIT / 2); // no yield + await wait(strategy.setDummyAmt(DEPOSIT / 2)); // no yield let collectorBal = await baseToken.balanceOf(collector.address); await vault.redeem(0, shares.div(2)); // Redeem half the position let newCollectorBal = await baseToken.balanceOf(collector.address); @@ -366,7 +372,7 @@ describe("Vault", function () { it("taxes if the position is in the black", async function () { let shares = await vault.balanceOf(0); - await strategy.setDummyAmt(DEPOSIT); // 100% yield + await wait(strategy.setDummyAmt(DEPOSIT)); // 100% yield let collectorBal = await baseToken.balanceOf(collector.address); await vault.redeem(0, shares.div(2)); // Redeem half the position let newCollectorBal = await baseToken.balanceOf(collector.address); @@ -378,7 +384,7 @@ describe("Vault", function () { it("updates the principles accordingly", async function () { let shares = await vault.balanceOf(0); - await strategy.setDummyAmt(DEPOSIT); // 100% yield + await wait(strategy.setDummyAmt(DEPOSIT)); // 100% yield await vault.redeem(0, shares.div(2)); // Redeem half the position let expectedPrinciple = DEPOSIT / 2; let principle = await vault.principleByAccountId(0); @@ -388,7 +394,7 @@ describe("Vault", function () { it("calls redeemAll if the redemption value is gt or equal to the position", async function () { let shares = await vault.balanceOf(0); - await strategy.setDummyAmt(DEPOSIT); // 100% yield + await wait(strategy.setDummyAmt(DEPOSIT)); // 100% yield await vault.redeem(0, shares); // Redeem the whole position let principle = await vault.principleByAccountId(0); expect(principle.baseToken).to.equal(0); // we zero out princ. on full redemption @@ -407,8 +413,8 @@ describe("Vault", function () { const TAX_RATE = 100; // bps before(async function () { registrar = await deployLocalRegistrarAsProxy(owner, admin); - await registrar.setVaultOperatorApproved(owner.address, true); - await registrar.setFeeSettingsByFeesType(0, TAX_RATE, collector.address); // establish tax collector + await wait(registrar.setVaultOperatorApproved(owner.address, true)); + await wait(registrar.setFeeSettingsByFeesType(0, TAX_RATE, collector.address)); // establish tax collector }); beforeEach(async function () { baseToken = await deployDummyERC20(owner); @@ -426,26 +432,26 @@ describe("Vault", function () { strategy: strategy.address, registrar: registrar.address, }); - await baseToken.mint(vault.address, DEPOSIT); - await yieldToken.mint(strategy.address, DEPOSIT * EX_RATE); - await strategy.setDummyAmt(DEPOSIT * EX_RATE); + await wait(baseToken.mint(vault.address, DEPOSIT)); + await wait(yieldToken.mint(strategy.address, DEPOSIT * EX_RATE)); + await wait(strategy.setDummyAmt(DEPOSIT * EX_RATE)); await vault.deposit(0, baseToken.address, DEPOSIT); }); it("reverts if the strategy is paused", async function () { - await strategy.pause(); + await wait(strategy.pause()); await expect(vault.redeemAll(0)).to.be.revertedWithCustomError(vault, "OnlyNotPaused"); - await strategy.unpause(); + await wait(strategy.unpause()); }); it("reverts if the caller isn't approved", async function () { - await registrar.setVaultOperatorApproved(owner.address, false); + await wait(registrar.setVaultOperatorApproved(owner.address, false)); await expect(vault.redeemAll(0)).to.be.revertedWithCustomError(vault, "OnlyApproved"); - await registrar.setVaultOperatorApproved(owner.address, true); + await wait(registrar.setVaultOperatorApproved(owner.address, true)); }); it("does not tax if the position hasn't earned yield", async function () { - await strategy.setDummyAmt(DEPOSIT); // no yield + await wait(strategy.setDummyAmt(DEPOSIT)); // no yield let collectorBal = await baseToken.balanceOf(collector.address); await vault.redeemAll(0); let newCollectorBal = await baseToken.balanceOf(collector.address); @@ -454,8 +460,8 @@ describe("Vault", function () { }); it("taxes if the position is in the black", async function () { - await strategy.setDummyAmt(DEPOSIT * 2); // 100% yield - await baseToken.mint(strategy.address, DEPOSIT); // add the yield to the strategy + await wait(strategy.setDummyAmt(DEPOSIT * 2)); // 100% yield + await wait(baseToken.mint(strategy.address, DEPOSIT)); // add the yield to the strategy let collectorBal = await baseToken.balanceOf(collector.address); await vault.redeemAll(0); // Redeem half the position let newCollectorBal = await baseToken.balanceOf(collector.address); @@ -466,7 +472,7 @@ describe("Vault", function () { }); it("updates the principles accordingly", async function () { - await strategy.setDummyAmt(DEPOSIT); // 100% yield + await wait(strategy.setDummyAmt(DEPOSIT)); // 100% yield await vault.redeemAll(0); // Redeem half the position let principle = await vault.principleByAccountId(0); expect(principle.baseToken).to.equal(0); // we zero out princ. on full redemption @@ -485,8 +491,8 @@ describe("Vault", function () { const PRECISION = BigNumber.from(10).pow(24); before(async function () { registrar = await deployLocalRegistrarAsProxy(owner, admin); - await registrar.setVaultOperatorApproved(owner.address, true); - await registrar.setFeeSettingsByFeesType(1, TAX_RATE, collector.address); // harvest fee type, establish tax collector + await wait(registrar.setVaultOperatorApproved(owner.address, true)); + await wait(registrar.setFeeSettingsByFeesType(1, TAX_RATE, collector.address)); // harvest fee type, establish tax collector }); describe("For liquid vaults", async function () { @@ -507,44 +513,44 @@ describe("Vault", function () { strategy: strategy.address, registrar: registrar.address, }); - await baseToken.mint(vault.address, DEPOSIT); - await yieldToken.mint(strategy.address, DEPOSIT * EX_RATE); - await strategy.setDummyAmt(DEPOSIT * EX_RATE); + await wait(baseToken.mint(vault.address, DEPOSIT)); + await wait(yieldToken.mint(strategy.address, DEPOSIT * EX_RATE)); + await wait(strategy.setDummyAmt(DEPOSIT * EX_RATE)); await vault.deposit(0, baseToken.address, DEPOSIT); }); it("reverts if the strategy is paused", async function () { - await strategy.pause(); + await wait(strategy.pause()); await expect(vault.harvest([0])).to.be.revertedWithCustomError(vault, "OnlyNotPaused"); - await strategy.unpause(); + await wait(strategy.unpause()); }); it("reverts if the caller isn't approved", async function () { - await registrar.setVaultOperatorApproved(owner.address, false); + await wait(registrar.setVaultOperatorApproved(owner.address, false)); await expect(vault.harvest([0])).to.be.revertedWithCustomError(vault, "OnlyApproved"); - await registrar.setVaultOperatorApproved(owner.address, true); + await wait(registrar.setVaultOperatorApproved(owner.address, true)); }); it("does not harvest if the position hasn't earned yield", async function () { - await strategy.setDummyPreviewAmt(DEPOSIT); + await wait(strategy.setDummyPreviewAmt(DEPOSIT)); let collectorBal = await baseToken.balanceOf(collector.address); - await vault.harvest([0]); + await expect(vault.harvest([0])).to.not.be.reverted; let newCollectorBal = await baseToken.balanceOf(collector.address); expect(newCollectorBal).to.equal(collectorBal); }); it("reverts if the yieldToken approve to strategy fails", async function () { - await strategy.setDummyPreviewAmt(DEPOSIT * 2); // 100% yield - await yieldToken.setApproveAllowed(false); + await wait(strategy.setDummyPreviewAmt(DEPOSIT * 2)); // 100% yield + await wait(yieldToken.setApproveAllowed(false)); await expect(vault.harvest([0])).to.be.reverted; - await baseToken.setTransferAllowed(true); + await wait(baseToken.setTransferAllowed(true)); }); it("reverts if the baseToken transfer fails", async function () { - await strategy.setDummyPreviewAmt(DEPOSIT * 2); // 100% yield - await baseToken.setTransferAllowed(false); + await wait(strategy.setDummyPreviewAmt(DEPOSIT * 2)); // 100% yield + await wait(baseToken.setTransferAllowed(false)); await expect(vault.harvest([0])).to.be.revertedWithCustomError(vault, "TransferFailed"); - await baseToken.setTransferAllowed(true); + await wait(baseToken.setTransferAllowed(true)); }); // @todo this test is a bit circular. We aren't effectively testing whether the vault is asking @@ -552,10 +558,10 @@ describe("Vault", function () { // We need better hooks in the dummy strategy to at least emit events with the queries so we can test // what the vault is sending. it("appropriately harevests yield", async function () { - await strategy.setDummyPreviewAmt(DEPOSIT * 2); // 100% yield + await wait(strategy.setDummyPreviewAmt(DEPOSIT * 2)); // 100% yield let expectedHarvestAmt = (DEPOSIT * TAX_RATE) / 10000; // DEPOSIT = position in yield, apply tax - await strategy.setDummyAmt(expectedHarvestAmt); - await vault.harvest([0]); + await wait(strategy.setDummyAmt(expectedHarvestAmt)); + await expect(vault.harvest([0])).to.not.be.reverted; let newCollectorBal = await baseToken.balanceOf(collector.address); expect(newCollectorBal).to.equal(expectedHarvestAmt); }); @@ -597,45 +603,49 @@ describe("Vault", function () { strategy: strategy.address, registrar: registrar.address, }); - await baseToken.mint(lockedVault.address, DEPOSIT); - await yieldToken.mint(strategy.address, DEPOSIT * EX_RATE); - await yieldToken.mint(liquidStrategy.address, DEPOSIT * EX_RATE); - await strategy.setDummyAmt(DEPOSIT * EX_RATE); - await lockedVault.deposit(0, baseToken.address, DEPOSIT); - await registrar.setStrategyParams( - DEFAULT_STRATEGY_SELECTOR, - DEFAULT_NETWORK, - lockedVault.address, - liquidVault.address, - StrategyApprovalState.APPROVED + await wait(baseToken.mint(lockedVault.address, DEPOSIT)); + await wait(yieldToken.mint(strategy.address, DEPOSIT * EX_RATE)); + await wait(yieldToken.mint(liquidStrategy.address, DEPOSIT * EX_RATE)); + await wait(strategy.setDummyAmt(DEPOSIT * EX_RATE)); + await wait(lockedVault.deposit(0, baseToken.address, DEPOSIT)); + await wait( + registrar.setStrategyParams( + DEFAULT_STRATEGY_SELECTOR, + DEFAULT_NETWORK, + lockedVault.address, + liquidVault.address, + StrategyApprovalState.APPROVED + ) + ); + await wait( + registrar.setRebalanceParams({ + rebalanceLiquidProfits: false, + lockedRebalanceToLiquid: REBAL_RATE, + interestDistribution: 0, + lockedPrincipleToLiquid: false, + principleDistribution: 0, + basis: BASIS, + }) ); - await registrar.setRebalanceParams({ - rebalanceLiquidProfits: false, - lockedRebalanceToLiquid: REBAL_RATE, - interestDistribution: 0, - lockedPrincipleToLiquid: false, - principleDistribution: 0, - basis: BASIS, - }); }); it("reverts if the baseToken transfer fails", async function () { - await strategy.setDummyPreviewAmt(DEPOSIT * 2); // 100% yield - await baseToken.setTransferAllowed(false); + await wait(strategy.setDummyPreviewAmt(DEPOSIT * 2)); // 100% yield + await wait(baseToken.setTransferAllowed(false)); await expect(lockedVault.harvest([0])).to.be.revertedWithCustomError( lockedVault, "TransferFailed" ); - await baseToken.setTransferAllowed(true); + await wait(baseToken.setTransferAllowed(true)); }); it("appropriately harvests yield and rebalances to the liquid sibling vault", async function () { - await strategy.setDummyPreviewAmt(DEPOSIT * 2); // 100% yield + await wait(strategy.setDummyPreviewAmt(DEPOSIT * 2)); // 100% yield let expectedTaxAmt = (DEPOSIT * TAX_RATE) / BASIS; // DEPOSIT = position in yield, apply tax let expectedRebalAmt = (DEPOSIT * REBAL_RATE) / BASIS; - await strategy.setDummyAmt(expectedTaxAmt + expectedRebalAmt); - await liquidStrategy.setDummyAmt(expectedRebalAmt * 2); - await lockedVault.harvest([0]); + await wait(strategy.setDummyAmt(expectedTaxAmt + expectedRebalAmt)); + await wait(liquidStrategy.setDummyAmt(expectedRebalAmt * 2)); + await expect(lockedVault.harvest([0])).to.not.be.reverted; let newCollectorBal = await baseToken.balanceOf(collector.address); expect(newCollectorBal).to.equal(expectedTaxAmt); let liquidPrinciple = await liquidVault.principleByAccountId(0); diff --git a/test/halo/Halo.ts b/test/halo/Halo.ts index 136763de6..909f2e956 100644 --- a/test/halo/Halo.ts +++ b/test/halo/Halo.ts @@ -19,6 +19,7 @@ describe("Halo token", function () { user = apTeam3; Halo = (await hre.ethers.getContractFactory("Halo", proxyAdmin)) as Halo__factory; halo = await Halo.deploy(user.address, INITIALSUPPLY); + await halo.deployed(); }); it("Sends the specified amount to the specified recipient", async function () { diff --git a/test/integrations/flux/FluxStrategy.ts b/test/integrations/flux/FluxStrategy.ts index 9aff298e1..3bb7bebae 100644 --- a/test/integrations/flux/FluxStrategy.ts +++ b/test/integrations/flux/FluxStrategy.ts @@ -1,17 +1,19 @@ -import {expect} from "chai"; -import {ethers, upgrades} from "hardhat"; import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; +import {expect} from "chai"; +import {BigNumber} from "ethers"; +import hre from "hardhat"; +import {DEFAULT_STRATEGY_SELECTOR, deployDummyERC20, deployDummyFUSDC, wait} from "test/utils"; import { DummyERC20, - IStrategy, + DummyFUSDC, FluxStrategy, FluxStrategy__factory, - DummyFUSDC, + IStrategy, } from "typechain-types"; -import {deployDummyFUSDC, deployDummyERC20, DEFAULT_STRATEGY_SELECTOR} from "test/utils"; -import {BigNumber} from "ethers"; describe("FluxStrategy", function () { + const {ethers} = hre; + let owner: SignerWithAddress; let user: SignerWithAddress; let collector: SignerWithAddress; @@ -79,14 +81,14 @@ describe("FluxStrategy", function () { await expect(flux.connect(user).pause()).to.be.revertedWithCustomError(flux, "AdminOnly"); }); it("reverts if a non-admin calls the `unpause` method", async function () { - await flux.pause(); + await expect(flux.pause()).to.not.be.reverted; await expect(flux.connect(user).unpause()).to.be.revertedWithCustomError(flux, "AdminOnly"); }); it("pauses and unpauses when called by the admin", async function () { - await flux.pause(); + await expect(flux.pause()).to.not.be.reverted; let status = await flux.paused(); expect(status).to.be.true; - await flux.unpause(); + await expect(flux.unpause()).to.not.be.reverted; status = await flux.paused(); expect(status).to.be.false; }); @@ -142,47 +144,47 @@ describe("FluxStrategy", function () { }); }); it("reverts when paused", async function () { - await flux.pause(); + await expect(flux.pause()).to.not.be.reverted; await expect(flux.deposit(1)).to.revertedWith("Pausable: paused"); }); it("reverts if the baseToken transfer fails", async function () { - await baseToken.mint(owner.address, 1); - await baseToken.setTransferAllowed(false); + await wait(baseToken.mint(owner.address, 1)); + await wait(baseToken.setTransferAllowed(false)); await expect(flux.deposit(1)).to.be.revertedWithCustomError(flux, "TransferFailed"); - await baseToken.setTransferAllowed(true); + await wait(baseToken.setTransferAllowed(true)); }); it("reverts if the baseToken approve fails", async function () { - await baseToken.mint(owner.address, 1); - await baseToken.approve(flux.address, 1); - await baseToken.setApproveAllowed(false); - await yieldToken.setResponseAmt(1); + await wait(baseToken.mint(owner.address, 1)); + await wait(baseToken.approve(flux.address, 1)); + await wait(baseToken.setApproveAllowed(false)); + await wait(yieldToken.setResponseAmt(1)); await expect(flux.deposit(1)).to.be.revertedWithCustomError(flux, "ApproveFailed"); - await baseToken.setApproveAllowed(true); + await wait(baseToken.setApproveAllowed(true)); }); it("reverts if the deposit fails", async function () { - await baseToken.mint(owner.address, 1); - await baseToken.approve(flux.address, 1); - await yieldToken.setResponseAmt(1); - await yieldToken.setMintAllowed(false); + await wait(baseToken.mint(owner.address, 1)); + await wait(baseToken.approve(flux.address, 1)); + await wait(yieldToken.setResponseAmt(1)); + await wait(yieldToken.setMintAllowed(false)); await expect(flux.deposit(1)).to.be.revertedWithCustomError(flux, "DepositFailed"); - await yieldToken.setMintAllowed(true); + await wait(yieldToken.setMintAllowed(true)); }); it("reverts if the yieldToken approve fails", async function () { - await baseToken.mint(owner.address, 1); - await baseToken.approve(flux.address, 1); - await yieldToken.setResponseAmt(1); - await yieldToken.setApproveAllowed(false); + await wait(baseToken.mint(owner.address, 1)); + await wait(baseToken.approve(flux.address, 1)); + await wait(yieldToken.setResponseAmt(1)); + await wait(yieldToken.setApproveAllowed(false)); await expect(flux.deposit(1)).to.be.revertedWithCustomError(flux, "ApproveFailed"); - await yieldToken.setApproveAllowed(true); + await wait(yieldToken.setApproveAllowed(true)); }); it("correctly executes the deposit", async function () { - await baseToken.mint(owner.address, 10); - await baseToken.approve(flux.address, 10); - await yieldToken.setResponseAmt(10); + await wait(baseToken.mint(owner.address, 10)); + await wait(baseToken.approve(flux.address, 10)); + await wait(yieldToken.setResponseAmt(10)); expect(await flux.deposit(10)); let baseTokenBal = await baseToken.balanceOf(yieldToken.address); let yieldBal = await yieldToken.balanceOf(flux.address); - expect(await yieldToken.transferFrom(flux.address, owner.address, yieldBal)); + await wait(yieldToken.transferFrom(flux.address, owner.address, yieldBal)); expect(baseTokenBal).to.equal(10); expect(yieldBal).to.equal(10); }); @@ -202,50 +204,50 @@ describe("FluxStrategy", function () { yieldToken: yieldToken.address, admin: owner.address, }); - await baseToken.mint(owner.address, DEPOSIT_AMT); - await baseToken.approve(flux.address, DEPOSIT_AMT); - await yieldToken.setResponseAmt(DEPOSIT_AMT); - await flux.deposit(DEPOSIT_AMT); - await yieldToken.transferFrom(flux.address, owner.address, DEPOSIT_AMT); + await wait(baseToken.mint(owner.address, DEPOSIT_AMT)); + await wait(baseToken.approve(flux.address, DEPOSIT_AMT)); + await wait(yieldToken.setResponseAmt(DEPOSIT_AMT)); + await wait(flux.deposit(DEPOSIT_AMT)); + await wait(yieldToken.transferFrom(flux.address, owner.address, DEPOSIT_AMT)); }); it("reverts when paused", async function () { - await flux.pause(); + await expect(flux.pause()).to.not.be.reverted; await expect(flux.withdraw(1)).to.revertedWith("Pausable: paused"); }); it("reverts if the yieldToken transfer fails", async function () { - await yieldToken.approve(flux.address, 1); - await yieldToken.setTransferAllowed(false); + await wait(yieldToken.approve(flux.address, 1)); + await wait(yieldToken.setTransferAllowed(false)); await expect(flux.withdraw(1)).to.be.revertedWithCustomError(flux, "TransferFailed"); - await yieldToken.setTransferAllowed(true); + await wait(yieldToken.setTransferAllowed(true)); }); it("reverts if the yieldToken approve fails", async function () { - await yieldToken.approve(flux.address, 1); - await yieldToken.setApproveAllowed(false); - await yieldToken.setResponseAmt(1); + await wait(yieldToken.approve(flux.address, 1)); + await wait(yieldToken.setApproveAllowed(false)); + await wait(yieldToken.setResponseAmt(1)); await expect(flux.withdraw(1)).to.be.revertedWithCustomError(flux, "ApproveFailed"); - await yieldToken.setApproveAllowed(true); + await wait(yieldToken.setApproveAllowed(true)); }); it("reverts if the withdraw fails", async function () { - await yieldToken.approve(flux.address, 1); - await yieldToken.setResponseAmt(1); - await yieldToken.setRedeemAllowed(false); + await wait(yieldToken.approve(flux.address, 1)); + await wait(yieldToken.setResponseAmt(1)); + await wait(yieldToken.setRedeemAllowed(false)); await expect(flux.withdraw(1)).to.be.revertedWithCustomError(flux, "WithdrawFailed"); - await yieldToken.setRedeemAllowed(true); + await wait(yieldToken.setRedeemAllowed(true)); }); it("reverts if the baseToken approve fails", async function () { - await yieldToken.approve(flux.address, 1); - await yieldToken.setResponseAmt(1); - await baseToken.setApproveAllowed(false); + await wait(yieldToken.approve(flux.address, 1)); + await wait(yieldToken.setResponseAmt(1)); + await wait(baseToken.setApproveAllowed(false)); await expect(flux.withdraw(1)).to.be.revertedWithCustomError(flux, "ApproveFailed"); - await baseToken.setApproveAllowed(true); + await wait(baseToken.setApproveAllowed(true)); }); it("correctly executes the redemption", async function () { - await yieldToken.approve(flux.address, 10); - await yieldToken.setResponseAmt(10); + await wait(yieldToken.approve(flux.address, 10)); + await wait(yieldToken.setResponseAmt(10)); expect(await flux.withdraw(10)); let baseTokenBal = await baseToken.balanceOf(flux.address); expect(baseTokenBal).to.equal(10); - expect(await baseToken.transferFrom(flux.address, owner.address, 10)); + await wait(baseToken.transferFrom(flux.address, owner.address, 10)); }); }); describe("upon previewDeposit and previewWithdraw", async function () { @@ -267,18 +269,18 @@ describe("FluxStrategy", function () { }); }); it("correctly applies the exchange rate for previewDeposit", async function () { - await yieldToken.setExRate(EXP_SCALE.div(DECIMAL_MAG)); // test 1:1 + await expect(yieldToken.setExRate(EXP_SCALE.div(DECIMAL_MAG))).to.not.be.reverted; // test 1:1 let previewedDeposit = await flux.previewDeposit(ONE_THOUSAND); expect(previewedDeposit).to.equal(ONE_THOUSAND.mul(DECIMAL_MAG)); - await yieldToken.setExRate(EXP_SCALE.div(2).div(DECIMAL_MAG)); // test 2 : 1 + await expect(yieldToken.setExRate(EXP_SCALE.div(2).div(DECIMAL_MAG))).to.not.be.reverted; // test 2 : 1 previewedDeposit = await flux.previewDeposit(ONE_THOUSAND); expect(previewedDeposit).to.equal(ONE_THOUSAND.mul(DECIMAL_MAG).mul(2)); }); it("correctly applies the exchange rate for previewWithdraw", async function () { - await yieldToken.setExRate(EXP_SCALE.div(DECIMAL_MAG)); // test 1:1 + await expect(yieldToken.setExRate(EXP_SCALE.div(DECIMAL_MAG))).to.not.be.reverted; // test 1:1 let previewedWithdraw = await flux.previewWithdraw(ONE_THOUSAND.mul(DECIMAL_MAG)); expect(previewedWithdraw).to.equal(ONE_THOUSAND); - await yieldToken.setExRate(EXP_SCALE.mul(2).div(DECIMAL_MAG)); // test 2 : 1 + await expect(yieldToken.setExRate(EXP_SCALE.mul(2).div(DECIMAL_MAG))).to.not.be.reverted; // test 2 : 1 previewedWithdraw = await flux.previewWithdraw(ONE_THOUSAND.mul(DECIMAL_MAG)); expect(previewedWithdraw).to.equal(ONE_THOUSAND.mul(2)); }); diff --git a/test/utils/Registrar.ts b/test/utils/Registrar.ts index be819f908..d1a918ddb 100644 --- a/test/utils/Registrar.ts +++ b/test/utils/Registrar.ts @@ -1,5 +1,4 @@ import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; -import {ethers} from "hardhat"; import { LocalRegistrar, LocalRegistrar__factory, @@ -8,6 +7,7 @@ import { ProxyContract__factory, } from "typechain-types"; import {DEFAULT_SPLIT_STRUCT} from "./helpers"; +import {HardhatRuntimeEnvironment} from "hardhat/types"; export async function deployLocalRegistrarAsProxy( owner: SignerWithAddress, @@ -15,6 +15,7 @@ export async function deployLocalRegistrarAsProxy( ): Promise { const LocalRegistrar = new LocalRegistrar__factory(proxyAdmin); const localRegistrarImpl = await LocalRegistrar.deploy(); + await localRegistrarImpl.deployed(); const data = localRegistrarImpl.interface.encodeFunctionData("initialize"); const proxyFactory = new ProxyContract__factory(owner); let proxy = await proxyFactory.deploy(localRegistrarImpl.address, proxyAdmin.address, data); @@ -24,7 +25,8 @@ export async function deployLocalRegistrarAsProxy( export async function deployRegistrarAsProxy( owner: SignerWithAddress, - proxyAdmin: SignerWithAddress + proxyAdmin: SignerWithAddress, + hre: HardhatRuntimeEnvironment ): Promise { const Registrar = new Registrar__factory(proxyAdmin); const registrarImpl = await Registrar.deploy(); @@ -33,11 +35,11 @@ export async function deployRegistrarAsProxy( "initialize((address,(uint256,uint256,uint256),address,address,address,string))", [ { - treasury: ethers.constants.AddressZero, + treasury: hre.ethers.constants.AddressZero, splitToLiquid: DEFAULT_SPLIT_STRUCT, - router: ethers.constants.AddressZero, - axelarGateway: ethers.constants.AddressZero, - axelarGasService: ethers.constants.AddressZero, + router: hre.ethers.constants.AddressZero, + axelarGateway: hre.ethers.constants.AddressZero, + axelarGasService: hre.ethers.constants.AddressZero, networkName: "localhost", }, ] diff --git a/test/utils/helpers/RouterHelpers.ts b/test/utils/helpers/RouterHelpers.ts index 167486240..566b1610e 100644 --- a/test/utils/helpers/RouterHelpers.ts +++ b/test/utils/helpers/RouterHelpers.ts @@ -1,8 +1,11 @@ import {IVault} from "typechain-types/contracts/core/accounts/facets/AccountsStrategy"; import {convertVaultActionStructToArray, convertArrayToVaultActionStruct} from "./IVaultHelpers"; -import {ethers} from "hardhat"; +import {HardhatRuntimeEnvironment} from "hardhat/types"; -export function packActionData(_actionData: IVault.VaultActionDataStruct): string { +export function packActionData( + _actionData: IVault.VaultActionDataStruct, + {ethers}: HardhatRuntimeEnvironment +): string { const TypeList = ["string", "bytes4", "bytes4", "uint[]", "address", "uint", "uint", "uint"]; return ethers.utils.defaultAbiCoder.encode( TypeList, @@ -10,7 +13,10 @@ export function packActionData(_actionData: IVault.VaultActionDataStruct): strin ); } -export function unpackActionData(_encodedActionData: string): IVault.VaultActionDataStruct { +export function unpackActionData( + _encodedActionData: string, + {ethers}: HardhatRuntimeEnvironment +): IVault.VaultActionDataStruct { const TypeList = ["string", "string", "string", "uint[]", "string", "uint", "uint", "uint"]; let decoded = ethers.utils.defaultAbiCoder.decode(TypeList, _encodedActionData); return convertArrayToVaultActionStruct(decoded); diff --git a/test/utils/helpers/accounts/defaults.ts b/test/utils/helpers/accounts/defaults.ts index d53857ddc..f3f6de75a 100644 --- a/test/utils/helpers/accounts/defaults.ts +++ b/test/utils/helpers/accounts/defaults.ts @@ -1,4 +1,3 @@ -import {ethers} from "hardhat"; import {AccountStorage} from "typechain-types/contracts/test/accounts/TestFacetProxyContract"; import {AccountMessages} from "typechain-types/contracts/core/accounts/facets/AccountsStrategy"; import {LibAccounts} from "typechain-types/contracts/multisigs/CharityApplications"; @@ -8,11 +7,12 @@ import {DEFAULT_STRATEGY_SELECTOR} from "test/utils"; import {IVault} from "typechain-types/contracts/core/accounts/facets/AccountsStrategy"; import {LocalRegistrarLib} from "typechain-types/contracts/core/registrar/LocalRegistrar"; import {IAccountsStrategy} from "typechain-types/contracts/core/registrar/interfaces/IRegistrar"; +import {ADDRESS_ZERO} from "utils"; export const DEFAULT_PERMISSIONS_STRUCT: LibAccounts.SettingsPermissionStruct = { locked: false, delegate: { - addr: ethers.constants.AddressZero, + addr: ADDRESS_ZERO, expires: 0, }, }; @@ -38,7 +38,7 @@ export const DEFAULT_SETTINGS_STRUCT: LibAccounts.SettingsControllerStruct = { }; export const DEFAULT_FEE_STRUCT: LibAccounts.FeeSettingStruct = { - payoutAddress: ethers.constants.AddressZero, + payoutAddress: ADDRESS_ZERO, bps: 0, }; @@ -49,7 +49,7 @@ export const DEFAULT_SPLIT_STRUCT: LibAccounts.SplitDetailsStruct = { }; export const DEFAULT_CHARITY_ENDOWMENT: AccountStorage.EndowmentStruct = { - owner: ethers.constants.AddressZero, + owner: ADDRESS_ZERO, name: "DEFAULT_PERMISSIONS_STRUCT", sdgs: [], tier: 0, @@ -66,11 +66,11 @@ export const DEFAULT_CHARITY_ENDOWMENT: AccountStorage.EndowmentStruct = { basis: 100, }, proposalLink: 0, - multisig: ethers.constants.AddressZero, - dao: ethers.constants.AddressZero, - daoToken: ethers.constants.AddressZero, + multisig: ADDRESS_ZERO, + dao: ADDRESS_ZERO, + daoToken: ADDRESS_ZERO, donationMatchActive: false, - donationMatchContract: ethers.constants.AddressZero, + donationMatchContract: ADDRESS_ZERO, allowlistedBeneficiaries: [], allowlistedContributors: [], maturityAllowlist: [], @@ -83,59 +83,59 @@ export const DEFAULT_CHARITY_ENDOWMENT: AccountStorage.EndowmentStruct = { ignoreUserSplits: false, splitToLiquid: DEFAULT_SPLIT_STRUCT, referralId: 0, - gasFwd: ethers.constants.AddressZero, + gasFwd: ADDRESS_ZERO, }; export const DEFAULT_ACCOUNTS_CONFIG: AccountStorage.ConfigStruct = { - owner: ethers.constants.AddressZero, + owner: ADDRESS_ZERO, networkName: "", version: "", - registrarContract: ethers.constants.AddressZero, + registrarContract: ADDRESS_ZERO, nextAccountId: 0, reentrancyGuardLocked: false, }; export const DEFAULT_NETWORK_INFO: IAccountsStrategy.NetworkInfoStruct = { chainId: 0, - router: ethers.constants.AddressZero, - axelarGateway: ethers.constants.AddressZero, + router: ADDRESS_ZERO, + axelarGateway: ADDRESS_ZERO, ibcChannel: "", transferChannel: "", - gasReceiver: ethers.constants.AddressZero, + gasReceiver: ADDRESS_ZERO, gasLimit: 0, }; export const DEFAULT_REGISTRAR_CONFIG: RegistrarStorage.ConfigStruct = { - indexFundContract: ethers.constants.AddressZero, - accountsContract: ethers.constants.AddressZero, - treasury: ethers.constants.AddressZero, - subdaoGovContract: ethers.constants.AddressZero, // Sub dao implementation - subdaoTokenContract: ethers.constants.AddressZero, // NewERC20 implementation - subdaoBondingTokenContract: ethers.constants.AddressZero, // Continous Token implementation - subdaoCw900Contract: ethers.constants.AddressZero, - subdaoDistributorContract: ethers.constants.AddressZero, - subdaoEmitter: ethers.constants.AddressZero, - donationMatchContract: ethers.constants.AddressZero, + indexFundContract: ADDRESS_ZERO, + accountsContract: ADDRESS_ZERO, + treasury: ADDRESS_ZERO, + subdaoGovContract: ADDRESS_ZERO, // Sub dao implementation + subdaoTokenContract: ADDRESS_ZERO, // NewERC20 implementation + subdaoBondingTokenContract: ADDRESS_ZERO, // Continous Token implementation + subdaoCw900Contract: ADDRESS_ZERO, + subdaoDistributorContract: ADDRESS_ZERO, + subdaoEmitter: ADDRESS_ZERO, + donationMatchContract: ADDRESS_ZERO, splitToLiquid: {max: 0, min: 0, defaultSplit: 0} as any, - haloToken: ethers.constants.AddressZero, - haloTokenLpContract: ethers.constants.AddressZero, - govContract: ethers.constants.AddressZero, - donationMatchEmitter: ethers.constants.AddressZero, + haloToken: ADDRESS_ZERO, + haloTokenLpContract: ADDRESS_ZERO, + govContract: ADDRESS_ZERO, + donationMatchEmitter: ADDRESS_ZERO, collectorShare: BigNumber.from(50), - charitySharesContract: ethers.constants.AddressZero, - fundraisingContract: ethers.constants.AddressZero, - uniswapRouter: ethers.constants.AddressZero, - uniswapFactory: ethers.constants.AddressZero, - lockedWithdrawal: ethers.constants.AddressZero, - proxyAdmin: ethers.constants.AddressZero, - usdcAddress: ethers.constants.AddressZero, - wMaticAddress: ethers.constants.AddressZero, - cw900lvAddress: ethers.constants.AddressZero, - charityApplications: ethers.constants.AddressZero, - multisigFactory: ethers.constants.AddressZero, - multisigEmitter: ethers.constants.AddressZero, - donationMatchCharitesContract: ethers.constants.AddressZero, - gasFwdFactory: ethers.constants.AddressZero, + charitySharesContract: ADDRESS_ZERO, + fundraisingContract: ADDRESS_ZERO, + uniswapRouter: ADDRESS_ZERO, + uniswapFactory: ADDRESS_ZERO, + lockedWithdrawal: ADDRESS_ZERO, + proxyAdmin: ADDRESS_ZERO, + usdcAddress: ADDRESS_ZERO, + wMaticAddress: ADDRESS_ZERO, + cw900lvAddress: ADDRESS_ZERO, + charityApplications: ADDRESS_ZERO, + multisigFactory: ADDRESS_ZERO, + multisigEmitter: ADDRESS_ZERO, + donationMatchCharitesContract: ADDRESS_ZERO, + gasFwdFactory: ADDRESS_ZERO, }; export const DEFAULT_INVEST_REQUEST: AccountMessages.InvestRequestStruct = { @@ -167,17 +167,17 @@ export const DEFAULT_STRATEGY_PARAMS: LocalRegistrarLib.StrategyParamsStruct = { network: "", Locked: { Type: 0, - vaultAddr: ethers.constants.AddressZero, + vaultAddr: ADDRESS_ZERO, }, Liquid: { Type: 1, - vaultAddr: ethers.constants.AddressZero, + vaultAddr: ADDRESS_ZERO, }, }; export const DEFAULT_AP_PARAMS: LocalRegistrarLib.AngelProtocolParamsStruct = { - refundAddr: ethers.constants.AddressZero, - routerAddr: ethers.constants.AddressZero, + refundAddr: ADDRESS_ZERO, + routerAddr: ADDRESS_ZERO, }; export const DEFAULT_ACTION_DATA: IVault.VaultActionDataStruct = { diff --git a/test/utils/index.ts b/test/utils/index.ts index 18c1d4b35..d1e36b493 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -7,4 +7,5 @@ export * from "./dummyVault"; export * from "./helpers"; export * from "./Registrar"; export * from "./integrations"; +export * from "./wait"; import "./assertions"; diff --git a/test/utils/wait.ts b/test/utils/wait.ts new file mode 100644 index 000000000..702535c05 --- /dev/null +++ b/test/utils/wait.ts @@ -0,0 +1,7 @@ +import {ContractReceipt, ContractTransaction} from "ethers"; + +export async function wait( + tx: ContractTransaction | Promise +): Promise { + return (await tx).wait(); +}