From e3154acf886039dbfcd33a7f25307b1c8f961c5c Mon Sep 17 00:00:00 2001 From: microHoffman Date: Tue, 16 Sep 2025 00:24:39 +0200 Subject: [PATCH 01/11] fix: make deployment work for v1.5 --- deployments/protocol/v1.5.json | 21 ++++++++++- foundry.toml | 60 +++++++++++++++--------------- script/PWN.s.sol | 68 ++++++++++++++++++++++++++++------ src/Deployments.sol | 3 ++ 4 files changed, 109 insertions(+), 43 deletions(-) diff --git a/deployments/protocol/v1.5.json b/deployments/protocol/v1.5.json index 0db3279..86830b8 100644 --- a/deployments/protocol/v1.5.json +++ b/deployments/protocol/v1.5.json @@ -1,3 +1,22 @@ { - + "11155111": { + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "crowdsourceLenderVault": "", + "hooks": { + "compoundLender": "", + "directLenderRepayment": "", + "refinanceBorrowerCreate": "" + }, + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loan": "", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "products": { + "installments": "" + }, + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "utilizedCredit": "0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D" + } } diff --git a/foundry.toml b/foundry.toml index a2a49c7..2ce7228 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,41 +14,41 @@ gas_reports = ["PWNSimpleLoan"] [rpc_endpoints] # Mainnets -mainnet = "${ETHEREUM_URL}" -polygon = "${POLYGON_URL}" -arbitrum = "${ARBITRUM_URL}" -optimism = "${OPTIMISM_URL}" -base = "${BASE_URL}" -cronos = "${CRONOS_URL}" -mantle = "${MANTLE_URL}" -bsc = "${BSC_URL}" -linea = "${LINEA_URL}" -gnosis = "${GNOSIS_URL}" -world = "${WORLDCHAIN_URL}" -unichain = "${UNICHAIN_URL}" -ink = "${INK_URL}" -sonic = "${SONIC_URL}" -celo = "${CELO_URL}" +#mainnet = "${ETHEREUM_URL}" +#polygon = "${POLYGON_URL}" +#arbitrum = "${ARBITRUM_URL}" +#optimism = "${OPTIMISM_URL}" +#base = "${BASE_URL}" +#cronos = "${CRONOS_URL}" +#mantle = "${MANTLE_URL}" +#bsc = "${BSC_URL}" +#linea = "${LINEA_URL}" +#gnosis = "${GNOSIS_URL}" +#world = "${WORLDCHAIN_URL}" +#unichain = "${UNICHAIN_URL}" +#ink = "${INK_URL}" +#sonic = "${SONIC_URL}" +#celo = "${CELO_URL}" # Testnets sepolia = "${SEPOLIA_URL}" -unichain-sepolia = "${UNICHAIN_SEPOLIA_URL}" +#unichain-sepolia = "${UNICHAIN_SEPOLIA_URL}" # Devnets -tenderly = "${TENDERLY_URL}" -local = "${LOCAL_URL}" +#tenderly = "${TENDERLY_URL}" +#local = "${LOCAL_URL}" [etherscan] -mainnet = { key = "${ETHERSCAN_MAINNET_API_KEY}" } -polygon = { key = "${POLYGONSCAN_API_KEY}" } -arbitrum = { key = "${ARBISCAN_API_KEY}" } -optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" } -base = { key = "${BASESCAN_API_KEY}" } -cronos = { key = "${CRONOSCAN_API_KEY}" } -bsc = { key = "${BSCSCAN_API_KEY}" } -linea = { key = "${LINEASCAN_API_KEY}" } -gnosis = { key = "${GNOSISSCAN_API_KEY}" } -world = { key = "${WORLDSCAN_API_KEY}" } -unichain = { key = "${UNISCAN_API_KEY}", url = "https://api.uniscan.xyz/api", chain = 130 } -sonic = { key = "${SONICSCAN_API_KEY}", url = "https://api.sonicscan.org/api", chain = 146 } +#mainnet = { key = "${ETHERSCAN_MAINNET_API_KEY}" } +#polygon = { key = "${POLYGONSCAN_API_KEY}" } +# arbitrum = { key = "${ARBISCAN_API_KEY}" } +# optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" } +# base = { key = "${BASESCAN_API_KEY}" } +#cronos = { key = "${CRONOSCAN_API_KEY}" } +#bsc = { key = "${BSCSCAN_API_KEY}" } +#linea = { key = "${LINEASCAN_API_KEY}" } +#gnosis = { key = "${GNOSISSCAN_API_KEY}" } +#world = { key = "${WORLDSCAN_API_KEY}" } +#unichain = { key = "${UNISCAN_API_KEY}", url = "https://api.uniscan.xyz/api", chain = 130 } +#sonic = { key = "${SONICSCAN_API_KEY}", url = "https://api.sonicscan.org/api", chain = 146 } sepolia = { key = "${ETHERSCAN_MAINNET_API_KEY}" } diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 84436fe..eccca13 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -21,9 +21,11 @@ import { MultiTokenCategoryRegistry, IChainlinkFeedRegistryLike, PWNStableProduct, - PWNWorldStableProduct + PWNInstallmentsProduct } from "pwn/Deployments.sol"; +import { PWNCrowdsourceLenderVault } from "pwn/periphery/crowdsource/PWNCrowdsourceLenderVault.sol"; + library PWNContractDeployerSalt { @@ -44,7 +46,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; @@ -100,36 +105,75 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ PWNContractDeployerSalt.LOAN, abi.encodePacked( type(PWNLoan).creationCode, - abi.encode(address(__d.hub), address(__d.loanToken), address(__d.config), address(__d.categoryRegistry), __e.permit2) + abi.encode(address(__d.loanToken), address(__d.config), address(__d.categoryRegistry)) ) ) ); - __d.products.stable = PWNStableProduct( + __d.products.installments = PWNInstallmentsProduct( _deploy( - PWNContractDeployerSalt.STABLE_PRODUCT, + PWNContractDeployerSalt.INSTALLMENTS_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") + type(PWNInstallmentsProduct).creationCode, + abi.encode(address(__d.hub), address(__d.revokedNonce), address(__d.utilizedCredit), address(__d.chainlinkFeedRegistry), __e.chainlinkL2SequencerUptimeFeed, __e.weth) ) ) ); console2.log("PWNLoan:", address(__d.loan)); - console2.log("PWNStableProduct:", address(__d.products.stable)); + console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); - address[] memory addrs = new address[](3); + address[] memory addrs = new address[](2); addrs[0] = address(__d.loan); - addrs[1] = address(__d.products.stable); - addrs[2] = address(__d.products.stable); + addrs[1] = address(__d.products.installments); - bytes32[] memory tags = new bytes32[](3); + bytes32[] memory tags = new bytes32[](2); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.LOAN_PROPOSAL; - tags[2] = PWNHubTags.NONCE_MANAGER; + // TODO on what contract this should be called? console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); + address[] memory feedIntermediaryDenominations = new address[](1); + // USDC / USD feed + ETH / USD feed + feedIntermediaryDenominations[0] = address(840); // USD representation in chainlink + bool[] memory feedInvertFlags = new bool[](2); + feedInvertFlags[0] = false; + feedInvertFlags[1] = true; + + __d.crowdsourceLenderVault = PWNCrowdsourceLenderVault( + _deploy( + PWNContractDeployerSalt.CROWDSOURCE_LENDER_VAULT, + abi.encodePacked( + type(PWNCrowdsourceLenderVault).creationCode, + // TODO Terms terms parameter + abi.encode( + address(__d.loan), + address(__d.products.installments), + address(__e.aave), + "PWNInstallmentsProduct", + "PWNInstallmentsProduct", + PWNCrowdsourceLenderVault.Terms({ + collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), + creditAddress: address(0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8), + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags, + loanToValue: 7500, // 75% + interestAPR: 1000, // 10% + postponement: 2592000, // 30 days in seconds + duration: 63072000, // 730 days (2 years) in seconds + minCreditAmount: 5000000000, // 5000 tokens (assuming 6 decimals) + expiration: block.timestamp + 10368000 // 120 days from now + }) + ) + ) + ) + ); + + console2.log("PWNCrowdsourceLenderVault:", address(__d.crowdsourceLenderVault)); + + // TODO anything else to do here? + vm.stopBroadcast(); } diff --git a/src/Deployments.sol b/src/Deployments.sol index 0539a6c..7852e6b 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; From 3dc3f1a1c6f2683c3e3592e465224b86cecbba2c Mon Sep 17 00:00:00 2001 From: microHoffman Date: Tue, 16 Sep 2025 23:41:26 +0200 Subject: [PATCH 02/11] chore: adjust deployment for solo crowdsource lender vault deployment, add addresses to v1.5.json --- deployments/protocol/v1.5.json | 14 ++++-- script/PWN.s.sol | 92 +++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 38 deletions(-) diff --git a/deployments/protocol/v1.5.json b/deployments/protocol/v1.5.json index 86830b8..b895df9 100644 --- a/deployments/protocol/v1.5.json +++ b/deployments/protocol/v1.5.json @@ -4,17 +4,23 @@ "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", - "crowdsourceLenderVault": "", + "crowdsourceLenderVault": "0x533373013Fd978C0c98Cd1Cb0e1684D6142055FB", "hooks": { "compoundLender": "", "directLenderRepayment": "", - "refinanceBorrowerCreate": "" + "refinanceBorrowerCreate": "", + "vaultLender": "", + "aaveLender": "" }, "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loan": "", + "loan": "0x7f53449251EF28991C99EA25698B37BC13b173B8", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", "products": { - "installments": "" + "stable": "", + "installments": "0xEc22A11214567f580ef0f1eD8541c6Ff10d1880d", + "fixed": "", + "uniswapV3Individual": "", + "uniswapV3Set": "" }, "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", "utilizedCredit": "0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D" diff --git a/script/PWN.s.sol b/script/PWN.s.sol index eccca13..3862e66 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -21,7 +21,8 @@ import { MultiTokenCategoryRegistry, IChainlinkFeedRegistryLike, PWNStableProduct, - PWNInstallmentsProduct + PWNInstallmentsProduct, + IAaveLike } from "pwn/Deployments.sol"; import { PWNCrowdsourceLenderVault } from "pwn/periphery/crowdsource/PWNCrowdsourceLenderVault.sol"; @@ -100,46 +101,55 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ _loadDeployedAddresses(); vm.startBroadcast(); - __d.loan = PWNLoan( - _deploy( - PWNContractDeployerSalt.LOAN, - abi.encodePacked( - type(PWNLoan).creationCode, - abi.encode(address(__d.loanToken), address(__d.config), address(__d.categoryRegistry)) - ) - ) - ); - - __d.products.installments = PWNInstallmentsProduct( - _deploy( - PWNContractDeployerSalt.INSTALLMENTS_PRODUCT, - abi.encodePacked( - type(PWNInstallmentsProduct).creationCode, - abi.encode(address(__d.hub), address(__d.revokedNonce), address(__d.utilizedCredit), address(__d.chainlinkFeedRegistry), __e.chainlinkL2SequencerUptimeFeed, __e.weth) - ) - ) - ); + // __d.loan = PWNLoan( + // _deploy( + // PWNContractDeployerSalt.LOAN, + // abi.encodePacked( + // type(PWNLoan).creationCode, + // abi.encode(address(__d.loanToken), address(__d.config), address(__d.categoryRegistry)) + // ) + // ) + // ); + + // __d.products.installments = PWNInstallmentsProduct( + // _deploy( + // PWNContractDeployerSalt.INSTALLMENTS_PRODUCT, + // abi.encodePacked( + // type(PWNInstallmentsProduct).creationCode, + // abi.encode(address(__d.hub), address(__d.revokedNonce), address(__d.utilizedCredit), address(__d.chainlinkFeedRegistry), __e.chainlinkL2SequencerUptimeFeed, __e.weth) + // ) + // ) + // ); + + // !!! LOADING ADDRESSES FROM JSON DOES NOT WORK SOMEHOW, SO I AM JUST HARDCODING THE ADDRESSES HERE !!! + + __d.loan = PWNLoan(0x7f53449251EF28991C99EA25698B37BC13b173B8); + __d.products.installments = PWNInstallmentsProduct(address(0xEc22A11214567f580ef0f1eD8541c6Ff10d1880d)); + __e.aave = IAaveLike(address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951)); console2.log("PWNLoan:", address(__d.loan)); console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); - address[] memory addrs = new address[](2); - addrs[0] = address(__d.loan); - addrs[1] = address(__d.products.installments); + // address[] memory addrs = new address[](2); + // addrs[0] = address(__d.loan); + // addrs[1] = address(__d.products.installments); - bytes32[] memory tags = new bytes32[](2); - tags[0] = PWNHubTags.ACTIVE_LOAN; - tags[1] = PWNHubTags.LOAN_PROPOSAL; + // bytes32[] memory tags = new bytes32[](2); + // tags[0] = PWNHubTags.ACTIVE_LOAN; + // tags[1] = PWNHubTags.LOAN_PROPOSAL; - // TODO on what contract this should be called? - console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); + // // TODO on what contract this should be called? + // console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); - address[] memory feedIntermediaryDenominations = new address[](1); + address[] memory feedIntermediaryDenominations = new address[](0); // USDC / USD feed + ETH / USD feed - feedIntermediaryDenominations[0] = address(840); // USD representation in chainlink - bool[] memory feedInvertFlags = new bool[](2); + // feedIntermediaryDenominations[0] = address(840); // USD representation in chainlink + // LINK / ETH feed + // feedIntermediaryDenominations[0] = address(0x42585eD362B3f1BCa95c640FdFf35Ef899212734); + bool[] memory feedInvertFlags = new bool[](1); feedInvertFlags[0] = false; - feedInvertFlags[1] = true; + // feedInvertFlags[0] = false; + // feedInvertFlags[1] = true; __d.crowdsourceLenderVault = PWNCrowdsourceLenderVault( _deploy( @@ -153,16 +163,30 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ address(__e.aave), "PWNInstallmentsProduct", "PWNInstallmentsProduct", + // USDC CREDIT on Sepolia + // PWNCrowdsourceLenderVault.Terms({ + // collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), + // creditAddress: address(0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8), + // feedIntermediaryDenominations: feedIntermediaryDenominations, + // feedInvertFlags: feedInvertFlags, + // loanToValue: 7500, // 75% + // interestAPR: 1000, // 10% + // postponement: 2592000, // 30 days in seconds + // duration: 63072000, // 730 days (2 years) in seconds + // minCreditAmount: 5000000000, // 5000 tokens (assuming 6 decimals) + // expiration: block.timestamp + 10368000 // 120 days from now + // }) + // LINK CREDIT on Sepolia PWNCrowdsourceLenderVault.Terms({ collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), - creditAddress: address(0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8), + creditAddress: address(0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5), feedIntermediaryDenominations: feedIntermediaryDenominations, feedInvertFlags: feedInvertFlags, loanToValue: 7500, // 75% interestAPR: 1000, // 10% postponement: 2592000, // 30 days in seconds duration: 63072000, // 730 days (2 years) in seconds - minCreditAmount: 5000000000, // 5000 tokens (assuming 6 decimals) + minCreditAmount: 500000000000000000000, expiration: block.timestamp + 10368000 // 120 days from now }) ) From 03170365bbdeca35652953953f71eb20cf5cb641 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Wed, 19 Nov 2025 03:02:40 +0100 Subject: [PATCH 03/11] fix: fixes after audit, hotfix updates to deployment script --- script/PWN.s.sol | 101 +++++++++++++----- .../crowdsource/PWNCrowdsourceLenderVault.sol | 15 ++- src/periphery/lib/Chainlink.sol | 4 +- .../product/PWNInstallmentsProduct.sol | 6 ++ 4 files changed, 99 insertions(+), 27 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 3862e66..a5eace3 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -101,56 +101,90 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ _loadDeployedAddresses(); vm.startBroadcast(); - // __d.loan = PWNLoan( - // _deploy( - // PWNContractDeployerSalt.LOAN, - // abi.encodePacked( - // type(PWNLoan).creationCode, - // abi.encode(address(__d.loanToken), address(__d.config), address(__d.categoryRegistry)) - // ) - // ) - // ); + // !!! TODO LOADING ADDRESSES FROM JSON DOES NOT WORK SOMEHOW, SO I AM JUST HARDCODING THE ADDRESSES HERE !!! + + // __d.loan = PWNLoan(0x7f53449251EF28991C99EA25698B37BC13b173B8); + __e.aave = IAaveLike(address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951)); + __d.hub = PWNHub(0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5); + __d.revokedNonce = PWNRevokedNonce(0x972204fF33348ee6889B2d0A3967dB67d7b08e4c); + __d.utilizedCredit = PWNUtilizedCredit(0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D); + __d.chainlinkFeedRegistry = IChainlinkFeedRegistryLike(0x8D5e90706E52a52853dA9A14fA1c63889a412851); + __e.chainlinkL2SequencerUptimeFeed = address(0x0000000000000000000000000000000000000000); + __e.weth = address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9); + __d.products.installments = PWNInstallmentsProduct(0x68669e7ec29070e3dfa684cb4893282Cd4C9E608); + + + __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.products.installments = PWNInstallmentsProduct( // _deploy( // PWNContractDeployerSalt.INSTALLMENTS_PRODUCT, // abi.encodePacked( // type(PWNInstallmentsProduct).creationCode, - // abi.encode(address(__d.hub), address(__d.revokedNonce), address(__d.utilizedCredit), address(__d.chainlinkFeedRegistry), __e.chainlinkL2SequencerUptimeFeed, __e.weth) + // abi.encode( + // address(__d.hub), + // address(__d.revokedNonce), + // address(__d.utilizedCredit), + // address(__d.chainlinkFeedRegistry), + // __e.chainlinkL2SequencerUptimeFeed, + // __e.weth + // ) // ) // ) // ); - // !!! LOADING ADDRESSES FROM JSON DOES NOT WORK SOMEHOW, SO I AM JUST HARDCODING THE ADDRESSES HERE !!! - - __d.loan = PWNLoan(0x7f53449251EF28991C99EA25698B37BC13b173B8); - __d.products.installments = PWNInstallmentsProduct(address(0xEc22A11214567f580ef0f1eD8541c6Ff10d1880d)); - __e.aave = IAaveLike(address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951)); - console2.log("PWNLoan:", address(__d.loan)); - console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); + // console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); + // console2.log("Aave:", address(__e.aave)); // address[] memory addrs = new address[](2); // addrs[0] = address(__d.loan); // addrs[1] = address(__d.products.installments); + address[] memory addrs = new address[](1); + addrs[0] = address(__d.loan); + // bytes32[] memory tags = new bytes32[](2); // tags[0] = PWNHubTags.ACTIVE_LOAN; // tags[1] = PWNHubTags.LOAN_PROPOSAL; + bytes32[] memory tags = new bytes32[](1); + tags[0] = PWNHubTags.ACTIVE_LOAN; + // // TODO on what contract this should be called? - // console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); + console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); - address[] memory feedIntermediaryDenominations = new address[](0); + address[] memory feedIntermediaryDenominations = new address[](1); + feedIntermediaryDenominations[0] = address(0x0000000000000000000000000000000000000348); // USDC / USD feed + ETH / USD feed // feedIntermediaryDenominations[0] = address(840); // USD representation in chainlink // LINK / ETH feed // feedIntermediaryDenominations[0] = address(0x42585eD362B3f1BCa95c640FdFf35Ef899212734); - bool[] memory feedInvertFlags = new bool[](1); + // EUR / ETH feed + // feedIntermediaryDenominations[0] = address(0x1a81afB8146aeFfCFc5E50e8479e826E7D55b910); + bool[] memory feedInvertFlags = new bool[](2); feedInvertFlags[0] = false; + feedInvertFlags[1] = true; // feedInvertFlags[0] = false; // feedInvertFlags[1] = true; + __d.crowdsourceLenderVault = PWNCrowdsourceLenderVault( _deploy( PWNContractDeployerSalt.CROWDSOURCE_LENDER_VAULT, @@ -177,17 +211,34 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ // expiration: block.timestamp + 10368000 // 120 days from now // }) // LINK CREDIT on Sepolia + // PWNCrowdsourceLenderVault.Terms({ + // collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), + // creditAddress: address(0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5), + // feedIntermediaryDenominations: feedIntermediaryDenominations, + // feedInvertFlags: feedInvertFlags, + // loanToValue: 7500, // 75% + // interestAPR: 1000, // 10% + // postponement: 2592000, // 30 days in seconds + // duration: 63072000, // 730 days (2 years) in seconds + // minCreditAmount: 500000000000000000000, + // expiration: block.timestamp + 10368000 // 120 days from now + // }) + // EURS CREDIT on Sepolia PWNCrowdsourceLenderVault.Terms({ collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), - creditAddress: address(0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5), + creditAddress: address(0x6d906e526a4e2Ca02097BA9d0caA3c382F52278E), feedIntermediaryDenominations: feedIntermediaryDenominations, feedInvertFlags: feedInvertFlags, loanToValue: 7500, // 75% interestAPR: 1000, // 10% - postponement: 2592000, // 30 days in seconds - duration: 63072000, // 730 days (2 years) in seconds - minCreditAmount: 500000000000000000000, - expiration: block.timestamp + 10368000 // 120 days from now + // postponement: 2592000, // 30 days in seconds + postponement: 1200, // 20 minutes in seconds + // duration: 63072000, // 730 days (2 years) in seconds + duration: 7200, // 2 hours in seconds + // minCreditAmount: 50000, // 500 EURS + minCreditAmount: 10000, // 100 EURS + // expiration: block.timestamp + 10368000 // 120 days from now + expiration: block.timestamp + 36000 // 10 hours from now }) ) ) diff --git a/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol b/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol index 7fe6e87..acd406a 100644 --- a/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol +++ b/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol @@ -99,6 +99,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, @@ -194,7 +199,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 +319,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/PWNInstallmentsProduct.sol b/src/periphery/product/PWNInstallmentsProduct.sol index 27c13c4..4db56aa 100644 --- a/src/periphery/product/PWNInstallmentsProduct.sol +++ b/src/periphery/product/PWNInstallmentsProduct.sol @@ -219,6 +219,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, @@ -268,6 +269,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(); From 8e842be2242df94c07ca49a567e3808b24751bf8 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Wed, 19 Nov 2025 13:25:53 +0100 Subject: [PATCH 04/11] chore: update v1.5 addresses in deployment json, remove commented code from PWN.s.sol --- deployments/protocol/v1.5.json | 6 +-- script/PWN.s.sol | 96 +++++++++------------------------- 2 files changed, 29 insertions(+), 73 deletions(-) diff --git a/deployments/protocol/v1.5.json b/deployments/protocol/v1.5.json index b895df9..a3e18c0 100644 --- a/deployments/protocol/v1.5.json +++ b/deployments/protocol/v1.5.json @@ -4,7 +4,7 @@ "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", - "crowdsourceLenderVault": "0x533373013Fd978C0c98Cd1Cb0e1684D6142055FB", + "crowdsourceLenderVault": "0x2474d856d1463435932866de8118CC40Ed3c01d8", "hooks": { "compoundLender": "", "directLenderRepayment": "", @@ -13,11 +13,11 @@ "aaveLender": "" }, "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loan": "0x7f53449251EF28991C99EA25698B37BC13b173B8", + "loan": "0xc58791ec351349a82036aE712976109C10e34217", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", "products": { "stable": "", - "installments": "0xEc22A11214567f580ef0f1eD8541c6Ff10d1880d", + "installments": "0x68669e7ec29070e3dfa684cb4893282Cd4C9E608", "fixed": "", "uniswapV3Individual": "", "uniswapV3Set": "" diff --git a/script/PWN.s.sol b/script/PWN.s.sol index a5eace3..84c93f6 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -103,7 +103,7 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ // !!! TODO LOADING ADDRESSES FROM JSON DOES NOT WORK SOMEHOW, SO I AM JUST HARDCODING THE ADDRESSES HERE !!! - // __d.loan = PWNLoan(0x7f53449251EF28991C99EA25698B37BC13b173B8); + __d.loan = PWNLoan(0xc58791ec351349a82036aE712976109C10e34217); __e.aave = IAaveLike(address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951)); __d.hub = PWNHub(0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5); __d.revokedNonce = PWNRevokedNonce(0x972204fF33348ee6889B2d0A3967dB67d7b08e4c); @@ -118,19 +118,21 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ __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( + // _deploy( + // PWNContractDeployerSalt.LOAN, + // abi.encodePacked( + // type(PWNLoan).creationCode, + // abi.encode( + // address(__d.loanToken), + // address(__d.config), + // address(__d.categoryRegistry) + // ) + // ) + // ) + // ); + + // console2.log("PWNLoan:", address(__d.loan)); // __d.products.installments = PWNInstallmentsProduct( // _deploy( @@ -149,81 +151,41 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ // ) // ); - console2.log("PWNLoan:", address(__d.loan)); // console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); - // console2.log("Aave:", address(__e.aave)); - // address[] memory addrs = new address[](2); + // address[] memory addrs = new address[](1); // addrs[0] = address(__d.loan); - // addrs[1] = address(__d.products.installments); - address[] memory addrs = new address[](1); - addrs[0] = address(__d.loan); - - // bytes32[] memory tags = new bytes32[](2); + // bytes32[] memory tags = new bytes32[](1); // tags[0] = PWNHubTags.ACTIVE_LOAN; - // tags[1] = PWNHubTags.LOAN_PROPOSAL; - - bytes32[] memory tags = new bytes32[](1); - tags[0] = PWNHubTags.ACTIVE_LOAN; - // // TODO on what contract this should be called? - console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); + // 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)); address[] memory feedIntermediaryDenominations = new address[](1); feedIntermediaryDenominations[0] = address(0x0000000000000000000000000000000000000348); - // USDC / USD feed + ETH / USD feed - // feedIntermediaryDenominations[0] = address(840); // USD representation in chainlink - // LINK / ETH feed - // feedIntermediaryDenominations[0] = address(0x42585eD362B3f1BCa95c640FdFf35Ef899212734); - // EUR / ETH feed - // feedIntermediaryDenominations[0] = address(0x1a81afB8146aeFfCFc5E50e8479e826E7D55b910); bool[] memory feedInvertFlags = new bool[](2); feedInvertFlags[0] = false; feedInvertFlags[1] = true; - // feedInvertFlags[0] = false; - // feedInvertFlags[1] = true; + __d.loan = PWNLoan(0xc58791ec351349a82036aE712976109C10e34217); + __d.products.installments = PWNInstallmentsProduct(0x68669e7ec29070e3dfa684cb4893282Cd4C9E608); + __e.aave = IAaveLike(address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951)); __d.crowdsourceLenderVault = PWNCrowdsourceLenderVault( _deploy( PWNContractDeployerSalt.CROWDSOURCE_LENDER_VAULT, abi.encodePacked( type(PWNCrowdsourceLenderVault).creationCode, - // TODO Terms terms parameter abi.encode( address(__d.loan), address(__d.products.installments), address(__e.aave), "PWNInstallmentsProduct", "PWNInstallmentsProduct", - // USDC CREDIT on Sepolia - // PWNCrowdsourceLenderVault.Terms({ - // collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), - // creditAddress: address(0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8), - // feedIntermediaryDenominations: feedIntermediaryDenominations, - // feedInvertFlags: feedInvertFlags, - // loanToValue: 7500, // 75% - // interestAPR: 1000, // 10% - // postponement: 2592000, // 30 days in seconds - // duration: 63072000, // 730 days (2 years) in seconds - // minCreditAmount: 5000000000, // 5000 tokens (assuming 6 decimals) - // expiration: block.timestamp + 10368000 // 120 days from now - // }) - // LINK CREDIT on Sepolia - // PWNCrowdsourceLenderVault.Terms({ - // collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), - // creditAddress: address(0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5), - // feedIntermediaryDenominations: feedIntermediaryDenominations, - // feedInvertFlags: feedInvertFlags, - // loanToValue: 7500, // 75% - // interestAPR: 1000, // 10% - // postponement: 2592000, // 30 days in seconds - // duration: 63072000, // 730 days (2 years) in seconds - // minCreditAmount: 500000000000000000000, - // expiration: block.timestamp + 10368000 // 120 days from now - // }) - // EURS CREDIT on Sepolia PWNCrowdsourceLenderVault.Terms({ collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), creditAddress: address(0x6d906e526a4e2Ca02097BA9d0caA3c382F52278E), @@ -231,13 +193,9 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ feedInvertFlags: feedInvertFlags, loanToValue: 7500, // 75% interestAPR: 1000, // 10% - // postponement: 2592000, // 30 days in seconds postponement: 1200, // 20 minutes in seconds - // duration: 63072000, // 730 days (2 years) in seconds duration: 7200, // 2 hours in seconds - // minCreditAmount: 50000, // 500 EURS minCreditAmount: 10000, // 100 EURS - // expiration: block.timestamp + 10368000 // 120 days from now expiration: block.timestamp + 36000 // 10 hours from now }) ) @@ -247,8 +205,6 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ console2.log("PWNCrowdsourceLenderVault:", address(__d.crowdsourceLenderVault)); - // TODO anything else to do here? - vm.stopBroadcast(); } From 3d76d24b516013c5d31fdbcb6187534f3b5c4410 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Thu, 20 Nov 2025 02:09:57 +0100 Subject: [PATCH 05/11] chore(PWN.s.sol): update deploy script for mainnet deploy --- script/PWN.s.sol | 139 +++++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 84c93f6..95a75f8 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -103,77 +103,86 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ // !!! TODO LOADING ADDRESSES FROM JSON DOES NOT WORK SOMEHOW, SO I AM JUST HARDCODING THE ADDRESSES HERE !!! - __d.loan = PWNLoan(0xc58791ec351349a82036aE712976109C10e34217); - __e.aave = IAaveLike(address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951)); + __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) + ) + ) + ) + ); + + 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(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9); - __d.products.installments = PWNInstallmentsProduct(0x68669e7ec29070e3dfa684cb4893282Cd4C9E608); + __e.weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + __d.products.installments = PWNInstallmentsProduct( + _deploy( + PWNContractDeployerSalt.INSTALLMENTS_PRODUCT, + abi.encodePacked( + type(PWNInstallmentsProduct).creationCode, + abi.encode( + address(__d.hub), + address(__d.revokedNonce), + address(__d.utilizedCredit), + address(__d.chainlinkFeedRegistry), + __e.chainlinkL2SequencerUptimeFeed, + __e.weth + ) + ) + ) + ); - __d.loanToken = PWNLOAN(0x4440C069272cC34b80C7B11bEE657D0349Ba9C23); - __d.config = PWNConfig(0xd52a2898d61636bB3eEF0d145f05352FF543bdCC); - __d.categoryRegistry = MultiTokenCategoryRegistry(0xbB2168d5546A94AE2DA9254e63D88F7f137B2534); + console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); + + address[] memory addrs = new address[](3); + addrs[0] = address(__d.loan); + addrs[1] = address(__d.products.installments); + addrs[2] = address(__d.products.installments); - // __d.loan = PWNLoan( - // _deploy( - // PWNContractDeployerSalt.LOAN, - // abi.encodePacked( - // type(PWNLoan).creationCode, - // abi.encode( - // address(__d.loanToken), - // address(__d.config), - // address(__d.categoryRegistry) - // ) - // ) - // ) - // ); - - // console2.log("PWNLoan:", address(__d.loan)); - - // __d.products.installments = PWNInstallmentsProduct( - // _deploy( - // PWNContractDeployerSalt.INSTALLMENTS_PRODUCT, - // abi.encodePacked( - // type(PWNInstallmentsProduct).creationCode, - // abi.encode( - // address(__d.hub), - // address(__d.revokedNonce), - // address(__d.utilizedCredit), - // address(__d.chainlinkFeedRegistry), - // __e.chainlinkL2SequencerUptimeFeed, - // __e.weth - // ) - // ) - // ) - // ); - - // console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); - - // address[] memory addrs = new address[](1); - // addrs[0] = address(__d.loan); - - // bytes32[] memory tags = new bytes32[](1); - // tags[0] = PWNHubTags.ACTIVE_LOAN; + bytes32[] memory tags = new bytes32[](3); + tags[0] = PWNHubTags.ACTIVE_LOAN; + tags[1] = PWNHubTags.LOAN_PROPOSAL; + tags[2] = PWNHubTags.NONCE_MANAGER; // 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)); - - address[] memory feedIntermediaryDenominations = new address[](1); - feedIntermediaryDenominations[0] = address(0x0000000000000000000000000000000000000348); - bool[] memory feedInvertFlags = new bool[](2); + console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); + + /* + USDC --> rETH route + 1) USDC --> USD ( 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6 , non inverted ) + 2) USD --> ETH ( 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 , inverted ) + 3) eth --> rETH ( 0x536218f9E9Eb48863970252233c8F271f554C2d0 , 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; - __d.loan = PWNLoan(0xc58791ec351349a82036aE712976109C10e34217); - __d.products.installments = PWNInstallmentsProduct(0x68669e7ec29070e3dfa684cb4893282Cd4C9E608); - __e.aave = IAaveLike(address(0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951)); + // __d.loan = PWNLoan(0xc58791ec351349a82036aE712976109C10e34217); + // __d.products.installments = PWNInstallmentsProduct(0x68669e7ec29070e3dfa684cb4893282Cd4C9E608); + __e.aave = IAaveLike(address(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2)); __d.crowdsourceLenderVault = PWNCrowdsourceLenderVault( _deploy( @@ -183,20 +192,20 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ abi.encode( address(__d.loan), address(__d.products.installments), - address(__e.aave), - "PWNInstallmentsProduct", - "PWNInstallmentsProduct", + address(__e.aave), + "BordelMortgageVaultShare", + "BORDEL", PWNCrowdsourceLenderVault.Terms({ - collateralAddress: address(0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9), - creditAddress: address(0x6d906e526a4e2Ca02097BA9d0caA3c382F52278E), + collateralAddress: address(0xae78736Cd615f374D3085123A210448E74Fc6393), // rETH + creditAddress: address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48), // USDC feedIntermediaryDenominations: feedIntermediaryDenominations, feedInvertFlags: feedInvertFlags, loanToValue: 7500, // 75% - interestAPR: 1000, // 10% - postponement: 1200, // 20 minutes in seconds - duration: 7200, // 2 hours in seconds - minCreditAmount: 10000, // 100 EURS - expiration: block.timestamp + 36000 // 10 hours from now + interestAPR: 200, // 2% + postponement: 15780000, // 6 months in seconds + duration: 157800000, // 5 years in seconds + minCreditAmount: 180000000000, // 180 000 USDC (6 decimals) + expiration: block.timestamp + 8640000 // 100 days from now }) ) ) From 3113439ef6664f78d645f2911f70f229a5f77641 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Thu, 20 Nov 2025 03:15:55 +0100 Subject: [PATCH 06/11] chore(PWN.s.sol): use weETH instead of rETH, add v1.5 deployments for mainnet --- deployments/protocol/v1.5.json | 26 ++++++++++++++++++++++++++ script/PWN.s.sol | 6 +++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/deployments/protocol/v1.5.json b/deployments/protocol/v1.5.json index a3e18c0..c256e0a 100644 --- a/deployments/protocol/v1.5.json +++ b/deployments/protocol/v1.5.json @@ -1,4 +1,30 @@ { + "1": { + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "crowdsourceLenderVault": "", + "hooks": { + "compoundLender": "", + "directLenderRepayment": "", + "refinanceBorrowerCreate": "", + "vaultLender": "", + "aaveLender": "" + }, + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loan": "", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "products": { + "stable": "", + "installments": "", + "fixed": "", + "uniswapV3Individual": "", + "uniswapV3Set": "" + }, + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "utilizedCredit": "0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D" + }, "11155111": { "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 95a75f8..d3acf8c 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -166,10 +166,10 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ console2.logBytes(abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true)); /* - USDC --> rETH route + USDC --> weETH route 1) USDC --> USD ( 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6 , non inverted ) 2) USD --> ETH ( 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 , inverted ) - 3) eth --> rETH ( 0x536218f9E9Eb48863970252233c8F271f554C2d0 , inverted ) + 3) eth --> weETH ( 0x5c9C449BbC9a6075A2c061dF312a35fd1E05fF22 , inverted ) */ address[] memory feedIntermediaryDenominations = new address[](2); @@ -196,7 +196,7 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ "BordelMortgageVaultShare", "BORDEL", PWNCrowdsourceLenderVault.Terms({ - collateralAddress: address(0xae78736Cd615f374D3085123A210448E74Fc6393), // rETH + collateralAddress: address(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee), // weETH creditAddress: address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48), // USDC feedIntermediaryDenominations: feedIntermediaryDenominations, feedInvertFlags: feedInvertFlags, From e56b2f4d902bd4d9cb7b11dcf5e4e0f77198baaf Mon Sep 17 00:00:00 2001 From: microHoffman Date: Sun, 14 Dec 2025 14:23:45 +0100 Subject: [PATCH 07/11] test: fix not working tests --- foundry.lock | 20 +++++++ foundry.toml | 60 +++++++++---------- .../fork/PWNStableInterestProposal.fork.t.sol | 13 +++- test/unit/Chainlink.t.sol | 3 +- 4 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 foundry.lock 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/foundry.toml b/foundry.toml index 2ce7228..a2a49c7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,41 +14,41 @@ gas_reports = ["PWNSimpleLoan"] [rpc_endpoints] # Mainnets -#mainnet = "${ETHEREUM_URL}" -#polygon = "${POLYGON_URL}" -#arbitrum = "${ARBITRUM_URL}" -#optimism = "${OPTIMISM_URL}" -#base = "${BASE_URL}" -#cronos = "${CRONOS_URL}" -#mantle = "${MANTLE_URL}" -#bsc = "${BSC_URL}" -#linea = "${LINEA_URL}" -#gnosis = "${GNOSIS_URL}" -#world = "${WORLDCHAIN_URL}" -#unichain = "${UNICHAIN_URL}" -#ink = "${INK_URL}" -#sonic = "${SONIC_URL}" -#celo = "${CELO_URL}" +mainnet = "${ETHEREUM_URL}" +polygon = "${POLYGON_URL}" +arbitrum = "${ARBITRUM_URL}" +optimism = "${OPTIMISM_URL}" +base = "${BASE_URL}" +cronos = "${CRONOS_URL}" +mantle = "${MANTLE_URL}" +bsc = "${BSC_URL}" +linea = "${LINEA_URL}" +gnosis = "${GNOSIS_URL}" +world = "${WORLDCHAIN_URL}" +unichain = "${UNICHAIN_URL}" +ink = "${INK_URL}" +sonic = "${SONIC_URL}" +celo = "${CELO_URL}" # Testnets sepolia = "${SEPOLIA_URL}" -#unichain-sepolia = "${UNICHAIN_SEPOLIA_URL}" +unichain-sepolia = "${UNICHAIN_SEPOLIA_URL}" # Devnets -#tenderly = "${TENDERLY_URL}" -#local = "${LOCAL_URL}" +tenderly = "${TENDERLY_URL}" +local = "${LOCAL_URL}" [etherscan] -#mainnet = { key = "${ETHERSCAN_MAINNET_API_KEY}" } -#polygon = { key = "${POLYGONSCAN_API_KEY}" } -# arbitrum = { key = "${ARBISCAN_API_KEY}" } -# optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" } -# base = { key = "${BASESCAN_API_KEY}" } -#cronos = { key = "${CRONOSCAN_API_KEY}" } -#bsc = { key = "${BSCSCAN_API_KEY}" } -#linea = { key = "${LINEASCAN_API_KEY}" } -#gnosis = { key = "${GNOSISSCAN_API_KEY}" } -#world = { key = "${WORLDSCAN_API_KEY}" } -#unichain = { key = "${UNISCAN_API_KEY}", url = "https://api.uniscan.xyz/api", chain = 130 } -#sonic = { key = "${SONICSCAN_API_KEY}", url = "https://api.sonicscan.org/api", chain = 146 } +mainnet = { key = "${ETHERSCAN_MAINNET_API_KEY}" } +polygon = { key = "${POLYGONSCAN_API_KEY}" } +arbitrum = { key = "${ARBISCAN_API_KEY}" } +optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" } +base = { key = "${BASESCAN_API_KEY}" } +cronos = { key = "${CRONOSCAN_API_KEY}" } +bsc = { key = "${BSCSCAN_API_KEY}" } +linea = { key = "${LINEASCAN_API_KEY}" } +gnosis = { key = "${GNOSISSCAN_API_KEY}" } +world = { key = "${WORLDSCAN_API_KEY}" } +unichain = { key = "${UNISCAN_API_KEY}", url = "https://api.uniscan.xyz/api", chain = 130 } +sonic = { key = "${SONICSCAN_API_KEY}", url = "https://api.sonicscan.org/api", chain = 146 } sepolia = { key = "${ETHERSCAN_MAINNET_API_KEY}" } diff --git a/test/fork/PWNStableInterestProposal.fork.t.sol b/test/fork/PWNStableInterestProposal.fork.t.sol index b0a4f31..481d064 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; @@ -134,7 +137,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); @@ -209,7 +215,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..4afab22 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,7 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { } function testFuzz_shouldReturnPriceAndDecimals(uint256 _price, uint8 _decimals) external { - _price = bound(_price, 0, uint256(type(int256).max)); + _price = bound(_price, 1, uint256(type(int256).max)); _mockFeedDecimals(aggregator, _decimals); _mockLastRoundData(aggregator, int256(_price), 1); From 7010eed4e97b56f6f6aad20fccef9892e48e64ae Mon Sep 17 00:00:00 2001 From: microHoffman Date: Sun, 14 Dec 2025 14:34:47 +0100 Subject: [PATCH 08/11] feat: add allowedAcceptor property to Proposal structs --- .../crowdsource/PWNCrowdsourceLenderVault.sol | 1 + src/periphery/product/PWNFixedProduct.sol | 15 ++++++-- .../product/PWNInstallmentsProduct.sol | 15 ++++++-- src/periphery/product/PWNStableProduct.sol | 15 ++++++-- .../product/PWNUniswapV3IndividualProduct.sol | 15 ++++++-- .../product/PWNUniswapV3SetProduct.sol | 15 ++++++-- .../fork/PWNCrowdsourceLenderVault.fork.t.sol | 1 + .../fork/PWNStableInterestProposal.fork.t.sol | 1 + test/unit/Chainlink.t.sol | 1 + test/unit/PWNFixedProduct.t.sol | 34 ++++++++++++++++++- test/unit/PWNInstallmentsProduct.t.sol | 34 ++++++++++++++++++- test/unit/PWNStableProduct.t.sol | 34 ++++++++++++++++++- test/unit/PWNUniswapV3IndividualProduct.t.sol | 34 ++++++++++++++++++- test/unit/PWNUniswapV3SetProduct.t.sol | 34 ++++++++++++++++++- 14 files changed, 234 insertions(+), 15 deletions(-) diff --git a/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol b/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol index acd406a..934f087 100644 --- a/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol +++ b/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol @@ -124,6 +124,7 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR createHook: this, createHookData: "", repaymentHook: this, repaymentHookData: "" })), isProposerLender: true, + allowedAcceptor: address(0), loanContract: address(loanContract) }) )); 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 4db56aa..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); @@ -236,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) { @@ -251,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 }); @@ -464,6 +473,7 @@ contract PWNInstallmentsProduct is IPWNProduct { uint256 expiration; bytes32 proposerSpecHash; bool isProposerLender; + address allowedAcceptor; address loanContract; } @@ -485,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/fork/PWNCrowdsourceLenderVault.fork.t.sol b/test/fork/PWNCrowdsourceLenderVault.fork.t.sol index eb87573..3989341 100644 --- a/test/fork/PWNCrowdsourceLenderVault.fork.t.sol +++ b/test/fork/PWNCrowdsourceLenderVault.fork.t.sol @@ -122,6 +122,7 @@ contract PWNCrowdsourceLenderVaultForkTest is DeploymentTest { expiration: terms.expiration, proposerSpecHash: __d.loan.getLenderSpecHash(lenderSpec), isProposerLender: true, + allowedAcceptor: address(0), loanContract: address(__d.loan) }); acceptorValues = PWNInstallmentsProduct.AcceptorValues({ diff --git a/test/fork/PWNStableInterestProposal.fork.t.sol b/test/fork/PWNStableInterestProposal.fork.t.sol index 481d064..a178ce9 100644 --- a/test/fork/PWNStableInterestProposal.fork.t.sol +++ b/test/fork/PWNStableInterestProposal.fork.t.sol @@ -48,6 +48,7 @@ contract PWNStableProductForkTest is DeploymentTest { expiration: block.timestamp + 7 days, proposerSpecHash: bytes32(0), isProposerLender: true, + allowedAcceptor: address(0), loanContract: address(__d.loan) }); diff --git a/test/unit/Chainlink.t.sol b/test/unit/Chainlink.t.sol index 4afab22..6ec5899 100644 --- a/test/unit/Chainlink.t.sol +++ b/test/unit/Chainlink.t.sol @@ -521,6 +521,7 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { } function testFuzz_shouldReturnPriceAndDecimals(uint256 _price, uint8 _decimals) external { + // Price must be > 0 because fetchPrice reverts on price <= 0 _price = bound(_price, 1, uint256(type(int256).max)); _mockFeedDecimals(aggregator, _decimals); 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()); + } + } From 523b5e17b860e4c41d0c34cc9947a1bf6c56de1e Mon Sep 17 00:00:00 2001 From: microHoffman Date: Sun, 14 Dec 2025 14:51:00 +0100 Subject: [PATCH 09/11] fix: try to fix loading contract addresses from json, use always fallback for tests until full protocol is deployed --- deployments/protocol/v1.5.json | 8 +-- src/Deployments.sol | 115 +++++++++++++++++++++++++++++++-- test/DeploymentTest.t.sol | 9 ++- 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/deployments/protocol/v1.5.json b/deployments/protocol/v1.5.json index c256e0a..c62e244 100644 --- a/deployments/protocol/v1.5.json +++ b/deployments/protocol/v1.5.json @@ -4,7 +4,7 @@ "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", - "crowdsourceLenderVault": "", + "crowdsourceLenderVault": "0x41dCf7E9ECDBd3f0bc69cba50b49f1B37ae65Ca2", "hooks": { "compoundLender": "", "directLenderRepayment": "", @@ -13,11 +13,11 @@ "aaveLender": "" }, "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loan": "", + "loan": "0xc58791ec351349a82036aE712976109C10e34217", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", "products": { "stable": "", - "installments": "", + "installments": "0x59Fd11B2518238E363bd4CC2aBb50455d1587966", "fixed": "", "uniswapV3Individual": "", "uniswapV3Set": "" @@ -51,4 +51,4 @@ "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", "utilizedCredit": "0x8E6F44DEa3c11d69C63655BDEcbA25Fa986BCE9D" } -} +} \ No newline at end of file diff --git a/src/Deployments.sol b/src/Deployments.sol index 7852e6b..2873d1d 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -116,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/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"); From 6b582f594bbc564aaec9495873ddc73ca1a4df21 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Mon, 15 Dec 2025 19:13:30 +0100 Subject: [PATCH 10/11] feat: add allowedAcceptor to the PWNCrowdsourceLenderVault constructor Terms arg --- script/PWN.s.sol | 4 +++- src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol | 3 ++- test/fork/PWNCrowdsourceLenderVault.fork.t.sol | 5 +++-- test/unit/PWNCrowdsourceLenderVault.t.sol | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index d3acf8c..ed2667e 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -205,7 +205,9 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ postponement: 15780000, // 6 months in seconds duration: 157800000, // 5 years in seconds minCreditAmount: 180000000000, // 180 000 USDC (6 decimals) - expiration: block.timestamp + 8640000 // 100 days from now + expiration: block.timestamp + 8640000, // 100 days from now + // TODO double check this is correct address + allowedAcceptor: address(0x8a7a728a181E674833E018706422cb28543abD87) }) ) ) diff --git a/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol b/src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol index 934f087..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.*/ @@ -115,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, @@ -124,7 +126,6 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR createHook: this, createHookData: "", repaymentHook: this, repaymentHookData: "" })), isProposerLender: true, - allowedAcceptor: address(0), loanContract: address(loanContract) }) )); diff --git a/test/fork/PWNCrowdsourceLenderVault.fork.t.sol b/test/fork/PWNCrowdsourceLenderVault.fork.t.sol index 3989341..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,7 +123,7 @@ contract PWNCrowdsourceLenderVaultForkTest is DeploymentTest { expiration: terms.expiration, proposerSpecHash: __d.loan.getLenderSpecHash(lenderSpec), isProposerLender: true, - allowedAcceptor: address(0), + allowedAcceptor: terms.allowedAcceptor, loanContract: address(__d.loan) }); acceptorValues = PWNInstallmentsProduct.AcceptorValues({ 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); From aeb5654b9a11e2b75264ec6d3774fa824439d608 Mon Sep 17 00:00:00 2001 From: microHoffman Date: Fri, 19 Dec 2025 16:46:59 +0100 Subject: [PATCH 11/11] chore: add new deployed addresses on v1.5, fix test --- deployments/external/external.json | 2 +- deployments/protocol/v1.5.json | 4 +- script/PWN.s.sol | 58 ++++++++++++------- .../fork/PWNStableInterestProposal.fork.t.sol | 2 +- 4 files changed, 42 insertions(+), 24 deletions(-) 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 c62e244..af867a1 100644 --- a/deployments/protocol/v1.5.json +++ b/deployments/protocol/v1.5.json @@ -4,7 +4,7 @@ "chainlinkFeedRegistry": "0x8D5e90706E52a52853dA9A14fA1c63889a412851", "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", - "crowdsourceLenderVault": "0x41dCf7E9ECDBd3f0bc69cba50b49f1B37ae65Ca2", + "crowdsourceLenderVault": "0x77A63952572Dd0EAa7Fd21a3fDeaa80b4071A5e8", "hooks": { "compoundLender": "", "directLenderRepayment": "", @@ -17,7 +17,7 @@ "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", "products": { "stable": "", - "installments": "0x59Fd11B2518238E363bd4CC2aBb50455d1587966", + "installments": "0xE24D470DfDA38Ace1E154f55997DF34859892970", "fixed": "", "uniswapV3Individual": "", "uniswapV3Set": "" diff --git a/script/PWN.s.sol b/script/PWN.s.sol index ed2667e..7c224aa 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -101,27 +101,28 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ _loadDeployedAddresses(); vm.startBroadcast(); - // !!! TODO LOADING ADDRESSES FROM JSON DOES NOT WORK SOMEHOW, SO I AM JUST HARDCODING THE ADDRESSES HERE !!! + // !!! 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) - ) - ) - ) - ); - - console2.log("PWNLoan:", address(__d.loan)); + // __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); @@ -146,7 +147,7 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ ) ) ); - + // __d.products.installments = PWNInstallmentsProduct(address(0x366a95aDB86282d53c905F9b7d62746b02C4F679)); console2.log("PWNInstallmentsProduct:", address(__d.products.installments)); address[] memory addrs = new address[](3); @@ -159,6 +160,10 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ 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 @@ -179,6 +184,20 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ 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); @@ -201,12 +220,11 @@ forge script script/PWN.s.sol:Deploy --sig "deploy()" \ feedIntermediaryDenominations: feedIntermediaryDenominations, feedInvertFlags: feedInvertFlags, loanToValue: 7500, // 75% - interestAPR: 200, // 2% + 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 + 8640000, // 100 days from now - // TODO double check this is correct address + expiration: block.timestamp + 6480000, // 75 days from now allowedAcceptor: address(0x8a7a728a181E674833E018706422cb28543abD87) }) ) diff --git a/test/fork/PWNStableInterestProposal.fork.t.sol b/test/fork/PWNStableInterestProposal.fork.t.sol index a178ce9..52ccbba 100644 --- a/test/fork/PWNStableInterestProposal.fork.t.sol +++ b/test/fork/PWNStableInterestProposal.fork.t.sol @@ -204,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 {