From 936ae7ab081e1abc118c57af6ba4e61cdfe2772e Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Wed, 8 May 2024 17:26:43 -0400 Subject: [PATCH] Synpress updates --- .env.example | 2 + contracts/common/mock/MockToken18Decimal.sol | 66 +++++++++++++++ lib/forge-std | 2 +- package.json | 1 + ...-all-local-test-pools-synpress-scenario.ts | 61 ++++++++++++++ scripts/deploy-local-test-pools.ts | 80 +++++++++++++++++-- scripts/utils.ts | 17 +++- tasks/advance-week-and-drawdown-receivable.ts | 5 +- tasks/utils.ts | 3 + 9 files changed, 223 insertions(+), 14 deletions(-) create mode 100644 .env.example create mode 100644 contracts/common/mock/MockToken18Decimal.sol create mode 100644 scripts/deploy-all-local-test-pools-synpress-scenario.ts diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..daa12287 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +LOCALHOST_CHAIN_ID= +LOCALHOST_MNEMONIC_PHRASE= diff --git a/contracts/common/mock/MockToken18Decimal.sol b/contracts/common/mock/MockToken18Decimal.sol new file mode 100644 index 00000000..99f56391 --- /dev/null +++ b/contracts/common/mock/MockToken18Decimal.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.23; + +import {ERC20, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +contract MockToken18Decimal is ERC20Permit, IERC165 { + using EnumerableSet for EnumerableSet.AddressSet; + + /// Transfers to these addresses will fail and return `false` so that we can test transfer failure handling. + EnumerableSet.AddressSet internal _softFailBlocklist; + /// Transfers to these addresses will fail and revert with an error so that we can test transfer failure handling. + EnumerableSet.AddressSet internal _revertingBlocklist; + + constructor() ERC20Permit("TestToken") ERC20("TestToken", "USDC") { + _mint(msg.sender, 1000 * 10 ** decimals()); + } + + function give1000To(address to) external { + _mint(to, 1000 * 10 ** decimals()); + } + + function give100000To(address to) external { + _mint(to, 100000 * 10 ** decimals()); + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external { + _burn(from, amount); + } + + function addToSoftFailBlocklist(address addr) external { + _softFailBlocklist.add(addr); + } + + function removeFromSoftFailBlocklist(address addr) external { + _softFailBlocklist.remove(addr); + } + + function addToRevertingBlocklist(address addr) external { + _revertingBlocklist.add(addr); + } + + function removeFromRevertingBlocklist(address addr) external { + _revertingBlocklist.remove(addr); + } + + function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { + return interfaceId == 0x36372b07 || interfaceId == 0x01ffc9a7; + } + + function transfer(address to, uint256 amount) public virtual override returns (bool) { + if (_softFailBlocklist.contains(to)) return false; + require(!_revertingBlocklist.contains(to)); + + return super.transfer(to, amount); + } + + function decimals() public view virtual override returns (uint8) { + return 18; + } +} diff --git a/lib/forge-std b/lib/forge-std index b6a506db..5475f852 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit b6a506db2262cad5ff982a87789ee6d1558ec861 +Subproject commit 5475f852e3f530d7e25dfb4596aa1f9baa8ffdfc diff --git a/package.json b/package.json index a8812c3b..82542802 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "deploy-local": "hardhat run ./scripts/deploy-all-local-test-pools.ts --network localhost", "deploy-local-credit": "hardhat run ./scripts/deploy-credit-line-pools.ts --network localhost", "deploy-local-receivable": "hardhat run ./scripts/deploy-receivable-backed-credit-line-pools.ts --network localhost", + "deploy-local-synpress-scenario": "hardhat run ./scripts/deploy-all-local-test-pools-synpress-scenario.ts --network localhost", "format": "prettier '**/*.{js,json,ts,yml,yaml}' --write --plugin=prettier-plugin-organize-imports", "format-solidity": "prettier 'contracts/**/*.sol' --write --plugin=prettier-plugin-solidity", "foundry": "bash scripts/install-foundry.sh", diff --git a/scripts/deploy-all-local-test-pools-synpress-scenario.ts b/scripts/deploy-all-local-test-pools-synpress-scenario.ts new file mode 100644 index 00000000..ad5102ef --- /dev/null +++ b/scripts/deploy-all-local-test-pools-synpress-scenario.ts @@ -0,0 +1,61 @@ +import { ethers } from "hardhat"; +import moment from "moment"; +import { getAccountSigners } from "../tasks/utils"; +import { toToken } from "../test/TestUtils"; +import { deployPools } from "./deploy-local-test-pools"; +import { LOCAL_PROVIDER, advanceChainBySeconds, advanceChainToTime } from "./utils"; + +(async () => { + try { + const { poolOwner, juniorLender, sentinelServiceAccount, borrowerInactive } = + await getAccountSigners(ethers); + + const contracts = await deployPools(); + + const creditContracts = contracts[0]; + const lpConfig = await creditContracts.poolConfigContract.getLPConfig(); + + // Allow for withdrawals immediately + await creditContracts.poolConfigContract + .connect(poolOwner) + .setLPConfig({ ...lpConfig, ...{ withdrawalLockoutPeriodInDays: 1 } }); + + // Advance time to allow for withdrawals + await advanceChainBySeconds(24 * 60 * 60 + 60); + + // Create redemption request + await creditContracts.juniorTrancheVaultContract + .connect(juniorLender) + .addRedemptionRequest(toToken(10)); + + // Advance time to next epoch + let block = await LOCAL_PROVIDER.getBlock("latest"); + await advanceChainToTime( + moment.unix(block.timestamp).utc().add(1, "month").startOf("month"), + ); + + // Process redemption requests by closing epoch + await creditContracts.juniorTrancheVaultContract + .connect(sentinelServiceAccount) + .processYieldForLenders(); + await creditContracts.seniorTrancheVaultContract + .connect(sentinelServiceAccount) + .processYieldForLenders(); + await creditContracts.epochManagerContract.connect(sentinelServiceAccount).closeEpoch(); + + // Revoking allowance for inactive borrower + await creditContracts.mockTokenContract + .connect(borrowerInactive) + .approve(creditContracts.creditContract.address, 0); + await creditContracts.mockTokenContract + .connect(borrowerInactive) + .approve(creditContracts.poolSafeContract.address, 0); + + console.log( + "Pools are deployed. Junior lender is ready to withdraw from the credit line pool", + ); + } catch (error) { + console.error(error); + process.exitCode = 1; + } +})(); diff --git a/scripts/deploy-local-test-pools.ts b/scripts/deploy-local-test-pools.ts index b6be9bfa..a5e91b9a 100644 --- a/scripts/deploy-local-test-pools.ts +++ b/scripts/deploy-local-test-pools.ts @@ -7,6 +7,7 @@ import moment from "moment"; import { getAccountSigners } from "../tasks/utils"; import { CreditContractName, + CreditContractType, CreditManagerContractName, deployAndSetupPoolContracts, deployProtocolContracts, @@ -17,12 +18,17 @@ import { overrideFirstLossCoverConfig, toToken } from "../test/TestUtils"; import { CreditLine, CreditLineManager, + EpochManager, FirstLossCover, MockToken, + Pool, + PoolConfig, + PoolSafe, ReceivableBackedCreditLine, ReceivableBackedCreditLineManager, + TrancheVault, } from "../typechain-types"; -import { advanceChainTime } from "./utils"; +import { advanceChainToTime } from "./utils"; const poolsToDeploy: { creditContract: CreditContractName; @@ -64,7 +70,16 @@ async function deployPool( creditContractName: CreditContractName, creditManagerContractName: CreditManagerContractName, poolName?: LocalPoolName, -) { +): Promise<{ + poolContract: Pool; + poolConfigContract: PoolConfig; + poolSafeContract: PoolSafe; + creditContract: CreditContractType; + epochManagerContract: EpochManager; + juniorTrancheVaultContract: TrancheVault; + seniorTrancheVaultContract: TrancheVault; + mockTokenContract: MockToken; +}> { console.log("====================================="); console.log(`Deploying pool with ${creditContractName} and ${creditManagerContractName}`); if (poolName) { @@ -84,6 +99,7 @@ async function deployPool( seniorLender, lenderRedemptionActive, borrowerActive, + borrowerInactive, } = await getAccountSigners(ethers); console.log("Deploying and setting up protocol contracts"); @@ -122,7 +138,7 @@ async function deployPool( treasury, poolOwnerTreasury, poolOperator, - [juniorLender, seniorLender, lenderRedemptionActive, borrowerActive], + [juniorLender, seniorLender, lenderRedemptionActive, borrowerActive, borrowerInactive], ); // Deposit first loss cover @@ -180,6 +196,17 @@ async function deployPool( 0, true, ); + await (creditManagerContract as CreditLineManager) + .connect(evaluationAgent) + .approveBorrower( + borrowerInactive.address, + toToken(100_000), + 5, // numOfPeriods + 1217, // yieldInBps + toToken(0), + 0, + true, + ); const borrowAmount = toToken(100_000); // Drawing down credit line @@ -219,6 +246,17 @@ async function deployPool( 0, true, ); + await (creditManagerContract as ReceivableBackedCreditLineManager) + .connect(evaluationAgent) + .approveBorrower( + borrowerInactive.address, + toToken(100_000), + 5, // numOfPeriods + 1217, // yieldInBps + toToken(0), + 0, + true, + ); const borrowAmount = toToken(100_000); const currentBlockTimestamp = await time.latest(); @@ -248,6 +286,7 @@ async function deployPool( console.log(`Junior lender: ${juniorLender.address}`); console.log(`Senior lender: ${seniorLender.address}`); console.log(`Borrower: ${borrowerActive.address}`); + console.log(`Inactive Borrower: ${borrowerInactive.address}`); console.log(`Sentinel Service: ${sentinelServiceAccount.address}`); console.log(`Pool owner: ${poolOwner.address}`); console.log("-------------------------------------"); @@ -256,7 +295,6 @@ async function deployPool( console.log(`Pool: ${poolContract.address}`); console.log(`Epoch manager: ${epochManagerContract.address}`); console.log(`Pool config: ${poolConfigContract.address}`); - console.log(`Pool credit: ${creditContract.address}`); console.log(`Junior tranche: ${juniorTrancheVaultContract.address}`); console.log(`Senior tranche: ${seniorTrancheVaultContract.address}`); console.log(`Pool safe: ${poolSafeContract.address}`); @@ -269,17 +307,41 @@ async function deployPool( console.log(`Receivable: ${receivableContract.address}`); } console.log("====================================="); + + return { + poolContract, + epochManagerContract, + poolSafeContract, + creditContract, + poolConfigContract, + juniorTrancheVaultContract, + seniorTrancheVaultContract, + mockTokenContract, + }; } export async function deployPools( onlyDeployPoolName: LocalPoolName | undefined = undefined, shouldAdvanceTime: boolean = true, -) { +): Promise< + Array<{ + poolContract: Pool; + poolSafeContract: PoolSafe; + epochManagerContract: EpochManager; + poolConfigContract: PoolConfig; + creditContract: CreditContractType; + juniorTrancheVaultContract: TrancheVault; + seniorTrancheVaultContract: TrancheVault; + mockTokenContract: MockToken; + }> +> { try { + const contracts = []; + if (shouldAdvanceTime) { // always set the date to the 1st of the next month const blockchainStartDate = moment().utc().add(1, "month").startOf("month"); - await advanceChainTime(blockchainStartDate); + await advanceChainToTime(blockchainStartDate); } if (onlyDeployPoolName) { @@ -298,11 +360,15 @@ export async function deployPools( } } else { for (const pool of poolsToDeploy) { - await deployPool(pool.creditContract, pool.manager, pool.poolName); + contracts.push(await deployPool(pool.creditContract, pool.manager, pool.poolName)); } } + + return contracts; } catch (error) { console.error(error); process.exitCode = 1; + + throw error; } } diff --git a/scripts/utils.ts b/scripts/utils.ts index 18368bcc..f7e32868 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,9 +1,9 @@ import { ethers } from "hardhat"; import moment from "moment"; -const LOCAL_PROVIDER = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545"); +export const LOCAL_PROVIDER = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545"); -export const advanceChainTime = async (date: moment.Moment) => { +export const advanceChainToTime = async (date: moment.Moment) => { console.log("Advancing to fix date"); let block = await LOCAL_PROVIDER.getBlock("latest"); const timeToAdvance = date.unix() - block.timestamp; @@ -22,3 +22,16 @@ export const advanceChainTime = async (date: moment.Moment) => { block = await LOCAL_PROVIDER.getBlock("latest"); console.log("Block timestamp after advancing: ", block.timestamp); }; + +export const advanceChainBySeconds = async (seconds: number) => { + console.log( + `Advancing blockchain by ${seconds} seconds (~${(seconds / 60 / 60 / 24).toPrecision( + 3, + )} days)`, + ); + + await LOCAL_PROVIDER.send("evm_increaseTime", [seconds]); + await LOCAL_PROVIDER.send("evm_mine", []); + const block = await LOCAL_PROVIDER.getBlock("latest"); + console.log("Block timestamp after advancing: ", block.timestamp); +}; diff --git a/tasks/advance-week-and-drawdown-receivable.ts b/tasks/advance-week-and-drawdown-receivable.ts index 13696cc2..f08b1a4b 100644 --- a/tasks/advance-week-and-drawdown-receivable.ts +++ b/tasks/advance-week-and-drawdown-receivable.ts @@ -10,10 +10,7 @@ task( "advanceWeekAndDrawdownReceivable", "Pays back to the pool and draws down with a receivable, then advances blockchain time by a week", ) - .addParam( - "poolConfigAddr", - "The PoolConfig contract address of the pool in question", - ) + .addParam("poolConfigAddr", "The PoolConfig contract address of the pool in question") .setAction(async (taskArgs: { poolConfigAddr: string }, hre: HardhatRuntimeEnvironment) => { const { borrowerActive } = await getAccountSigners(hre.ethers); diff --git a/tasks/utils.ts b/tasks/utils.ts index 8cedf600..6fad8355 100644 --- a/tasks/utils.ts +++ b/tasks/utils.ts @@ -16,6 +16,7 @@ export const getAccountSigners = async ( seniorLender: SignerWithAddress; lenderRedemptionActive: SignerWithAddress; borrowerActive: SignerWithAddress; + borrowerInactive: SignerWithAddress; }> => { const [ defaultDeployer, @@ -30,6 +31,7 @@ export const getAccountSigners = async ( seniorLender, lenderRedemptionActive, borrowerActive, + borrowerInactive, ] = await ethersClient.getSigners(); return { @@ -45,5 +47,6 @@ export const getAccountSigners = async ( seniorLender, lenderRedemptionActive, borrowerActive, + borrowerInactive, }; };