diff --git a/foundry.toml b/foundry.toml index 4ba7667..45cfee2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,8 +4,10 @@ out = "artifacts" libs = ["lib"] via_ir = true optimizer = true -optimizer_runs = 200 +optimizer_runs = 1 solc_version = "0.8.22" +bytecode_hash = "none" +cbor_metadata = false remappings = [ "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", diff --git a/script/DeployAllAndSetupGoalBasedPaymentTreasury.s.sol b/script/DeployAllAndSetupGoalBasedPaymentTreasury.s.sol new file mode 100644 index 0000000..0c91a17 --- /dev/null +++ b/script/DeployAllAndSetupGoalBasedPaymentTreasury.s.sol @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {console2} from "forge-std/console2.sol"; +import {TestToken} from "../test/mocks/TestToken.sol"; +import {GlobalParams} from "src/GlobalParams.sol"; +import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {TreasuryFactory} from "src/TreasuryFactory.sol"; +import {GoalBasedPaymentTreasury} from "src/treasuries/GoalBasedPaymentTreasury.sol"; +import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; +import {DeployBase} from "./lib/DeployBase.s.sol"; + +/** + * @notice Script to deploy and setup all needed contracts for the protocol with GoalBasedPaymentTreasury + */ +contract DeployAllAndSetupGoalBasedPaymentTreasury is DeployBase { + // Customizable values (set through environment variables) + bytes32 platformHash; + uint256 protocolFeePercent; + uint256 platformFeePercent; + uint256 tokenMintAmount; + bool simulate; + uint256 bufferTime; + uint256 campaignLaunchBuffer; + uint256 minimumCampaignDuration; + uint256 maxPaymentExpiration; + + // Contract addresses + address testToken; + address globalParams; + address globalParamsImplementation; + address campaignInfoImplementation; + address treasuryFactory; + address treasuryFactoryImplementation; + address campaignInfoFactory; + address campaignInfoFactoryImplementation; + address goalBasedPaymentTreasuryImplementation; + + // User addresses + address deployerAddress; + address finalProtocolAdmin; + address finalPlatformAdmin; + address platformAdapter; + address backer1; + address backer2; + + // Flags to track what was completed + bool platformEnlisted = false; + bool implementationRegistered = false; + bool implementationApproved = false; + bool adminRightsTransferred = false; + + // Flags for contract deployment or reuse + bool testTokenDeployed = false; + bool globalParamsDeployed = false; + bool treasuryFactoryDeployed = false; + bool campaignInfoFactoryDeployed = false; + bool goalBasedPaymentTreasuryDeployed = false; + + // Configure parameters based on environment variables + function setupParams() internal { + // Get customizable values + string memory platformName = vm.envOr("PLATFORM_NAME", string("E-Commerce")); + + platformHash = keccak256(abi.encodePacked(platformName)); + protocolFeePercent = vm.envOr("PROTOCOL_FEE_PERCENT", uint256(100)); // Default 1% + platformFeePercent = vm.envOr("PLATFORM_FEE_PERCENT", uint256(400)); // Default 4% + tokenMintAmount = vm.envOr("TOKEN_MINT_AMOUNT", uint256(10000000e18)); + simulate = vm.envOr("SIMULATE", false); + bufferTime = vm.envOr("BUFFER_TIME", uint256(0)); + campaignLaunchBuffer = vm.envOr("CAMPAIGN_LAUNCH_BUFFER", uint256(0)); + minimumCampaignDuration = vm.envOr("MINIMUM_CAMPAIGN_DURATION", uint256(0)); + maxPaymentExpiration = vm.envOr("MAX_PAYMENT_EXPIRATION", uint256(0)); + + // Get user addresses + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + deployerAddress = vm.addr(deployerKey); + + // These are the final admin addresses that will receive control + finalProtocolAdmin = vm.envOr("PROTOCOL_ADMIN_ADDRESS", deployerAddress); + finalPlatformAdmin = vm.envOr("PLATFORM_ADMIN_ADDRESS", deployerAddress); + platformAdapter = vm.envOr("PLATFORM_ADAPTER_ADDRESS", address(0)); + backer1 = vm.envOr("BACKER1_ADDRESS", address(0)); + backer2 = vm.envOr("BACKER2_ADDRESS", address(0)); + + // Check for existing contract addresses + testToken = vm.envOr("TOKEN_ADDRESS", address(0)); + globalParams = vm.envOr("GLOBAL_PARAMS_ADDRESS", address(0)); + treasuryFactory = vm.envOr("TREASURY_FACTORY_ADDRESS", address(0)); + campaignInfoFactory = vm.envOr("CAMPAIGN_INFO_FACTORY_ADDRESS", address(0)); + goalBasedPaymentTreasuryImplementation = + vm.envOr("GOAL_BASED_PAYMENT_TREASURY_IMPLEMENTATION_ADDRESS", address(0)); + + console2.log("Using platform hash for:", platformName); + console2.log("Protocol fee percent:", protocolFeePercent); + console2.log("Platform fee percent:", platformFeePercent); + console2.log("Simulation mode:", simulate); + console2.log("Deployer address:", deployerAddress); + console2.log("Final protocol admin:", finalProtocolAdmin); + console2.log("Final platform admin:", finalPlatformAdmin); + console2.log("Platform adapter (trusted forwarder):", platformAdapter); + console2.log("Buffer time (seconds):", bufferTime); + console2.log("Campaign launch buffer (seconds):", campaignLaunchBuffer); + console2.log("Minimum campaign duration (seconds):", minimumCampaignDuration); + console2.log("Max payment expiration (seconds):", maxPaymentExpiration); + } + + function setRegistryValues() internal { + if (!globalParamsDeployed) { + console2.log("Skipping setRegistryValues - using existing GlobalParams"); + return; + } + + console2.log("Setting registry values on GlobalParams"); + // Only use startPrank in simulation mode + if (simulate) { + vm.startPrank(deployerAddress); + } + + GlobalParams(globalParams).addToRegistry(DataRegistryKeys.BUFFER_TIME, bytes32(bufferTime)); + GlobalParams(globalParams).addToRegistry(DataRegistryKeys.CAMPAIGN_LAUNCH_BUFFER, bytes32(campaignLaunchBuffer)); + GlobalParams(globalParams).addToRegistry( + DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION, bytes32(minimumCampaignDuration) + ); + + if (simulate) { + vm.stopPrank(); + } + } + + function setPlatformScopedMaxPaymentExpiration() internal { + if (maxPaymentExpiration == 0) { + console2.log("Skipping setPlatformScopedMaxPaymentExpiration - value is 0"); + return; + } + + if (!platformEnlisted) { + console2.log("Skipping setPlatformScopedMaxPaymentExpiration - platform not enlisted"); + return; + } + + console2.log("Setting platform-scoped MAX_PAYMENT_EXPIRATION"); + // Only use startPrank in simulation mode + if (simulate) { + vm.startPrank(deployerAddress); + } + + bytes32 platformScopedKey = + DataRegistryKeys.scopedToPlatform(DataRegistryKeys.MAX_PAYMENT_EXPIRATION, platformHash); + + GlobalParams(globalParams).addToRegistry(platformScopedKey, bytes32(maxPaymentExpiration)); + + if (simulate) { + vm.stopPrank(); + } + console2.log("Platform-scoped MAX_PAYMENT_EXPIRATION set successfully"); + } + + // Deploy or reuse contracts + function deployContracts() internal { + console2.log("Setting up contracts..."); + + // Deploy or reuse TestToken + // Only deploy TestToken if CURRENCIES is not provided (backward compatibility) + string memory tokenName = vm.envOr("TOKEN_NAME", string("TestToken")); + string memory tokenSymbol = vm.envOr("TOKEN_SYMBOL", string("TST")); + + if (testToken == address(0) && shouldDeployTestToken()) { + uint8 decimals = uint8(vm.envOr("TOKEN_DECIMALS", uint256(18))); + testToken = address(new TestToken(tokenName, tokenSymbol, decimals)); + testTokenDeployed = true; + console2.log("TestToken deployed at:", testToken); + } else if (testToken != address(0)) { + console2.log("Reusing TestToken at:", testToken); + } else { + console2.log("Skipping TestToken deployment - using custom tokens for currencies"); + } + + // Deploy or reuse GlobalParams + if (globalParams == address(0)) { + (bytes32[] memory currencies, address[][] memory tokensPerCurrency) = loadCurrenciesAndTokens(testToken); + + // Deploy GlobalParams with UUPS proxy + GlobalParams globalParamsImpl = new GlobalParams(); + globalParamsImplementation = address(globalParamsImpl); + bytes memory globalParamsInitData = abi.encodeWithSelector( + GlobalParams.initialize.selector, deployerAddress, protocolFeePercent, currencies, tokensPerCurrency + ); + ERC1967Proxy globalParamsProxy = new ERC1967Proxy(address(globalParamsImpl), globalParamsInitData); + globalParams = address(globalParamsProxy); + globalParamsDeployed = true; + console2.log("GlobalParams proxy deployed at:", globalParams); + console2.log(" Implementation:", globalParamsImplementation); + } else { + console2.log("Reusing GlobalParams at:", globalParams); + } + + // GlobalParams is required to continue + require(globalParams != address(0), "GlobalParams address is required"); + + // Deploy CampaignInfo implementation if needed for new deployments + if (campaignInfoFactory == address(0)) { + campaignInfoImplementation = address(new CampaignInfo()); + console2.log("CampaignInfo implementation deployed at:", campaignInfoImplementation); + } + + // Deploy or reuse TreasuryFactory + if (treasuryFactory == address(0)) { + // Deploy TreasuryFactory with UUPS proxy + TreasuryFactory treasuryFactoryImpl = new TreasuryFactory(); + treasuryFactoryImplementation = address(treasuryFactoryImpl); + bytes memory treasuryFactoryInitData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(globalParams)); + ERC1967Proxy treasuryFactoryProxy = new ERC1967Proxy(address(treasuryFactoryImpl), treasuryFactoryInitData); + treasuryFactory = address(treasuryFactoryProxy); + treasuryFactoryDeployed = true; + console2.log("TreasuryFactory proxy deployed at:", treasuryFactory); + console2.log(" Implementation:", treasuryFactoryImplementation); + } else { + console2.log("Reusing TreasuryFactory at:", treasuryFactory); + } + + // Deploy or reuse CampaignInfoFactory + if (campaignInfoFactory == address(0)) { + // Deploy CampaignInfoFactory with UUPS proxy + CampaignInfoFactory campaignFactoryImpl = new CampaignInfoFactory(); + campaignInfoFactoryImplementation = address(campaignFactoryImpl); + bytes memory campaignFactoryInitData = abi.encodeWithSelector( + CampaignInfoFactory.initialize.selector, + deployerAddress, + IGlobalParams(globalParams), + campaignInfoImplementation, + treasuryFactory + ); + ERC1967Proxy campaignFactoryProxy = new ERC1967Proxy(address(campaignFactoryImpl), campaignFactoryInitData); + campaignInfoFactory = address(campaignFactoryProxy); + campaignInfoFactoryDeployed = true; + console2.log("CampaignInfoFactory proxy deployed at:", campaignInfoFactory); + console2.log(" Implementation:", campaignInfoFactoryImplementation); + } else { + console2.log("Reusing CampaignInfoFactory at:", campaignInfoFactory); + } + + // Deploy or reuse GoalBasedPaymentTreasury implementation + if (goalBasedPaymentTreasuryImplementation == address(0)) { + goalBasedPaymentTreasuryImplementation = address(new GoalBasedPaymentTreasury()); + goalBasedPaymentTreasuryDeployed = true; + console2.log( + "GoalBasedPaymentTreasury implementation deployed at:", goalBasedPaymentTreasuryImplementation + ); + } else { + console2.log( + "Reusing GoalBasedPaymentTreasury implementation at:", goalBasedPaymentTreasuryImplementation + ); + } + } + + // Setup steps when deployer has all roles + function enlistPlatform() internal { + // Skip if we didn't deploy GlobalParams (assuming it's already set up) + if (!globalParamsDeployed) { + console2.log("Skipping enlistPlatform - using existing GlobalParams"); + platformEnlisted = true; + return; + } + + console2.log("Setting up: enlistPlatform"); + // Only use startPrank in simulation mode + if (simulate) { + vm.startPrank(deployerAddress); + } + + GlobalParams(globalParams).enlistPlatform( + platformHash, + deployerAddress, // Initially deployer is platform admin + platformFeePercent, + platformAdapter // Platform adapter (trusted forwarder) - can be set later with setPlatformAdapter + ); + + if (simulate) { + vm.stopPrank(); + } + platformEnlisted = true; + console2.log("Platform enlisted successfully"); + } + + function registerTreasuryImplementation() internal { + // Skip only if both TreasuryFactory and implementation are reused (assuming already set up) + if (!treasuryFactoryDeployed && !goalBasedPaymentTreasuryDeployed) { + console2.log("Skipping registerTreasuryImplementation - using existing contracts"); + implementationRegistered = true; + return; + } + + console2.log("Setting up: registerTreasuryImplementation"); + // Only use startPrank in simulation mode + if (simulate) { + vm.startPrank(deployerAddress); + } + + TreasuryFactory(treasuryFactory).registerTreasuryImplementation( + platformHash, + 0, // Implementation ID + goalBasedPaymentTreasuryImplementation + ); + + if (simulate) { + vm.stopPrank(); + } + implementationRegistered = true; + console2.log("Treasury implementation registered successfully"); + } + + function approveTreasuryImplementation() internal { + // Skip only if both TreasuryFactory and implementation are reused (assuming already set up) + if (!treasuryFactoryDeployed && !goalBasedPaymentTreasuryDeployed) { + console2.log("Skipping approveTreasuryImplementation - using existing contracts"); + implementationApproved = true; + return; + } + + console2.log("Setting up: approveTreasuryImplementation"); + // Only use startPrank in simulation mode + if (simulate) { + vm.startPrank(deployerAddress); + } + + TreasuryFactory(treasuryFactory).approveTreasuryImplementation( + platformHash, + 0 // Implementation ID + ); + + if (simulate) { + vm.stopPrank(); + } + implementationApproved = true; + console2.log("Treasury implementation approved successfully"); + } + + function mintTokens() internal { + // Only mint tokens if we deployed TestToken + if (!testTokenDeployed) { + console2.log("Skipping mintTokens - using existing TestToken"); + return; + } + + if (backer1 != address(0) && backer2 != address(0)) { + console2.log("Minting tokens to test backers"); + TestToken(testToken).mint(backer1, tokenMintAmount); + if (backer1 != backer2) { + TestToken(testToken).mint(backer2, tokenMintAmount); + } + console2.log("Tokens minted successfully"); + } + } + + // Transfer admin rights to final addresses + function transferAdminRights() internal { + // Skip if we didn't deploy GlobalParams (assuming it's already set up) + if (!globalParamsDeployed) { + console2.log("Skipping transferAdminRights - using existing GlobalParams"); + adminRightsTransferred = true; + return; + } + + console2.log("Transferring admin rights to final addresses..."); + // Only use startPrank in simulation mode + if (simulate) { + vm.startPrank(deployerAddress); + } + + // Only transfer if the final addresses are different from deployer + if (finalPlatformAdmin != deployerAddress) { + console2.log("Updating platform admin address for platform hash:", vm.toString(platformHash)); + GlobalParams(globalParams).updatePlatformAdminAddress(platformHash, finalPlatformAdmin); + } + + if (finalProtocolAdmin != deployerAddress) { + console2.log("Transferring protocol admin rights to:", finalProtocolAdmin); + GlobalParams(globalParams).updateProtocolAdminAddress(finalProtocolAdmin); + + //Transfer admin rights to the final protocol admin + GlobalParams(globalParams).transferOwnership(finalProtocolAdmin); + console2.log("GlobalParams transferred to:", finalProtocolAdmin); + if (campaignInfoFactoryDeployed) { + CampaignInfoFactory(campaignInfoFactory).transferOwnership(finalProtocolAdmin); + console2.log("CampaignInfoFactory transferred to:", finalProtocolAdmin); + } + } + + if (simulate) { + vm.stopPrank(); + } + + adminRightsTransferred = true; + console2.log("Admin rights transferred successfully"); + } + + function run() external { + // Load configuration + setupParams(); + + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + // Start broadcast with deployer key (skip in simulation mode) + if (!simulate) { + vm.startBroadcast(deployerKey); + } + + // Deploy or reuse contracts + deployContracts(); + setRegistryValues(); + + // Setup the protocol with individual transactions in the correct order + // Since deployer is both protocol and platform admin initially, we can do all steps + enlistPlatform(); + setPlatformScopedMaxPaymentExpiration(); + registerTreasuryImplementation(); + approveTreasuryImplementation(); + + // Mint tokens if needed + mintTokens(); + + // Finally, transfer admin rights to the final addresses + transferAdminRights(); + + // Stop broadcast (skip in simulation mode) + if (!simulate) { + vm.stopBroadcast(); + } + + // Output summary + console2.log("\n==========================================="); + console2.log(" Deployment & Setup Summary"); + console2.log("==========================================="); + console2.log("\n--- Core Protocol Contracts (UUPS Proxies) ---"); + console2.log("GLOBAL_PARAMS_PROXY:", globalParams); + if (globalParamsImplementation != address(0)) { + console2.log(" Implementation:", globalParamsImplementation); + } + console2.log("TREASURY_FACTORY_PROXY:", treasuryFactory); + if (treasuryFactoryImplementation != address(0)) { + console2.log(" Implementation:", treasuryFactoryImplementation); + } + console2.log("CAMPAIGN_INFO_FACTORY_PROXY:", campaignInfoFactory); + if (campaignInfoFactoryImplementation != address(0)) { + console2.log(" Implementation:", campaignInfoFactoryImplementation); + } + + console2.log("\n--- Treasury Implementation Contracts ---"); + if (campaignInfoImplementation != address(0)) { + console2.log("CAMPAIGN_INFO_IMPLEMENTATION:", campaignInfoImplementation); + } + console2.log("GOAL_BASED_PAYMENT_TREASURY_IMPLEMENTATION:", goalBasedPaymentTreasuryImplementation); + + console2.log("\n--- Platform Configuration ---"); + console2.log("Platform Name Hash:", vm.toString(platformHash)); + console2.log("Protocol Admin:", finalProtocolAdmin); + console2.log("Platform Admin:", finalPlatformAdmin); + console2.log("Platform Adapter (Trusted Forwarder):", platformAdapter); + console2.log("GlobalParams owner:", GlobalParams(globalParams).owner()); + console2.log("CampaignInfoFactory owner:", CampaignInfoFactory(campaignInfoFactory).owner()); + + console2.log("\n--- Supported Currencies & Tokens ---"); + string memory currenciesConfig = vm.envOr("CURRENCIES", string("")); + if (bytes(currenciesConfig).length > 0) { + string[] memory currencyStrings = _split(currenciesConfig, ","); + string memory tokensConfig = vm.envOr("TOKENS_PER_CURRENCY", string("")); + string[] memory perCurrencyConfigs = _split(tokensConfig, ";"); + + for (uint256 i = 0; i < currencyStrings.length; i++) { + string memory currency = _trimWhitespace(currencyStrings[i]); + console2.log(string(abi.encodePacked("Currency: ", currency))); + + string[] memory tokenStrings = _split(perCurrencyConfigs[i], ","); + for (uint256 j = 0; j < tokenStrings.length; j++) { + console2.log(" Token:", _trimWhitespace(tokenStrings[j])); + } + } + } else { + console2.log("Currency: USD (default)"); + console2.log(" Token:", testToken); + if (testTokenDeployed) { + console2.log(" (TestToken deployed for testing)"); + } + } + + if (backer1 != address(0)) { + console2.log("\n--- Test Backers (Tokens Minted) ---"); + console2.log("Backer1:", backer1); + if (backer2 != address(0) && backer1 != backer2) { + console2.log("Backer2:", backer2); + } + } + + console2.log("\n--- Setup Steps ---"); + console2.log("Platform enlisted:", platformEnlisted); + console2.log("Treasury implementation registered:", implementationRegistered); + console2.log("Treasury implementation approved:", implementationApproved); + console2.log("Admin rights transferred:", adminRightsTransferred); + + console2.log("\n==========================================="); + console2.log("Deployment and setup completed successfully!"); + console2.log("==========================================="); + } +} + diff --git a/script/DeployGoalBasedPaymentTreasuryImplementation.s.sol b/script/DeployGoalBasedPaymentTreasuryImplementation.s.sol new file mode 100644 index 0000000..98c569c --- /dev/null +++ b/script/DeployGoalBasedPaymentTreasuryImplementation.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {GoalBasedPaymentTreasury} from "src/treasuries/GoalBasedPaymentTreasury.sol"; + +contract DeployGoalBasedPaymentTreasuryImplementation is Script { + function deploy() public returns (address) { + console2.log("Deploying GoalBasedPaymentTreasuryImplementation..."); + GoalBasedPaymentTreasury goalBasedPaymentTreasuryImplementation = new GoalBasedPaymentTreasury(); + console2.log("GoalBasedPaymentTreasuryImplementation deployed at:", address(goalBasedPaymentTreasuryImplementation)); + return address(goalBasedPaymentTreasuryImplementation); + } + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + bool simulate = vm.envOr("SIMULATE", false); + + if (!simulate) { + vm.startBroadcast(deployerKey); + } + + address implementationAddress = deploy(); + + if (!simulate) { + vm.stopBroadcast(); + } + + console2.log("GOAL_BASED_PAYMENT_TREASURY_IMPLEMENTATION_ADDRESS", implementationAddress); + } +} + diff --git a/src/CampaignInfo.sol b/src/CampaignInfo.sol index cd650eb..43a11b7 100644 --- a/src/CampaignInfo.sol +++ b/src/CampaignInfo.sol @@ -319,6 +319,10 @@ contract CampaignInfo is address tempTreasury; for (uint256 i = 0; i < length; i++) { tempTreasury = s_platformTreasuryAddress[tempPlatforms[i]]; + // Skip cancelled treasuries + if (ICampaignTreasury(tempTreasury).cancelled()) { + continue; + } // Try to call getExpectedAmount - will only work for payment treasuries try ICampaignPaymentTreasury(tempTreasury).getExpectedAmount() returns (uint256 expectedAmount) { amount += expectedAmount; diff --git a/src/treasuries/GoalBasedPaymentTreasury.sol b/src/treasuries/GoalBasedPaymentTreasury.sol new file mode 100644 index 0000000..8864a96 --- /dev/null +++ b/src/treasuries/GoalBasedPaymentTreasury.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {BasePaymentTreasury} from "../utils/BasePaymentTreasury.sol"; +import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; + +/** + * @title GoalBasedPaymentTreasury + * @notice A payment treasury with goal-based success conditions. + */ +contract GoalBasedPaymentTreasury is BasePaymentTreasury { + error GoalBasedPaymentTreasuryInvalidTimestamp(); + + /** + * @notice Initializes the GoalBasedPaymentTreasury contract. + * @param _platformHash The platform hash identifier. + * @param _infoAddress The address of the CampaignInfo contract. + * @param _trustedForwarder The address of the trusted forwarder for meta-transactions. + */ + function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer { + __BaseContract_init(_platformHash, _infoAddress, _trustedForwarder); + } + + function _revertIfCurrentTimeIsNotGreater(uint256 inputTime) private view { + if (block.timestamp <= inputTime) { + revert GoalBasedPaymentTreasuryInvalidTimestamp(); + } + } + + function _revertIfCurrentTimeIsNotWithinRange(uint256 initialTime, uint256 finalTime) private view { + uint256 currentTime = block.timestamp; + if (currentTime < initialTime || currentTime > finalTime) { + revert GoalBasedPaymentTreasuryInvalidTimestamp(); + } + } + + /** + * @dev Internal function to check if current time is within launchTime → deadline range. + */ + function _checkCreateTimeRange() private view { + _revertIfCurrentTimeIsNotWithinRange(INFO.getLaunchTime(), INFO.getDeadline()); + } + + /** + * @dev Internal function to check if current time is within launchTime → deadline + buffer range. + */ + function _checkConfirmTimeRange() private view { + _revertIfCurrentTimeIsNotWithinRange(INFO.getLaunchTime(), INFO.getDeadline() + INFO.getBufferTime()); + } + + /** + * @dev Internal function to check if current time is greater than launchTime. + */ + function _checkAfterLaunch() private view { + _revertIfCurrentTimeIsNotGreater(INFO.getLaunchTime()); + } + + /** + * @dev Internal function to check if current time is greater than deadline. + */ + function _checkAfterDeadline() private view { + _revertIfCurrentTimeIsNotGreater(INFO.getDeadline()); + } + + /** + * @dev Internal function to check optimistic lock for refunds. + * Blocks refunds if the campaign is projected to succeed (Confirmed + Pending >= Goal). + */ + function _checkRefundAllowed() private view { + _checkAfterLaunch(); + uint256 deadline = INFO.getDeadline(); + if (block.timestamp >= deadline) { + uint256 raised = INFO.getTotalRaisedAmount(); + uint256 progress = block.timestamp > deadline + INFO.getBufferTime() + ? raised + : raised + INFO.getTotalExpectedAmount(); + if (progress >= INFO.getGoalAmount()) { + revert PaymentTreasurySuccessConditionNotFulfilled(); + } + } + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Create operations are only allowed during launchTime → deadline. + */ + function createPayment( + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + address paymentToken, + uint256 amount, + uint256 expiration, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ) public override whenNotPaused whenNotCancelled { + _checkCreateTimeRange(); + super.createPayment(paymentId, buyerId, itemId, paymentToken, amount, expiration, lineItems, externalFees); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Create operations are only allowed during launchTime → deadline. + */ + function createPaymentBatch( + bytes32[] calldata paymentIds, + bytes32[] calldata buyerIds, + bytes32[] calldata itemIds, + address[] calldata paymentTokens, + uint256[] calldata amounts, + uint256[] calldata expirations, + ICampaignPaymentTreasury.LineItem[][] calldata lineItemsArray, + ICampaignPaymentTreasury.ExternalFees[][] calldata externalFeesArray + ) public override whenNotPaused whenNotCancelled { + _checkCreateTimeRange(); + super.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, lineItemsArray, externalFeesArray + ); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Create operations are only allowed during launchTime → deadline. + */ + function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ) public override whenNotPaused whenNotCancelled { + _checkCreateTimeRange(); + super.processCryptoPayment(paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Confirm operations are allowed during launchTime → deadline + buffer. + */ + function confirmPayment(bytes32 paymentId, address buyerAddress) + public + override + whenNotPaused + whenNotCancelled + { + _checkConfirmTimeRange(); + super.confirmPayment(paymentId, buyerAddress); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Confirm operations are allowed during launchTime → deadline + buffer. + */ + function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) + public + override + whenNotPaused + whenNotCancelled + { + _checkConfirmTimeRange(); + super.confirmPaymentBatch(paymentIds, buyerAddresses); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Cancel operations are allowed during launchTime → deadline + buffer. + */ + function cancelPayment(bytes32 paymentId) + public + override + whenNotPaused + whenNotCancelled + { + _checkConfirmTimeRange(); + super.cancelPayment(paymentId); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Refunds are allowed after launchTime, but blocked after deadline if goal is met. + */ + function claimRefund(bytes32 paymentId, address refundAddress) + public + override + whenNotPaused + { + _checkRefundAllowed(); + super.claimRefund(paymentId, refundAddress); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Refunds are allowed after launchTime, but blocked after deadline if goal is met. + */ + function claimRefund(bytes32 paymentId) + public + override + whenNotPaused + { + _checkRefundAllowed(); + super.claimRefund(paymentId); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Claiming expired funds is allowed after launchTime. + */ + function claimExpiredFunds() public override whenNotPaused { + _checkAfterLaunch(); + super.claimExpiredFunds(); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Fee disbursement is only allowed after deadline and requires goal to be met. + */ + function disburseFees() public override whenNotPaused { + _checkAfterDeadline(); + if (!_checkSuccessCondition()) { + revert PaymentTreasurySuccessConditionNotFulfilled(); + } + super.disburseFees(); + } + + /** + * @inheritdoc BasePaymentTreasury + * @dev Claiming non-goal line items is allowed after launchTime. + */ + function claimNonGoalLineItems(address token) public override whenNotPaused { + _checkAfterLaunch(); + super.claimNonGoalLineItems(token); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev Withdrawal is only allowed after deadline. + * Success condition is checked in the base implementation. + */ + function withdraw() public override whenNotPaused whenNotCancelled { + _checkAfterDeadline(); + super.withdraw(); + } + + /** + * @inheritdoc BasePaymentTreasury + * @dev This function is overridden to allow the platform admin and the campaign owner to cancel a treasury. + */ + function cancelTreasury(bytes32 message) public override onlyPlatformAdminOrCampaignOwner { + _cancel(message); + } + + /** + * @inheritdoc BasePaymentTreasury + * @dev Success condition: confirmed raised amount >= goal amount. + */ + function _checkSuccessCondition() internal view virtual override returns (bool) { + return INFO.getTotalRaisedAmount() >= INFO.getGoalAmount(); + } +} + diff --git a/src/treasuries/TimeConstrainedPaymentTreasury.sol b/src/treasuries/TimeConstrainedPaymentTreasury.sol index cf2f182..2a0541b 100644 --- a/src/treasuries/TimeConstrainedPaymentTreasury.sol +++ b/src/treasuries/TimeConstrainedPaymentTreasury.sol @@ -125,16 +125,18 @@ contract TimeConstrainedPaymentTreasury is BasePaymentTreasury, TimestampChecker /** * @inheritdoc ICampaignPaymentTreasury + * @dev Refunds remain available even when treasury is cancelled. */ - function claimRefund(bytes32 paymentId, address refundAddress) public override whenNotPaused whenNotCancelled { + function claimRefund(bytes32 paymentId, address refundAddress) public override whenNotPaused { _checkTimeIsGreater(); super.claimRefund(paymentId, refundAddress); } /** * @inheritdoc ICampaignPaymentTreasury + * @dev Refunds remain available even when treasury is cancelled. */ - function claimRefund(bytes32 paymentId) public override whenNotPaused whenNotCancelled { + function claimRefund(bytes32 paymentId) public override whenNotPaused { _checkTimeIsGreater(); super.claimRefund(paymentId); } diff --git a/src/utils/BasePaymentTreasury.sol b/src/utils/BasePaymentTreasury.sol index f7e45f4..b8caa85 100644 --- a/src/utils/BasePaymentTreasury.sol +++ b/src/utils/BasePaymentTreasury.sol @@ -485,7 +485,7 @@ abstract contract BasePaymentTreasury is /** * @inheritdoc ICampaignPaymentTreasury */ - function getExpectedAmount() external view returns (uint256) { + function getExpectedAmount() public view returns (uint256) { address[] memory acceptedTokens = INFO.getAcceptedTokens(); uint256 totalNormalized = 0; diff --git a/test/foundry/integration/GoalBasedPaymentTreasury/GoalBasedPaymentTreasury.t.sol b/test/foundry/integration/GoalBasedPaymentTreasury/GoalBasedPaymentTreasury.t.sol new file mode 100644 index 0000000..e22e725 --- /dev/null +++ b/test/foundry/integration/GoalBasedPaymentTreasury/GoalBasedPaymentTreasury.t.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import {GoalBasedPaymentTreasury} from "src/treasuries/GoalBasedPaymentTreasury.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; +import {LogDecoder} from "../../utils/LogDecoder.sol"; +import {Base_Test} from "../../Base.t.sol"; +import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; + +/// @notice Common testing logic needed by all GoalBasedPaymentTreasury integration tests. +abstract contract GoalBasedPaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Test { + address campaignAddress; + address treasuryAddress; + GoalBasedPaymentTreasury internal goalBasedPaymentTreasury; + + // Payment test data + bytes32 internal constant PAYMENT_ID_1 = keccak256("payment1"); + bytes32 internal constant PAYMENT_ID_2 = keccak256("payment2"); + bytes32 internal constant PAYMENT_ID_3 = keccak256("payment3"); + bytes32 internal constant ITEM_ID_1 = keccak256("item1"); + bytes32 internal constant ITEM_ID_2 = keccak256("item2"); + uint256 internal constant PAYMENT_AMOUNT_1 = 1000e18; + uint256 internal constant PAYMENT_AMOUNT_2 = 2000e18; + uint256 internal constant PAYMENT_EXPIRATION = 7 days; + bytes32 internal constant BUYER_ID_1 = keccak256("buyer1"); + bytes32 internal constant BUYER_ID_2 = keccak256("buyer2"); + bytes32 internal constant BUYER_ID_3 = keccak256("buyer3"); + + // Time constraint test data + uint256 internal constant BUFFER_TIME = 1 days; + uint256 internal campaignLaunchTime; + uint256 internal campaignDeadline; + uint256 internal campaignGoalAmount; + + /// @dev Initial dependent functions setup included for GoalBasedPaymentTreasury Integration Tests. + function setUp() public virtual override { + super.setUp(); + console.log("setUp: enlistPlatform"); + + // Enlist Platform + enlistPlatform(PLATFORM_1_HASH); + console.log("enlisted platform"); + + registerTreasuryImplementation(PLATFORM_1_HASH); + console.log("registered treasury"); + + approveTreasuryImplementation(PLATFORM_1_HASH); + console.log("approved treasury"); + + // Set buffer time in GlobalParams + setBufferTime(); + console.log("set buffer time"); + + // Create Campaign with specific time constraints + createCampaignWithTimeConstraints(PLATFORM_1_HASH); + console.log("created campaign with time constraints"); + + // Deploy Treasury Contract + deploy(PLATFORM_1_HASH); + console.log("deployed treasury"); + } + + /** + * @notice Sets buffer time in GlobalParams dataRegistry + */ + function setBufferTime() internal { + vm.startPrank(users.protocolAdminAddress); + globalParams.addToRegistry(DataRegistryKeys.BUFFER_TIME, bytes32(BUFFER_TIME)); + vm.stopPrank(); + } + + /** + * @notice Implements enlistPlatform helper function. + * @param platformHash The platform bytes. + */ + function enlistPlatform(bytes32 platformHash) internal { + vm.startPrank(users.protocolAdminAddress); + globalParams.enlistPlatform(platformHash, users.platform1AdminAddress, PLATFORM_FEE_PERCENT, address(0)); + vm.stopPrank(); + } + + function registerTreasuryImplementation(bytes32 platformHash) internal { + GoalBasedPaymentTreasury implementation = new GoalBasedPaymentTreasury(); + vm.startPrank(users.platform1AdminAddress); + treasuryFactory.registerTreasuryImplementation(platformHash, 4, address(implementation)); + vm.stopPrank(); + } + + function approveTreasuryImplementation(bytes32 platformHash) internal { + vm.startPrank(users.protocolAdminAddress); + treasuryFactory.approveTreasuryImplementation(platformHash, 4); + vm.stopPrank(); + } + + /** + * @notice Creates campaign with specific time constraints for testing + * @param platformHash The platform bytes. + */ + function createCampaignWithTimeConstraints(bytes32 platformHash) internal { + bytes32 identifierHash = keccak256(abi.encodePacked(platformHash)); + bytes32[] memory selectedPlatformHash = new bytes32[](1); + selectedPlatformHash[0] = platformHash; + + bytes32[] memory platformDataKey = new bytes32[](0); + bytes32[] memory platformDataValue = new bytes32[](0); + + vm.startPrank(users.creator1Address); + vm.recordLogs(); + + campaignInfoFactory.createCampaign( + users.creator1Address, + identifierHash, + selectedPlatformHash, + platformDataKey, + platformDataValue, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + + campaignAddress = campaignInfoFactory.identifierToCampaignInfo(identifierHash); + campaignInfo = CampaignInfo(campaignAddress); + + // Store the actual campaign times for testing + campaignLaunchTime = campaignInfo.getLaunchTime(); + campaignDeadline = campaignInfo.getDeadline(); + campaignGoalAmount = campaignInfo.getGoalAmount(); + + // Set specific launch time and deadline for testing + vm.warp(campaignLaunchTime); + vm.stopPrank(); + } + + /** + * @notice Implements deploy helper function. It deploys new treasury contract + * @param platformHash The platform bytes. + */ + function deploy(bytes32 platformHash) internal { + vm.startPrank(users.platform1AdminAddress); + vm.recordLogs(); + + treasuryAddress = treasuryFactory.deploy( + platformHash, + campaignAddress, + 4 // GoalBasedPaymentTreasury type + ); + + goalBasedPaymentTreasury = GoalBasedPaymentTreasury(treasuryAddress); + vm.stopPrank(); + } + + /** + * @notice Helper function to advance time to within the allowed range (before deadline) + */ + function advanceToWithinRange() internal { + uint256 currentTime = campaignLaunchTime + (campaignDeadline - campaignLaunchTime) / 2; // Middle of the range + vm.warp(currentTime); + } + + /** + * @notice Helper function to advance time to before launch time + */ + function advanceToBeforeLaunch() internal { + vm.warp(campaignLaunchTime - 1); + } + + /** + * @notice Helper function to advance time to after deadline (but before buffer ends) + */ + function advanceToAfterDeadline() internal { + vm.warp(campaignDeadline + 1); + } + + /** + * @notice Helper function to advance time to after deadline + buffer time + */ + function advanceToAfterDeadlinePlusBuffer() internal { + vm.warp(campaignDeadline + BUFFER_TIME + 1); + } + + /** + * @notice Helper function to advance time to after launch time but before deadline + */ + function advanceToAfterLaunch() internal { + vm.warp(campaignLaunchTime + 1); + } + + /** + * @notice Helper function to advance time to exactly at the deadline + */ + function advanceToDeadline() internal { + vm.warp(campaignDeadline); + } + + /** + * @notice Helper function to advance time to exactly at the deadline + buffer + */ + function advanceToDeadlinePlusBuffer() internal { + vm.warp(campaignDeadline + BUFFER_TIME); + } + + /** + * @notice Helper function to create and fund a payment + * @dev Uses expiration that extends past deadline + buffer to avoid expiration during tests + */ + function _createAndFundPayment( + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + uint256 amount, + address buyerAddress + ) internal { + // Fund buyer + deal(address(testToken), buyerAddress, amount); + + // Buyer approves treasury + vm.prank(buyerAddress); + testToken.approve(treasuryAddress, amount); + + // Create payment with expiration that extends past deadline + buffer + // This ensures payments don't expire during buffer period tests + uint256 expiration = campaignDeadline + BUFFER_TIME + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + ICampaignPaymentTreasury.ExternalFees[] memory emptyExternalFees = + new ICampaignPaymentTreasury.ExternalFees[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + paymentId, buyerId, itemId, address(testToken), amount, expiration, emptyLineItems, emptyExternalFees + ); + + // Transfer tokens from buyer to treasury + vm.prank(buyerAddress); + testToken.transfer(treasuryAddress, amount); + } + + /** + * @notice Helper function to create and process a crypto payment + */ + function _createAndProcessCryptoPayment(bytes32 paymentId, bytes32 itemId, uint256 amount, address buyerAddress) + internal + { + // Fund buyer + deal(address(testToken), buyerAddress, amount); + + // Buyer approves treasury + vm.prank(buyerAddress); + testToken.approve(treasuryAddress, amount); + + // Process crypto payment + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.processCryptoPayment( + paymentId, + itemId, + buyerAddress, + address(testToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /** + * @notice Helper function to fund the campaign to meet the goal + */ + function _fundCampaignToMeetGoal() internal { + // Fund with goal amount using crypto payments + deal(address(testToken), users.backer1Address, campaignGoalAmount); + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, campaignGoalAmount); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.processCryptoPayment( + keccak256("goalPayment"), + ITEM_ID_1, + users.backer1Address, + address(testToken), + campaignGoalAmount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /** + * @notice Helper function to fund the campaign with pending payments to meet goal + */ + function _fundCampaignWithPendingPaymentsToMeetGoal() internal { + // Create pending payment for goal amount + deal(address(testToken), users.backer1Address, campaignGoalAmount); + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, campaignGoalAmount); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + keccak256("goalPendingPayment"), + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + campaignGoalAmount, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Transfer tokens to treasury + vm.prank(users.backer1Address); + testToken.transfer(treasuryAddress, campaignGoalAmount); + } + +} + diff --git a/test/foundry/integration/GoalBasedPaymentTreasury/GoalBasedPaymentTreasuryFunction.t.sol b/test/foundry/integration/GoalBasedPaymentTreasury/GoalBasedPaymentTreasuryFunction.t.sol new file mode 100644 index 0000000..85a3208 --- /dev/null +++ b/test/foundry/integration/GoalBasedPaymentTreasury/GoalBasedPaymentTreasuryFunction.t.sol @@ -0,0 +1,595 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "./GoalBasedPaymentTreasury.t.sol"; +import "forge-std/Vm.sol"; +import "forge-std/Test.sol"; +import {Defaults} from "../../utils/Defaults.sol"; +import {Constants} from "../../utils/Constants.sol"; +import {Users} from "../../utils/Types.sol"; +import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; +import {GoalBasedPaymentTreasury} from "src/treasuries/GoalBasedPaymentTreasury.sol"; +import {BasePaymentTreasury} from "src/utils/BasePaymentTreasury.sol"; +import {CampaignAccessChecker} from "src/utils/CampaignAccessChecker.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {TestToken} from "../../../mocks/TestToken.sol"; + +contract GoalBasedPaymentTreasuryFunction_Integration_Shared_Test is + GoalBasedPaymentTreasury_Integration_Shared_Test +{ + function setUp() public virtual override { + super.setUp(); + + // Fund test users with tokens + deal(address(testToken), users.backer1Address, 1_000_000e18); + deal(address(testToken), users.backer2Address, 1_000_000e18); + deal(address(testToken), users.creator1Address, 1_000_000e18); + deal(address(testToken), users.platform1AdminAddress, 1_000_000e18); + } + + /*////////////////////////////////////////////////////////////// + BASIC PAYMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function test_createPayment() external { + advanceToWithinRange(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Payment created successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + assertEq(goalBasedPaymentTreasury.getAvailableRaisedAmount(), 0); + } + + function test_createPaymentBatch() external { + advanceToWithinRange(); + + bytes32[] memory paymentIds = new bytes32[](2); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + + bytes32[] memory buyerIds = new bytes32[](2); + buyerIds[0] = BUYER_ID_1; + buyerIds[1] = BUYER_ID_2; + + bytes32[] memory itemIds = new bytes32[](2); + itemIds[0] = ITEM_ID_1; + itemIds[1] = ITEM_ID_2; + + address[] memory paymentTokens = new address[](2); + paymentTokens[0] = address(testToken); + paymentTokens[1] = address(testToken); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = PAYMENT_AMOUNT_1; + amounts[1] = PAYMENT_AMOUNT_2; + + uint256[] memory expirations = new uint256[](2); + expirations[0] = block.timestamp + PAYMENT_EXPIRATION; + expirations[1] = block.timestamp + PAYMENT_EXPIRATION; + + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = new ICampaignPaymentTreasury.LineItem[][](2); + emptyLineItemsArray[0] = new ICampaignPaymentTreasury.LineItem[](0); + emptyLineItemsArray[1] = new ICampaignPaymentTreasury.LineItem[](0); + + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](2); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + externalFeesArray[1] = new ICampaignPaymentTreasury.ExternalFees[](0); + + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, emptyLineItemsArray, externalFeesArray + ); + + // Payments created successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_processCryptoPayment() external { + advanceToWithinRange(); + + // Approve tokens for the treasury + vm.prank(users.backer1Address); + testToken.approve(address(goalBasedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payment processed successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + assertEq(goalBasedPaymentTreasury.getAvailableRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function test_cancelPayment() external { + advanceToWithinRange(); + + // First create a payment + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Then cancel it + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.cancelPayment(PAYMENT_ID_1); + + // Payment cancelled successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_confirmPayment() external { + advanceToWithinRange(); + + // Create and fund payment + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Confirm payment + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, users.backer1Address); + + // Payment confirmed successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + assertEq(goalBasedPaymentTreasury.getAvailableRaisedAmount(), PAYMENT_AMOUNT_1); + } + + /*////////////////////////////////////////////////////////////// + TIME CONSTRAINT TESTS - CREATE PAYMENTS + //////////////////////////////////////////////////////////////*/ + + function test_createPayment_RevertWhenBeforeLaunchTime() external { + advanceToBeforeLaunch(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_createPayment_RevertWhenAfterDeadline() external { + advanceToAfterDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_createPayment_SucceedsAtExactDeadline() external { + advanceToDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Should succeed at exact deadline + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_processCryptoPayment_RevertWhenAfterDeadline() external { + advanceToAfterDeadline(); + + vm.prank(users.backer1Address); + testToken.approve(address(goalBasedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /*////////////////////////////////////////////////////////////// + TIME CONSTRAINT TESTS - CONFIRM PAYMENTS + //////////////////////////////////////////////////////////////*/ + + function test_confirmPayment_SucceedsWithinBufferPeriod() external { + advanceToWithinRange(); + + // Create and fund payment + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance to buffer period + advanceToAfterDeadline(); + + // Confirm payment should still work during buffer + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, users.backer1Address); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function test_confirmPayment_RevertWhenAfterBufferPeriod() external { + advanceToWithinRange(); + + // Create and fund payment + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past buffer period + advanceToAfterDeadlinePlusBuffer(); + + // Confirm payment should fail after buffer + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, users.backer1Address); + } + + /*////////////////////////////////////////////////////////////// + REFUND TESTS - OPTIMISTIC LOCK + //////////////////////////////////////////////////////////////*/ + + function test_claimRefund_BeforeDeadline_Succeeds() external { + advanceToWithinRange(); + + // Create and process crypto payment (first crypto payment = tokenId 1) + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Claim refund before deadline - should succeed + vm.prank(users.backer1Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + uint256 balanceAfter = testToken.balanceOf(users.backer1Address); + assertEq(balanceAfter - balanceBefore, PAYMENT_AMOUNT_1, "Refund should be returned"); + } + + function test_claimRefund_AfterDeadline_GoalNotMet_Succeeds() external { + advanceToWithinRange(); + + // Create and process crypto payment (first crypto payment = tokenId 1) + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past deadline + advanceToAfterDeadline(); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Claim refund - should succeed since goal not met + vm.prank(users.backer1Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + uint256 balanceAfter = testToken.balanceOf(users.backer1Address); + assertEq(balanceAfter - balanceBefore, PAYMENT_AMOUNT_1, "Refund should be returned when goal not met"); + } + + function test_claimRefund_AfterDeadline_GoalMet_Reverts() external { + advanceToWithinRange(); + + // Fund campaign to meet goal (tokenId 1) + _fundCampaignToMeetGoal(); + + // Create another payment for refund test (tokenId 2) + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer2Address); + + // Advance past deadline + advanceToAfterDeadline(); + + // Approve treasury to burn NFT + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 2); + + // Claim refund should revert since goal is met + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + vm.prank(users.backer2Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + } + + function test_claimRefund_AfterDeadline_OptimisticLock_WithPendingPayments() external { + advanceToWithinRange(); + + // Create confirmed payment below goal (first crypto payment = tokenId 1) + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer2Address); + + // Create pending payment that would meet goal when added + uint256 pendingAmount = campaignGoalAmount; // This will make pending + confirmed >= goal + _createAndFundPayment(PAYMENT_ID_2, BUYER_ID_1, ITEM_ID_2, pendingAmount, users.backer1Address); + + // Advance past deadline (during buffer period) + advanceToAfterDeadline(); + + // Approve treasury to burn NFT + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Refund should be blocked due to optimistic lock (pending + confirmed >= goal) + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + vm.prank(users.backer2Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + } + + function test_claimRefund_AfterBufferEnd_PendingFailed_Succeeds() external { + advanceToWithinRange(); + + // Create confirmed payment below goal (first crypto payment = tokenId 1) + uint256 smallAmount = campaignGoalAmount / 10; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, smallAmount, users.backer2Address); + + // Create pending payment that would meet goal when added + uint256 pendingAmount = campaignGoalAmount; + _createAndFundPayment(PAYMENT_ID_2, BUYER_ID_1, ITEM_ID_2, pendingAmount, users.backer1Address); + + // Advance past buffer period - pending payments can no longer be confirmed + advanceToAfterDeadlinePlusBuffer(); + + // Approve treasury to burn NFT + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Refund should now succeed since goal is not met with only confirmed payments + vm.prank(users.backer2Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + // Verify refund was processed + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0, "Raised amount should be 0 after refund"); + } + + /*////////////////////////////////////////////////////////////// + WITHDRAW AND DISBURSE FEES TESTS + //////////////////////////////////////////////////////////////*/ + + function test_withdraw_GoalMet_AfterDeadline_Succeeds() external { + advanceToWithinRange(); + + // Fund campaign to meet goal + _fundCampaignToMeetGoal(); + + // Advance past deadline + advanceToAfterDeadline(); + + uint256 creatorBalanceBefore = testToken.balanceOf(users.creator1Address); + + // Withdraw should succeed + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + + uint256 creatorBalanceAfter = testToken.balanceOf(users.creator1Address); + assertTrue(creatorBalanceAfter > creatorBalanceBefore, "Creator should receive funds"); + } + + function test_withdraw_GoalNotMet_Reverts() external { + advanceToWithinRange(); + + // Create payment that doesn't meet goal + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past deadline + advanceToAfterDeadline(); + + // Withdraw should fail since goal not met + vm.expectRevert(); + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + } + + function test_withdraw_BeforeDeadline_Reverts() external { + advanceToWithinRange(); + + // Fund campaign to meet goal + _fundCampaignToMeetGoal(); + + // Withdraw should fail before deadline + vm.expectRevert(); + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + } + + function test_disburseFees_GoalMet_AfterDeadline_Succeeds() external { + advanceToWithinRange(); + + // Fund campaign to meet goal + _fundCampaignToMeetGoal(); + + // First withdraw to generate fees + advanceToAfterDeadline(); + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + + uint256 protocolBalanceBefore = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); + + // Disburse fees + goalBasedPaymentTreasury.disburseFees(); + + uint256 protocolBalanceAfter = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformBalanceAfter = testToken.balanceOf(users.platform1AdminAddress); + + assertTrue(protocolBalanceAfter > protocolBalanceBefore, "Protocol should receive fees"); + assertTrue(platformBalanceAfter > platformBalanceBefore, "Platform should receive fees"); + } + + function test_disburseFees_GoalNotMet_Reverts() external { + advanceToWithinRange(); + + // Create payment that doesn't meet goal + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past deadline + advanceToAfterDeadline(); + + // Disburse fees should fail since goal not met + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + goalBasedPaymentTreasury.disburseFees(); + } + + /*////////////////////////////////////////////////////////////// + CANCEL TREASURY TESTS + //////////////////////////////////////////////////////////////*/ + + function test_cancelTreasury_ByPlatformAdmin() external { + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.cancelTreasury(keccak256("Test cancellation")); + + assertTrue(goalBasedPaymentTreasury.cancelled(), "Treasury should be cancelled"); + } + + function test_cancelTreasury_ByCampaignOwner() external { + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.cancelTreasury(keccak256("Test cancellation")); + + assertTrue(goalBasedPaymentTreasury.cancelled(), "Treasury should be cancelled"); + } + + function test_cancelTreasury_ByUnauthorized_Reverts() external { + vm.expectRevert(CampaignAccessChecker.AccessCheckerUnauthorized.selector); + vm.prank(users.backer1Address); + goalBasedPaymentTreasury.cancelTreasury(keccak256("Test cancellation")); + } + + /*////////////////////////////////////////////////////////////// + EDGE CASE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_createPayment_AtExactLaunchTime() external { + vm.warp(campaignLaunchTime); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Should succeed at exact launch time + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_confirmPayment_AtExactDeadlinePlusBuffer() external { + advanceToWithinRange(); + + // Create and fund payment + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance to exact deadline + buffer + advanceToDeadlinePlusBuffer(); + + // Confirm payment should still work at exact buffer end + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, users.backer1Address); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function test_claimExpiredFunds_AfterLaunchTime() external { + advanceToAfterLaunch(); + + // Fund the treasury with some tokens first + vm.prank(users.backer1Address); + testToken.approve(address(goalBasedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Should not revert when called after launch time + // Note: claimExpiredFunds has additional requirements (claim delay etc.) + // We're just testing the time constraint here + vm.expectRevert(); // Will revert due to claim window not reached, but not due to time constraint + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.claimExpiredFunds(); + } + + function test_claimNonGoalLineItems_AfterLaunchTime() external { + advanceToAfterLaunch(); + + // Without any non-goal line items, this should revert with invalid input + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.claimNonGoalLineItems(address(testToken)); + } +} + diff --git a/test/foundry/unit/GoalBasedPaymentTreasury.t.sol b/test/foundry/unit/GoalBasedPaymentTreasury.t.sol new file mode 100644 index 0000000..1248f6d --- /dev/null +++ b/test/foundry/unit/GoalBasedPaymentTreasury.t.sol @@ -0,0 +1,733 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "../integration/GoalBasedPaymentTreasury/GoalBasedPaymentTreasuryFunction.t.sol"; +import "forge-std/Test.sol"; +import {GoalBasedPaymentTreasury} from "src/treasuries/GoalBasedPaymentTreasury.sol"; +import {BasePaymentTreasury} from "src/utils/BasePaymentTreasury.sol"; +import {CampaignAccessChecker} from "src/utils/CampaignAccessChecker.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {TestToken} from "../../mocks/TestToken.sol"; +import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; + +contract GoalBasedPaymentTreasury_UnitTest is Test, GoalBasedPaymentTreasuryFunction_Integration_Shared_Test { + // Helper function to create payment tokens array with same token for all payments + function _createPaymentTokensArray(uint256 length, address token) internal pure returns (address[] memory) { + address[] memory paymentTokens = new address[](length); + for (uint256 i = 0; i < length; i++) { + paymentTokens[i] = token; + } + return paymentTokens; + } + + function setUp() public virtual override { + super.setUp(); + // Fund test addresses + deal(address(testToken), users.backer1Address, 10_000_000e18); + deal(address(testToken), users.backer2Address, 10_000_000e18); + // Label addresses + vm.label(users.protocolAdminAddress, "ProtocolAdmin"); + vm.label(users.platform1AdminAddress, "PlatformAdmin"); + vm.label(users.creator1Address, "CampaignOwner"); + vm.label(users.backer1Address, "Backer1"); + vm.label(users.backer2Address, "Backer2"); + vm.label(address(goalBasedPaymentTreasury), "GoalBasedPaymentTreasury"); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + function testInitialize() public { + // Create a new campaign for this test + bytes32 newIdentifierHash = keccak256(abi.encodePacked("newGoalBasedCampaign")); + bytes32[] memory selectedPlatformHash = new bytes32[](1); + selectedPlatformHash[0] = PLATFORM_1_HASH; + + bytes32[] memory platformDataKey = new bytes32[](0); + bytes32[] memory platformDataValue = new bytes32[](0); + + vm.prank(users.creator1Address); + campaignInfoFactory.createCampaign( + users.creator1Address, + newIdentifierHash, + selectedPlatformHash, + platformDataKey, + platformDataValue, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + address newCampaignAddress = campaignInfoFactory.identifierToCampaignInfo(newIdentifierHash); + + // Deploy a new treasury + vm.prank(users.platform1AdminAddress); + address newTreasury = treasuryFactory.deploy( + PLATFORM_1_HASH, + newCampaignAddress, + 4 // GoalBasedPaymentTreasury type + ); + GoalBasedPaymentTreasury newContract = GoalBasedPaymentTreasury(newTreasury); + CampaignInfo newCampaignInfo = CampaignInfo(newCampaignAddress); + + // NFT name and symbol are now on CampaignInfo, not treasury + assertEq(newCampaignInfo.name(), "Campaign Pledge NFT"); + assertEq(newCampaignInfo.symbol(), "PLEDGE"); + assertEq(newContract.getplatformHash(), PLATFORM_1_HASH); + assertEq(newContract.getplatformFeePercent(), PLATFORM_FEE_PERCENT); + } + + /*////////////////////////////////////////////////////////////// + CREATE PAYMENT TIME CONSTRAINT TESTS + //////////////////////////////////////////////////////////////*/ + + function testCreatePaymentWithinTimeRange() public { + advanceToWithinRange(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Payment created successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + assertEq(goalBasedPaymentTreasury.getAvailableRaisedAmount(), 0); + } + + function testCreatePaymentRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenAfterDeadline() public { + advanceToAfterDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentBatchWithinTimeRange() public { + advanceToWithinRange(); + + bytes32[] memory paymentIds = new bytes32[](2); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + + bytes32[] memory buyerIds = new bytes32[](2); + buyerIds[0] = BUYER_ID_1; + buyerIds[1] = BUYER_ID_2; + + bytes32[] memory itemIds = new bytes32[](2); + itemIds[0] = ITEM_ID_1; + itemIds[1] = ITEM_ID_2; + + address[] memory paymentTokens = _createPaymentTokensArray(2, address(testToken)); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = PAYMENT_AMOUNT_1; + amounts[1] = PAYMENT_AMOUNT_2; + + uint256[] memory expirations = new uint256[](2); + expirations[0] = block.timestamp + PAYMENT_EXPIRATION; + expirations[1] = block.timestamp + PAYMENT_EXPIRATION; + + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = new ICampaignPaymentTreasury.LineItem[][](2); + emptyLineItemsArray[0] = new ICampaignPaymentTreasury.LineItem[](0); + emptyLineItemsArray[1] = new ICampaignPaymentTreasury.LineItem[](0); + + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](2); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + externalFeesArray[1] = new ICampaignPaymentTreasury.ExternalFees[](0); + + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, emptyLineItemsArray, externalFeesArray + ); + + // Payments created successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function testCreatePaymentBatchRevertWhenAfterDeadline() public { + advanceToAfterDeadline(); + + bytes32[] memory paymentIds = new bytes32[](1); + paymentIds[0] = PAYMENT_ID_1; + + bytes32[] memory buyerIds = new bytes32[](1); + buyerIds[0] = BUYER_ID_1; + + bytes32[] memory itemIds = new bytes32[](1); + itemIds[0] = ITEM_ID_1; + + address[] memory paymentTokens = _createPaymentTokensArray(1, address(testToken)); + + uint256[] memory amounts = new uint256[](1); + amounts[0] = PAYMENT_AMOUNT_1; + + uint256[] memory expirations = new uint256[](1); + expirations[0] = block.timestamp + PAYMENT_EXPIRATION; + + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = new ICampaignPaymentTreasury.LineItem[][](1); + emptyLineItemsArray[0] = new ICampaignPaymentTreasury.LineItem[](0); + + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](1); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, emptyLineItemsArray, externalFeesArray + ); + } + + function testProcessCryptoPaymentWithinTimeRange() public { + advanceToWithinRange(); + + // Approve tokens for the treasury + vm.prank(users.backer1Address); + testToken.approve(address(goalBasedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + goalBasedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payment processed successfully + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function testProcessCryptoPaymentRevertWhenAfterDeadline() public { + advanceToAfterDeadline(); + + vm.prank(users.backer1Address); + testToken.approve(address(goalBasedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + goalBasedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /*////////////////////////////////////////////////////////////// + CONFIRM PAYMENT TIME CONSTRAINT TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfirmPaymentWithinBufferPeriod() public { + advanceToWithinRange(); + + // Create and fund payment + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance to within buffer period + advanceToAfterDeadline(); + + // Confirm should succeed during buffer + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, users.backer1Address); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function testConfirmPaymentRevertAfterBufferPeriod() public { + advanceToWithinRange(); + + // Create and fund payment + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past buffer period + advanceToAfterDeadlinePlusBuffer(); + + // Confirm should fail after buffer + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, users.backer1Address); + } + + function testConfirmPaymentBatchWithinBufferPeriod() public { + advanceToWithinRange(); + + // Create and fund payments + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + _createAndFundPayment(PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, PAYMENT_AMOUNT_2, users.backer2Address); + + // Advance to buffer period + advanceToAfterDeadline(); + + // Confirm batch should succeed during buffer + bytes32[] memory paymentIds = new bytes32[](2); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + + address[] memory buyerAddresses = new address[](2); + buyerAddresses[0] = users.backer1Address; + buyerAddresses[1] = users.backer2Address; + + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPaymentBatch(paymentIds, buyerAddresses); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2); + } + + /*////////////////////////////////////////////////////////////// + CANCEL PAYMENT TIME CONSTRAINT TESTS + //////////////////////////////////////////////////////////////*/ + + function testCancelPaymentWithinBufferPeriod() public { + advanceToWithinRange(); + + // Create payment with expiration that extends past buffer + uint256 expiration = campaignDeadline + BUFFER_TIME + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to buffer period + advanceToAfterDeadline(); + + // Cancel should succeed during buffer + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.cancelPayment(PAYMENT_ID_1); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function testCancelPaymentRevertAfterBufferPeriod() public { + advanceToWithinRange(); + + // Create payment + uint256 expiration = campaignDeadline + BUFFER_TIME + PAYMENT_EXPIRATION; // Long expiration + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance past buffer period + advanceToAfterDeadlinePlusBuffer(); + + // Cancel should fail after buffer + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.cancelPayment(PAYMENT_ID_1); + } + + /*////////////////////////////////////////////////////////////// + REFUND OPTIMISTIC LOCK TESTS + //////////////////////////////////////////////////////////////*/ + + function testRefundBeforeDeadlineSucceeds() public { + advanceToWithinRange(); + + // Create and process crypto payment (first crypto payment = tokenId 1) + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Refund should succeed before deadline regardless of goal status + vm.prank(users.backer1Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + assertEq(testToken.balanceOf(users.backer1Address) - balanceBefore, PAYMENT_AMOUNT_1); + } + + function testRefundAfterDeadlineGoalNotMetSucceeds() public { + advanceToWithinRange(); + + // Create payment that doesn't meet goal (first crypto payment = tokenId 1) + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past deadline + advanceToAfterDeadline(); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Refund should succeed when goal not met + vm.prank(users.backer1Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + assertEq(testToken.balanceOf(users.backer1Address) - balanceBefore, PAYMENT_AMOUNT_1); + } + + function testRefundAfterDeadlineGoalMetReverts() public { + advanceToWithinRange(); + + // Fund to meet goal (tokenId 1) + _fundCampaignToMeetGoal(); + + // Create another payment (tokenId 2) + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer2Address); + + // Advance past deadline + advanceToAfterDeadline(); + + // Approve treasury to burn NFT + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 2); + + // Refund should fail when goal met + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + vm.prank(users.backer2Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + } + + function testRefundOptimisticLockWithPendingPayments() public { + advanceToWithinRange(); + + // Create small confirmed payment below goal (first crypto payment = tokenId 1) + uint256 smallAmount = campaignGoalAmount / 10; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, smallAmount, users.backer2Address); + + // Create large pending payment (would meet goal) + _createAndFundPayment(PAYMENT_ID_2, BUYER_ID_1, ITEM_ID_2, campaignGoalAmount, users.backer1Address); + + // Advance past deadline (into buffer) + advanceToAfterDeadline(); + + // Approve treasury to burn NFT + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Refund should fail due to optimistic lock + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + vm.prank(users.backer2Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + } + + function testRefundAfterBufferWithFailedPendingPayments() public { + advanceToWithinRange(); + + // Create small confirmed payment below goal (first crypto payment = tokenId 1) + uint256 smallAmount = campaignGoalAmount / 10; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, smallAmount, users.backer2Address); + + // Create pending payment (would meet goal if confirmed) + _createAndFundPayment(PAYMENT_ID_2, BUYER_ID_1, ITEM_ID_2, campaignGoalAmount, users.backer1Address); + + // During buffer: optimistic lock should block refunds + advanceToAfterDeadline(); + + // Advance past buffer: pending payments can no longer be confirmed + advanceToAfterDeadlinePlusBuffer(); + + // Approve treasury to burn NFT + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + + // Refund should now succeed since goal not met + vm.prank(users.backer2Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function testRefundWithAddressOverloadAfterDeadlineGoalNotMet() public { + advanceToWithinRange(); + + // Create and fund payment + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Confirm the payment + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + // Advance past deadline + advanceToAfterDeadline(); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Platform admin claims refund on behalf of user + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + + assertEq(testToken.balanceOf(users.backer1Address) - balanceBefore, PAYMENT_AMOUNT_1); + } + + /*////////////////////////////////////////////////////////////// + WITHDRAW AND DISBURSE FEES TESTS + //////////////////////////////////////////////////////////////*/ + + function testWithdrawGoalMetSucceeds() public { + advanceToWithinRange(); + + // Fund to meet goal + _fundCampaignToMeetGoal(); + + // Advance past deadline + advanceToAfterDeadline(); + + uint256 creatorBalanceBefore = testToken.balanceOf(users.creator1Address); + + // Withdraw should succeed + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + + assertTrue(testToken.balanceOf(users.creator1Address) > creatorBalanceBefore); + } + + function testWithdrawGoalNotMetReverts() public { + advanceToWithinRange(); + + // Create payment below goal + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past deadline + advanceToAfterDeadline(); + + // Withdraw should fail + vm.expectRevert(); + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + } + + function testWithdrawBeforeDeadlineReverts() public { + advanceToWithinRange(); + + // Fund to meet goal + _fundCampaignToMeetGoal(); + + // Withdraw should fail before deadline + vm.expectRevert(); + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + } + + function testDisburseFeesGoalMetSucceeds() public { + advanceToWithinRange(); + + // Fund to meet goal + _fundCampaignToMeetGoal(); + + // Advance and withdraw + advanceToAfterDeadline(); + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.withdraw(); + + uint256 protocolBefore = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformBefore = testToken.balanceOf(users.platform1AdminAddress); + + // Disburse fees + goalBasedPaymentTreasury.disburseFees(); + + assertTrue(testToken.balanceOf(users.protocolAdminAddress) > protocolBefore); + assertTrue(testToken.balanceOf(users.platform1AdminAddress) > platformBefore); + } + + function testDisburseFeesGoalNotMetReverts() public { + advanceToWithinRange(); + + // Create payment below goal + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance past deadline + advanceToAfterDeadline(); + + // Disburse should fail + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + goalBasedPaymentTreasury.disburseFees(); + } + + function testDisburseFeesBeforeDeadlineReverts() public { + advanceToWithinRange(); + + // Fund to meet goal + _fundCampaignToMeetGoal(); + + // Disburse should fail before deadline + vm.expectRevert(); + goalBasedPaymentTreasury.disburseFees(); + } + + /*////////////////////////////////////////////////////////////// + CANCEL TREASURY TESTS + //////////////////////////////////////////////////////////////*/ + + function testCancelTreasuryByPlatformAdmin() public { + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.cancelTreasury(keccak256("cancel")); + + assertTrue(goalBasedPaymentTreasury.cancelled()); + } + + function testCancelTreasuryByCampaignOwner() public { + vm.prank(users.creator1Address); + goalBasedPaymentTreasury.cancelTreasury(keccak256("cancel")); + + assertTrue(goalBasedPaymentTreasury.cancelled()); + } + + function testCancelTreasuryByUnauthorizedReverts() public { + vm.expectRevert(CampaignAccessChecker.AccessCheckerUnauthorized.selector); + vm.prank(users.backer1Address); + goalBasedPaymentTreasury.cancelTreasury(keccak256("cancel")); + } + + /*////////////////////////////////////////////////////////////// + EDGE CASE TESTS + //////////////////////////////////////////////////////////////*/ + + function testOperationsAtExactLaunchTime() public { + vm.warp(campaignLaunchTime); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function testOperationsAtExactDeadline() public { + advanceToDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), 0); + } + + function testOperationsAtExactDeadlinePlusBuffer() public { + advanceToWithinRange(); + + // Create payment during campaign + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + // Advance to exact deadline + buffer + advanceToDeadlinePlusBuffer(); + + // Confirm should still work at exact buffer end + vm.prank(users.platform1AdminAddress); + goalBasedPaymentTreasury.confirmPayment(PAYMENT_ID_1, users.backer1Address); + + assertEq(goalBasedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function testMultipleRefundsOptimisticLock() public { + advanceToWithinRange(); + + // Create multiple small confirmed payments (tokenId 1, tokenId 2) + uint256 smallAmount = PAYMENT_AMOUNT_1; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, smallAmount, users.backer1Address); + _createAndProcessCryptoPayment(PAYMENT_ID_2, ITEM_ID_2, smallAmount, users.backer2Address); + + // Create pending payment that would meet goal (using creator1 as a third participant) + deal(address(testToken), users.creator1Address, campaignGoalAmount); + _createAndFundPayment(PAYMENT_ID_3, BUYER_ID_3, keccak256("item3"), campaignGoalAmount, users.creator1Address); + + // Advance past deadline + advanceToAfterDeadline(); + + // Both refunds should be blocked due to optimistic lock + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 1); + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + vm.prank(users.backer1Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(goalBasedPaymentTreasury), 2); + vm.expectRevert(BasePaymentTreasury.PaymentTreasurySuccessConditionNotFulfilled.selector); + vm.prank(users.backer2Address); + goalBasedPaymentTreasury.claimRefund(PAYMENT_ID_2); + } +} +