diff --git a/deployments/external/external.json b/deployments/external/external.json index 1a8ce25..8c2eb47 100644 --- a/deployments/external/external.json +++ b/deployments/external/external.json @@ -216,7 +216,7 @@ "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", "chainlinkL2SequencerUptimeFeed": "0x0000000000000000000000000000000000000000", - "weth": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + "weth": "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", "isL2": false, "uniswapV3Factory": "0x0227628f3F023bb0B980b67D528571c95c6DaC1c", "uniswapV3NFTPositionManager": "0x1238536071E1c677A632429e3655c799b22cDA52", diff --git a/deployments/protocol/v1.5.json b/deployments/protocol/v1.5.json index 0db3279..af867a1 100644 --- a/deployments/protocol/v1.5.json +++ b/deployments/protocol/v1.5.json @@ -1,3 +1,54 @@ { - -} + "1": { + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "crowdsourceLenderVault": "0x77A63952572Dd0EAa7Fd21a3fDeaa80b4071A5e8", + "hooks": { + "compoundLender": "", + "directLenderRepayment": "", + "refinanceBorrowerCreate": "", + "vaultLender": "", + "aaveLender": "" + }, + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loan": "0xc58791ec351349a82036aE712976109C10e34217", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "products": { + "stable": "", + "installments": "0xE24D470DfDA38Ace1E154f55997DF34859892970", + "fixed": "", + "uniswapV3Individual": "", + "uniswapV3Set": "" + }, + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "utilizedCredit": "0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D" + }, + "11155111": { + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "crowdsourceLenderVault": "0x2474d856d1463435932866de8118CC40Ed3c01d8", + "hooks": { + "compoundLender": "", + "directLenderRepayment": "", + "refinanceBorrowerCreate": "", + "vaultLender": "", + "aaveLender": "" + }, + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loan": "0xc58791ec351349a82036aE712976109C10e34217", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "products": { + "stable": "", + "installments": "0x68669e7ec29070e3dfa684cb4893282Cd4C9E608", + "fixed": "", + "uniswapV3Individual": "", + "uniswapV3Set": "" + }, + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "utilizedCredit": "0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D" + } +} \ No newline at end of file diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..285f866 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,20 @@ +{ + "lib/MultiToken": { + "rev": "863dcd8b4c60494d1deda231fb95b48073d85659" + }, + "lib/forge-std": { + "rev": "ae570fec082bfe1c1f45b0acca4a2b4f84d345ce" + }, + "lib/openzeppelin-contracts": { + "rev": "bd325d56b4c62c9c5c1aff048c37c6bb18ac0290" + }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "a40cb0bda838c2ef3dfc252c179f5c37c32e80c4" + }, + "lib/v3-core": { + "rev": "6562c52e8f75f0c10f9deaf44861847585fc8129" + }, + "lib/v3-periphery": { + "rev": "50c696768e1e6cf13aa4d4d382ab3fec0c7d3b2e" + } +} \ No newline at end of file diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 84436fe..7c224aa 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -21,9 +21,12 @@ import { MultiTokenCategoryRegistry, IChainlinkFeedRegistryLike, PWNStableProduct, - PWNWorldStableProduct + PWNInstallmentsProduct, + IAaveLike } from "pwn/Deployments.sol"; +import { PWNCrowdsourceLenderVault } from "pwn/periphery/crowdsource/PWNCrowdsourceLenderVault.sol"; + library PWNContractDeployerSalt { @@ -44,7 +47,10 @@ library PWNContractDeployerSalt { bytes32 internal constant FIXED_PRODUCT = keccak256("PWNFixedProduct"); bytes32 internal constant UNISWAP_V3_INDIVIDUAL_PRODUCT = keccak256("PWNUniswapV3IndividualProduct"); bytes32 internal constant UNISWAP_V3_SET_PRODUCT = keccak256("PWNUniswapV3SetProduct"); + bytes32 internal constant INSTALLMENTS_PRODUCT = keccak256("PWNInstallmentsProduct"); + // Others + bytes32 internal constant CROWDSOURCE_LENDER_VAULT = keccak256("PWNCrowdsourceLenderVault"); } using GnosisSafeUtils for GnosisSafeLike; @@ -95,41 +101,139 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ _loadDeployedAddresses(); vm.startBroadcast(); - __d.loan = PWNLoan( + // !!! IMPORTANT: LOADING ADDRESSES FROM JSON DOES NOT WORK SOMEHOW, SO I AM JUST HARDCODING THE ADDRESSES HERE !!! + + __d.loanToken = PWNLOAN(0x4440C069272cC34b80C7B11bEE657D0349Ba9C23); + __d.config = PWNConfig(0xd52a2898d61636bB3eEF0d145f05352FF543bdCC); + __d.categoryRegistry = MultiTokenCategoryRegistry(0xbB2168d5546A94AE2DA9254e63D88F7f137B2534); + + // __d.loan = PWNLoan( + // _deploy( + // PWNContractDeployerSalt.LOAN, + // abi.encodePacked( + // type(PWNLoan).creationCode, + // abi.encode( + // address(__d.loanToken), + // address(__d.config), + // address(__d.categoryRegistry) + // ) + // ) + // ) + // ); + __d.loan = PWNLoan(0xc58791ec351349a82036aE712976109C10e34217); + + // console2.log("PWNLoan:", address(__d.loan)); + + __d.hub = PWNHub(0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5); + __d.revokedNonce = PWNRevokedNonce(0x972204fF33348ee6889B2d0A3967dB67d7b08e4c); + __d.utilizedCredit = PWNUtilizedCredit(0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D); + __d.chainlinkFeedRegistry = IChainlinkFeedRegistryLike(0x8D5e90706E52a52853dA9A14fA1c63889a412851); + __e.chainlinkL2SequencerUptimeFeed = address(0x0000000000000000000000000000000000000000); + __e.weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + __d.products.installments = PWNInstallmentsProduct( _deploy( - PWNContractDeployerSalt.LOAN, + PWNContractDeployerSalt.INSTALLMENTS_PRODUCT, abi.encodePacked( - type(PWNLoan).creationCode, - abi.encode(address(__d.hub), address(__d.loanToken), address(__d.config), address(__d.categoryRegistry), __e.permit2) + type(PWNInstallmentsProduct).creationCode, + abi.encode( + address(__d.hub), + address(__d.revokedNonce), + address(__d.utilizedCredit), + address(__d.chainlinkFeedRegistry), + __e.chainlinkL2SequencerUptimeFeed, + __e.weth + ) ) ) ); - - __d.products.stable = PWNStableProduct( - _deploy( - PWNContractDeployerSalt.STABLE_PRODUCT, - abi.encodePacked( - type(PWNWorldStableProduct).creationCode, - abi.encode(address(__d.hub), address(__d.revokedNonce), address(__d.utilizedCredit), address(__d.chainlinkFeedRegistry), __e.chainlinkL2SequencerUptimeFeed, __e.weth, 0x17B354dD2595411ff79041f930e491A4Df39A278, "app_17abe44eaf47c99566f5378aa4e19463", "verify-humanness") - ) - ) - ); - - console2.log("PWNLoan:", address(__d.loan)); - console2.log("PWNStableProduct:", address(__d.products.stable)); + // __d.products.installments = PWNInstallmentsProduct(address(0x366a95aDB86282d53c905F9b7d62746b02C4F679)); + console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); address[] memory addrs = new address[](3); addrs[0] = address(__d.loan); - addrs[1] = address(__d.products.stable); - addrs[2] = address(__d.products.stable); + addrs[1] = address(__d.products.installments); + addrs[2] = address(__d.products.installments); bytes32[] memory tags = new bytes32[](3); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.LOAN_PROPOSAL; tags[2] = PWNHubTags.NONCE_MANAGER; +// 0xf12715a1000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000c58791ec351349a82036ae712976109c10e34217000000000000000000000000e24d470dfda38ace1e154f55997df34859892970000000000000000000000000e24d470dfda38ace1e154f55997df3485989297000000000000000000000000000000000000000000000000000000000000000039e56ea094d7a53440eef11fa42b63159fbf703b4ee579494a6ae85afc5603594c0ba7a416221f318a8087fd62f9ff407488b7f5501e79caf9b0666c2df326b9ce41b33e4d1c538d376dd219215a123562fbb87b8c85fa2aa4ebbd8810c2454d9 + + // 0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000c58791ec351349a82036ae712976109c10e34217000000000000000000000000e24d470dfda38ace1e154f55997df34859892970000000000000000000000000e24d470dfda38ace1e154f55997df3485989297000000000000000000000000000000000000000000000000000000000000000039e56ea094d7a53440eef11fa42b63159fbf703b4ee579494a6ae85afc5603594c0ba7a416221f318a8087fd62f9ff407488b7f5501e79caf9b0666c2df326b9ce41b33e4d1c538d376dd219215a123562fbb87b8c85fa2aa4ebbd8810c2454d9 + + // note: this should be called on the protocolTimelock contract and use `schedule` and then `execute` + // functions where the target arg is the PWNHub and the data is the encoded bytes logged below + // note 2: when setting tags for proposal, it needs to have both LOAN_PROPOSAL and NONCE_MANAGER + // tags in order to work fully correctly console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); + /* + USDC --> weETH route + 1) USDC --> USD ( 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6 , non inverted ) + 2) USD --> ETH ( 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 , inverted ) + 3) eth --> weETH ( 0x5c9C449BbC9a6075A2c061dF312a35fd1E05fF22 , inverted ) + */ + + address[] memory feedIntermediaryDenominations = new address[](2); + feedIntermediaryDenominations[0] = address(0x0000000000000000000000000000000000000348); // USD + feedIntermediaryDenominations[1] = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); // ETH + bool[] memory feedInvertFlags = new bool[](3); + feedInvertFlags[0] = false; + feedInvertFlags[1] = true; + feedInvertFlags[2] = true; + + /* + SEPOLIA ROUTE + EURS --> WETH route + - assuming EURS === EUR as there is no separate EURS feed on Sepolia + - assuming that WETH === ETH as there is no separate WETH feed on Sepolia + 1) EUR --> USD (0x1a81afB8146aeFfCFc5E50e8479e826E7D55b910, non inverted) + 2) USD --> ETH (0x694AA1769357215DE4FAC081bf1f309aDC325306, inverted) + address[] memory feedIntermediaryDenominations = new address[](1); + feedIntermediaryDenominations[0] = address(0x0000000000000000000000000000000000000348); // USD + bool[] memory feedInvertFlags = new bool[](2); + feedInvertFlags[0] = false; + feedInvertFlags[1] = true; + */ + + // __d.loan = PWNLoan(0xc58791ec351349a82036aE712976109C10e34217); + // __d.products.installments = PWNInstallmentsProduct(0x68669e7ec29070e3dfa684cb4893282Cd4C9E608); + __e.aave = IAaveLike(address(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2)); + + __d.crowdsourceLenderVault = PWNCrowdsourceLenderVault( + _deploy( + PWNContractDeployerSalt.CROWDSOURCE_LENDER_VAULT, + abi.encodePacked( + type(PWNCrowdsourceLenderVault).creationCode, + abi.encode( + address(__d.loan), + address(__d.products.installments), + address(__e.aave), + "BordelMortgageVaultShare", + "BORDEL", + PWNCrowdsourceLenderVault.Terms({ + collateralAddress: address(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee), // weETH + creditAddress: address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48), // USDC + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags, + loanToValue: 7500, // 75% + interestAPR: 250, // 2.5% + postponement: 15780000, // 6 months in seconds + duration: 157800000, // 5 years in seconds + minCreditAmount: 180000000000, // 180 000 USDC (6 decimals) + expiration: block.timestamp + 6480000, // 75 days from now + allowedAcceptor: address(0x8a7a728a181E674833E018706422cb28543abD87) + }) + ) + ) + ) + ); + + console2.log("PWNCrowdsourceLenderVault:", address(__d.crowdsourceLenderVault)); + vm.stopBroadcast(); } diff --git a/src/Deployments.sol b/src/Deployments.sol index 0539a6c..2873d1d 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -31,6 +31,8 @@ import { PWNDirectLenderRepaymentHook } from "pwn/periphery/hook/lender/PWNDirec import { PWNRevokedNonce } from "pwn/periphery/auxiliary/PWNRevokedNonce.sol"; import { PWNUtilizedCredit } from "pwn/periphery/auxiliary/PWNUtilizedCredit.sol"; +import { PWNCrowdsourceLenderVault } from "pwn/periphery/crowdsource/PWNCrowdsourceLenderVault.sol"; + interface IPWNDeployer { function owner() external returns (address); @@ -72,6 +74,7 @@ abstract contract Deployments is CommonBase { IChainlinkFeedRegistryLike chainlinkFeedRegistry; PWNConfig config; PWNConfig configSingleton; + PWNCrowdsourceLenderVault crowdsourceLenderVault; Hooks hooks; PWNHub hub; PWNLoan loan; @@ -113,27 +116,128 @@ abstract contract Deployments is CommonBase { function _loadDeployedAddresses() internal { string memory root = vm.projectRoot(); + string memory chainIdKey = block.chainid.toString(); + + // Load creation code + _loadCreationCode(root); + + // Load external addresses + _loadExternalAddresses(root, chainIdKey); + + // Load deployment addresses + _loadDeploymentAddresses(root, chainIdKey); + } + function _loadCreationCode(string memory root) internal { string memory creationJson = vm.readFile(string.concat(root, deploymentsSubpath, "/deployments/creation/creationCode.json")); - bytes memory rawCreation = creationJson.parseRaw("."); - __cc = abi.decode(rawCreation, (CreationCode)); + __cc.categoryRegistry = creationJson.readBytes(".categoryRegistry"); + __cc.chainlinkFeedRegistry = creationJson.readBytes(".chainlinkFeedRegistry"); + __cc.config = creationJson.readBytes(".config"); + __cc.configSingleton_v1_2 = creationJson.readBytes(".configSingleton_v1_2"); + __cc.hub = creationJson.readBytes(".hub"); + __cc.loanToken = creationJson.readBytes(".loanToken"); + __cc.revokedNonce = creationJson.readBytes(".revokedNonce"); + __cc.utilizedCredit = creationJson.readBytes(".utilizedCredit"); + } + function _loadExternalAddresses(string memory root, string memory chainIdKey) internal { string memory externalJson = vm.readFile(string.concat(root, deploymentsSubpath, "/deployments/external/external.json")); - bytes memory rawExternal = externalJson.parseRaw(string.concat(".", block.chainid.toString())); - __e = abi.decode(rawExternal, (External)); + string memory externalKey = string.concat(".", chainIdKey); + __e.aave = IAaveLike(externalJson.readAddress(string.concat(externalKey, ".aave"))); + __e.adminTimelock = externalJson.readAddress(string.concat(externalKey, ".adminTimelock")); + __e.chainlinkL2SequencerUptimeFeed = externalJson.readAddress(string.concat(externalKey, ".chainlinkL2SequencerUptimeFeed")); + __e.dao = externalJson.readAddress(string.concat(externalKey, ".dao")); + __e.daoSafe = externalJson.readAddress(string.concat(externalKey, ".daoSafe")); + __e.deployer = IPWNDeployer(externalJson.readAddress(string.concat(externalKey, ".deployer"))); + __e.deployerSafe = externalJson.readAddress(string.concat(externalKey, ".deployerSafe")); + __e.isL2 = externalJson.readBool(string.concat(externalKey, ".isL2")); + __e.protocolTimelock = externalJson.readAddress(string.concat(externalKey, ".protocolTimelock")); + __e.uniswapV3Factory = externalJson.readAddress(string.concat(externalKey, ".uniswapV3Factory")); + __e.uniswapV3NFTPositionManager = externalJson.readAddress(string.concat(externalKey, ".uniswapV3NFTPositionManager")); + __e.weth = externalJson.readAddress(string.concat(externalKey, ".weth")); + } + function _loadDeploymentAddresses(string memory root, string memory chainIdKey) private { string memory deploymentsJson = vm.readFile(string.concat(root, deploymentsSubpath, "/deployments/protocol/v1.5.json")); - bytes memory rawDeployment = deploymentsJson.parseRaw(string.concat(".", block.chainid.toString())); - + string memory deploymentKey = string.concat(".", chainIdKey); + + // Check if deployment exists for this chain by checking if raw bytes exist + bytes memory rawDeployment = deploymentsJson.parseRaw(deploymentKey); + if (rawDeployment.length > 0) { wasPredeployedOnFork = true; - __d = abi.decode(rawDeployment, (Deployment)); + _loadDeploymentTopLevel(deploymentsJson, deploymentKey); + _loadDeploymentProducts(deploymentsJson, deploymentKey); + _loadDeploymentHooks(deploymentsJson, deploymentKey); } else { wasPredeployedOnFork = false; _protocolNotDeployedOnSelectedChain(); } } + function _safeReadAddress(string memory json, string memory key) private pure returns (address) { + string memory addrStr = json.readString(key); + if (bytes(addrStr).length == 0) { + return address(0); + } + return json.readAddress(key); + } + + function _loadDeploymentTopLevel(string memory deploymentsJson, string memory deploymentKey) private { + __d.categoryRegistry = MultiTokenCategoryRegistry(_safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".categoryRegistry"))); + __d.chainlinkFeedRegistry = IChainlinkFeedRegistryLike(_safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".chainlinkFeedRegistry"))); + __d.config = PWNConfig(_safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".config"))); + __d.configSingleton = PWNConfig(_safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".configSingleton"))); + __d.hub = PWNHub(_safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".hub"))); + __d.loanToken = PWNLOAN(_safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".loanToken"))); + + address addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".crowdsourceLenderVault")); + if (addr != address(0)) __d.crowdsourceLenderVault = PWNCrowdsourceLenderVault(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".loan")); + if (addr != address(0)) __d.loan = PWNLoan(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".revokedNonce")); + if (addr != address(0)) __d.revokedNonce = PWNRevokedNonce(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".utilizedCredit")); + if (addr != address(0)) __d.utilizedCredit = PWNUtilizedCredit(addr); + } + + function _loadDeploymentProducts(string memory deploymentsJson, string memory deploymentKey) private { + address addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".products.stable")); + if (addr != address(0)) __d.products.stable = PWNStableProduct(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".products.installments")); + if (addr != address(0)) __d.products.installments = PWNInstallmentsProduct(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".products.fixed")); + if (addr != address(0)) __d.products._fixed = PWNFixedProduct(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".products.uniswapV3Individual")); + if (addr != address(0)) __d.products.uniswapV3Individual = PWNUniswapV3IndividualProduct(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".products.uniswapV3Set")); + if (addr != address(0)) __d.products.uniswapV3Set = PWNUniswapV3SetProduct(addr); + } + + function _loadDeploymentHooks(string memory deploymentsJson, string memory deploymentKey) private { + address addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".hooks.aaveLender")); + if (addr != address(0)) __d.hooks.aaveLender = PWNAaveLenderHook(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".hooks.compoundLender")); + if (addr != address(0)) __d.hooks.compoundLender = PWNCompoundLenderHook(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".hooks.directLenderRepayment")); + if (addr != address(0)) __d.hooks.directLenderRepayment = PWNDirectLenderRepaymentHook(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".hooks.refinanceBorrowerCreate")); + if (addr != address(0)) __d.hooks.refinanceBorrowerCreate = PWNRefinanceBorrowerCreateHook(addr); + + addr = _safeReadAddress(deploymentsJson, string.concat(deploymentKey, ".hooks.vaultLender")); + if (addr != address(0)) __d.hooks.vaultLender = PWN4626VaultLenderHook(addr); + } + function _protocolNotDeployedOnSelectedChain() internal virtual { // Override } diff --git a/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol b/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol index 7fe6e87..152d9e2 100644 --- a/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol +++ b/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol @@ -74,6 +74,7 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR uint256 duration; uint256 minCreditAmount; uint256 expiration; + address allowedAcceptor; } /*** @notice Emitted when collateral is withdrawn.*/ @@ -99,6 +100,11 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR } collateralDecimals = decimals; + // TODO should we check the values here that they are correct? + + // TODO should we check here that the getCollateralAmount on installments product contract returns + // positive result (to ensure that the feeds are set up correctly)? + proposalHash = loanContract.makeProposalAcceptable(product, abi.encode( PWNInstallmentsProduct.Proposal({ collateralAddress: _terms.collateralAddress, @@ -110,6 +116,7 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR duration: _terms.duration, postponement: _terms.postponement, minCreditAmount: _terms.minCreditAmount, + allowedAcceptor: _terms.allowedAcceptor, availableCreditLimit: 0, utilizedCreditId: bytes32(0), nonceSpace: 0, @@ -194,7 +201,7 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR function maxRedeem(address owner) public view override returns (uint256 max) { max = balanceOf(owner); if (stage() == Stage.RUNNING) { - max = Math.min(max, _convertToShares(_availableLiquidity(), Math.Rounding.Up)); + max = Math.min(max, _convertToShares(_availableLiquidity(), Math.Rounding.Down)); } } @@ -314,6 +321,14 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR return IERC20(collateralAddr).balanceOf(address(this)) + additionalCollateralAssets; } + // TODO shall we keep this as `public`, or only as `external` since so far it's not used internally anywhere? + // TODO same question for: + // 1) totalAssets + // 2) deposit + // 3) mint + // 4) withdraw + // 5) redeem + // 6) previewCollateralRedeem /** * @notice ERC4626-like function that allows an on-chain or off-chain user to simulate the effects * of their collateral redeemption at the current block, given current on-chain conditions. diff --git a/src/periphery/lib/Chainlink.sol b/src/periphery/lib/Chainlink.sol index f4dae72..7a75dca 100644 --- a/src/periphery/lib/Chainlink.sol +++ b/src/periphery/lib/Chainlink.sol @@ -210,7 +210,9 @@ library Chainlink { // Note: registry reverts with "Feed not found" for no registered feed (, int256 price,, uint256 updatedAt,) = feed.latestRoundData(); - if (price < 0) { + // TODO should we adjust this to <= also in other Product contracts? + // TODO should we revert on any other place if encountered price is 0 or negative, or is this okay to do just here? + if (price <= 0) { revert ChainlinkFeedReturnedNegativePrice({ feed: address(feed), price: price, updatedAt: updatedAt }); } if (block.timestamp - updatedAt > MAX_CHAINLINK_FEED_PRICE_AGE) { diff --git a/src/periphery/product/PWNFixedProduct.sol b/src/periphery/product/PWNFixedProduct.sol index b2c99fe..b1ea702 100644 --- a/src/periphery/product/PWNFixedProduct.sol +++ b/src/periphery/product/PWNFixedProduct.sol @@ -58,7 +58,7 @@ contract PWNFixedProduct is IPWNProduct { bytes32 public immutable DOMAIN_SEPARATOR; /** @dev EIP-712 proposal type hash.*/ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address loanContract)" + "Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address allowedAcceptor,address loanContract)" ); /** @@ -80,6 +80,7 @@ contract PWNFixedProduct is IPWNProduct { * @param expiration Expiration timestamp of the proposal. * @param proposerSpecHash Hash of proposer-specific data. * @param isProposerLender Boolean indicating if the proposer is the lender. + * @param allowedAcceptor Address allowed to accept the proposal. If zero address, anyone except the proposer can accept. * @param loanContract The address of the loan contract to be used. */ struct Proposal { @@ -106,6 +107,7 @@ contract PWNFixedProduct is IPWNProduct { // General proposal bytes32 proposerSpecHash; bool isProposerLender; + address allowedAcceptor; address loanContract; } @@ -167,6 +169,8 @@ contract PWNFixedProduct is IPWNProduct { error LoanToValueZero(); /** @notice Thrown when the loan to value is outside of acceptable limits for the proposal.*/ error InvalidLoanToValue(); + /** @notice Thrown when caller is not allowed to accept the proposal.*/ + error CallerNotAllowedAcceptor(address current, address allowed); /*----------------------------------------------------------*| @@ -240,7 +244,7 @@ contract PWNFixedProduct is IPWNProduct { function acceptProposal( uint256 loanId, - address /* acceptor */, + address acceptor, address proposer, bytes calldata proposalData ) external returns (Terms memory loanTerms) { @@ -255,6 +259,11 @@ contract PWNFixedProduct is IPWNProduct { revert AddressMissingHubTag({ addr: proposal.loanContract, tag: PWNHubTags.ACTIVE_LOAN }); } + // Check allowed acceptor + if (proposal.allowedAcceptor != address(0) && acceptor != proposal.allowedAcceptor) { + revert CallerNotAllowedAcceptor({ current: acceptor, allowed: proposal.allowedAcceptor }); + } + // Check proposal is not expired if (block.timestamp >= proposal.expiration) { revert Expired({ current: block.timestamp, expiration: proposal.expiration }); @@ -507,6 +516,7 @@ contract PWNFixedProduct is IPWNProduct { uint256 expiration; bytes32 proposerSpecHash; bool isProposerLender; + address allowedAcceptor; address loanContract; } @@ -528,6 +538,7 @@ contract PWNFixedProduct is IPWNProduct { expiration: proposal.expiration, proposerSpecHash: proposal.proposerSpecHash, isProposerLender: proposal.isProposerLender, + allowedAcceptor: proposal.allowedAcceptor, loanContract: proposal.loanContract }); return abi.encode(erc712Proposal); diff --git a/src/periphery/product/PWNInstallmentsProduct.sol b/src/periphery/product/PWNInstallmentsProduct.sol index 27c13c4..3824e37 100644 --- a/src/periphery/product/PWNInstallmentsProduct.sol +++ b/src/periphery/product/PWNInstallmentsProduct.sol @@ -59,7 +59,7 @@ contract PWNInstallmentsProduct is IPWNProduct { bytes32 public immutable DOMAIN_SEPARATOR; /** @dev EIP-712 proposal type hash.*/ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 loanToValue,uint256 interestAPR,uint256 postponement,uint256 duration,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address loanContract)" + "Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 loanToValue,uint256 interestAPR,uint256 postponement,uint256 duration,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address allowedAcceptor,address loanContract)" ); /** @@ -81,6 +81,7 @@ contract PWNInstallmentsProduct is IPWNProduct { * @param expiration Expiration timestamp of the proposal. * @param proposerSpecHash Hash of proposer-specific data. * @param isProposerLender Boolean indicating if the proposer is the lender. + * @param allowedAcceptor Address allowed to accept the proposal. If zero address, anyone except the proposer can accept. * @param loanContract The address of the loan contract to be used. */ struct Proposal { @@ -106,6 +107,7 @@ contract PWNInstallmentsProduct is IPWNProduct { // General proposal bytes32 proposerSpecHash; bool isProposerLender; + address allowedAcceptor; address loanContract; } @@ -161,6 +163,8 @@ contract PWNInstallmentsProduct is IPWNProduct { error LoanToValueZero(); /** @notice Thrown when the liquidator is not the LOAN token owner.*/ error LiquidatorNotLoanOwner(address owner, address liquidator, address loanContract, uint256 loanId); + /** @notice Thrown when caller is not allowed to accept the proposal.*/ + error CallerNotAllowedAcceptor(address current, address allowed); @@ -219,6 +223,7 @@ contract PWNInstallmentsProduct is IPWNProduct { uint256 loanToValue ) public view returns (uint256) { if (loanToValue == 0) revert LoanToValueZero(); + // throws if returned price from chainlink pracle is negative or zero return _chainlink.convertDenomination({ amount: creditAmount, oldDenomination: creditAddress, @@ -235,7 +240,7 @@ contract PWNInstallmentsProduct is IPWNProduct { function acceptProposal( uint256 loanId, - address /* acceptor */, + address acceptor, address proposer, bytes calldata proposalData ) external returns (Terms memory loanTerms) { @@ -250,6 +255,11 @@ contract PWNInstallmentsProduct is IPWNProduct { revert AddressMissingHubTag({ addr: proposal.loanContract, tag: PWNHubTags.ACTIVE_LOAN }); } + // Check allowed acceptor + if (proposal.allowedAcceptor != address(0) && acceptor != proposal.allowedAcceptor) { + revert CallerNotAllowedAcceptor({ current: acceptor, allowed: proposal.allowedAcceptor }); + } + // Check proposal is not expired if (block.timestamp >= proposal.expiration) { revert Expired({ current: block.timestamp, expiration: proposal.expiration }); @@ -268,6 +278,11 @@ contract PWNInstallmentsProduct is IPWNProduct { revert DurationTooShort(); } + // TODO should here be >= or just > + if (proposal.postponement >= proposal.duration) { + revert PostponementBiggerThanDuration(); + } + // Check min credit amount if (proposal.minCreditAmount == 0) { revert MinCreditAmountNotSet(); @@ -458,6 +473,7 @@ contract PWNInstallmentsProduct is IPWNProduct { uint256 expiration; bytes32 proposerSpecHash; bool isProposerLender; + address allowedAcceptor; address loanContract; } @@ -479,6 +495,7 @@ contract PWNInstallmentsProduct is IPWNProduct { expiration: proposal.expiration, proposerSpecHash: proposal.proposerSpecHash, isProposerLender: proposal.isProposerLender, + allowedAcceptor: proposal.allowedAcceptor, loanContract: proposal.loanContract }); return abi.encode(erc712Proposal); diff --git a/src/periphery/product/PWNStableProduct.sol b/src/periphery/product/PWNStableProduct.sol index 3525415..a3cba3a 100644 --- a/src/periphery/product/PWNStableProduct.sol +++ b/src/periphery/product/PWNStableProduct.sol @@ -58,7 +58,7 @@ contract PWNStableProduct is IPWNProduct { bytes32 public immutable DOMAIN_SEPARATOR; /** @dev EIP-712 proposal type hash.*/ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address loanContract)" + "Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address allowedAcceptor,address loanContract)" ); /** @@ -80,6 +80,7 @@ contract PWNStableProduct is IPWNProduct { * @param expiration Expiration timestamp of the proposal. * @param proposerSpecHash Hash of proposer-specific data. * @param isProposerLender Boolean indicating if the proposer is the lender. + * @param allowedAcceptor Address allowed to accept the proposal. If zero address, anyone except the proposer can accept. * @param loanContract The address of the loan contract to be used. */ struct Proposal { @@ -106,6 +107,7 @@ contract PWNStableProduct is IPWNProduct { // General proposal bytes32 proposerSpecHash; bool isProposerLender; + address allowedAcceptor; address loanContract; } @@ -165,6 +167,8 @@ contract PWNStableProduct is IPWNProduct { error LoanToValueZero(); /** @notice Thrown when the loan to value is outside of acceptable limits for the proposal.*/ error InvalidLoanToValue(); + /** @notice Thrown when caller is not allowed to accept the proposal.*/ + error CallerNotAllowedAcceptor(address current, address allowed); /*----------------------------------------------------------*| @@ -238,7 +242,7 @@ contract PWNStableProduct is IPWNProduct { function acceptProposal( uint256 loanId, - address /* acceptor */, + address acceptor, address proposer, bytes calldata proposalData ) external returns (Terms memory loanTerms) { @@ -253,6 +257,11 @@ contract PWNStableProduct is IPWNProduct { revert AddressMissingHubTag({ addr: proposal.loanContract, tag: PWNHubTags.ACTIVE_LOAN }); } + // Check allowed acceptor + if (proposal.allowedAcceptor != address(0) && acceptor != proposal.allowedAcceptor) { + revert CallerNotAllowedAcceptor({ current: acceptor, allowed: proposal.allowedAcceptor }); + } + // Check proposal is not expired if (block.timestamp >= proposal.expiration) { revert Expired({ current: block.timestamp, expiration: proposal.expiration }); @@ -504,6 +513,7 @@ contract PWNStableProduct is IPWNProduct { uint256 expiration; bytes32 proposerSpecHash; bool isProposerLender; + address allowedAcceptor; address loanContract; } @@ -525,6 +535,7 @@ contract PWNStableProduct is IPWNProduct { expiration: proposal.expiration, proposerSpecHash: proposal.proposerSpecHash, isProposerLender: proposal.isProposerLender, + allowedAcceptor: proposal.allowedAcceptor, loanContract: proposal.loanContract }); return abi.encode(erc712Proposal); diff --git a/src/periphery/product/PWNUniswapV3IndividualProduct.sol b/src/periphery/product/PWNUniswapV3IndividualProduct.sol index f16f2ef..1c7d237 100644 --- a/src/periphery/product/PWNUniswapV3IndividualProduct.sol +++ b/src/periphery/product/PWNUniswapV3IndividualProduct.sol @@ -69,7 +69,7 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { bytes32 public immutable DOMAIN_SEPARATOR; /** @dev EIP-712 proposal type hash.*/ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(uint256 collateralId,bool token0Denominator,address creditAddress,uint256 creditAmount,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address loanContract)" + "Proposal(uint256 collateralId,bool token0Denominator,address creditAddress,uint256 creditAmount,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address allowedAcceptor,address loanContract)" ); /** @@ -88,6 +88,7 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { * @param nonce Additional value to enable identical proposals in time. Without it, it would be impossible to make again proposal, which was once revoked. Can be used to create a group of proposals, where accepting one proposal will make other proposals in the group revoked. * @param expiration Proposal expiration timestamp in seconds. * @param proposerSpecHash Hash of a proposer specific data, which must be provided during a loan creation. + * @param allowedAcceptor Address allowed to accept the proposal. If zero address, anyone except the proposer can accept. * @param loanContract Address of a loan contract that will create a loan from the proposal. */ struct Proposal { @@ -112,6 +113,7 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { uint256 expiration; // General proposal bytes32 proposerSpecHash; + address allowedAcceptor; address loanContract; } @@ -163,6 +165,8 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { error DurationTooShort(); /** @notice Thrown when the loan to value is outside of acceptable limits for the proposal.*/ error InvalidLoanToValue(uint256 current, uint256 limit); + /** @notice Thrown when caller is not allowed to accept the proposal.*/ + error CallerNotAllowedAcceptor(address current, address allowed); /*----------------------------------------------------------*| @@ -240,7 +244,7 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { function acceptProposal( uint256 loanId, - address /* acceptor */, + address acceptor, address proposer, bytes calldata proposalData ) override external returns (Terms memory loanTerms) { @@ -255,6 +259,11 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { revert AddressMissingHubTag({ addr: proposal.loanContract, tag: PWNHubTags.ACTIVE_LOAN }); } + // Check allowed acceptor + if (proposal.allowedAcceptor != address(0) && acceptor != proposal.allowedAcceptor) { + revert CallerNotAllowedAcceptor({ current: acceptor, allowed: proposal.allowedAcceptor }); + } + // Check proposal is not expired if (block.timestamp >= proposal.expiration) { revert Expired({ current: block.timestamp, expiration: proposal.expiration }); @@ -499,6 +508,7 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { uint256 nonce; uint256 expiration; bytes32 proposerSpecHash; + address allowedAcceptor; address loanContract; } @@ -517,6 +527,7 @@ contract PWNUniswapV3IndividualProduct is IPWNProduct, IERC721Receiver { nonce: proposal.nonce, expiration: proposal.expiration, proposerSpecHash: proposal.proposerSpecHash, + allowedAcceptor: proposal.allowedAcceptor, loanContract: proposal.loanContract }); return abi.encode(erc712Proposal); diff --git a/src/periphery/product/PWNUniswapV3SetProduct.sol b/src/periphery/product/PWNUniswapV3SetProduct.sol index f464290..8a563ba 100644 --- a/src/periphery/product/PWNUniswapV3SetProduct.sol +++ b/src/periphery/product/PWNUniswapV3SetProduct.sol @@ -72,7 +72,7 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { bytes32 public immutable DOMAIN_SEPARATOR; /** @dev EIP-712 proposal type hash.*/ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(address[] tokenAAllowlist,address[] tokenBAllowlist,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address loanContract)" + "Proposal(address[] tokenAAllowlist,address[] tokenBAllowlist,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address allowedAcceptor,address loanContract)" ); /** @@ -93,6 +93,7 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { * @param nonce Additional value to enable identical proposals in time. Without it, it would be impossible to make again proposal, which was once revoked. Can be used to create a group of proposals, where accepting one proposal will make other proposals in the group revoked. * @param expiration Proposal expiration timestamp in seconds. * @param proposerSpecHash Hash of a proposer specific data, which must be provided during a loan creation. + * @param allowedAcceptor Address allowed to accept the proposal. If zero address, anyone except the proposer can accept. * @param loanContract Address of a loan contract that will create a loan from the proposal. */ struct Proposal { @@ -119,6 +120,7 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { uint256 expiration; // General proposal bytes32 proposerSpecHash; + address allowedAcceptor; address loanContract; } @@ -184,6 +186,8 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { error DurationTooShort(); /** @notice Thrown when the loan to value is outside of acceptable limits for the proposal.*/ error InvalidLoanToValue(uint256 current, uint256 limit); + /** @notice Thrown when caller is not allowed to accept the proposal.*/ + error CallerNotAllowedAcceptor(address current, address allowed); /*----------------------------------------------------------*| @@ -263,7 +267,7 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { function acceptProposal( uint256 loanId, - address /* acceptor */, + address acceptor, address proposer, bytes calldata proposalData ) override external returns (Terms memory loanTerms) { @@ -278,6 +282,11 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { revert AddressMissingHubTag({ addr: proposal.loanContract, tag: PWNHubTags.ACTIVE_LOAN }); } + // Check allowed acceptor + if (proposal.allowedAcceptor != address(0) && acceptor != proposal.allowedAcceptor) { + revert CallerNotAllowedAcceptor({ current: acceptor, allowed: proposal.allowedAcceptor }); + } + // Check proposal is not expired if (block.timestamp >= proposal.expiration) { revert Expired({ current: block.timestamp, expiration: proposal.expiration }); @@ -565,6 +574,7 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { uint256 nonce; uint256 expiration; bytes32 proposerSpecHash; + address allowedAcceptor; address loanContract; } @@ -585,6 +595,7 @@ contract PWNUniswapV3SetProduct is IPWNProduct, IERC721Receiver { nonce: proposal.nonce, expiration: proposal.expiration, proposerSpecHash: proposal.proposerSpecHash, + allowedAcceptor: proposal.allowedAcceptor, loanContract: proposal.loanContract }); return abi.encode(erc712Proposal); diff --git a/test/DeploymentTest.t.sol b/test/DeploymentTest.t.sol index 02cdbb8..66ab566 100644 --- a/test/DeploymentTest.t.sol +++ b/test/DeploymentTest.t.sol @@ -41,7 +41,14 @@ abstract contract DeploymentTest is Deployments, Test { address borrower; function setUp() public virtual { - _loadDeployedAddresses(); + _loadCreationCode(vm.projectRoot()); + _loadExternalAddresses(vm.projectRoot(), "1"); + + // note: there is issue that tests are failing if only some of the contracts/products + // are deployed (e.g. only Installments product is deployed, while Stable product not yet) + // and because of this we are calling the _protocolNotDeployedOnSelectedChain() fallback + // _loadDeployedAddresses(); + _protocolNotDeployedOnSelectedChain(); (lender, lenderPK) = makeAddrAndKey("lender"); (borrower, borrowerPK) = makeAddrAndKey("borrower"); diff --git a/test/fork/PWNCrowdsourceLenderVault.fork.t.sol b/test/fork/PWNCrowdsourceLenderVault.fork.t.sol index eb87573..0d8d35b 100644 --- a/test/fork/PWNCrowdsourceLenderVault.fork.t.sol +++ b/test/fork/PWNCrowdsourceLenderVault.fork.t.sol @@ -94,7 +94,8 @@ contract PWNCrowdsourceLenderVaultForkTest is DeploymentTest { postponement: 120 days, duration: 730 days, minCreditAmount: 150_000 * 10 ** decimals, - expiration: block.timestamp + 60 days + expiration: block.timestamp + 60 days, + allowedAcceptor: address(0) }); terms.feedIntermediaryDenominations[0] = USD; terms.feedInvertFlags[0] = false; @@ -122,6 +123,7 @@ contract PWNCrowdsourceLenderVaultForkTest is DeploymentTest { expiration: terms.expiration, proposerSpecHash: __d.loan.getLenderSpecHash(lenderSpec), isProposerLender: true, + allowedAcceptor: terms.allowedAcceptor, loanContract: address(__d.loan) }); acceptorValues = PWNInstallmentsProduct.AcceptorValues({ diff --git a/test/fork/PWNStableInterestProposal.fork.t.sol b/test/fork/PWNStableInterestProposal.fork.t.sol index b0a4f31..52ccbba 100644 --- a/test/fork/PWNStableInterestProposal.fork.t.sol +++ b/test/fork/PWNStableInterestProposal.fork.t.sol @@ -17,6 +17,9 @@ import { contract PWNStableProductForkTest is DeploymentTest { + // Known USDT holder address (Tether Treasury) + address constant USDT_HOLDER = 0x5754284f345afc66a98fbB0a0Afe71e0F007B949; + PWNLoan.ProposalSpec proposalSpec; PWNLoan.LenderSpec lenderSpec; PWNLoan.BorrowerSpec borrowerSpec; @@ -45,6 +48,7 @@ contract PWNStableProductForkTest is DeploymentTest { expiration: block.timestamp + 7 days, proposerSpecHash: bytes32(0), isProposerLender: true, + allowedAcceptor: address(0), loanContract: address(__d.loan) }); @@ -134,7 +138,10 @@ contract PWNStableProductForkTest is DeploymentTest { deal(lender, 10000 ether); deal(borrower, 10000 ether); deal(address(WETH), borrower, 1e18, false); - deal(address(USDT), lender, 1000e6, false); + // USDT has non-standard storage layout, so we transfer from a known holder instead of using deal() + vm.prank(USDT_HOLDER); + (bool transferSuccess, ) = address(USDT).call(abi.encodeWithSignature("transfer(address,uint256)", lender, 1000e6)); + require(transferSuccess, "USDT transfer failed"); // Register USDT/USD & ETH/USD feed _registerFeed(address(USDT), ChainlinkDenominations.USD, USDT_USD_Feed); @@ -197,7 +204,7 @@ contract PWNStableProductForkTest is DeploymentTest { (, int256 ethPrice,,,) = IChainlinkAggregatorLike(ETH_USD_Feed).latestRoundData(); uint256 coll = 500e18 * uint256(arbPrice) / uint256(ethPrice) * 10 / 8; - assertApproxEqRel(WETH.balanceOf(address(__d.loan)), coll, 0.0001 ether); // 0.01% tolerance + assertApproxEqRel(WETH.balanceOf(address(__d.loan)), coll, 0.000125 ether); // 0.0125% tolerance } function test_twoFeeds_USDT_ARB() external { @@ -209,7 +216,10 @@ contract PWNStableProductForkTest is DeploymentTest { deal(lender, 10000 ether); deal(borrower, 10000 ether); deal(address(ARB), borrower, 5000e18, false); - deal(address(USDT), lender, 1000e6, false); + // USDT has non-standard storage layout, so we transfer from a known holder instead of using deal() + vm.prank(USDT_HOLDER); + (bool transferSuccess, ) = address(USDT).call(abi.encodeWithSignature("transfer(address,uint256)", lender, 1000e6)); + require(transferSuccess, "USDT transfer failed"); // Register ARB/USD & ETH/USD feed _registerFeed(address(ARB), ChainlinkDenominations.USD, ARB_USD_Feed); diff --git a/test/unit/Chainlink.t.sol b/test/unit/Chainlink.t.sol index 9ff13d5..6ec5899 100644 --- a/test/unit/Chainlink.t.sol +++ b/test/unit/Chainlink.t.sol @@ -470,6 +470,7 @@ contract Chainlink_ConvertPriceDenomination_Test is ChainlinkTest { |* # FETCH PRICE *| |*----------------------------------------------------------*/ +// TODO should we add test that will check if we threw exception when price is 0? contract Chainlink_FetchPrice_Test is ChainlinkTest { address denominator = makeAddr("denominator"); @@ -520,7 +521,8 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { } function testFuzz_shouldReturnPriceAndDecimals(uint256 _price, uint8 _decimals) external { - _price = bound(_price, 0, uint256(type(int256).max)); + // Price must be > 0 because fetchPrice reverts on price <= 0 + _price = bound(_price, 1, uint256(type(int256).max)); _mockFeedDecimals(aggregator, _decimals); _mockLastRoundData(aggregator, int256(_price), 1); diff --git a/test/unit/PWNCrowdsourceLenderVault.t.sol b/test/unit/PWNCrowdsourceLenderVault.t.sol index 41a76b5..05bfe81 100644 --- a/test/unit/PWNCrowdsourceLenderVault.t.sol +++ b/test/unit/PWNCrowdsourceLenderVault.t.sol @@ -56,7 +56,9 @@ abstract contract PWNCrowdsourceLenderVaultTest is Test { postponement: 90 days, duration: 365 days, minCreditAmount: 1 ether, - expiration: uint40(block.timestamp + 7 days) + expiration: uint40(block.timestamp + 7 days), + // TODO do we also need to test with non zero address? + allowedAcceptor: address(0) }); _mockAaveReserveData(aaveReserveData); diff --git a/test/unit/PWNFixedProduct.t.sol b/test/unit/PWNFixedProduct.t.sol index 59b86d8..76d85d3 100644 --- a/test/unit/PWNFixedProduct.t.sol +++ b/test/unit/PWNFixedProduct.t.sol @@ -65,6 +65,7 @@ abstract contract PWNFixedProductTest is Test { expiration: block.timestamp + 1 days, proposerSpecHash: bytes32(0), isProposerLender: true, + allowedAcceptor: address(0), loanContract: loanContract }); proposal.feedInvertFlags[0] = false; @@ -105,7 +106,7 @@ abstract contract PWNFixedProductTest is Test { function _hashProposalTypedData(PWNFixedProduct.Proposal memory _proposal) internal view returns (bytes32) { return keccak256(abi.encodePacked( - keccak256("Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address loanContract)"), + keccak256("Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address allowedAcceptor,address loanContract)"), product.exposed_erc712EncodeProposal(_proposal) )); } @@ -469,6 +470,37 @@ contract PWNFixedProduct_acceptProposal_Test is PWNFixedProductTest { assertEq(terms.principal, acceptorValues.creditAmount); } + function test_shouldSucceed_whenAllowedAcceptorIsZero() external { + proposal.allowedAcceptor = address(0); + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function test_shouldSucceed_whenAcceptorMatchesAllowedAcceptor() external { + proposal.allowedAcceptor = acceptor; + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function testFuzz_shouldFail_whenAcceptorDoesNotMatchAllowedAcceptor(address allowedAddr, address currentAcceptor) external { + vm.assume(allowedAddr != address(0)); + vm.assume(currentAcceptor != allowedAddr); + + proposal.allowedAcceptor = allowedAddr; + + vm.expectRevert( + abi.encodeWithSelector( + PWNFixedProduct.CallerNotAllowedAcceptor.selector, + currentAcceptor, + allowedAddr + ) + ); + vm.prank(loanContract); + product.acceptProposal(loanId, currentAcceptor, proposer, _proposalData()); + } + } diff --git a/test/unit/PWNInstallmentsProduct.t.sol b/test/unit/PWNInstallmentsProduct.t.sol index 733140e..884711f 100644 --- a/test/unit/PWNInstallmentsProduct.t.sol +++ b/test/unit/PWNInstallmentsProduct.t.sol @@ -64,6 +64,7 @@ abstract contract PWNInstallmentsProductTest is Test { expiration: block.timestamp + 1 days, proposerSpecHash: bytes32(0), isProposerLender: true, + allowedAcceptor: address(0), loanContract: loanContract }); proposal.feedInvertFlags[0] = false; @@ -103,7 +104,7 @@ abstract contract PWNInstallmentsProductTest is Test { function _hashProposalTypedData(PWNInstallmentsProduct.Proposal memory _proposal) internal view returns (bytes32) { return keccak256(abi.encodePacked( - keccak256("Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 loanToValue,uint256 interestAPR,uint256 postponement,uint256 duration,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address loanContract)"), + keccak256("Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 loanToValue,uint256 interestAPR,uint256 postponement,uint256 duration,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address allowedAcceptor,address loanContract)"), product.exposed_erc712EncodeProposal(_proposal) )); } @@ -433,6 +434,37 @@ contract PWNInstallmentsProduct_acceptProposal_Test is PWNInstallmentsProductTes assertEq(terms.principal, acceptorValues.creditAmount); } + function test_shouldSucceed_whenAllowedAcceptorIsZero() external { + proposal.allowedAcceptor = address(0); + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function test_shouldSucceed_whenAcceptorMatchesAllowedAcceptor() external { + proposal.allowedAcceptor = acceptor; + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function testFuzz_shouldFail_whenAcceptorDoesNotMatchAllowedAcceptor(address allowedAddr, address currentAcceptor) external { + vm.assume(allowedAddr != address(0)); + vm.assume(currentAcceptor != allowedAddr); + + proposal.allowedAcceptor = allowedAddr; + + vm.expectRevert( + abi.encodeWithSelector( + PWNInstallmentsProduct.CallerNotAllowedAcceptor.selector, + currentAcceptor, + allowedAddr + ) + ); + vm.prank(loanContract); + product.acceptProposal(loanId, currentAcceptor, proposer, _proposalData()); + } + } diff --git a/test/unit/PWNStableProduct.t.sol b/test/unit/PWNStableProduct.t.sol index aa514b7..f20226b 100644 --- a/test/unit/PWNStableProduct.t.sol +++ b/test/unit/PWNStableProduct.t.sol @@ -65,6 +65,7 @@ abstract contract PWNStableProductTest is Test { expiration: block.timestamp + 1 days, proposerSpecHash: bytes32(0), isProposerLender: true, + allowedAcceptor: address(0), loanContract: loanContract }); proposal.feedInvertFlags[0] = false; @@ -105,7 +106,7 @@ abstract contract PWNStableProductTest is Test { function _hashProposalTypedData(PWNStableProduct.Proposal memory _proposal) internal view returns (bytes32) { return keccak256(abi.encodePacked( - keccak256("Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address loanContract)"), + keccak256("Proposal(address collateralAddress,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,bool isProposerLender,address allowedAcceptor,address loanContract)"), product.exposed_erc712EncodeProposal(_proposal) )); } @@ -467,6 +468,37 @@ contract PWNStableProduct_acceptProposal_Test is PWNStableProductTest { assertEq(terms.principal, acceptorValues.creditAmount); } + function test_shouldSucceed_whenAllowedAcceptorIsZero() external { + proposal.allowedAcceptor = address(0); + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function test_shouldSucceed_whenAcceptorMatchesAllowedAcceptor() external { + proposal.allowedAcceptor = acceptor; + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function testFuzz_shouldFail_whenAcceptorDoesNotMatchAllowedAcceptor(address allowedAddr, address currentAcceptor) external { + vm.assume(allowedAddr != address(0)); + vm.assume(currentAcceptor != allowedAddr); + + proposal.allowedAcceptor = allowedAddr; + + vm.expectRevert( + abi.encodeWithSelector( + PWNStableProduct.CallerNotAllowedAcceptor.selector, + currentAcceptor, + allowedAddr + ) + ); + vm.prank(loanContract); + product.acceptProposal(loanId, currentAcceptor, proposer, _proposalData()); + } + } diff --git a/test/unit/PWNUniswapV3IndividualProduct.t.sol b/test/unit/PWNUniswapV3IndividualProduct.t.sol index b15afbc..cacbd01 100644 --- a/test/unit/PWNUniswapV3IndividualProduct.t.sol +++ b/test/unit/PWNUniswapV3IndividualProduct.t.sol @@ -71,6 +71,7 @@ abstract contract PWNUniswapV3IndividualProductTest is Test { nonce: 0, expiration: block.timestamp + 1 days, proposerSpecHash: bytes32(0), + allowedAcceptor: address(0), loanContract: loanContract }); @@ -104,7 +105,7 @@ abstract contract PWNUniswapV3IndividualProductTest is Test { function _hashProposalTypedData(PWNUniswapV3IndividualProduct.Proposal memory _proposal) internal view returns (bytes32) { return keccak256(abi.encodePacked( - keccak256("Proposal(uint256 collateralId,bool token0Denominator,address creditAddress,uint256 creditAmount,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address loanContract)"), + keccak256("Proposal(uint256 collateralId,bool token0Denominator,address creditAddress,uint256 creditAmount,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address allowedAcceptor,address loanContract)"), product.exposed_erc712EncodeProposal(_proposal) )); } @@ -412,6 +413,37 @@ contract PWNUniswapV3IndividualProduct_acceptProposal_Test is PWNUniswapV3Indivi assertEq(terms.principal, proposal.creditAmount); } + function test_shouldSucceed_whenAllowedAcceptorIsZero() external { + proposal.allowedAcceptor = address(0); + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function test_shouldSucceed_whenAcceptorMatchesAllowedAcceptor() external { + proposal.allowedAcceptor = acceptor; + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function testFuzz_shouldFail_whenAcceptorDoesNotMatchAllowedAcceptor(address allowedAddr, address currentAcceptor) external { + vm.assume(allowedAddr != address(0)); + vm.assume(currentAcceptor != allowedAddr); + + proposal.allowedAcceptor = allowedAddr; + + vm.expectRevert( + abi.encodeWithSelector( + PWNUniswapV3IndividualProduct.CallerNotAllowedAcceptor.selector, + currentAcceptor, + allowedAddr + ) + ); + vm.prank(loanContract); + product.acceptProposal(loanId, currentAcceptor, proposer, _proposalData()); + } + } diff --git a/test/unit/PWNUniswapV3SetProduct.t.sol b/test/unit/PWNUniswapV3SetProduct.t.sol index 921646f..f27a2ac 100644 --- a/test/unit/PWNUniswapV3SetProduct.t.sol +++ b/test/unit/PWNUniswapV3SetProduct.t.sol @@ -76,6 +76,7 @@ abstract contract PWNUniswapV3SetProductTest is Test { nonce: 0, expiration: block.timestamp + 1 days, proposerSpecHash: bytes32(0), + allowedAcceptor: address(0), loanContract: loanContract }); proposal.tokenAAllowlist.push(token1); @@ -118,7 +119,7 @@ abstract contract PWNUniswapV3SetProductTest is Test { function _hashProposalTypedData(PWNUniswapV3SetProduct.Proposal memory _proposal) internal view returns (bytes32) { return keccak256(abi.encodePacked( - keccak256("Proposal(address[] tokenAAllowlist,address[] tokenBAllowlist,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address loanContract)"), + keccak256("Proposal(address[] tokenAAllowlist,address[] tokenBAllowlist,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 acceptableLoanToValue,uint256 interestAPR,uint256 duration,uint256 liquidationLoanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 nonceSpace,uint256 nonce,uint256 expiration,bytes32 proposerSpecHash,address allowedAcceptor,address loanContract)"), product.exposed_erc712EncodeProposal(_proposal) )); } @@ -520,6 +521,37 @@ contract PWNUniswapV3SetProduct_acceptProposal_Test is PWNUniswapV3SetProductTes assertEq(terms.principal, acceptorValues.creditAmount); } + function test_shouldSucceed_whenAllowedAcceptorIsZero() external { + proposal.allowedAcceptor = address(0); + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function test_shouldSucceed_whenAcceptorMatchesAllowedAcceptor() external { + proposal.allowedAcceptor = acceptor; + + vm.prank(loanContract); + product.acceptProposal(loanId, acceptor, proposer, _proposalData()); + } + + function testFuzz_shouldFail_whenAcceptorDoesNotMatchAllowedAcceptor(address allowedAddr, address currentAcceptor) external { + vm.assume(allowedAddr != address(0)); + vm.assume(currentAcceptor != allowedAddr); + + proposal.allowedAcceptor = allowedAddr; + + vm.expectRevert( + abi.encodeWithSelector( + PWNUniswapV3SetProduct.CallerNotAllowedAcceptor.selector, + currentAcceptor, + allowedAddr + ) + ); + vm.prank(loanContract); + product.acceptProposal(loanId, currentAcceptor, proposer, _proposalData()); + } + }