diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0af678be..cf5ca5d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,12 +34,6 @@ jobs: ls -la cat foundry.toml - - name: Install dependencies - run: | - # Install OpenZeppelin contracts version 5.3.0 - forge install OpenZeppelin/openzeppelin-contracts@v5.3.0 - ls -la lib/ - - name: Run Forge build run: | forge remappings diff --git a/.gitignore b/.gitignore index e8be93c1..7de7cd25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +/lib .env .deps .temp diff --git a/.gitmodules b/.gitmodules index 690924b6..9296efd5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/README.md b/README.md index 68de14d6..a63df3b2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Creative Crowdfunding Protocol (CC Protocol) Smart Contracts +# Oak Network Smart Contracts ## Overview -CC Protocol is a decentralized crowdfunding protocol designed to help creators launch and manage campaigns across multiple platforms. By providing a standardized infrastructure, the protocol simplifies the process of creating, funding, and managing crowdfunding initiatives in web3 across different platforms. +Oak Network is a decentralized crowdfunding protocol designed to help creators launch and manage campaigns across multiple platforms. By providing a standardized infrastructure, the protocol simplifies the process of creating, funding, and managing crowdfunding initiatives in web3 across different platforms. ## Features @@ -10,19 +10,23 @@ CC Protocol is a decentralized crowdfunding protocol designed to help creators l - Multiple treasury models - Secure fund management - Customizable protocol parameters +- Currency-based multi-token campaigns +- Campaign-level Pledge NFTs (one ERC721 collection per campaign) +- ERC-2771 meta-transactions for platform admin operations using multisig wallets +- UUPS upgradeability for core protocol contracts ## Prerequisites - [Foundry](https://book.getfoundry.sh/) -- Solidity ^0.8.20 +- Solidity ^0.8.22 ## Installation 1. Clone the repository: ```bash -git clone https://github.com/ccprotocol/ccprotocol-contracts.git -cd ccprotocol-contracts +git clone https://github.com/oak-network/contracts.git +cd contracts ``` 2. Install dependencies: @@ -94,6 +98,18 @@ forge script script/DeployAll.s.sol:DeployAll --rpc-url http://localhost:8545 -- forge script script/DeployAll.s.sol:DeployAll --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast ``` +#### Deploy core + setup a specific treasury model + +If you want a one-shot script that deploys the protocol (UUPS proxies), configures `GlobalParams`, and registers + approves a treasury implementation for a platform, you can run one of the `DeployAllAndSetup*.s.sol` scripts. + +```bash +# Example: deploy and setup PaymentTreasury +forge script script/DeployAllAndSetupPaymentTreasury.s.sol:DeployAllAndSetupPaymentTreasury \ + --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast +``` + +> These scripts read configuration from `.env` (e.g. `PLATFORM_NAME`, `PROTOCOL_FEE_PERCENT`, `PLATFORM_FEE_PERCENT`, `CURRENCIES`/`TOKENS_PER_CURRENCY`, and optional `PLATFORM_ADAPTER_ADDRESS` for meta-txs). + ## Contract Architecture ### Core Contracts @@ -105,6 +121,9 @@ forge script script/DeployAll.s.sol:DeployAll --rpc-url $RPC_URL --private-key $ ### Treasury Models - `AllOrNothing`: Funds refunded if campaign goal not met +- `KeepWhatsRaised`: Flexible treasury that keeps funds regardless of goal achievement (tips, configurable fees, withdrawal gating) +- `PaymentTreasury`: Payment-style treasury (off-chain payment creation + on-chain confirmation, line items, optional NFT mint) +- `TimeConstrainedPaymentTreasury`: PaymentTreasury variant gated by `launchTime → deadline + bufferTime` ### Notes on Mock Contracts @@ -113,9 +132,13 @@ forge script script/DeployAll.s.sol:DeployAll --rpc-url $RPC_URL --private-key $ ## Deployment Workflow -1. Deploy `GlobalParams` -2. Deploy `TreasuryFactory` -3. Deploy `CampaignInfoFactory` +At a high level: + +1. Deploy `GlobalParams` (UUPS proxy + implementation) +2. Deploy `TreasuryFactory` (UUPS proxy + implementation) +3. Deploy `CampaignInfoFactory` (UUPS proxy + implementation) +4. Configure currencies/tokens + data registry keys + platforms (and optional platform adapters) +5. Register and approve treasury implementations per platform, then deploy treasuries per campaign > For local testing or development, the `TestToken` mock token needs to be deployed before interacting with contracts requiring an ERC20 token. @@ -130,6 +153,8 @@ Key environment variables to configure in `.env`: For a complete list of variables, refer to `.env.example`. +> Tip: `script/` contains deployment, setup, and upgrade scripts for each treasury type (including UUPS upgrade scripts). + ## Security ### Audits @@ -138,7 +163,7 @@ Security audit reports can be found in the [`audits/`](./audits/) folder. We reg ## Contributing -We welcome all contributions to the Creative Crowdfunding Protocol. If you're interested in helping, here's how you can contribute: +We welcome all contributions to the Oak Network. If you're interested in helping, here's how you can contribute: - **Report bugs** by opening issues - **Suggest enhancements** or new features @@ -154,18 +179,18 @@ Before contributing, please read our detailed [Contributing Guidelines](./CONTRI ### Community -Join our community on [Discord](https://discord.gg/4tR9rWc3QE) for questions and discussions. +Join our community on [Discord](https://discord.gg/tnBhVxSDDS) for questions and discussions. Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectful. ## Contributors - - + + Made with [contrib.rocks](https://contrib.rocks). ## License -This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file +This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). diff --git a/audits/Immunefi-Audit-Report-CreativeCrowdfunding_v1.0.pdf b/audits/Immunefi-Audit-Report-CreativeCrowdfunding_v1.0.pdf new file mode 100644 index 00000000..2b336219 Binary files /dev/null and b/audits/Immunefi-Audit-Report-CreativeCrowdfunding_v1.0.pdf differ diff --git a/docs/book.toml b/docs/book.toml index d8a41741..fd06e1bc 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -7,7 +7,7 @@ no-section-label = true additional-js = ["solidity.min.js"] additional-css = ["book.css"] mathjax-support = true -git-repository-url = "https://github.com/ccprotocol/ccprotocol-contracts" +git-repository-url = "https://github.com/oak-network/contracts" [output.html.fold] enable = true diff --git a/docs/src/README.md b/docs/src/README.md index 68de14d6..0c8d1630 100644 --- a/docs/src/README.md +++ b/docs/src/README.md @@ -1,8 +1,8 @@ -# Creative Crowdfunding Protocol (CC Protocol) Smart Contracts +# Oak Network Smart Contracts ## Overview -CC Protocol is a decentralized crowdfunding protocol designed to help creators launch and manage campaigns across multiple platforms. By providing a standardized infrastructure, the protocol simplifies the process of creating, funding, and managing crowdfunding initiatives in web3 across different platforms. +Oak Network is a decentralized crowdfunding protocol designed to help creators launch and manage campaigns across multiple platforms. By providing a standardized infrastructure, the protocol simplifies the process of creating, funding, and managing crowdfunding initiatives in web3 across different platforms. ## Features @@ -21,8 +21,8 @@ CC Protocol is a decentralized crowdfunding protocol designed to help creators l 1. Clone the repository: ```bash -git clone https://github.com/ccprotocol/ccprotocol-contracts.git -cd ccprotocol-contracts +git clone https://github.com/oak-network/contracts.git +cd contracts ``` 2. Install dependencies: @@ -138,7 +138,7 @@ Security audit reports can be found in the [`audits/`](./audits/) folder. We reg ## Contributing -We welcome all contributions to the Creative Crowdfunding Protocol. If you're interested in helping, here's how you can contribute: +We welcome all contributions to the Oak Network. If you're interested in helping, here's how you can contribute: - **Report bugs** by opening issues - **Suggest enhancements** or new features @@ -154,18 +154,18 @@ Before contributing, please read our detailed [Contributing Guidelines](./CONTRI ### Community -Join our community on [Discord](https://discord.gg/4tR9rWc3QE) for questions and discussions. +Join our community on [Discord](https://discord.gg/tnBhVxSDDS) for questions and discussions. Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectful. ## Contributors - - + + Made with [contrib.rocks](https://contrib.rocks). ## License -This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file +This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index aafa9997..b8ac6e2f 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,25 +1,38 @@ # Summary - [Home](README.md) # src + - [❱ constants](src/constants/README.md) + - [DataRegistryKeys](src/constants/DataRegistryKeys.sol/library.DataRegistryKeys.md) - [❱ interfaces](src/interfaces/README.md) - [ICampaignData](src/interfaces/ICampaignData.sol/interface.ICampaignData.md) - [ICampaignInfo](src/interfaces/ICampaignInfo.sol/interface.ICampaignInfo.md) - [ICampaignInfoFactory](src/interfaces/ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md) + - [ICampaignPaymentTreasury](src/interfaces/ICampaignPaymentTreasury.sol/interface.ICampaignPaymentTreasury.md) - [ICampaignTreasury](src/interfaces/ICampaignTreasury.sol/interface.ICampaignTreasury.md) - [IGlobalParams](src/interfaces/IGlobalParams.sol/interface.IGlobalParams.md) - [IItem](src/interfaces/IItem.sol/interface.IItem.md) - [IReward](src/interfaces/IReward.sol/interface.IReward.md) - [ITreasuryFactory](src/interfaces/ITreasuryFactory.sol/interface.ITreasuryFactory.md) + - [❱ storage](src/storage/README.md) + - [AdminAccessCheckerStorage](src/storage/AdminAccessCheckerStorage.sol/library.AdminAccessCheckerStorage.md) + - [CampaignInfoFactoryStorage](src/storage/CampaignInfoFactoryStorage.sol/library.CampaignInfoFactoryStorage.md) + - [GlobalParamsStorage](src/storage/GlobalParamsStorage.sol/library.GlobalParamsStorage.md) + - [TreasuryFactoryStorage](src/storage/TreasuryFactoryStorage.sol/library.TreasuryFactoryStorage.md) - [❱ treasuries](src/treasuries/README.md) - [AllOrNothing](src/treasuries/AllOrNothing.sol/contract.AllOrNothing.md) + - [KeepWhatsRaised](src/treasuries/KeepWhatsRaised.sol/contract.KeepWhatsRaised.md) + - [PaymentTreasury](src/treasuries/PaymentTreasury.sol/contract.PaymentTreasury.md) + - [TimeConstrainedPaymentTreasury](src/treasuries/TimeConstrainedPaymentTreasury.sol/contract.TimeConstrainedPaymentTreasury.md) - [❱ utils](src/utils/README.md) - [AdminAccessChecker](src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md) + - [BasePaymentTreasury](src/utils/BasePaymentTreasury.sol/abstract.BasePaymentTreasury.md) - [BaseTreasury](src/utils/BaseTreasury.sol/abstract.BaseTreasury.md) - [CampaignAccessChecker](src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md) - [Counters](src/utils/Counters.sol/library.Counters.md) - [FiatEnabled](src/utils/FiatEnabled.sol/abstract.FiatEnabled.md) - [ItemRegistry](src/utils/ItemRegistry.sol/contract.ItemRegistry.md) - [PausableCancellable](src/utils/PausableCancellable.sol/abstract.PausableCancellable.md) + - [PledgeNFT](src/utils/PledgeNFT.sol/abstract.PledgeNFT.md) - [TimestampChecker](src/utils/TimestampChecker.sol/abstract.TimestampChecker.md) - [CampaignInfo](src/CampaignInfo.sol/contract.CampaignInfo.md) - [CampaignInfoFactory](src/CampaignInfoFactory.sol/contract.CampaignInfoFactory.md) diff --git a/docs/src/src/CampaignInfo.sol/contract.CampaignInfo.md b/docs/src/src/CampaignInfo.sol/contract.CampaignInfo.md index 3543572d..f867bb73 100644 --- a/docs/src/src/CampaignInfo.sol/contract.CampaignInfo.md +++ b/docs/src/src/CampaignInfo.sol/contract.CampaignInfo.md @@ -1,8 +1,8 @@ # CampaignInfo -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/CampaignInfo.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/CampaignInfo.sol) **Inherits:** -[ICampaignData](/src/interfaces/ICampaignData.sol/interface.ICampaignData.md), [ICampaignInfo](/src/interfaces/ICampaignInfo.sol/interface.ICampaignInfo.md), Ownable, [PausableCancellable](/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md), [TimestampChecker](/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md), [AdminAccessChecker](/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md), Initializable +[ICampaignData](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ICampaignData.sol/interface.ICampaignData.md), [ICampaignInfo](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ICampaignInfo.sol/interface.ICampaignInfo.md), Ownable, [PausableCancellable](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md), [TimestampChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md), [AdminAccessChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md), [PledgeNFT](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/PledgeNFT.sol/abstract.PledgeNFT.md), Initializable Manages campaign information and platform data. @@ -11,49 +11,70 @@ Manages campaign information and platform data. ### s_campaignData ```solidity -CampaignData private s_campaignData; +CampaignData private s_campaignData ``` ### s_platformTreasuryAddress ```solidity -mapping(bytes32 => address) private s_platformTreasuryAddress; +mapping(bytes32 => address) private s_platformTreasuryAddress ``` ### s_platformFeePercent ```solidity -mapping(bytes32 => uint256) private s_platformFeePercent; +mapping(bytes32 => uint256) private s_platformFeePercent ``` ### s_isSelectedPlatform ```solidity -mapping(bytes32 => bool) private s_isSelectedPlatform; +mapping(bytes32 => bool) private s_isSelectedPlatform ``` ### s_isApprovedPlatform ```solidity -mapping(bytes32 => bool) private s_isApprovedPlatform; +mapping(bytes32 => bool) private s_isApprovedPlatform ``` ### s_platformData ```solidity -mapping(bytes32 => bytes32) private s_platformData; +mapping(bytes32 => bytes32) private s_platformData ``` ### s_approvedPlatformHashes ```solidity -bytes32[] private s_approvedPlatformHashes; +bytes32[] private s_approvedPlatformHashes +``` + + +### s_acceptedTokens + +```solidity +address[] private s_acceptedTokens +``` + + +### s_isAcceptedToken + +```solidity +mapping(address => bool) private s_isAcceptedToken +``` + + +### s_isLocked + +```solidity +bool private s_isLocked ``` @@ -65,11 +86,37 @@ bytes32[] private s_approvedPlatformHashes; function getApprovedPlatformHashes() external view returns (bytes32[] memory); ``` +### isLocked + +Returns whether the campaign is locked (after treasury deployment). + + +```solidity +function isLocked() external view override returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the campaign is locked, false otherwise.| + + +### whenNotLocked + +Modifier that checks if the campaign is not locked. + + +```solidity +modifier whenNotLocked() ; +``` + ### constructor +Constructor passes empty strings to ERC721 + ```solidity -constructor(address creator) Ownable(creator); +constructor() Ownable(_msgSender()) ERC721("", ""); ``` ### initialize @@ -82,7 +129,12 @@ function initialize( bytes32[] calldata selectedPlatformHash, bytes32[] calldata platformDataKey, bytes32[] calldata platformDataValue, - CampaignData calldata campaignData + CampaignData calldata campaignData, + address[] calldata acceptedTokens, + string calldata nftName, + string calldata nftSymbol, + string calldata nftImageURI, + string calldata nftContractURI ) external initializer; ``` @@ -116,7 +168,7 @@ function checkIfPlatformSelected(bytes32 platformHash) public view override retu ### checkIfPlatformApproved -*Check if a platform is already approved* +Check if a platform is already approved ```solidity @@ -167,7 +219,9 @@ function getProtocolAdminAddress() public view override returns (address); ### getTotalRaisedAmount -Retrieves the total amount raised in the campaign. +Retrieves the total amount raised across non-cancelled treasuries. + +This excludes cancelled treasuries and is affected by refunds. ```solidity @@ -180,6 +234,100 @@ function getTotalRaisedAmount() external view override returns (uint256); |``|`uint256`|The total amount raised in the campaign.| +### getTotalLifetimeRaisedAmount + +Retrieves the total lifetime raised amount across all treasuries. + +This amount never decreases even when refunds are processed. +It represents the sum of all pledges/payments ever made to the campaign, +regardless of cancellations or refunds. + + +```solidity +function getTotalLifetimeRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total lifetime raised amount as a uint256 value.| + + +### getTotalRefundedAmount + +Retrieves the total refunded amount across all treasuries. + +This is calculated as the difference between lifetime raised amount +and current raised amount. It represents the sum of all refunds +that have been processed across all treasuries. + + +```solidity +function getTotalRefundedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total refunded amount as a uint256 value.| + + +### getTotalAvailableRaisedAmount + +Retrieves the total available raised amount across all treasuries. + +This includes funds from both active and cancelled treasuries, +and is affected by refunds. It represents the actual current +balance of funds across all treasuries. + + +```solidity +function getTotalAvailableRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total available raised amount as a uint256 value.| + + +### getTotalCancelledAmount + +Retrieves the total raised amount from cancelled treasuries only. + +This is the opposite of getTotalRaisedAmount(), which only includes +non-cancelled treasuries. This function only sums up raised amounts +from treasuries that have been cancelled. + + +```solidity +function getTotalCancelledAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total raised amount from cancelled treasuries as a uint256 value.| + + +### getTotalExpectedAmount + +Retrieves the total expected (pending) amount across payment treasuries. + +This only applies to payment treasuries and represents payments that +have been created but not yet confirmed. Regular treasuries are skipped. + + +```solidity +function getTotalExpectedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total expected amount as a uint256 value.| + + ### getPlatformAdminAddress Retrieves the address of the platform administrator for a specific platform. @@ -246,39 +394,75 @@ function getGoalAmount() external view override returns (uint256); |``|`uint256`|The funding goal amount of the campaign.| -### getTokenAddress +### getProtocolFeePercent -Retrieves the address of the token used in the campaign. +Retrieves the protocol fee percentage for the campaign. ```solidity -function getTokenAddress() external view override returns (address); +function getProtocolFeePercent() external view override returns (uint256); ``` **Returns** |Name|Type|Description| |----|----|-----------| -|``|`address`|The address of the campaign's token.| +|``|`uint256`|The protocol fee percentage applied to the campaign.| -### getProtocolFeePercent +### getCampaignCurrency -Retrieves the protocol fee percentage for the campaign. +Retrieves the campaign's currency identifier. ```solidity -function getProtocolFeePercent() external view override returns (uint256); +function getCampaignCurrency() external view override returns (bytes32); ``` **Returns** |Name|Type|Description| |----|----|-----------| -|``|`uint256`|The protocol fee percentage applied to the campaign.| +|``|`bytes32`|The bytes32 currency identifier for the campaign.| + + +### getAcceptedTokens + +Retrieves the cached accepted tokens for the campaign. + + +```solidity +function getAcceptedTokens() external view override returns (address[] memory); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address[]`|An array of token addresses accepted for the campaign.| + + +### isTokenAccepted + +Checks if a token is accepted for the campaign. + + +```solidity +function isTokenAccepted(address token) external view override returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address to check.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the token is accepted; otherwise, false.| ### paused -*Returns true if the campaign is paused, and false otherwise.* +Returns true if the campaign is paused, and false otherwise. ```solidity @@ -287,7 +471,7 @@ function paused() public view override(ICampaignInfo, PausableCancellable) retur ### cancelled -*Returns true if the campaign is cancelled, and false otherwise.* +Returns true if the campaign is cancelled, and false otherwise. ```solidity @@ -315,6 +499,27 @@ function getPlatformFeePercent(bytes32 platformHash) external view override retu |``|`uint256`|The platform fee percentage applied to the campaign on the platform.| +### getPlatformClaimDelay + +Retrieves the claim delay (in seconds) configured for the given platform. + + +```solidity +function getPlatformClaimDelay(bytes32 platformHash) external view override returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The claim delay in seconds.| + + ### getPlatformData Retrieves platform-specific data for the campaign. @@ -351,10 +556,84 @@ function getIdentifierHash() external view override returns (bytes32); |``|`bytes32`|The bytes32 hash that uniquely identifies the campaign.| +### getDataFromRegistry + +Retrieves a value from the GlobalParams data registry. + + +```solidity +function getDataFromRegistry(bytes32 key) external view override returns (bytes32 value); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`key`|`bytes32`|The registry key.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`value`|`bytes32`|The registry value.| + + +### getBufferTime + +Retrieves the buffer time from the GlobalParams data registry. + + +```solidity +function getBufferTime() external view override returns (uint256 bufferTime); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`bufferTime`|`uint256`|The buffer time value.| + + +### getLineItemType + +Retrieves a platform-specific line item type configuration from GlobalParams. + + +```solidity +function getLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + override + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`exists`|`bool`|Whether this line item type exists and is active.| +|`label`|`string`|The label identifier for the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred.| + + ### transferOwnership -*Transfers ownership of the contract to a new account (`newOwner`). -Can only be called by the current owner.* +Transfers ownership of the contract to a new account (`newOwner`). +Can only be called by the current owner. ```solidity @@ -376,9 +655,9 @@ function updateLaunchTime(uint256 launchTime) external override onlyOwner - currentTimeIsLess(getLaunchTime()) whenNotPaused - whenNotCancelled; + whenNotCancelled + whenNotLocked; ``` **Parameters** @@ -393,13 +672,7 @@ Updates the campaign's deadline. ```solidity -function updateDeadline(uint256 deadline) - external - override - onlyOwner - currentTimeIsLess(getLaunchTime()) - whenNotPaused - whenNotCancelled; +function updateDeadline(uint256 deadline) external override onlyOwner whenNotPaused whenNotCancelled whenNotLocked; ``` **Parameters** @@ -418,9 +691,9 @@ function updateGoalAmount(uint256 goalAmount) external override onlyOwner - currentTimeIsLess(getLaunchTime()) whenNotPaused - whenNotCancelled; + whenNotCancelled + whenNotLocked; ``` **Parameters** @@ -433,17 +706,16 @@ function updateGoalAmount(uint256 goalAmount) Updates the selection status of a platform for the campaign. -*It can only be called for a platform if its not approved i.e. the platform treasury is not deployed* +It can only be called for a platform if its not approved i.e. the platform treasury is not deployed ```solidity -function updateSelectedPlatform(bytes32 platformHash, bool selection) - external - override - onlyOwner - currentTimeIsLess(getLaunchTime()) - whenNotPaused - whenNotCancelled; +function updateSelectedPlatform( + bytes32 platformHash, + bool selection, + bytes32[] calldata platformDataKey, + bytes32[] calldata platformDataValue +) external override onlyOwner currentTimeIsLess(getLaunchTime()) whenNotPaused whenNotCancelled; ``` **Parameters** @@ -451,11 +723,13 @@ function updateSelectedPlatform(bytes32 platformHash, bool selection) |----|----|-----------| |`platformHash`|`bytes32`|The bytes32 identifier of the platform.| |`selection`|`bool`|The new selection status (true or false).| +|`platformDataKey`|`bytes32[]`|An array of platform-specific data keys.| +|`platformDataValue`|`bytes32[]`|An array of platform-specific data values.| ### _pauseCampaign -*External function to pause the campaign.* +External function to pause the campaign. ```solidity @@ -464,7 +738,7 @@ function _pauseCampaign(bytes32 message) external onlyProtocolAdmin; ### _unpauseCampaign -*External function to unpause the campaign.* +External function to unpause the campaign. ```solidity @@ -473,16 +747,79 @@ function _unpauseCampaign(bytes32 message) external onlyProtocolAdmin; ### _cancelCampaign -*External function to cancel the campaign.* +External function to cancel the campaign. ```solidity function _cancelCampaign(bytes32 message) external; ``` +### setImageURI + +Sets the image URI for NFT metadata + +Can only be updated before campaign launch + + +```solidity +function setImageURI(string calldata newImageURI) + external + override(ICampaignInfo, PledgeNFT) + onlyOwner + currentTimeIsLess(getLaunchTime()); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newImageURI`|`string`|The new image URI| + + +### updateContractURI + +Updates the contract-level metadata URI + +Can only be updated before campaign launch + + +```solidity +function updateContractURI(string calldata newContractURI) + external + override(ICampaignInfo, PledgeNFT) + onlyOwner + currentTimeIsLess(getLaunchTime()); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newContractURI`|`string`|The new contract URI| + + +### mintNFTForPledge + + +```solidity +function mintNFTForPledge( + address backer, + bytes32 reward, + address tokenAddress, + uint256 amount, + uint256 shippingFee, + uint256 tipAmount +) public override(ICampaignInfo, PledgeNFT) returns (uint256 tokenId); +``` + +### burn + + +```solidity +function burn(uint256 tokenId) public override(ICampaignInfo, PledgeNFT); +``` + ### _setPlatformInfo -*Sets platform information for the campaign.* +Sets platform information for the campaign and grants treasury role. ```solidity @@ -498,7 +835,7 @@ function _setPlatformInfo(bytes32 platformHash, address platformTreasuryAddress) ## Events ### CampaignInfoLaunchTimeUpdated -*Emitted when the launch time of the campaign is updated.* +Emitted when the launch time of the campaign is updated. ```solidity @@ -512,7 +849,7 @@ event CampaignInfoLaunchTimeUpdated(uint256 newLaunchTime); |`newLaunchTime`|`uint256`|The new launch time.| ### CampaignInfoDeadlineUpdated -*Emitted when the deadline of the campaign is updated.* +Emitted when the deadline of the campaign is updated. ```solidity @@ -526,7 +863,7 @@ event CampaignInfoDeadlineUpdated(uint256 newDeadline); |`newDeadline`|`uint256`|The new deadline.| ### CampaignInfoGoalAmountUpdated -*Emitted when the goal amount of the campaign is updated.* +Emitted when the goal amount of the campaign is updated. ```solidity @@ -540,7 +877,7 @@ event CampaignInfoGoalAmountUpdated(uint256 newGoalAmount); |`newGoalAmount`|`uint256`|The new goal amount.| ### CampaignInfoSelectedPlatformUpdated -*Emitted when the selection state of a platform is updated.* +Emitted when the selection state of a platform is updated. ```solidity @@ -555,7 +892,7 @@ event CampaignInfoSelectedPlatformUpdated(bytes32 indexed platformHash, bool sel |`selection`|`bool`|The new selection state.| ### CampaignInfoPlatformInfoUpdated -*Emitted when platform information is updated for the campaign.* +Emitted when platform information is updated for the campaign. ```solidity @@ -571,7 +908,7 @@ event CampaignInfoPlatformInfoUpdated(bytes32 indexed platformHash, address inde ## Errors ### CampaignInfoInvalidPlatformUpdate -*Emitted when an invalid platform update is attempted.* +Emitted when an invalid platform update is attempted. ```solidity @@ -586,7 +923,7 @@ error CampaignInfoInvalidPlatformUpdate(bytes32 platformHash, bool selection); |`selection`|`bool`|The selection state (true/false).| ### CampaignInfoUnauthorized -*Emitted when an unauthorized action is attempted.* +Emitted when an unauthorized action is attempted. ```solidity @@ -594,7 +931,7 @@ error CampaignInfoUnauthorized(); ``` ### CampaignInfoInvalidInput -*Emitted when an invalid input is detected.* +Emitted when an invalid input is detected. ```solidity @@ -602,7 +939,7 @@ error CampaignInfoInvalidInput(); ``` ### CampaignInfoPlatformNotSelected -*Emitted when a platform is not selected for the campaign.* +Emitted when a platform is not selected for the campaign. ```solidity @@ -616,7 +953,7 @@ error CampaignInfoPlatformNotSelected(bytes32 platformHash); |`platformHash`|`bytes32`|The bytes32 identifier of the platform.| ### CampaignInfoPlatformAlreadyApproved -*Emitted when a platform is already approved for the campaign.* +Emitted when a platform is already approved for the campaign. ```solidity @@ -629,13 +966,20 @@ error CampaignInfoPlatformAlreadyApproved(bytes32 platformHash); |----|----|-----------| |`platformHash`|`bytes32`|The bytes32 identifier of the platform.| +### CampaignInfoIsLocked +Emitted when an operation is attempted on a locked campaign. + + +```solidity +error CampaignInfoIsLocked(); +``` + ## Structs ### Config ```solidity struct Config { address treasuryFactory; - address token; uint256 protocolFeePercent; bytes32 identifierHash; } diff --git a/docs/src/src/CampaignInfoFactory.sol/contract.CampaignInfoFactory.md b/docs/src/src/CampaignInfoFactory.sol/contract.CampaignInfoFactory.md index 17439f11..ce2f4806 100644 --- a/docs/src/src/CampaignInfoFactory.sol/contract.CampaignInfoFactory.md +++ b/docs/src/src/CampaignInfoFactory.sol/contract.CampaignInfoFactory.md @@ -1,89 +1,71 @@ # CampaignInfoFactory -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/CampaignInfoFactory.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/CampaignInfoFactory.sol) **Inherits:** -Initializable, [ICampaignInfoFactory](/src/interfaces/ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md), Ownable +Initializable, [ICampaignInfoFactory](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md), OwnableUpgradeable, UUPSUpgradeable Factory contract for creating campaign information contracts. +UUPS Upgradeable contract with ERC-7201 namespaced storage -## State Variables -### GLOBAL_PARAMS -```solidity -IGlobalParams private GLOBAL_PARAMS; -``` - - -### s_treasuryFactoryAddress - -```solidity -address private s_treasuryFactoryAddress; -``` - - -### s_initialized - -```solidity -bool private s_initialized; -``` - - -### s_implementation - -```solidity -address private s_implementation; -``` - - -### isValidCampaignInfo - -```solidity -mapping(address => bool) public isValidCampaignInfo; -``` +## Functions +### constructor +Constructor that disables initializers to prevent implementation contract initialization -### identifierToCampaignInfo ```solidity -mapping(bytes32 => address) public identifierToCampaignInfo; +constructor() ; ``` +### initialize -## Functions -### constructor +Initializes the CampaignInfoFactory contract. ```solidity -constructor(IGlobalParams globalParams, address campaignImplementation) Ownable(msg.sender); +function initialize( + address initialOwner, + IGlobalParams globalParams, + address campaignImplementation, + address treasuryFactoryAddress +) public initializer; ``` **Parameters** |Name|Type|Description| |----|----|-----------| +|`initialOwner`|`address`|The address that will own the factory| |`globalParams`|`IGlobalParams`|The address of the global parameters contract.| -|`campaignImplementation`|`address`|| +|`campaignImplementation`|`address`|The address of the campaign implementation contract.| +|`treasuryFactoryAddress`|`address`|The address of the treasury factory contract.| -### _initialize +### _authorizeUpgrade -*Initializes the factory with treasury factory address.* +Function that authorizes an upgrade to a new implementation ```solidity -function _initialize(address treasuryFactoryAddress, address globalParams) external onlyOwner initializer; +function _authorizeUpgrade(address newImplementation) internal override onlyOwner; ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`treasuryFactoryAddress`|`address`|The address of the treasury factory contract.| -|`globalParams`|`address`|The address of the global parameters contract.| +|`newImplementation`|`address`|Address of the new implementation| ### createCampaign -Creates a new campaign information contract. +Creates a new campaign with NFT + +IMPORTANT: Protocol and platform fees are retrieved at execution time and locked +permanently in the campaign contract. Users should verify current fees before +calling this function or using intermediate contracts that check fees haven't +changed from expected values. The protocol fee is stored as immutable in the cloned +contract and platform fees are stored during initialization. ```solidity @@ -93,19 +75,27 @@ function createCampaign( bytes32[] calldata selectedPlatformHash, bytes32[] calldata platformDataKey, bytes32[] calldata platformDataValue, - CampaignData calldata campaignData + CampaignData calldata campaignData, + string calldata nftName, + string calldata nftSymbol, + string calldata nftImageURI, + string calldata contractURI ) external override; ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`creator`|`address`|The address of the creator of the campaign.| -|`identifierHash`|`bytes32`|The unique identifier hash of the campaign.| -|`selectedPlatformHash`|`bytes32[]`|An array of platform identifiers selected for the campaign.| -|`platformDataKey`|`bytes32[]`|An array of platform-specific data keys.| -|`platformDataValue`|`bytes32[]`|An array of platform-specific data values.| -|`campaignData`|`CampaignData`|The struct containing campaign launch details.| +|`creator`|`address`|The campaign creator address| +|`identifierHash`|`bytes32`|The unique identifier hash for the campaign| +|`selectedPlatformHash`|`bytes32[]`|Array of selected platform hashes| +|`platformDataKey`|`bytes32[]`|Array of platform data keys| +|`platformDataValue`|`bytes32[]`|Array of platform data values| +|`campaignData`|`CampaignData`|The campaign data| +|`nftName`|`string`|NFT collection name| +|`nftSymbol`|`string`|NFT collection symbol| +|`nftImageURI`|`string`|NFT image URI for individual tokens| +|`contractURI`|`string`|IPFS URI for contract-level metadata (constructed off-chain)| ### updateImplementation @@ -123,17 +113,51 @@ function updateImplementation(address newImplementation) external override onlyO |`newImplementation`|`address`|The address of the camapaignInfo implementation contract.| -## Errors -### CampaignInfoFactoryAlreadyInitialized -*Emitted when the factory is initialized.* +### isValidCampaignInfo + +Check if a campaign info address is valid + + +```solidity +function isValidCampaignInfo(address campaignInfo) external view returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`campaignInfo`|`address`|The campaign info address to check| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|bool True if valid, false otherwise| + + +### identifierToCampaignInfo + +Get campaign info address from identifier ```solidity -error CampaignInfoFactoryAlreadyInitialized(); +function identifierToCampaignInfo(bytes32 identifierHash) external view returns (address); ``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`identifierHash`|`bytes32`|The identifier hash| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address`|address The campaign info address| + +## Errors ### CampaignInfoFactoryInvalidInput -*Emitted when invalid input is provided.* +Emitted when invalid input is provided. ```solidity @@ -141,7 +165,7 @@ error CampaignInfoFactoryInvalidInput(); ``` ### CampaignInfoFactoryCampaignInitializationFailed -*Emitted when campaign creation fails.* +Emitted when campaign creation fails. ```solidity @@ -160,3 +184,11 @@ error CampaignInfoFactoryPlatformNotListed(bytes32 platformHash); error CampaignInfoFactoryCampaignWithSameIdentifierExists(bytes32 identifierHash, address cloneExists); ``` +### CampaignInfoInvalidTokenList +Emitted when the campaign currency has no tokens. + + +```solidity +error CampaignInfoInvalidTokenList(); +``` + diff --git a/docs/src/src/GlobalParams.sol/contract.GlobalParams.md b/docs/src/src/GlobalParams.sol/contract.GlobalParams.md index c48beca0..4bafc292 100644 --- a/docs/src/src/GlobalParams.sol/contract.GlobalParams.md +++ b/docs/src/src/GlobalParams.sol/contract.GlobalParams.md @@ -1,130 +1,137 @@ # GlobalParams -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/GlobalParams.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/GlobalParams.sol) **Inherits:** -[IGlobalParams](/src/interfaces/IGlobalParams.sol/interface.IGlobalParams.md), Ownable +Initializable, [IGlobalParams](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/IGlobalParams.sol/interface.IGlobalParams.md), OwnableUpgradeable, UUPSUpgradeable Manages global parameters and platform information. +UUPS Upgradeable contract with ERC-7201 namespaced storage + ## State Variables ### ZERO_BYTES ```solidity -bytes32 private constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000; +bytes32 private constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000 ``` -### s_protocolAdminAddress - -```solidity -address private s_protocolAdminAddress; -``` +## Functions +### notAddressZero +Reverts if the input address is zero. -### s_tokenAddress ```solidity -address private s_tokenAddress; +modifier notAddressZero(address account) ; ``` +### onlyPlatformAdmin + +Modifier that restricts function access to platform administrators of a specific platform. +Users attempting to execute functions with this modifier must be the platform admin for the given platform. -### s_protocolFeePercent ```solidity -uint256 private s_protocolFeePercent; +modifier onlyPlatformAdmin(bytes32 platformHash) ; ``` +**Parameters** +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| -### s_platformIsListed - -```solidity -mapping(bytes32 => bool) private s_platformIsListed; -``` +### platformIsListed -### s_platformAdminAddress ```solidity -mapping(bytes32 => address) private s_platformAdminAddress; +modifier platformIsListed(bytes32 platformHash) ; ``` +### constructor + +Constructor that disables initializers to prevent implementation contract initialization -### s_platformFeePercent ```solidity -mapping(bytes32 => uint256) private s_platformFeePercent; +constructor() ; ``` +### initialize + +Initializer function (replaces constructor) -### s_platformDataOwner ```solidity -mapping(bytes32 => bytes32) private s_platformDataOwner; +function initialize( + address protocolAdminAddress, + uint256 protocolFeePercent, + bytes32[] memory currencies, + address[][] memory tokensPerCurrency +) public initializer; ``` +**Parameters** +|Name|Type|Description| +|----|----|-----------| +|`protocolAdminAddress`|`address`|The address of the protocol admin.| +|`protocolFeePercent`|`uint256`|The protocol fee percentage.| +|`currencies`|`bytes32[]`|The array of currency identifiers.| +|`tokensPerCurrency`|`address[][]`|The array of token arrays for each currency.| -### s_platformData -```solidity -mapping(bytes32 => bool) private s_platformData; -``` +### _authorizeUpgrade +Function that authorizes an upgrade to a new implementation -### s_numberOfListedPlatforms ```solidity -Counters.Counter private s_numberOfListedPlatforms; +function _authorizeUpgrade(address newImplementation) internal override onlyOwner; ``` +**Parameters** +|Name|Type|Description| +|----|----|-----------| +|`newImplementation`|`address`|Address of the new implementation| -## Functions -### notAddressZero - -*Reverts if the input address is zero.* - - -```solidity -modifier notAddressZero(address account); -``` -### onlyPlatformAdmin +### addToRegistry -*Modifier that restricts function access to platform administrators of a specific platform. -Users attempting to execute functions with this modifier must be the platform admin for the given platform.* +Adds a key-value pair to the data registry. ```solidity -modifier onlyPlatformAdmin(bytes32 platformHash); +function addToRegistry(bytes32 key, bytes32 value) external onlyOwner; ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`platformHash`|`bytes32`|The unique identifier of the platform.| - - -### platformIsListed +|`key`|`bytes32`|The registry key.| +|`value`|`bytes32`|The registry value.| -```solidity -modifier platformIsListed(bytes32 platformHash); -``` +### getFromRegistry -### constructor +Retrieves a value from the data registry. ```solidity -constructor(address protocolAdminAddress, address tokenAddress, uint256 protocolFeePercent) - Ownable(protocolAdminAddress); +function getFromRegistry(bytes32 key) external view returns (bytes32 value); ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`protocolAdminAddress`|`address`|The address of the protocol admin.| -|`tokenAddress`|`address`|The address of the token contract.| -|`protocolFeePercent`|`uint256`|The protocol fee percentage.| +|`key`|`bytes32`|The registry key.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`value`|`bytes32`|The registry value.| ### getPlatformAdminAddress @@ -183,48 +190,59 @@ function getProtocolAdminAddress() external view override returns (address); |``|`address`|The admin address of the protocol.| -### getTokenAddress +### getProtocolFeePercent -Retrieves the address of the protocol's native token. +Retrieves the protocol fee percentage. ```solidity -function getTokenAddress() external view override returns (address); +function getProtocolFeePercent() external view override returns (uint256); ``` **Returns** |Name|Type|Description| |----|----|-----------| -|``|`address`|The address of the native token.| +|``|`uint256`|The protocol fee percentage as a uint256 value.| -### getProtocolFeePercent +### getPlatformFeePercent -Retrieves the protocol fee percentage. +Retrieves the platform fee percentage for a specific platform. ```solidity -function getProtocolFeePercent() external view override returns (uint256); +function getPlatformFeePercent(bytes32 platformHash) + external + view + override + platformIsListed(platformHash) + returns (uint256 platformFeePercent); ``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| + **Returns** |Name|Type|Description| |----|----|-----------| -|``|`uint256`|The protocol fee percentage as a uint256 value.| +|`platformFeePercent`|`uint256`|The platform fee percentage as a uint256 value.| -### getPlatformFeePercent +### getPlatformClaimDelay -Retrieves the platform fee percentage for a specific platform. +Retrieves the claim delay (in seconds) for a specific platform. ```solidity -function getPlatformFeePercent(bytes32 platformHash) +function getPlatformClaimDelay(bytes32 platformHash) external view override platformIsListed(platformHash) - returns (uint256 platformFeePercent); + returns (uint256 claimDelay); ``` **Parameters** @@ -236,7 +254,7 @@ function getPlatformFeePercent(bytes32 platformHash) |Name|Type|Description| |----|----|-----------| -|`platformFeePercent`|`uint256`|The platform fee percentage as a uint256 value.| +|`claimDelay`|`uint256`|The claim delay in seconds.| ### getPlatformDataOwner @@ -304,16 +322,18 @@ function checkIfPlatformDataKeyValid(bytes32 platformDataKey) external view over ### enlistPlatform -Enlists a platform with its admin address and fee percentage. +Enlists a platform with its admin address, fee percentage, and optional adapter. -*The platformFeePercent can be any value including zero.* +The platformFeePercent can be any value including zero. ```solidity -function enlistPlatform(bytes32 platformHash, address platformAdminAddress, uint256 platformFeePercent) - external - onlyOwner - notAddressZero(platformAdminAddress); +function enlistPlatform( + bytes32 platformHash, + address platformAdminAddress, + uint256 platformFeePercent, + address platformAdapter +) external onlyOwner notAddressZero(platformAdminAddress); ``` **Parameters** @@ -322,6 +342,7 @@ function enlistPlatform(bytes32 platformHash, address platformAdminAddress, uint |`platformHash`|`bytes32`|The platform's identifier.| |`platformAdminAddress`|`address`|The platform's admin address.| |`platformFeePercent`|`uint256`|The platform's fee percentage.| +|`platformAdapter`|`address`|The platform's adapter (trusted forwarder) address for ERC-2771 meta-transactions. Can be address(0) if not needed.| ### delistPlatform @@ -396,60 +417,259 @@ function updateProtocolAdminAddress(address protocolAdminAddress) |`protocolAdminAddress`|`address`|| -### updateTokenAddress +### updateProtocolFeePercent -Updates the address of the protocol's native token. +Updates the protocol fee percentage. ```solidity -function updateTokenAddress(address tokenAddress) external override onlyOwner notAddressZero(tokenAddress); +function updateProtocolFeePercent(uint256 protocolFeePercent) external override onlyOwner; ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`tokenAddress`|`address`|| +|`protocolFeePercent`|`uint256`|| -### updateProtocolFeePercent +### updatePlatformAdminAddress -Updates the protocol fee percentage. +Updates the admin address of a platform. ```solidity -function updateProtocolFeePercent(uint256 protocolFeePercent) external override onlyOwner; +function updatePlatformAdminAddress(bytes32 platformHash, address platformAdminAddress) + external + override + onlyOwner + platformIsListed(platformHash) + notAddressZero(platformAdminAddress); ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`protocolFeePercent`|`uint256`|| +|`platformHash`|`bytes32`|| +|`platformAdminAddress`|`address`|| -### updatePlatformAdminAddress +### updatePlatformClaimDelay -Updates the admin address of a platform. +Updates the claim delay for a specific platform. ```solidity -function updatePlatformAdminAddress(bytes32 platformHash, address platformAdminAddress) +function updatePlatformClaimDelay(bytes32 platformHash, uint256 claimDelay) + external + override + platformIsListed(platformHash) + onlyPlatformAdmin(platformHash); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| +|`claimDelay`|`uint256`|The claim delay in seconds.| + + +### getPlatformAdapter + +Retrieves the adapter (trusted forwarder) address for a platform. + + +```solidity +function getPlatformAdapter(bytes32 platformHash) + external + view + override + platformIsListed(platformHash) + returns (address); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address`|The adapter address for ERC-2771 meta-transactions.| + + +### setPlatformAdapter + +Sets the adapter (trusted forwarder) address for a platform. + +Only callable by the protocol admin (owner). + + +```solidity +function setPlatformAdapter(bytes32 platformHash, address adapter) + external + override + onlyOwner + platformIsListed(platformHash); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| +|`adapter`|`address`|The address of the adapter contract.| + + +### addTokenToCurrency + +Adds a token to a currency. + + +```solidity +function addTokenToCurrency(bytes32 currency, address token) external override onlyOwner notAddressZero(token); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| +|`token`|`address`|The token address to add.| + + +### removeTokenFromCurrency + +Removes a token from a currency. + + +```solidity +function removeTokenFromCurrency(bytes32 currency, address token) external override onlyOwner + notAddressZero(token); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| +|`token`|`address`|The token address to remove.| + + +### getTokensForCurrency + +Retrieves all tokens accepted for a specific currency. + + +```solidity +function getTokensForCurrency(bytes32 currency) external view override returns (address[] memory); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address[]`|An array of token addresses accepted for the currency.| + + +### setPlatformLineItemType + +Sets or updates a platform-specific line item type configuration. + +Only callable by the platform admin. + + +```solidity +function setPlatformLineItemType( + bytes32 platformHash, + bytes32 typeId, + string calldata label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer +) external platformIsListed(platformHash) onlyPlatformAdmin(platformHash); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| +|`label`|`string`|The label identifier for the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred. Constraints: - If countsTowardGoal is true, then applyProtocolFee must be false, canRefund must be true, and instantTransfer must be false. - Non-goal instant transfer items cannot be refundable.| + + +### removePlatformLineItemType + +Removes a platform-specific line item type by setting its exists flag to false. + +Only callable by the platform admin. This prevents the type from being used in new pledges. + + +```solidity +function removePlatformLineItemType(bytes32 platformHash, bytes32 typeId) + external platformIsListed(platformHash) - notAddressZero(platformAdminAddress); + onlyPlatformAdmin(platformHash); ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`platformHash`|`bytes32`|| -|`platformAdminAddress`|`address`|| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type to remove.| + + +### getPlatformLineItemType + +Retrieves a platform-specific line item type configuration. + + +```solidity +function getPlatformLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`exists`|`bool`|Whether this line item type exists and is active.| +|`label`|`string`|The label identifier for the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred to platform admin after payment confirmation.| ### _revertIfAddressZero -*Reverts if the input address is zero.* +Reverts if the input address is zero. ```solidity @@ -458,8 +678,8 @@ function _revertIfAddressZero(address account) internal pure; ### _onlyPlatformAdmin -*Internal function to check if the sender is the platform administrator for a specific platform. -If the sender is not the platform admin, it reverts with AdminAccessCheckerUnauthorized error.* +Internal function to check if the sender is the platform administrator for a specific platform. +If the sender is not the platform admin, it reverts with GlobalParamsUnauthorized error. ```solidity @@ -474,11 +694,13 @@ function _onlyPlatformAdmin(bytes32 platformHash) private view; ## Events ### PlatformEnlisted -*Emitted when a platform is enlisted.* +Emitted when a platform is enlisted. ```solidity -event PlatformEnlisted(bytes32 indexed platformHash, address indexed platformAdminAddress, uint256 platformFeePercent); +event PlatformEnlisted( + bytes32 indexed platformHash, address indexed platformAdminAddress, uint256 platformFeePercent +); ``` **Parameters** @@ -490,7 +712,7 @@ event PlatformEnlisted(bytes32 indexed platformHash, address indexed platformAdm |`platformFeePercent`|`uint256`|The fee percentage of the enlisted platform.| ### PlatformDelisted -*Emitted when a platform is delisted.* +Emitted when a platform is delisted. ```solidity @@ -504,7 +726,7 @@ event PlatformDelisted(bytes32 indexed platformHash); |`platformHash`|`bytes32`|The identifier of the delisted platform.| ### ProtocolAdminAddressUpdated -*Emitted when the protocol admin address is updated.* +Emitted when the protocol admin address is updated. ```solidity @@ -517,22 +739,38 @@ event ProtocolAdminAddressUpdated(address indexed newAdminAddress); |----|----|-----------| |`newAdminAddress`|`address`|The new protocol admin address.| -### TokenAddressUpdated -*Emitted when the token address is updated.* +### TokenAddedToCurrency +Emitted when a token is added to a currency. + + +```solidity +event TokenAddedToCurrency(bytes32 indexed currency, address indexed token); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| +|`token`|`address`|The token address added.| + +### TokenRemovedFromCurrency +Emitted when a token is removed from a currency. ```solidity -event TokenAddressUpdated(address indexed newTokenAddress); +event TokenRemovedFromCurrency(bytes32 indexed currency, address indexed token); ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`newTokenAddress`|`address`|The new token address.| +|`currency`|`bytes32`|The currency identifier.| +|`token`|`address`|The token address removed.| ### ProtocolFeePercentUpdated -*Emitted when the protocol fee percent is updated.* +Emitted when the protocol fee percent is updated. ```solidity @@ -546,7 +784,7 @@ event ProtocolFeePercentUpdated(uint256 newFeePercent); |`newFeePercent`|`uint256`|The new protocol fee percentage.| ### PlatformAdminAddressUpdated -*Emitted when the platform admin address is updated.* +Emitted when the platform admin address is updated. ```solidity @@ -561,7 +799,7 @@ event PlatformAdminAddressUpdated(bytes32 indexed platformHash, address indexed |`newAdminAddress`|`address`|The new admin address of the platform.| ### PlatformDataAdded -*Emitted when platform data is added.* +Emitted when platform data is added. ```solidity @@ -576,7 +814,7 @@ event PlatformDataAdded(bytes32 indexed platformHash, bytes32 indexed platformDa |`platformDataKey`|`bytes32`|The data key added to the platform.| ### PlatformDataRemoved -*Emitted when platform data is removed.* +Emitted when platform data is removed. ```solidity @@ -590,9 +828,88 @@ event PlatformDataRemoved(bytes32 indexed platformHash, bytes32 platformDataKey) |`platformHash`|`bytes32`|The identifier of the platform.| |`platformDataKey`|`bytes32`|The data key removed from the platform.| +### DataAddedToRegistry +Emitted when data is added to the registry. + + +```solidity +event DataAddedToRegistry(bytes32 indexed key, bytes32 value); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`key`|`bytes32`|The registry key.| +|`value`|`bytes32`|The registry value.| + +### PlatformLineItemTypeSet +Emitted when a platform-specific line item type is set or updated. + + +```solidity +event PlatformLineItemTypeSet( + bytes32 indexed platformHash, + bytes32 indexed typeId, + string label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer +); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| +|`label`|`string`|The label of the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred to platform admin after payment confirmation.| + +### PlatformClaimDelayUpdated + +```solidity +event PlatformClaimDelayUpdated(bytes32 indexed platformHash, uint256 claimDelay); +``` + +### PlatformAdapterSet +Emitted when a platform adapter (trusted forwarder) is set. + + +```solidity +event PlatformAdapterSet(bytes32 indexed platformHash, address indexed adapter); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`adapter`|`address`|The address of the adapter contract.| + +### PlatformLineItemTypeRemoved +Emitted when a platform-specific line item type is removed. + + +```solidity +event PlatformLineItemTypeRemoved(bytes32 indexed platformHash, bytes32 indexed typeId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the removed line item type.| + ## Errors ### GlobalParamsInvalidInput -*Throws when the input address is zero.* +Throws when the input address is zero. ```solidity @@ -600,7 +917,7 @@ error GlobalParamsInvalidInput(); ``` ### GlobalParamsPlatformNotListed -*Throws when the platform is not listed.* +Throws when the platform is not listed. ```solidity @@ -614,7 +931,7 @@ error GlobalParamsPlatformNotListed(bytes32 platformHash); |`platformHash`|`bytes32`|The identifier of the platform.| ### GlobalParamsPlatformAlreadyListed -*Throws when the platform is already listed.* +Throws when the platform is already listed. ```solidity @@ -628,7 +945,7 @@ error GlobalParamsPlatformAlreadyListed(bytes32 platformHash); |`platformHash`|`bytes32`|The identifier of the platform.| ### GlobalParamsPlatformAdminNotSet -*Throws when the platform admin is not set.* +Throws when the platform admin is not set. ```solidity @@ -642,7 +959,7 @@ error GlobalParamsPlatformAdminNotSet(bytes32 platformHash); |`platformHash`|`bytes32`|The identifier of the platform.| ### GlobalParamsPlatformFeePercentIsZero -*Throws when the platform fee percent is zero.* +Throws when the platform fee percent is zero. ```solidity @@ -656,7 +973,7 @@ error GlobalParamsPlatformFeePercentIsZero(bytes32 platformHash); |`platformHash`|`bytes32`|The identifier of the platform.| ### GlobalParamsPlatformDataAlreadySet -*Throws when the platform data is already set.* +Throws when the platform data is already set. ```solidity @@ -664,7 +981,7 @@ error GlobalParamsPlatformDataAlreadySet(); ``` ### GlobalParamsPlatformDataNotSet -*Throws when the platform data is not set.* +Throws when the platform data is not set. ```solidity @@ -672,7 +989,7 @@ error GlobalParamsPlatformDataNotSet(); ``` ### GlobalParamsPlatformDataSlotTaken -*Throws when the platform data slot is already taken.* +Throws when the platform data slot is already taken. ```solidity @@ -680,10 +997,62 @@ error GlobalParamsPlatformDataSlotTaken(); ``` ### GlobalParamsUnauthorized -*Throws when the caller is not authorized.* +Throws when the caller is not authorized. ```solidity error GlobalParamsUnauthorized(); ``` +### GlobalParamsCurrencyTokenLengthMismatch +Throws when currency and token arrays length mismatch. + + +```solidity +error GlobalParamsCurrencyTokenLengthMismatch(); +``` + +### GlobalParamsCurrencyHasNoTokens +Throws when a currency has no tokens registered. + + +```solidity +error GlobalParamsCurrencyHasNoTokens(bytes32 currency); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| + +### GlobalParamsTokenNotInCurrency +Throws when a token is not found in a currency. + + +```solidity +error GlobalParamsTokenNotInCurrency(bytes32 currency, address token); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| +|`token`|`address`|The token address.| + +### GlobalParamsPlatformLineItemTypeNotFound +Throws when a platform-specific line item type is not found. + + +```solidity +error GlobalParamsPlatformLineItemTypeNotFound(bytes32 platformHash, bytes32 typeId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| + diff --git a/docs/src/src/README.md b/docs/src/src/README.md index 7592aab6..954c176d 100644 --- a/docs/src/src/README.md +++ b/docs/src/src/README.md @@ -1,7 +1,9 @@ # Contents +- [constants](/src/constants) - [interfaces](/src/interfaces) +- [storage](/src/storage) - [treasuries](/src/treasuries) - [utils](/src/utils) - [CampaignInfo](CampaignInfo.sol/contract.CampaignInfo.md) diff --git a/docs/src/src/TestUSD.sol/contract.TestUSD.md b/docs/src/src/TestUSD.sol/contract.TestUSD.md index e317536d..0d401e69 100644 --- a/docs/src/src/TestUSD.sol/contract.TestUSD.md +++ b/docs/src/src/TestUSD.sol/contract.TestUSD.md @@ -1,8 +1,12 @@ # TestUSD +<<<<<<< Updated upstream [Git Source](https://github.com/ccprotocol/reference-client-sc/blob/32b7b1617200d0c6f3248845ef972180411f1f65/src/TestUSD.sol) +======= +[Git Source](https://github.com/ccprotocol/ccprotocol-contracts-internal/blob/4245ef0ad7914158999986aa0d8b5d2614efc6c2/src/TestUSD.sol) +>>>>>>> Stashed changes **Inherits:** -ERC20, Ownable +[ERC20](/src/.deps/npm/@openzeppelin/contracts/token/ERC20/ERC20.sol/abstract.ERC20.md), [Ownable](/src/.deps/npm/@openzeppelin/contracts/access/Ownable.sol/abstract.Ownable.md) A test token `tUSD` which is used in the tests. diff --git a/docs/src/src/TreasuryFactory.sol/contract.TreasuryFactory.md b/docs/src/src/TreasuryFactory.sol/contract.TreasuryFactory.md index 17455ce4..34f4ab81 100644 --- a/docs/src/src/TreasuryFactory.sol/contract.TreasuryFactory.md +++ b/docs/src/src/TreasuryFactory.sol/contract.TreasuryFactory.md @@ -1,42 +1,59 @@ # TreasuryFactory -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/TreasuryFactory.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/TreasuryFactory.sol) **Inherits:** -[ITreasuryFactory](/src/interfaces/ITreasuryFactory.sol/interface.ITreasuryFactory.md), [AdminAccessChecker](/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md) +Initializable, [ITreasuryFactory](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ITreasuryFactory.sol/interface.ITreasuryFactory.md), [AdminAccessChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md), UUPSUpgradeable +Factory contract for creating treasury contracts + +UUPS Upgradeable contract with ERC-7201 namespaced storage + + +## Functions +### constructor + +Constructor that disables initializers to prevent implementation contract initialization -## State Variables -### implementationMap ```solidity -mapping(bytes32 => mapping(uint256 => address)) private implementationMap; +constructor() ; ``` +### initialize + +Initializes the TreasuryFactory contract. -### approvedImplementations ```solidity -mapping(address => bool) private approvedImplementations; +function initialize(IGlobalParams globalParams) public initializer; ``` +**Parameters** +|Name|Type|Description| +|----|----|-----------| +|`globalParams`|`IGlobalParams`|The address of the GlobalParams contract| -## Functions -### constructor -Initializes the TreasuryFactory contract. +### _authorizeUpgrade -*This constructor sets the address of the GlobalParams contract as the admin.* +Function that authorizes an upgrade to a new implementation ```solidity -constructor(IGlobalParams globalParams); +function _authorizeUpgrade(address newImplementation) internal override onlyProtocolAdmin; ``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newImplementation`|`address`|Address of the new implementation| + ### registerTreasuryImplementation Registers a treasury implementation for a given platform. -*Callable only by the platform admin.* +Callable only by the platform admin. ```solidity @@ -58,7 +75,7 @@ function registerTreasuryImplementation(bytes32 platformHash, uint256 implementa Approves a previously registered implementation. -*Callable only by the protocol admin.* +Callable only by the protocol admin. ```solidity @@ -113,17 +130,15 @@ function removeTreasuryImplementation(bytes32 platformHash, uint256 implementati Deploys a treasury clone using an approved implementation. -*Callable only by the platform admin.* +Callable only by the platform admin. ```solidity -function deploy( - bytes32 platformHash, - address infoAddress, - uint256 implementationId, - string calldata name, - string calldata symbol -) external override onlyPlatformAdmin(platformHash) returns (address clone); +function deploy(bytes32 platformHash, address infoAddress, uint256 implementationId) + external + override + onlyPlatformAdmin(platformHash) + returns (address clone); ``` **Parameters** @@ -132,8 +147,6 @@ function deploy( |`platformHash`|`bytes32`|The platform identifier.| |`infoAddress`|`address`|The address of the campaign info contract.| |`implementationId`|`uint256`|The ID of the implementation to use.| -|`name`|`string`|The name of the treasury token.| -|`symbol`|`string`|The symbol of the treasury token.| **Returns** diff --git a/docs/src/src/constants/DataRegistryKeys.sol/library.DataRegistryKeys.md b/docs/src/src/constants/DataRegistryKeys.sol/library.DataRegistryKeys.md new file mode 100644 index 00000000..07ee7fe8 --- /dev/null +++ b/docs/src/src/constants/DataRegistryKeys.sol/library.DataRegistryKeys.md @@ -0,0 +1,61 @@ +# DataRegistryKeys +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/constants/DataRegistryKeys.sol) + +Centralized storage for all dataRegistry keys used in GlobalParams + +This library provides a single source of truth for all dataRegistry keys +to ensure consistency across contracts and prevent key collisions. + + +## State Variables +### BUFFER_TIME + +```solidity +bytes32 public constant BUFFER_TIME = keccak256("bufferTime") +``` + + +### MAX_PAYMENT_EXPIRATION + +```solidity +bytes32 public constant MAX_PAYMENT_EXPIRATION = keccak256("maxPaymentExpiration") +``` + + +### CAMPAIGN_LAUNCH_BUFFER + +```solidity +bytes32 public constant CAMPAIGN_LAUNCH_BUFFER = keccak256("campaignLaunchBuffer") +``` + + +### MINIMUM_CAMPAIGN_DURATION + +```solidity +bytes32 public constant MINIMUM_CAMPAIGN_DURATION = keccak256("minimumCampaignDuration") +``` + + +## Functions +### scopedToPlatform + +Generates a namespaced registry key scoped to a specific platform. + + +```solidity +function scopedToPlatform(bytes32 baseKey, bytes32 platformHash) internal pure returns (bytes32 platformKey); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`baseKey`|`bytes32`|The base registry key.| +|`platformHash`|`bytes32`|The identifier of the platform.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`platformKey`|`bytes32`|The platform-scoped registry key.| + + diff --git a/docs/src/src/constants/README.md b/docs/src/src/constants/README.md new file mode 100644 index 00000000..fd3796c8 --- /dev/null +++ b/docs/src/src/constants/README.md @@ -0,0 +1,4 @@ + + +# Contents +- [DataRegistryKeys](DataRegistryKeys.sol/library.DataRegistryKeys.md) diff --git a/docs/src/src/interfaces/ICampaignData.sol/interface.ICampaignData.md b/docs/src/src/interfaces/ICampaignData.sol/interface.ICampaignData.md index c159c65d..3ff11ea7 100644 --- a/docs/src/src/interfaces/ICampaignData.sol/interface.ICampaignData.md +++ b/docs/src/src/interfaces/ICampaignData.sol/interface.ICampaignData.md @@ -1,19 +1,20 @@ # ICampaignData -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/ICampaignData.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/ICampaignData.sol) An interface for managing campaign data in a CCP. ## Structs ### CampaignData -*Struct to represent campaign data, including launch time, deadline, and goal amount.* +Struct to represent campaign data, including launch time, deadline, goal amount, and currency. ```solidity struct CampaignData { - uint256 launchTime; - uint256 deadline; - uint256 goalAmount; + uint256 launchTime; // Timestamp when the campaign is launched. + uint256 deadline; // Timestamp or block number when the campaign ends. + uint256 goalAmount; // Funding goal amount that the campaign aims to achieve. + bytes32 currency; // Currency identifier for the campaign (e.g., bytes32("USD")). } ``` diff --git a/docs/src/src/interfaces/ICampaignInfo.sol/interface.ICampaignInfo.md b/docs/src/src/interfaces/ICampaignInfo.sol/interface.ICampaignInfo.md index 09b0d8e5..e42e5088 100644 --- a/docs/src/src/interfaces/ICampaignInfo.sol/interface.ICampaignInfo.md +++ b/docs/src/src/interfaces/ICampaignInfo.sol/interface.ICampaignInfo.md @@ -1,8 +1,13 @@ # ICampaignInfo -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/ICampaignInfo.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/ICampaignInfo.sol) + +**Inherits:** +IERC721 An interface for managing campaign information in a crowdfunding system. +Inherits from IERC721 as CampaignInfo is an ERC721 NFT collection + ## Functions ### owner @@ -43,7 +48,9 @@ function checkIfPlatformSelected(bytes32 platformHash) external view returns (bo ### getTotalRaisedAmount -Retrieves the total amount raised in the campaign. +Retrieves the total amount raised across non-cancelled treasuries. + +This excludes cancelled treasuries and is affected by refunds. ```solidity @@ -56,6 +63,100 @@ function getTotalRaisedAmount() external view returns (uint256); |``|`uint256`|The total amount raised in the campaign.| +### getTotalLifetimeRaisedAmount + +Retrieves the total lifetime raised amount across all treasuries. + +This amount never decreases even when refunds are processed. +It represents the sum of all pledges/payments ever made to the campaign, +regardless of cancellations or refunds. + + +```solidity +function getTotalLifetimeRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total lifetime raised amount as a uint256 value.| + + +### getTotalRefundedAmount + +Retrieves the total refunded amount across all treasuries. + +This is calculated as the difference between lifetime raised amount +and current raised amount. It represents the sum of all refunds +that have been processed across all treasuries. + + +```solidity +function getTotalRefundedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total refunded amount as a uint256 value.| + + +### getTotalAvailableRaisedAmount + +Retrieves the total available raised amount across all treasuries. + +This includes funds from both active and cancelled treasuries, +and is affected by refunds. It represents the actual current +balance of funds across all treasuries. + + +```solidity +function getTotalAvailableRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total available raised amount as a uint256 value.| + + +### getTotalCancelledAmount + +Retrieves the total raised amount from cancelled treasuries only. + +This is the opposite of getTotalRaisedAmount(), which only includes +non-cancelled treasuries. This function only sums up raised amounts +from treasuries that have been cancelled. + + +```solidity +function getTotalCancelledAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total raised amount from cancelled treasuries as a uint256 value.| + + +### getTotalExpectedAmount + +Retrieves the total expected (pending) amount across payment treasuries. + +This only applies to payment treasuries and represents payments that +have been created but not yet confirmed. Regular treasuries are skipped. + + +```solidity +function getTotalExpectedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total expected amount as a uint256 value.| + + ### getProtocolAdminAddress Retrieves the address of the protocol administrator. @@ -137,34 +238,70 @@ function getGoalAmount() external view returns (uint256); |``|`uint256`|The funding goal amount of the campaign.| -### getTokenAddress +### getProtocolFeePercent -Retrieves the address of the token used in the campaign. +Retrieves the protocol fee percentage for the campaign. ```solidity -function getTokenAddress() external view returns (address); +function getProtocolFeePercent() external view returns (uint256); ``` **Returns** |Name|Type|Description| |----|----|-----------| -|``|`address`|The address of the campaign's token.| +|``|`uint256`|The protocol fee percentage applied to the campaign.| -### getProtocolFeePercent +### getCampaignCurrency -Retrieves the protocol fee percentage for the campaign. +Retrieves the campaign's currency identifier. ```solidity -function getProtocolFeePercent() external view returns (uint256); +function getCampaignCurrency() external view returns (bytes32); ``` **Returns** |Name|Type|Description| |----|----|-----------| -|``|`uint256`|The protocol fee percentage applied to the campaign.| +|``|`bytes32`|The bytes32 currency identifier for the campaign.| + + +### getAcceptedTokens + +Retrieves the cached accepted tokens for the campaign. + + +```solidity +function getAcceptedTokens() external view returns (address[] memory); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address[]`|An array of token addresses accepted for the campaign.| + + +### isTokenAccepted + +Checks if a token is accepted for the campaign. + + +```solidity +function isTokenAccepted(address token) external view returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address to check.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the token is accepted; otherwise, false.| ### getPlatformFeePercent @@ -188,6 +325,27 @@ function getPlatformFeePercent(bytes32 platformHash) external view returns (uint |``|`uint256`|The platform fee percentage applied to the campaign on the platform.| +### getPlatformClaimDelay + +Retrieves the claim delay (in seconds) configured for the given platform. + + +```solidity +function getPlatformClaimDelay(bytes32 platformHash) external view returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The claim delay in seconds.| + + ### getPlatformData Retrieves platform-specific data for the campaign. @@ -288,11 +446,16 @@ function updateGoalAmount(uint256 goalAmount) external; Updates the selection status of a platform for the campaign. -*It can only be called for a platform if its not approved i.e. the platform treasury is not deployed* +It can only be called for a platform if its not approved i.e. the platform treasury is not deployed ```solidity -function updateSelectedPlatform(bytes32 platformHash, bool selection) external; +function updateSelectedPlatform( + bytes32 platformHash, + bool selection, + bytes32[] calldata platformDataKey, + bytes32[] calldata platformDataValue +) external; ``` **Parameters** @@ -300,11 +463,13 @@ function updateSelectedPlatform(bytes32 platformHash, bool selection) external; |----|----|-----------| |`platformHash`|`bytes32`|The bytes32 identifier of the platform.| |`selection`|`bool`|The new selection status (true or false).| +|`platformDataKey`|`bytes32[]`|An array of platform-specific data keys.| +|`platformDataValue`|`bytes32[]`|An array of platform-specific data values.| ### paused -*Returns true if the campaign is paused, and false otherwise.* +Returns true if the campaign is paused, and false otherwise. ```solidity @@ -313,10 +478,172 @@ function paused() external view returns (bool); ### cancelled -*Returns true if the campaign is cancelled, and false otherwise.* +Returns true if the campaign is cancelled, and false otherwise. ```solidity function cancelled() external view returns (bool); ``` +### getDataFromRegistry + +Retrieves a value from the GlobalParams data registry. + + +```solidity +function getDataFromRegistry(bytes32 key) external view returns (bytes32 value); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`key`|`bytes32`|The registry key.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`value`|`bytes32`|The registry value.| + + +### getBufferTime + +Retrieves the buffer time from the GlobalParams data registry. + + +```solidity +function getBufferTime() external view returns (uint256 bufferTime); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`bufferTime`|`uint256`|The buffer time value.| + + +### getLineItemType + +Retrieves a platform-specific line item type configuration from GlobalParams. + + +```solidity +function getLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`exists`|`bool`|Whether this line item type exists and is active.| +|`label`|`string`|The label identifier for the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred.| + + +### mintNFTForPledge + +Mints a pledge NFT for a backer + +Can only be called by treasuries with MINTER_ROLE + + +```solidity +function mintNFTForPledge( + address backer, + bytes32 reward, + address tokenAddress, + uint256 amount, + uint256 shippingFee, + uint256 tipAmount +) external returns (uint256 tokenId); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`backer`|`address`|The backer address| +|`reward`|`bytes32`|The reward identifier| +|`tokenAddress`|`address`|The address of the token used for the pledge| +|`amount`|`uint256`|The pledge amount| +|`shippingFee`|`uint256`|The shipping fee| +|`tipAmount`|`uint256`|The tip amount| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The minted token ID (pledge ID)| + + +### setImageURI + +Sets the image URI for NFT metadata + + +```solidity +function setImageURI(string calldata newImageURI) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newImageURI`|`string`|The new image URI| + + +### updateContractURI + +Updates the contract-level metadata URI + + +```solidity +function updateContractURI(string calldata newContractURI) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newContractURI`|`string`|The new contract URI| + + +### burn + +Burns a pledge NFT + + +```solidity +function burn(uint256 tokenId) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The token ID to burn| + + +### isLocked + +Returns true if the campaign is locked (after treasury deployment), and false otherwise. + + +```solidity +function isLocked() external view returns (bool); +``` + diff --git a/docs/src/src/interfaces/ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md b/docs/src/src/interfaces/ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md index b82c3002..5faecbec 100644 --- a/docs/src/src/interfaces/ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md +++ b/docs/src/src/interfaces/ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md @@ -1,8 +1,8 @@ # ICampaignInfoFactory -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/ICampaignInfoFactory.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/ICampaignInfoFactory.sol) **Inherits:** -[ICampaignData](/src/interfaces/ICampaignData.sol/interface.ICampaignData.md) +[ICampaignData](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ICampaignData.sol/interface.ICampaignData.md) An interface for creating and managing campaign information contracts. @@ -10,7 +10,13 @@ An interface for creating and managing campaign information contracts. ## Functions ### createCampaign -Creates a new campaign information contract. +Creates a new campaign information contract with NFT. + +IMPORTANT: Protocol and platform fees are retrieved at execution time and locked +permanently in the campaign contract. Users should verify current fees before +calling this function or using intermediate contracts that check fees haven't +changed from expected values. The protocol fee is stored as immutable in the cloned +contract and platform fees are stored during initialization. ```solidity @@ -20,7 +26,11 @@ function createCampaign( bytes32[] calldata selectedPlatformHash, bytes32[] calldata platformDataKey, bytes32[] calldata platformDataValue, - CampaignData calldata campaignData + CampaignData calldata campaignData, + string calldata nftName, + string calldata nftSymbol, + string calldata nftImageURI, + string calldata contractURI ) external; ``` **Parameters** @@ -32,7 +42,11 @@ function createCampaign( |`selectedPlatformHash`|`bytes32[]`|An array of platform identifiers selected for the campaign.| |`platformDataKey`|`bytes32[]`|An array of platform-specific data keys.| |`platformDataValue`|`bytes32[]`|An array of platform-specific data values.| -|`campaignData`|`CampaignData`|The struct containing campaign launch details.| +|`campaignData`|`CampaignData`|The struct containing campaign launch details (including currency).| +|`nftName`|`string`|NFT collection name| +|`nftSymbol`|`string`|NFT collection symbol| +|`nftImageURI`|`string`|NFT image URI for individual tokens| +|`contractURI`|`string`|IPFS URI for contract-level metadata| ### updateImplementation diff --git a/docs/src/src/interfaces/ICampaignPaymentTreasury.sol/interface.ICampaignPaymentTreasury.md b/docs/src/src/interfaces/ICampaignPaymentTreasury.sol/interface.ICampaignPaymentTreasury.md new file mode 100644 index 00000000..6fcc7ec2 --- /dev/null +++ b/docs/src/src/interfaces/ICampaignPaymentTreasury.sol/interface.ICampaignPaymentTreasury.md @@ -0,0 +1,456 @@ +# ICampaignPaymentTreasury +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/ICampaignPaymentTreasury.sol) + +An interface for managing campaign payment treasury contracts. + + +## Functions +### createPayment + +Creates a new payment entry with the specified details. + + +```solidity +function createPayment( + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + address paymentToken, + uint256 amount, + uint256 expiration, + LineItem[] calldata lineItems, + ExternalFees[] calldata externalFees +) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|A unique identifier for the payment.| +|`buyerId`|`bytes32`|The id of the buyer initiating the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`expiration`|`uint256`|The timestamp after which the payment expires.| +|`lineItems`|`LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### createPaymentBatch + +Creates multiple payment entries in a single transaction to prevent nonce conflicts. + + +```solidity +function createPaymentBatch( + bytes32[] calldata paymentIds, + bytes32[] calldata buyerIds, + bytes32[] calldata itemIds, + address[] calldata paymentTokens, + uint256[] calldata amounts, + uint256[] calldata expirations, + LineItem[][] calldata lineItemsArray, + ExternalFees[][] calldata externalFeesArray +) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique identifiers for the payments.| +|`buyerIds`|`bytes32[]`|An array of buyer IDs corresponding to each payment.| +|`itemIds`|`bytes32[]`|An array of item identifiers corresponding to each payment.| +|`paymentTokens`|`address[]`|An array of tokens corresponding to each payment.| +|`amounts`|`uint256[]`|An array of amounts corresponding to each payment.| +|`expirations`|`uint256[]`|An array of expiration timestamps corresponding to each payment.| +|`lineItemsArray`|`LineItem[][]`|An array of line item arrays, one for each payment.| +|`externalFeesArray`|`ExternalFees[][]`|An array of external fee metadata arrays, one for each payment (informational only).| + + +### processCryptoPayment + +Allows a buyer to make a direct crypto payment for an item. + +This function transfers tokens directly from the buyer's wallet and confirms the payment immediately. + + +```solidity +function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + LineItem[] calldata lineItems, + ExternalFees[] calldata externalFees +) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`buyerAddress`|`address`|The address of the buyer making the payment.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`lineItems`|`LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### cancelPayment + +Cancels an existing payment with the given payment ID. + + +```solidity +function cancelPayment(bytes32 paymentId) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to cancel.| + + +### confirmPayment + +Confirms and finalizes the payment associated with the given payment ID. + + +```solidity +function confirmPayment(bytes32 paymentId, address buyerAddress) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to confirm.| +|`buyerAddress`|`address`|Optional buyer address to mint NFT to. Pass address(0) to skip NFT minting.| + + +### confirmPaymentBatch + +Confirms and finalizes multiple payments in a single transaction. + + +```solidity +function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique payment identifiers to be confirmed.| +|`buyerAddresses`|`address[]`|Array of buyer addresses to mint NFTs to. Must match paymentIds length. Pass address(0) to skip NFT minting for specific payments.| + + +### disburseFees + +Disburses fees collected by the treasury. + + +```solidity +function disburseFees() external; +``` + +### withdraw + +Withdraws funds from the treasury. + + +```solidity +function withdraw() external; +``` + +### claimRefund + +Claims a refund for non-NFT payments (payments without minted NFTs). + +Only callable by platform admin. Used for payments confirmed without a buyer address. + + +```solidity +function claimRefund(bytes32 paymentId, address refundAddress) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must NOT have an NFT).| +|`refundAddress`|`address`|The address where the refunded amount should be sent.| + + +### claimRefund + +Claims a refund for NFT payments (payments with minted NFTs). + +Burns the NFT associated with the payment. Caller must have approved the treasury for the NFT. +Used for processCryptoPayment and confirmPayment (with buyer address) transactions. + + +```solidity +function claimRefund(bytes32 paymentId) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must have an NFT).| + + +### claimExpiredFunds + +Allows platform admin to claim all remaining funds once the claim window has opened. + + +```solidity +function claimExpiredFunds() external; +``` + +### getplatformHash + +Retrieves the platform identifier associated with the treasury. + + +```solidity +function getplatformHash() external view returns (bytes32); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bytes32`|The platform identifier as a bytes32 value.| + + +### getplatformFeePercent + +Retrieves the platform fee percentage for the treasury. + + +```solidity +function getplatformFeePercent() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The platform fee percentage as a uint256 value.| + + +### getRaisedAmount + +Retrieves the total raised amount in the treasury. + + +```solidity +function getRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total raised amount as a uint256 value.| + + +### getAvailableRaisedAmount + +Retrieves the currently available raised amount in the treasury. + + +```solidity +function getAvailableRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The current available raised amount as a uint256 value.| + + +### getPaymentData + +Retrieves comprehensive payment data including payment info, token, line items, and external fees. + + +```solidity +function getPaymentData(bytes32 paymentId) external view returns (PaymentData memory); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`PaymentData`|A PaymentData struct containing all payment information.| + + +### getLifetimeRaisedAmount + +Retrieves the lifetime raised amount in the treasury (never decreases with refunds). + + +```solidity +function getLifetimeRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The lifetime raised amount as a uint256 value.| + + +### getRefundedAmount + +Retrieves the total refunded amount in the treasury. + + +```solidity +function getRefundedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total refunded amount as a uint256 value.| + + +### getExpectedAmount + +Retrieves the total expected (pending) amount in the treasury. + +This represents payments that have been created but not yet confirmed. + + +```solidity +function getExpectedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total expected amount as a uint256 value.| + + +### cancelled + +Checks if the treasury has been cancelled. + + +```solidity +function cancelled() external view returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the treasury is cancelled, false otherwise.| + + +## Structs +### PaymentLineItem +Represents a stored line item with its configuration snapshot. + + +```solidity +struct PaymentLineItem { + bytes32 typeId; + uint256 amount; + string label; + bool countsTowardGoal; + bool applyProtocolFee; + bool canRefund; + bool instantTransfer; +} +``` + +**Properties** + +|Name|Type|Description| +|----|----|-----------| +|`typeId`|`bytes32`|The type identifier of the line item.| +|`amount`|`uint256`|The amount of the line item.| +|`label`|`string`|The human-readable label of the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether protocol fee applies to this line item.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item is transferred instantly.| + +### PaymentData +Comprehensive payment data structure containing all payment information. + + +```solidity +struct PaymentData { + address buyerAddress; + bytes32 buyerId; + bytes32 itemId; + uint256 amount; + uint256 expiration; + bool isConfirmed; + bool isCryptoPayment; + uint256 lineItemCount; + address paymentToken; + PaymentLineItem[] lineItems; + ExternalFees[] externalFees; +} +``` + +**Properties** + +|Name|Type|Description| +|----|----|-----------| +|`buyerAddress`|`address`|The address of the buyer who made the payment.| +|`buyerId`|`bytes32`|The ID of the buyer.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`amount`|`uint256`|The amount to be paid for the item (in token's native decimals).| +|`expiration`|`uint256`|The timestamp after which the payment expires.| +|`isConfirmed`|`bool`|Boolean indicating whether the payment has been confirmed.| +|`isCryptoPayment`|`bool`|Boolean indicating whether the payment is made using direct crypto payment.| +|`lineItemCount`|`uint256`|The number of line items associated with this payment.| +|`paymentToken`|`address`|The token address used for this payment.| +|`lineItems`|`PaymentLineItem[]`|Array of stored line items with their configuration snapshots.| +|`externalFees`|`ExternalFees[]`|Array of external fee metadata associated with this payment (informational only).| + +### LineItem +Represents a line item in a payment. + + +```solidity +struct LineItem { + bytes32 typeId; + uint256 amount; +} +``` + +**Properties** + +|Name|Type|Description| +|----|----|-----------| +|`typeId`|`bytes32`|The type identifier of the line item (must exist in GlobalParams).| +|`amount`|`uint256`|The amount of the line item (denominated in pledge token).| + +### ExternalFees +Represents metadata about external fees associated with a payment. + +These values are informational only and do not affect treasury balances or transfers. + + +```solidity +struct ExternalFees { + bytes32 feeType; + uint256 feeAmount; +} +``` + +**Properties** + +|Name|Type|Description| +|----|----|-----------| +|`feeType`|`bytes32`|The type identifier of the external fee.| +|`feeAmount`|`uint256`|The amount of the external fee.| + diff --git a/docs/src/src/interfaces/ICampaignTreasury.sol/interface.ICampaignTreasury.md b/docs/src/src/interfaces/ICampaignTreasury.sol/interface.ICampaignTreasury.md index bfa758f0..f69115ad 100644 --- a/docs/src/src/interfaces/ICampaignTreasury.sol/interface.ICampaignTreasury.md +++ b/docs/src/src/interfaces/ICampaignTreasury.sol/interface.ICampaignTreasury.md @@ -1,5 +1,5 @@ # ICampaignTreasury -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/ICampaignTreasury.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/ICampaignTreasury.sol) An interface for managing campaign treasury contracts. @@ -83,3 +83,48 @@ function getRaisedAmount() external view returns (uint256); |``|`uint256`|The total raised amount as a uint256 value.| +### getLifetimeRaisedAmount + +Retrieves the lifetime raised amount in the treasury (never decreases with refunds). + + +```solidity +function getLifetimeRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The lifetime raised amount as a uint256 value.| + + +### getRefundedAmount + +Retrieves the total refunded amount in the treasury. + + +```solidity +function getRefundedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total refunded amount as a uint256 value.| + + +### cancelled + +Checks if the treasury has been cancelled. + + +```solidity +function cancelled() external view returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the treasury is cancelled, false otherwise.| + + diff --git a/docs/src/src/interfaces/IGlobalParams.sol/interface.IGlobalParams.md b/docs/src/src/interfaces/IGlobalParams.sol/interface.IGlobalParams.md index 447c2b9a..5701c397 100644 --- a/docs/src/src/interfaces/IGlobalParams.sol/interface.IGlobalParams.md +++ b/docs/src/src/interfaces/IGlobalParams.sol/interface.IGlobalParams.md @@ -1,5 +1,5 @@ # IGlobalParams -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/IGlobalParams.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/IGlobalParams.sol) An interface for accessing and managing global parameters of the protocol. @@ -77,21 +77,6 @@ function getProtocolAdminAddress() external view returns (address); |``|`address`|The admin address of the protocol.| -### getTokenAddress - -Retrieves the address of the protocol's native token. - - -```solidity -function getTokenAddress() external view returns (address); -``` -**Returns** - -|Name|Type|Description| -|----|----|-----------| -|``|`address`|The address of the native token.| - - ### getProtocolFeePercent Retrieves the protocol fee percentage. @@ -149,55 +134,61 @@ function getPlatformFeePercent(bytes32 platformHash) external view returns (uint |``|`uint256`|The platform fee percentage as a uint256 value.| -### checkIfPlatformDataKeyValid +### getPlatformClaimDelay -Checks if a platform-specific data key is valid. +Retrieves the claim delay (in seconds) for a specific platform. ```solidity -function checkIfPlatformDataKeyValid(bytes32 platformDataKey) external view returns (bool isValid); +function getPlatformClaimDelay(bytes32 platformHash) external view returns (uint256); ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`platformDataKey`|`bytes32`|The key of the platform-specific data.| +|`platformHash`|`bytes32`|The unique identifier of the platform.| **Returns** |Name|Type|Description| |----|----|-----------| -|`isValid`|`bool`|True if the data key is valid; otherwise, false.| +|``|`uint256`|The claim delay in seconds.| -### updateProtocolAdminAddress +### checkIfPlatformDataKeyValid -Updates the admin address of the protocol. +Checks if a platform-specific data key is valid. ```solidity -function updateProtocolAdminAddress(address _protocolAdminAddress) external; +function checkIfPlatformDataKeyValid(bytes32 platformDataKey) external view returns (bool isValid); ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`_protocolAdminAddress`|`address`|The new admin address of the protocol.| +|`platformDataKey`|`bytes32`|The key of the platform-specific data.| +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`isValid`|`bool`|True if the data key is valid; otherwise, false.| -### updateTokenAddress -Updates the address of the protocol's native token. +### updateProtocolAdminAddress + +Updates the admin address of the protocol. ```solidity -function updateTokenAddress(address _tokenAddress) external; +function updateProtocolAdminAddress(address _protocolAdminAddress) external; ``` **Parameters** |Name|Type|Description| |----|----|-----------| -|`_tokenAddress`|`address`|The new address of the native token.| +|`_protocolAdminAddress`|`address`|The new admin address of the protocol.| ### updateProtocolFeePercent @@ -231,3 +222,214 @@ function updatePlatformAdminAddress(bytes32 _platformHash, address _platformAdmi |`_platformAdminAddress`|`address`|The new admin address of the platform.| +### updatePlatformClaimDelay + +Updates the claim delay for a specific platform. + + +```solidity +function updatePlatformClaimDelay(bytes32 platformHash, uint256 claimDelay) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| +|`claimDelay`|`uint256`|The claim delay in seconds.| + + +### getPlatformAdapter + +Retrieves the adapter (trusted forwarder) address for a platform. + + +```solidity +function getPlatformAdapter(bytes32 platformHash) external view returns (address); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address`|The adapter address for ERC-2771 meta-transactions.| + + +### setPlatformAdapter + +Sets the adapter (trusted forwarder) address for a platform. + +Only callable by the protocol admin (owner). + + +```solidity +function setPlatformAdapter(bytes32 platformHash, address adapter) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The unique identifier of the platform.| +|`adapter`|`address`|The address of the adapter contract.| + + +### addTokenToCurrency + +Adds a token to a currency. + + +```solidity +function addTokenToCurrency(bytes32 currency, address token) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| +|`token`|`address`|The token address to add.| + + +### removeTokenFromCurrency + +Removes a token from a currency. + + +```solidity +function removeTokenFromCurrency(bytes32 currency, address token) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| +|`token`|`address`|The token address to remove.| + + +### getTokensForCurrency + +Retrieves all tokens accepted for a specific currency. + + +```solidity +function getTokensForCurrency(bytes32 currency) external view returns (address[] memory); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`currency`|`bytes32`|The currency identifier.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`address[]`|An array of token addresses accepted for the currency.| + + +### getFromRegistry + +Retrieves a value from the data registry. + + +```solidity +function getFromRegistry(bytes32 key) external view returns (bytes32 value); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`key`|`bytes32`|The registry key.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`value`|`bytes32`|The registry value.| + + +### setPlatformLineItemType + +Sets or updates a platform-specific line item type configuration. + + +```solidity +function setPlatformLineItemType( + bytes32 platformHash, + bytes32 typeId, + string calldata label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer +) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| +|`label`|`string`|The label identifier for the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred.| + + +### removePlatformLineItemType + +Removes a platform-specific line item type by setting its exists flag to false. + + +```solidity +function removePlatformLineItemType(bytes32 platformHash, bytes32 typeId) external; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type to remove.| + + +### getPlatformLineItemType + +Retrieves a platform-specific line item type configuration. + + +```solidity +function getPlatformLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The identifier of the platform.| +|`typeId`|`bytes32`|The identifier of the line item type.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`exists`|`bool`|Whether this line item type exists and is active.| +|`label`|`string`|The label identifier for the line item type.| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal.| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation.| +|`canRefund`|`bool`|Whether this line item can be refunded.| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred.| + + diff --git a/docs/src/src/interfaces/IItem.sol/interface.IItem.md b/docs/src/src/interfaces/IItem.sol/interface.IItem.md index 1371e884..0eb64109 100644 --- a/docs/src/src/interfaces/IItem.sol/interface.IItem.md +++ b/docs/src/src/interfaces/IItem.sol/interface.IItem.md @@ -1,5 +1,5 @@ # IItem -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/IItem.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/IItem.sol) An interface for managing items and their attributes. @@ -50,12 +50,12 @@ Represents the attributes of an item. ```solidity struct Item { - uint256 actualWeight; - uint256 height; - uint256 width; - uint256 length; - bytes32 category; - bytes32 declaredCurrency; + uint256 actualWeight; // The actual weight of the item. + uint256 height; // The height of the item. + uint256 width; // The width of the item. + uint256 length; // The length of the item. + bytes32 category; // The category of the item. + bytes32 declaredCurrency; // The declared currency of the item. } ``` diff --git a/docs/src/src/interfaces/IReward.sol/interface.IReward.md b/docs/src/src/interfaces/IReward.sol/interface.IReward.md index bd0fdf23..957d116c 100644 --- a/docs/src/src/interfaces/IReward.sol/interface.IReward.md +++ b/docs/src/src/interfaces/IReward.sol/interface.IReward.md @@ -1,5 +1,5 @@ # IReward -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/IReward.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/IReward.sol) An interface for managing rewards in a campaign. diff --git a/docs/src/src/interfaces/ITreasuryFactory.sol/interface.ITreasuryFactory.md b/docs/src/src/interfaces/ITreasuryFactory.sol/interface.ITreasuryFactory.md index 5acfbf2b..9d6b9877 100644 --- a/docs/src/src/interfaces/ITreasuryFactory.sol/interface.ITreasuryFactory.md +++ b/docs/src/src/interfaces/ITreasuryFactory.sol/interface.ITreasuryFactory.md @@ -1,7 +1,7 @@ # ITreasuryFactory -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/interfaces/ITreasuryFactory.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/interfaces/ITreasuryFactory.sol) -*Interface for the TreasuryFactory contract, which registers, approves, and deploys treasury clones.* +Interface for the TreasuryFactory contract, which registers, approves, and deploys treasury clones. ## Functions @@ -9,7 +9,7 @@ Registers a treasury implementation for a given platform. -*Callable only by the platform admin.* +Callable only by the platform admin. ```solidity @@ -29,7 +29,7 @@ function registerTreasuryImplementation(bytes32 platformHash, uint256 implementa Approves a previously registered implementation. -*Callable only by the protocol admin.* +Callable only by the protocol admin. ```solidity @@ -78,17 +78,13 @@ function removeTreasuryImplementation(bytes32 platformHash, uint256 implementati Deploys a treasury clone using an approved implementation. -*Callable only by the platform admin.* +Callable only by the platform admin. ```solidity -function deploy( - bytes32 platformHash, - address infoAddress, - uint256 implementationId, - string calldata name, - string calldata symbol -) external returns (address clone); +function deploy(bytes32 platformHash, address infoAddress, uint256 implementationId) + external + returns (address clone); ``` **Parameters** @@ -97,8 +93,6 @@ function deploy( |`platformHash`|`bytes32`|The platform identifier.| |`infoAddress`|`address`|The address of the campaign info contract.| |`implementationId`|`uint256`|The ID of the implementation to use.| -|`name`|`string`|The name of the treasury token.| -|`symbol`|`string`|The symbol of the treasury token.| **Returns** @@ -109,12 +103,15 @@ function deploy( ## Events ### TreasuryFactoryTreasuryDeployed -*Emitted when a new treasury is deployed.* +Emitted when a new treasury is deployed. ```solidity event TreasuryFactoryTreasuryDeployed( - bytes32 indexed platformHash, uint256 indexed implementationId, address indexed infoAddress, address treasuryAddress + bytes32 indexed platformHash, + uint256 indexed implementationId, + address indexed infoAddress, + address treasuryAddress ); ``` @@ -127,3 +124,51 @@ event TreasuryFactoryTreasuryDeployed( |`infoAddress`|`address`|The campaign info address linked to the treasury.| |`treasuryAddress`|`address`|The deployed treasury address.| +### TreasuryImplementationRegistered +Emitted when a treasury implementation is registered for a platform. + + +```solidity +event TreasuryImplementationRegistered( + bytes32 indexed platformHash, uint256 indexed implementationId, address indexed implementation +); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The platform identifier.| +|`implementationId`|`uint256`|The ID of the implementation.| +|`implementation`|`address`|The contract address of the implementation.| + +### TreasuryImplementationRemoved +Emitted when a treasury implementation is removed from a platform. + + +```solidity +event TreasuryImplementationRemoved(bytes32 indexed platformHash, uint256 indexed implementationId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`platformHash`|`bytes32`|The platform identifier.| +|`implementationId`|`uint256`|The ID of the implementation.| + +### TreasuryImplementationApproval +Emitted when a treasury implementation is approved or disapproved by the protocol admin. + + +```solidity +event TreasuryImplementationApproval(address indexed implementation, bool isApproved); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`implementation`|`address`|The contract address of the implementation.| +|`isApproved`|`bool`|True if approved, false if disapproved.| + diff --git a/docs/src/src/interfaces/README.md b/docs/src/src/interfaces/README.md index e6bafb40..be6bb97a 100644 --- a/docs/src/src/interfaces/README.md +++ b/docs/src/src/interfaces/README.md @@ -4,6 +4,7 @@ - [ICampaignData](ICampaignData.sol/interface.ICampaignData.md) - [ICampaignInfo](ICampaignInfo.sol/interface.ICampaignInfo.md) - [ICampaignInfoFactory](ICampaignInfoFactory.sol/interface.ICampaignInfoFactory.md) +- [ICampaignPaymentTreasury](ICampaignPaymentTreasury.sol/interface.ICampaignPaymentTreasury.md) - [ICampaignTreasury](ICampaignTreasury.sol/interface.ICampaignTreasury.md) - [IGlobalParams](IGlobalParams.sol/interface.IGlobalParams.md) - [IItem](IItem.sol/interface.IItem.md) diff --git a/docs/src/src/storage/AdminAccessCheckerStorage.sol/library.AdminAccessCheckerStorage.md b/docs/src/src/storage/AdminAccessCheckerStorage.sol/library.AdminAccessCheckerStorage.md new file mode 100644 index 00000000..6c9f2fa6 --- /dev/null +++ b/docs/src/src/storage/AdminAccessCheckerStorage.sol/library.AdminAccessCheckerStorage.md @@ -0,0 +1,37 @@ +# AdminAccessCheckerStorage +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/storage/AdminAccessCheckerStorage.sol) + +Storage contract for AdminAccessChecker using ERC-7201 namespaced storage + +This contract contains the storage layout and accessor functions for AdminAccessChecker + + +## State Variables +### ADMIN_ACCESS_CHECKER_STORAGE_LOCATION + +```solidity +bytes32 private constant ADMIN_ACCESS_CHECKER_STORAGE_LOCATION = + 0x7c2f08fa04c2c7c7ab255a45dbf913d4c236b91c59858917e818398e997f8800 +``` + + +## Functions +### _getAdminAccessCheckerStorage + + +```solidity +function _getAdminAccessCheckerStorage() internal pure returns (Storage storage $); +``` + +## Structs +### Storage +**Note:** +storage-location: erc7201:ccprotocol.storage.AdminAccessChecker + + +```solidity +struct Storage { + IGlobalParams globalParams; +} +``` + diff --git a/docs/src/src/storage/CampaignInfoFactoryStorage.sol/library.CampaignInfoFactoryStorage.md b/docs/src/src/storage/CampaignInfoFactoryStorage.sol/library.CampaignInfoFactoryStorage.md new file mode 100644 index 00000000..26fe6530 --- /dev/null +++ b/docs/src/src/storage/CampaignInfoFactoryStorage.sol/library.CampaignInfoFactoryStorage.md @@ -0,0 +1,41 @@ +# CampaignInfoFactoryStorage +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/storage/CampaignInfoFactoryStorage.sol) + +Storage contract for CampaignInfoFactory using ERC-7201 namespaced storage + +This contract contains the storage layout and accessor functions for CampaignInfoFactory + + +## State Variables +### CAMPAIGN_INFO_FACTORY_STORAGE_LOCATION + +```solidity +bytes32 private constant CAMPAIGN_INFO_FACTORY_STORAGE_LOCATION = + 0x2857858a392b093e1f8b3f368c2276ce911f27cef445605a2932ebe945968d00 +``` + + +## Functions +### _getCampaignInfoFactoryStorage + + +```solidity +function _getCampaignInfoFactoryStorage() internal pure returns (Storage storage $); +``` + +## Structs +### Storage +**Note:** +storage-location: erc7201:ccprotocol.storage.CampaignInfoFactory + + +```solidity +struct Storage { + IGlobalParams globalParams; + address treasuryFactoryAddress; + address implementation; + mapping(address => bool) isValidCampaignInfo; + mapping(bytes32 => address) identifierToCampaignInfo; +} +``` + diff --git a/docs/src/src/storage/GlobalParamsStorage.sol/library.GlobalParamsStorage.md b/docs/src/src/storage/GlobalParamsStorage.sol/library.GlobalParamsStorage.md new file mode 100644 index 00000000..071475e3 --- /dev/null +++ b/docs/src/src/storage/GlobalParamsStorage.sol/library.GlobalParamsStorage.md @@ -0,0 +1,77 @@ +# GlobalParamsStorage +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/storage/GlobalParamsStorage.sol) + +Storage contract for GlobalParams using ERC-7201 namespaced storage + +This contract contains the storage layout and accessor functions for GlobalParams + + +## State Variables +### GLOBAL_PARAMS_STORAGE_LOCATION + +```solidity +bytes32 private constant GLOBAL_PARAMS_STORAGE_LOCATION = + 0x83d0145f7c1378f10048390769ec94f999b3ba6d94904b8fd7251512962b1c00 +``` + + +## Functions +### _getGlobalParamsStorage + + +```solidity +function _getGlobalParamsStorage() internal pure returns (Storage storage $); +``` + +## Structs +### LineItemType +Line item type configuration + + +```solidity +struct LineItemType { + bool exists; + string label; + bool countsTowardGoal; + bool applyProtocolFee; + bool canRefund; + bool instantTransfer; +} +``` + +**Properties** + +|Name|Type|Description| +|----|----|-----------| +|`exists`|`bool`|Whether this line item type exists and is active| +|`label`|`string`|The label identifier for the line item type (e.g., "shipping_fee")| +|`countsTowardGoal`|`bool`|Whether this line item counts toward the campaign goal| +|`applyProtocolFee`|`bool`|Whether this line item is included in protocol fee calculation| +|`canRefund`|`bool`|Whether this line item can be refunded| +|`instantTransfer`|`bool`|Whether this line item amount can be instantly transferred| + +### Storage +**Note:** +storage-location: erc7201:ccprotocol.storage.GlobalParams + + +```solidity +struct Storage { + address protocolAdminAddress; + uint256 protocolFeePercent; + mapping(bytes32 => bool) platformIsListed; + mapping(bytes32 => address) platformAdminAddress; + mapping(bytes32 => uint256) platformFeePercent; + mapping(bytes32 => bytes32) platformDataOwner; + mapping(bytes32 => bool) platformData; + mapping(bytes32 => bytes32) dataRegistry; + mapping(bytes32 => address[]) currencyToTokens; + // Platform-specific line item types: mapping(platformHash => mapping(typeId => LineItemType)) + mapping(bytes32 => mapping(bytes32 => LineItemType)) platformLineItemTypes; + mapping(bytes32 => uint256) platformClaimDelay; + // Platform adapter (trusted forwarder) for ERC-2771 meta-transactions: mapping(platformHash => adapterAddress) + mapping(bytes32 => address) platformAdapter; + Counters.Counter numberOfListedPlatforms; +} +``` + diff --git a/docs/src/src/storage/README.md b/docs/src/src/storage/README.md new file mode 100644 index 00000000..8d689510 --- /dev/null +++ b/docs/src/src/storage/README.md @@ -0,0 +1,7 @@ + + +# Contents +- [AdminAccessCheckerStorage](AdminAccessCheckerStorage.sol/library.AdminAccessCheckerStorage.md) +- [CampaignInfoFactoryStorage](CampaignInfoFactoryStorage.sol/library.CampaignInfoFactoryStorage.md) +- [GlobalParamsStorage](GlobalParamsStorage.sol/library.GlobalParamsStorage.md) +- [TreasuryFactoryStorage](TreasuryFactoryStorage.sol/library.TreasuryFactoryStorage.md) diff --git a/docs/src/src/storage/TreasuryFactoryStorage.sol/library.TreasuryFactoryStorage.md b/docs/src/src/storage/TreasuryFactoryStorage.sol/library.TreasuryFactoryStorage.md new file mode 100644 index 00000000..b0a68ad5 --- /dev/null +++ b/docs/src/src/storage/TreasuryFactoryStorage.sol/library.TreasuryFactoryStorage.md @@ -0,0 +1,38 @@ +# TreasuryFactoryStorage +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/storage/TreasuryFactoryStorage.sol) + +Storage contract for TreasuryFactory using ERC-7201 namespaced storage + +This contract contains the storage layout and accessor functions for TreasuryFactory + + +## State Variables +### TREASURY_FACTORY_STORAGE_LOCATION + +```solidity +bytes32 private constant TREASURY_FACTORY_STORAGE_LOCATION = + 0x96b7de8c171ef460648aea35787d043e89feb6b6de2623a1e6f17a91b9c9e900 +``` + + +## Functions +### _getTreasuryFactoryStorage + + +```solidity +function _getTreasuryFactoryStorage() internal pure returns (Storage storage $); +``` + +## Structs +### Storage +**Note:** +storage-location: erc7201:ccprotocol.storage.TreasuryFactory + + +```solidity +struct Storage { + mapping(bytes32 => mapping(uint256 => address)) implementationMap; + mapping(address => bool) approvedImplementations; +} +``` + diff --git a/docs/src/src/treasuries/AllOrNothing.sol/contract.AllOrNothing.md b/docs/src/src/treasuries/AllOrNothing.sol/contract.AllOrNothing.md index 5d719fbc..3b74f7b6 100644 --- a/docs/src/src/treasuries/AllOrNothing.sol/contract.AllOrNothing.md +++ b/docs/src/src/treasuries/AllOrNothing.sol/contract.AllOrNothing.md @@ -1,8 +1,8 @@ # AllOrNothing -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/treasuries/AllOrNothing.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/treasuries/AllOrNothing.sol) **Inherits:** -[IReward](/src/interfaces/IReward.sol/interface.IReward.md), [BaseTreasury](/src/utils/BaseTreasury.sol/abstract.BaseTreasury.md), [TimestampChecker](/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md), ERC721Burnable +[IReward](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/IReward.sol/interface.IReward.md), [BaseTreasury](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/BaseTreasury.sol/abstract.BaseTreasury.md), [TimestampChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md), ReentrancyGuard A contract for handling crowdfunding campaigns with rewards. @@ -11,83 +11,53 @@ A contract for handling crowdfunding campaigns with rewards. ### s_tokenToTotalCollectedAmount ```solidity -mapping(uint256 => uint256) private s_tokenToTotalCollectedAmount; +mapping(uint256 => uint256) private s_tokenToTotalCollectedAmount ``` ### s_tokenToPledgedAmount ```solidity -mapping(uint256 => uint256) private s_tokenToPledgedAmount; +mapping(uint256 => uint256) private s_tokenToPledgedAmount ``` ### s_reward ```solidity -mapping(bytes32 => Reward) private s_reward; +mapping(bytes32 => Reward) private s_reward ``` -### s_tokenIdCounter +### s_tokenIdToPledgeToken ```solidity -Counters.Counter private s_tokenIdCounter; +mapping(uint256 => address) private s_tokenIdToPledgeToken ``` ### s_rewardCounter ```solidity -Counters.Counter private s_rewardCounter; -``` - - -### s_name - -```solidity -string private s_name; -``` - - -### s_symbol - -```solidity -string private s_symbol; +Counters.Counter private s_rewardCounter ``` ## Functions ### constructor -*Constructor for the AllOrNothing contract.* +Constructor for the AllOrNothing contract. ```solidity -constructor() ERC721("", ""); +constructor() ; ``` ### initialize ```solidity -function initialize(bytes32 _platformHash, address _infoAddress, string calldata _name, string calldata _symbol) - external - initializer; -``` - -### name - - -```solidity -function name() public view override returns (string memory); -``` - -### symbol - - -```solidity -function symbol() public view override returns (string memory); +function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer; ``` ### getReward @@ -126,14 +96,44 @@ function getRaisedAmount() external view override returns (uint256); |``|`uint256`|The total raised amount as a uint256 value.| +### getLifetimeRaisedAmount + +Retrieves the lifetime raised amount in the treasury (never decreases with refunds). + + +```solidity +function getLifetimeRaisedAmount() external view override returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The lifetime raised amount as a uint256 value.| + + +### getRefundedAmount + +Retrieves the total refunded amount in the treasury. + + +```solidity +function getRefundedAmount() external view override returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total refunded amount as a uint256 value.| + + ### addRewards Adds multiple rewards in a batch. -*This function allows for both reward tiers and non-reward tiers. +This function allows for both reward tiers and non-reward tiers. For both types, rewards must have non-zero value. If items are specified (non-empty arrays), the itemId, itemValue, and itemQuantity arrays must match in length. -Empty arrays are allowed for both reward tiers and non-reward tiers.* +Empty arrays are allowed for both reward tiers and non-reward tiers. ```solidity @@ -178,13 +178,14 @@ function removeReward(bytes32 rewardName) Allows a backer to pledge for a reward. -*The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. -The non-reward tiers cannot be pledged for without a reward.* +The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. +The non-reward tiers cannot be pledged for without a reward. ```solidity -function pledgeForAReward(address backer, uint256 shippingFee, bytes32[] calldata reward) +function pledgeForAReward(address backer, address pledgeToken, uint256 shippingFee, bytes32[] calldata reward) external + nonReentrant currentTimeIsWithinRange(INFO.getLaunchTime(), INFO.getDeadline()) whenCampaignNotPaused whenNotPaused @@ -196,7 +197,8 @@ function pledgeForAReward(address backer, uint256 shippingFee, bytes32[] calldat |Name|Type|Description| |----|----|-----------| |`backer`|`address`|The address of the backer making the pledge.| -|`shippingFee`|`uint256`|| +|`pledgeToken`|`address`|The token address to use for the pledge.| +|`shippingFee`|`uint256`|The shipping fee amount.| |`reward`|`bytes32[]`|An array of reward names.| @@ -206,8 +208,9 @@ Allows a backer to pledge without selecting a reward. ```solidity -function pledgeWithoutAReward(address backer, uint256 pledgeAmount) +function pledgeWithoutAReward(address backer, address pledgeToken, uint256 pledgeAmount) external + nonReentrant currentTimeIsWithinRange(INFO.getLaunchTime(), INFO.getDeadline()) whenCampaignNotPaused whenNotPaused @@ -219,6 +222,7 @@ function pledgeWithoutAReward(address backer, uint256 pledgeAmount) |Name|Type|Description| |----|----|-----------| |`backer`|`address`|The address of the backer making the pledge.| +|`pledgeToken`|`address`|The token address to use for the pledge.| |`pledgeAmount`|`uint256`|The amount of the pledge.| @@ -261,7 +265,7 @@ function withdraw() public override whenNotPaused whenNotCancelled; ### cancelTreasury -*This function is overridden to allow the platform admin and the campaign owner to cancel a treasury.* +This function is overridden to allow the platform admin and the campaign owner to cancel a treasury. ```solidity @@ -270,7 +274,7 @@ function cancelTreasury(bytes32 message) public override; ### _checkSuccessCondition -*Internal function to check the success condition for fee disbursement.* +Internal function to check the success condition for fee disbursement. ```solidity @@ -289,30 +293,24 @@ function _checkSuccessCondition() internal view virtual override returns (bool); ```solidity function _pledge( address backer, + address pledgeToken, bytes32 reward, uint256 pledgeAmount, uint256 shippingFee, - uint256 tokenId, bytes32[] memory rewards ) private; ``` -### supportsInterface - - -```solidity -function supportsInterface(bytes4 interfaceId) public view override returns (bool); -``` - ## Events ### Receipt -*Emitted when a backer makes a pledge.* +Emitted when a backer makes a pledge. ```solidity event Receipt( address indexed backer, - bytes32 indexed reward, + address indexed pledgeToken, + bytes32 reward, uint256 pledgeAmount, uint256 shippingFee, uint256 tokenId, @@ -325,6 +323,7 @@ event Receipt( |Name|Type|Description| |----|----|-----------| |`backer`|`address`|The address of the backer making the pledge.| +|`pledgeToken`|`address`|The token used for the pledge.| |`reward`|`bytes32`|The name of the reward.| |`pledgeAmount`|`uint256`|The amount pledged.| |`shippingFee`|`uint256`|| @@ -332,7 +331,7 @@ event Receipt( |`rewards`|`bytes32[]`|An array of reward names.| ### RewardsAdded -*Emitted when rewards are added to the campaign.* +Emitted when rewards are added to the campaign. ```solidity @@ -347,7 +346,7 @@ event RewardsAdded(bytes32[] rewardNames, Reward[] rewards); |`rewards`|`Reward[]`|The details of the rewards.| ### RewardRemoved -*Emitted when a reward is removed from the campaign.* +Emitted when a reward is removed from the campaign. ```solidity @@ -361,7 +360,7 @@ event RewardRemoved(bytes32 indexed rewardName); |`rewardName`|`bytes32`|The name of the reward.| ### RefundClaimed -*Emitted when a refund is claimed.* +Emitted when a refund is claimed. ```solidity @@ -378,7 +377,7 @@ event RefundClaimed(uint256 tokenId, uint256 refundAmount, address claimer); ## Errors ### AllOrNothingUnAuthorized -*Emitted when an unauthorized action is attempted.* +Emitted when an unauthorized action is attempted. ```solidity @@ -386,7 +385,7 @@ error AllOrNothingUnAuthorized(); ``` ### AllOrNothingInvalidInput -*Emitted when an invalid input is detected.* +Emitted when an invalid input is detected. ```solidity @@ -394,7 +393,7 @@ error AllOrNothingInvalidInput(); ``` ### AllOrNothingTransferFailed -*Emitted when a token transfer fails.* +Emitted when a token transfer fails. ```solidity @@ -402,7 +401,7 @@ error AllOrNothingTransferFailed(); ``` ### AllOrNothingNotSuccessful -*Emitted when the campaign is not successful.* +Emitted when the campaign is not successful. ```solidity @@ -410,7 +409,7 @@ error AllOrNothingNotSuccessful(); ``` ### AllOrNothingFeeNotDisbursed -*Emitted when fees are not disbursed.* +Emitted when fees are not disbursed. ```solidity @@ -418,7 +417,7 @@ error AllOrNothingFeeNotDisbursed(); ``` ### AllOrNothingFeeAlreadyDisbursed -*Emitted when `disburseFees` after fee is disbursed already.* +Emitted when `disburseFees` after fee is disbursed already. ```solidity @@ -426,15 +425,23 @@ error AllOrNothingFeeAlreadyDisbursed(); ``` ### AllOrNothingRewardExists -*Emitted when a `Reward` already exists for given input.* +Emitted when a `Reward` already exists for given input. ```solidity error AllOrNothingRewardExists(); ``` +### AllOrNothingTokenNotAccepted +Emitted when a token is not accepted for the campaign. + + +```solidity +error AllOrNothingTokenNotAccepted(address token); +``` + ### AllOrNothingNotClaimable -*Emitted when claiming an unclaimable refund.* +Emitted when claiming an unclaimable refund. ```solidity diff --git a/docs/src/src/treasuries/KeepWhatsRaised.sol/contract.KeepWhatsRaised.md b/docs/src/src/treasuries/KeepWhatsRaised.sol/contract.KeepWhatsRaised.md index 7d89ed06..6f13e93a 100644 --- a/docs/src/src/treasuries/KeepWhatsRaised.sol/contract.KeepWhatsRaised.md +++ b/docs/src/src/treasuries/KeepWhatsRaised.sol/contract.KeepWhatsRaised.md @@ -1,41 +1,1283 @@ # KeepWhatsRaised - -[Git Source](https://github.com/ccprotocol/campaign-utils-contracts-aggregator/blob/79d78188e565502f83e2c0309c9a4ea3b35cee91/src/treasuries/KeepWhatsRaised.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/treasuries/KeepWhatsRaised.sol) **Inherits:** -[AllOrNothing](/src/treasuries/AllOrNothing.sol/contract.AllOrNothing.md) +[IReward](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/IReward.sol/interface.IReward.md), [BaseTreasury](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/BaseTreasury.sol/abstract.BaseTreasury.md), [TimestampChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md), [ICampaignData](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ICampaignData.sol/interface.ICampaignData.md), ReentrancyGuard A contract that keeps all the funds raised, regardless of the success condition. -_This contract inherits from the `AllOrNothing` contract and overrides the `_checkSuccessCondition` function to always return true._ + +## State Variables +### s_tokenToPledgedAmount + +```solidity +mapping(uint256 => uint256) private s_tokenToPledgedAmount +``` + + +### s_tokenToTippedAmount + +```solidity +mapping(uint256 => uint256) private s_tokenToTippedAmount +``` + + +### s_tokenToPaymentFee + +```solidity +mapping(uint256 => uint256) private s_tokenToPaymentFee +``` + + +### s_reward + +```solidity +mapping(bytes32 => Reward) private s_reward +``` + + +### s_processedPledges +Tracks whether a pledge with a specific ID has already been processed + + +```solidity +mapping(bytes32 => bool) public s_processedPledges +``` + + +### s_paymentGatewayFees +Mapping to store payment gateway fees by unique pledge ID + + +```solidity +mapping(bytes32 => uint256) public s_paymentGatewayFees +``` + + +### s_feeValues +Mapping that stores fee values indexed by their corresponding fee keys. + + +```solidity +mapping(bytes32 => uint256) private s_feeValues +``` + + +### s_tokenIdToPledgeToken + +```solidity +mapping(uint256 => address) private s_tokenIdToPledgeToken +``` + + +### s_protocolFeePerToken + +```solidity +mapping(address => uint256) private s_protocolFeePerToken +``` + + +### s_platformFeePerToken + +```solidity +mapping(address => uint256) private s_platformFeePerToken +``` + + +### s_tipPerToken + +```solidity +mapping(address => uint256) private s_tipPerToken +``` + + +### s_availablePerToken + +```solidity +mapping(address => uint256) private s_availablePerToken +``` + + +### s_rewardCounter + +```solidity +Counters.Counter private s_rewardCounter +``` + + +### s_cancellationTime + +```solidity +uint256 private s_cancellationTime +``` + + +### s_isWithdrawalApproved + +```solidity +bool private s_isWithdrawalApproved +``` + + +### s_tipClaimed + +```solidity +bool private s_tipClaimed +``` + + +### s_fundClaimed + +```solidity +bool private s_fundClaimed +``` + + +### s_feeKeys + +```solidity +FeeKeys private s_feeKeys +``` + + +### s_config + +```solidity +Config private s_config +``` + + +### s_campaignData + +```solidity +CampaignData private s_campaignData +``` + ## Functions +### withdrawalEnabled + +Ensures that withdrawals are currently enabled. +Reverts with `KeepWhatsRaisedDisabled` if the withdrawal approval flag is not set. + + +```solidity +modifier withdrawalEnabled() ; +``` + +### onlyBeforeConfigLock + +Restricts execution to only occur before the configuration lock period. +Reverts with `KeepWhatsRaisedConfigLocked` if called too close to or after the campaign deadline. +The lock period is defined as the duration before the deadline during which configuration changes are not allowed. + + +```solidity +modifier onlyBeforeConfigLock() ; +``` + +### onlyPlatformAdminOrCampaignOwner + +Restricts access to only the platform admin or the campaign owner. + +Checks if `_msgSender()` is either the platform admin (via `INFO.getPlatformAdminAddress`) +or the campaign owner (via `INFO.owner()`). Reverts with `KeepWhatsRaisedUnAuthorized` if not authorized. + + +```solidity +modifier onlyPlatformAdminOrCampaignOwner() ; +``` ### constructor -_Initializes the KeepWhatsRaised contract._ +Constructor for the KeepWhatsRaised contract. + + +```solidity +constructor() ; +``` + +### initialize + ```solidity -constructor(bytes32 platformHash, address infoAddress) AllOrNothing(platformHash, infoAddress); +function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer; ``` +### getWithdrawalApprovalStatus + +Retrieves the withdrawal approval status. + + +```solidity +function getWithdrawalApprovalStatus() public view returns (bool); +``` + +### getReward + +Retrieves the details of a reward. + + +```solidity +function getReward(bytes32 rewardName) external view returns (Reward memory reward); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`rewardName`|`bytes32`|The name of the reward.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`reward`|`Reward`|The details of the reward as a `Reward` struct.| + + +### getRaisedAmount + +Retrieves the total raised amount in the treasury. + + +```solidity +function getRaisedAmount() external view override returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total raised amount as a uint256 value.| + + +### getLifetimeRaisedAmount + +Retrieves the lifetime raised amount in the treasury (never decreases with refunds). + + +```solidity +function getLifetimeRaisedAmount() external view override returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The lifetime raised amount as a uint256 value.| + + +### getRefundedAmount + +Retrieves the total refunded amount in the treasury. + + +```solidity +function getRefundedAmount() external view override returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total refunded amount as a uint256 value.| + + +### getAvailableRaisedAmount + +Retrieves the currently available raised amount in the treasury. + + +```solidity +function getAvailableRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The current available raised amount as a uint256 value.| + + +### getLaunchTime + +Retrieves the campaign's launch time. + + +```solidity +function getLaunchTime() public view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The timestamp when the campaign was launched.| + + +### getDeadline + +Retrieves the campaign's deadline. + + +```solidity +function getDeadline() public view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The timestamp when the campaign ends.| + + +### getGoalAmount + +Retrieves the campaign's funding goal amount. + + +```solidity +function getGoalAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The funding goal amount of the campaign.| + + +### getPaymentGatewayFee + +Retrieves the payment gateway fee for a given pledge ID. + + +```solidity +function getPaymentGatewayFee(bytes32 pledgeId) public view returns (uint256); +``` **Parameters** -| Name | Type | Description | -| -------------- | --------- | ------------------------------------------------------------ | -| `platformHash` | `bytes32` | The unique identifier of the platform. | -| `infoAddress` | `address` | The address of the associated campaign information contract. | +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The fixed gateway fee amount associated with the pledge ID.| + -### \_checkSuccessCondition +### getFeeValue + +Retrieves the fee value associated with a specific fee key from storage. -_Internal function to check the success condition for fee disbursement._ ```solidity -function _checkSuccessCondition() internal pure override returns (bool); +function getFeeValue(bytes32 feeKey) public view returns (uint256); ``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`feeKey`|`bytes32`|| **Returns** -| Name | Type | Description | -| -------- | ------ | ------------------------------------- | -| `` | `bool` | Whether the success condition is met. | +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|{uint256} The fee value corresponding to the provided fee key.| + + +### setPaymentGatewayFee + +Sets the fixed payment gateway fee for a specific pledge. + + +```solidity +function setPaymentGatewayFee(bytes32 pledgeId, uint256 fee) + public + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| +|`fee`|`uint256`|The gateway fee amount to be associated with the given pledge ID.| + + +### approveWithdrawal + +Approves the withdrawal of the treasury by the platform admin. + + +```solidity +function approveWithdrawal() + external + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` + +### configureTreasury + +Configures the treasury for a campaign by setting the system parameters, +campaign-specific data, and fee configuration keys. + + +```solidity +function configureTreasury( + Config memory config, + CampaignData memory campaignData, + FeeKeys memory feeKeys, + FeeValues memory feeValues +) + external + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`config`|`Config`|The configuration settings including withdrawal delay, refund delay, fee exemption threshold, and configuration lock period.| +|`campaignData`|`CampaignData`|The campaign-related metadata such as deadlines and funding goals.| +|`feeKeys`|`FeeKeys`|The set of keys used to reference applicable flat and percentage-based fees.| +|`feeValues`|`FeeValues`|The fee values corresponding to the fee keys.| + + +### updateDeadline + +Updates the campaign's deadline. + + +```solidity +function updateDeadline(uint256 deadline) + external + onlyPlatformAdminOrCampaignOwner + onlyBeforeConfigLock + whenNotPaused + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`deadline`|`uint256`|The new deadline timestamp for the campaign. Requirements: - Must be called before the configuration lock period (see `onlyBeforeConfigLock`). - The new deadline must be a future timestamp.| + + +### updateGoalAmount + +Updates the funding goal amount for the campaign. + + +```solidity +function updateGoalAmount(uint256 goalAmount) + external + onlyPlatformAdminOrCampaignOwner + onlyBeforeConfigLock + whenNotPaused + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`goalAmount`|`uint256`|The new goal amount. Requirements: - Must be called before the configuration lock period (see `onlyBeforeConfigLock`).| + + +### addRewards + +Adds multiple rewards in a batch. + +This function allows for both reward tiers and non-reward tiers. +For both types, rewards must have non-zero value. +If items are specified (non-empty arrays), the itemId, itemValue, and itemQuantity arrays must match in length. +Empty arrays are allowed for both reward tiers and non-reward tiers. + + +```solidity +function addRewards(bytes32[] calldata rewardNames, Reward[] calldata rewards) + external + onlyCampaignOwner + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`rewardNames`|`bytes32[]`|An array of reward names.| +|`rewards`|`Reward[]`|An array of `Reward` structs containing reward details.| + + +### removeReward + +Removes a reward from the campaign. + + +```solidity +function removeReward(bytes32 rewardName) + external + onlyCampaignOwner + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`rewardName`|`bytes32`|The name of the reward.| + + +### setFeeAndPledge + +Sets the payment gateway fee and executes a pledge in a single transaction. + + +```solidity +function setFeeAndPledge( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 pledgeAmount, + uint256 tip, + uint256 fee, + bytes32[] calldata reward, + bool isPledgeForAReward +) + external + nonReentrant + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| +|`backer`|`address`|The address of the backer making the pledge.| +|`pledgeToken`|`address`|| +|`pledgeAmount`|`uint256`|The amount of the pledge.| +|`tip`|`uint256`|An optional tip can be added during the process.| +|`fee`|`uint256`|The payment gateway fee to associate with this pledge.| +|`reward`|`bytes32[]`|An array of reward names.| +|`isPledgeForAReward`|`bool`|A boolean indicating whether this pledge is for a reward or without..| + + +### pledgeForAReward + +Allows a backer to pledge for a reward. + +The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. +The non-reward tiers cannot be pledged for without a reward. + + +```solidity +function pledgeForAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 tip, + bytes32[] calldata reward +) + public + nonReentrant + currentTimeIsWithinRange(getLaunchTime(), getDeadline()) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| +|`backer`|`address`|The address of the backer making the pledge.| +|`pledgeToken`|`address`|The token to use for the pledge.| +|`tip`|`uint256`|An optional tip can be added during the process.| +|`reward`|`bytes32[]`|An array of reward names.| + + +### _pledgeForAReward + +Internal function that allows a backer to pledge for a reward with tokens transferred from a specified source. + +The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. +The non-reward tiers cannot be pledged for without a reward. +This function is called internally by both public pledgeForAReward (with backer as token source) and +setFeeAndPledge (with admin as token source). + + +```solidity +function _pledgeForAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 tip, + bytes32[] calldata reward, + address tokenSource +) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| +|`backer`|`address`|The address of the backer making the pledge (receives the NFT).| +|`pledgeToken`|`address`|The token to use for the pledge.| +|`tip`|`uint256`|An optional tip can be added during the process.| +|`reward`|`bytes32[]`|An array of reward names.| +|`tokenSource`|`address`|The address from which tokens will be transferred (either backer for direct calls or admin for setFeeAndPledge calls).| + + +### pledgeWithoutAReward + +Allows a backer to pledge without selecting a reward. + + +```solidity +function pledgeWithoutAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 pledgeAmount, + uint256 tip +) + public + nonReentrant + currentTimeIsWithinRange(getLaunchTime(), getDeadline()) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| +|`backer`|`address`|The address of the backer making the pledge.| +|`pledgeToken`|`address`|The token to use for the pledge.| +|`pledgeAmount`|`uint256`|The amount of the pledge.| +|`tip`|`uint256`|An optional tip can be added during the process.| + + +### _pledgeWithoutAReward + +Internal function that allows a backer to pledge without selecting a reward with tokens transferred from a specified source. + +This function is called internally by both public pledgeWithoutAReward (with backer as token source) and +setFeeAndPledge (with admin as token source). + + +```solidity +function _pledgeWithoutAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 pledgeAmount, + uint256 tip, + address tokenSource +) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| +|`backer`|`address`|The address of the backer making the pledge (receives the NFT).| +|`pledgeToken`|`address`|The token to use for the pledge.| +|`pledgeAmount`|`uint256`|The amount of the pledge.| +|`tip`|`uint256`|An optional tip can be added during the process.| +|`tokenSource`|`address`|The address from which tokens will be transferred (either backer for direct calls or admin for setFeeAndPledge calls).| + + +### withdraw + +Withdraws funds from the treasury. + + +```solidity +function withdraw() public view override whenNotPaused whenNotCancelled; +``` + +### withdraw + +Allows the campaign owner or platform admin to withdraw funds, applying required fees and taxes. + + +```solidity +function withdraw(address token, uint256 amount) + public + onlyPlatformAdminOrCampaignOwner + currentTimeIsLess(getDeadline() + s_config.withdrawalDelay) + whenNotPaused + whenNotCancelled + withdrawalEnabled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token to withdraw.| +|`amount`|`uint256`|The withdrawal amount (ignored for final withdrawals). Requirements: - Caller must be authorized. - Withdrawals must be enabled, not paused, and within the allowed time. - Token must be accepted for the campaign. - For partial withdrawals: - `amount` > 0 and `amount + fees` ≤ available balance. - For final withdrawals: - Available balance > 0 and fees ≤ available balance. Effects: - Deducts fees (flat, cumulative, and Colombian tax if applicable). - Updates available balance per token. - Transfers net funds to the recipient. Reverts: - If insufficient funds or invalid input. Emits: - `WithdrawalWithFeeSuccessful`.| + + +### claimRefund + +Allows a backer to claim a refund associated with a specific pledge (token ID). + + +```solidity +function claimRefund(uint256 tokenId) + external + currentTimeIsGreater(getLaunchTime()) + whenCampaignNotPaused + whenNotPaused; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The ID of the token representing the backer's pledge. Requirements: - Refund delay must have passed. - The token must be eligible for a refund and not previously claimed.| + + +### disburseFees + +Disburses all accumulated fees to the appropriate fee collector or treasury. +Requirements: +- Only callable when fees are available. + + +```solidity +function disburseFees() public override whenNotPaused whenNotCancelled; +``` + +### claimTip + +Allows an authorized claimer to collect tips contributed during the campaign. +Requirements: +- Caller must be authorized to claim tips. +- Tip amount must be non-zero. + + +```solidity +function claimTip() external onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenNotPaused; +``` + +### claimFund + +Allows the platform admin to claim the remaining funds from a campaign. +Requirements: +- Claim period must have started and funds must be available. +- Cannot be previously claimed. + + +```solidity +function claimFund() external onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenNotPaused; +``` + +### cancelTreasury + +This function is overridden to allow the platform admin and the campaign owner to cancel a treasury. + + +```solidity +function cancelTreasury(bytes32 message) public override onlyPlatformAdminOrCampaignOwner; +``` + +### _checkSuccessCondition + +Internal function to check the success condition for fee disbursement. + + +```solidity +function _checkSuccessCondition() internal view virtual override returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|Whether the success condition is met.| + + +### _pledge + + +```solidity +function _pledge( + bytes32 pledgeId, + address backer, + address pledgeToken, + bytes32 reward, + uint256 pledgeAmount, + uint256 tip, + bytes32[] memory rewards, + address tokenSource +) private; +``` + +### _calculateNetAvailable + +Calculates the net amount available from a pledge after deducting +all applicable fees. + +The function performs the following: +- Applies all configured gross percentage-based fees +- Applies payment gateway fee for the given pledge +- Applies protocol fee based on protocol configuration +- Accumulates total platform and protocol fees per token +- Records the total deducted fee for the token + + +```solidity +function _calculateNetAvailable(bytes32 pledgeId, address pledgeToken, uint256 tokenId, uint256 pledgeAmount) + internal + returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge| +|`pledgeToken`|`address`|The token used for the pledge| +|`tokenId`|`uint256`|The token ID representing the pledge| +|`pledgeAmount`|`uint256`|The original pledged amount before deductions| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The net available amount after all fees are deducted| + + +### _checkRefundPeriodStatus + +Refund period logic: +- If campaign is cancelled: refund period is active until s_cancellationTime + s_config.refundDelay +- If campaign is not cancelled: refund period is active until deadline + s_config.refundDelay +- Before deadline (non-cancelled): not in refund period + +Checks the refund period status based on campaign state + +This function handles both cancelled and non-cancelled campaign scenarios + + +```solidity +function _checkRefundPeriodStatus(bool checkIfOver) internal view returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`checkIfOver`|`bool`|If true, returns whether refund period is over; if false, returns whether currently within refund period| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|bool Status based on checkIfOver parameter| + + +## Events +### Receipt +Emitted when a backer makes a pledge. + + +```solidity +event Receipt( + address indexed backer, + address indexed pledgeToken, + bytes32 reward, + uint256 pledgeAmount, + uint256 tip, + uint256 tokenId, + bytes32[] rewards +); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`backer`|`address`|The address of the backer making the pledge.| +|`pledgeToken`|`address`|The token used for the pledge.| +|`reward`|`bytes32`|The name of the reward.| +|`pledgeAmount`|`uint256`|The amount pledged.| +|`tip`|`uint256`|An optional tip can be added during the process.| +|`tokenId`|`uint256`|The ID of the token representing the pledge.| +|`rewards`|`bytes32[]`|An array of reward names.| + +### RewardsAdded +Emitted when rewards are added to the campaign. + + +```solidity +event RewardsAdded(bytes32[] rewardNames, Reward[] rewards); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`rewardNames`|`bytes32[]`|The names of the rewards.| +|`rewards`|`Reward[]`|The details of the rewards.| + +### RewardRemoved +Emitted when a reward is removed from the campaign. + + +```solidity +event RewardRemoved(bytes32 indexed rewardName); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`rewardName`|`bytes32`|The name of the reward.| + +### WithdrawalApproved +Emitted when withdrawal functionality has been approved by the platform admin. + + +```solidity +event WithdrawalApproved(); +``` + +### TreasuryConfigured +Emitted when the treasury configuration is updated. + + +```solidity +event TreasuryConfigured(Config config, CampaignData campaignData, FeeKeys feeKeys, FeeValues feeValues); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`config`|`Config`|The updated configuration parameters (e.g., delays, exemptions).| +|`campaignData`|`CampaignData`|The campaign-related data associated with the treasury setup.| +|`feeKeys`|`FeeKeys`|The set of keys used to determine applicable fees.| +|`feeValues`|`FeeValues`|The fee values corresponding to the fee keys.| + +### WithdrawalWithFeeSuccessful +Emitted when a withdrawal is successfully processed along with the applied fee. + + +```solidity +event WithdrawalWithFeeSuccessful(address indexed to, uint256 amount, uint256 fee); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`to`|`address`|The recipient address receiving the funds.| +|`amount`|`uint256`|The total amount withdrawn (excluding fee).| +|`fee`|`uint256`|The fee amount deducted from the withdrawal.| + +### TipClaimed +Emitted when a tip is claimed from the contract. + + +```solidity +event TipClaimed(uint256 amount, address indexed claimer); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`amount`|`uint256`|The amount of tip claimed.| +|`claimer`|`address`|The address that claimed the tip.| + +### FundClaimed +Emitted when campaign or user's remaining funds are successfully claimed by the platform admin. + + +```solidity +event FundClaimed(uint256 amount, address indexed claimer); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`amount`|`uint256`|The amount of funds claimed.| +|`claimer`|`address`|The address that claimed the funds.| + +### RefundClaimed +Emitted when a refund is claimed. + + +```solidity +event RefundClaimed(uint256 indexed tokenId, uint256 refundAmount, address indexed claimer); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The ID of the token representing the pledge.| +|`refundAmount`|`uint256`|The refund amount claimed.| +|`claimer`|`address`|The address of the claimer.| + +### KeepWhatsRaisedDeadlineUpdated +Emitted when the deadline of the campaign is updated. + + +```solidity +event KeepWhatsRaisedDeadlineUpdated(uint256 newDeadline); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newDeadline`|`uint256`|The new deadline.| + +### KeepWhatsRaisedGoalAmountUpdated +Emitted when the goal amount for a campaign is updated. + + +```solidity +event KeepWhatsRaisedGoalAmountUpdated(uint256 newGoalAmount); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newGoalAmount`|`uint256`|The new goal amount set for the campaign.| + +### KeepWhatsRaisedPaymentGatewayFeeSet +Emitted when a gateway fee is set for a specific pledge. + + +```solidity +event KeepWhatsRaisedPaymentGatewayFeeSet(bytes32 indexed pledgeId, uint256 fee); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge.| +|`fee`|`uint256`|The amount of the payment gateway fee set.| + +## Errors +### KeepWhatsRaisedUnAuthorized +Emitted when an unauthorized action is attempted. + + +```solidity +error KeepWhatsRaisedUnAuthorized(); +``` + +### KeepWhatsRaisedInvalidInput +Emitted when an invalid input is detected. + + +```solidity +error KeepWhatsRaisedInvalidInput(); +``` + +### KeepWhatsRaisedTokenNotAccepted +Emitted when a token is not accepted for the campaign. + + +```solidity +error KeepWhatsRaisedTokenNotAccepted(address token); +``` + +### KeepWhatsRaisedRewardExists +Emitted when a `Reward` already exists for given input. + + +```solidity +error KeepWhatsRaisedRewardExists(); +``` + +### KeepWhatsRaisedDisabled +Emitted when anyone called a disabled function. + + +```solidity +error KeepWhatsRaisedDisabled(); +``` + +### KeepWhatsRaisedAlreadyEnabled +Emitted when any functionality is already enabled and cannot be re-enabled. + + +```solidity +error KeepWhatsRaisedAlreadyEnabled(); +``` + +### KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee +Emitted when a withdrawal attempt exceeds the available funds after accounting for the fee. + + +```solidity +error KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee( + uint256 availableAmount, uint256 withdrawalAmount, uint256 fee +); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`availableAmount`|`uint256`|The maximum amount that can be withdrawn.| +|`withdrawalAmount`|`uint256`|The attempted withdrawal amount.| +|`fee`|`uint256`|The fee that would be applied to the withdrawal.| + +### KeepWhatsRaisedInsufficientFundsForFee +Emitted when the fee exceeds the requested withdrawal amount. + + +```solidity +error KeepWhatsRaisedInsufficientFundsForFee(uint256 withdrawalAmount, uint256 fee); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`withdrawalAmount`|`uint256`|The amount requested for withdrawal.| +|`fee`|`uint256`|The calculated fee, which is greater than the withdrawal amount.| + +### KeepWhatsRaisedAlreadyWithdrawn +Emitted when a withdrawal has already been made and cannot be repeated. + + +```solidity +error KeepWhatsRaisedAlreadyWithdrawn(); +``` + +### KeepWhatsRaisedAlreadyClaimed +Emitted when funds or rewards have already been claimed for the given context. + + +```solidity +error KeepWhatsRaisedAlreadyClaimed(); +``` + +### KeepWhatsRaisedNotClaimable +Emitted when a token or pledge is not eligible for claiming (e.g., claim period not reached or not valid). + + +```solidity +error KeepWhatsRaisedNotClaimable(uint256 tokenId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The ID of the token that was attempted to be claimed.| + +### KeepWhatsRaisedNotClaimableAdmin +Emitted when an admin attempts to claim funds that are not yet claimable according to the rules. + + +```solidity +error KeepWhatsRaisedNotClaimableAdmin(); +``` + +### KeepWhatsRaisedConfigLocked +Emitted when a configuration change is attempted during the lock period. + + +```solidity +error KeepWhatsRaisedConfigLocked(); +``` + +### KeepWhatsRaisedDisbursementBlocked +Emitted when a disbursement is attempted before the refund period has ended. + + +```solidity +error KeepWhatsRaisedDisbursementBlocked(); +``` + +### KeepWhatsRaisedPledgeAlreadyProcessed +Emitted when a pledge is submitted using a pledgeId that has already been processed. + + +```solidity +error KeepWhatsRaisedPledgeAlreadyProcessed(bytes32 pledgeId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`pledgeId`|`bytes32`|The unique identifier of the pledge that was already used.| + +## Structs +### FeeKeys +Represents keys used to reference different fee configurations. +These keys are typically used to look up fee values stored in `s_platformData`. + + +```solidity +struct FeeKeys { + /// @dev Key for a flat fee applied to an operation. + bytes32 flatFeeKey; + /// @dev Key for a cumulative flat fee, potentially across multiple actions. + bytes32 cumulativeFlatFeeKey; + /// @dev Keys for gross percentage-based fees (calculated before deductions). + bytes32[] grossPercentageFeeKeys; +} +``` + +### FeeValues +Represents the complete fee structure values for treasury operations. +These values correspond to the fees that will be applied to transactions +and are typically retrieved using keys from `FeeKeys` struct. + + +```solidity +struct FeeValues { + /// @dev Value for a flat fee applied to an operation. + uint256 flatFeeValue; + /// @dev Value for a cumulative flat fee, potentially across multiple actions. + uint256 cumulativeFlatFeeValue; + /// @dev Values for gross percentage-based fees (calculated before deductions). + uint256[] grossPercentageFeeValues; +} +``` + +### Config +System configuration parameters related to withdrawal and refund behavior. + + +```solidity +struct Config { + /// @dev The minimum withdrawal amount required to qualify for fee exemption. + uint256 minimumWithdrawalForFeeExemption; + /// @dev Time delay (in timestamp) enforced before a withdrawal can be completed. + uint256 withdrawalDelay; + /// @dev Time delay (in timestamp) before a refund becomes claimable or processed. + uint256 refundDelay; + /// @dev Duration (in timestamp) for which config changes are locked to prevent immediate updates. + uint256 configLockPeriod; + /// @dev True if the creator is Colombian, false otherwise. + bool isColombianCreator; +} +``` + diff --git a/docs/src/src/treasuries/PaymentTreasury.sol/contract.PaymentTreasury.md b/docs/src/src/treasuries/PaymentTreasury.sol/contract.PaymentTreasury.md new file mode 100644 index 00000000..da91f77a --- /dev/null +++ b/docs/src/src/treasuries/PaymentTreasury.sol/contract.PaymentTreasury.md @@ -0,0 +1,278 @@ +# PaymentTreasury +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/treasuries/PaymentTreasury.sol) + +**Inherits:** +[BasePaymentTreasury](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/BasePaymentTreasury.sol/abstract.BasePaymentTreasury.md) + + +## Functions +### constructor + +Constructor for the PaymentTreasury contract. + + +```solidity +constructor() ; +``` + +### initialize + + +```solidity +function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer; +``` + +### createPayment + +Creates a new payment entry with the specified details. + + +```solidity +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; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|A unique identifier for the payment.| +|`buyerId`|`bytes32`|The id of the buyer initiating the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`expiration`|`uint256`|The timestamp after which the payment expires.| +|`lineItems`|`ICampaignPaymentTreasury.LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ICampaignPaymentTreasury.ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### createPaymentBatch + +Creates multiple payment entries in a single transaction to prevent nonce conflicts. + + +```solidity +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; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique identifiers for the payments.| +|`buyerIds`|`bytes32[]`|An array of buyer IDs corresponding to each payment.| +|`itemIds`|`bytes32[]`|An array of item identifiers corresponding to each payment.| +|`paymentTokens`|`address[]`|An array of tokens corresponding to each payment.| +|`amounts`|`uint256[]`|An array of amounts corresponding to each payment.| +|`expirations`|`uint256[]`|An array of expiration timestamps corresponding to each payment.| +|`lineItemsArray`|`ICampaignPaymentTreasury.LineItem[][]`|An array of line item arrays, one for each payment.| +|`externalFeesArray`|`ICampaignPaymentTreasury.ExternalFees[][]`|An array of external fee metadata arrays, one for each payment (informational only).| + + +### processCryptoPayment + +Allows a buyer to make a direct crypto payment for an item. + +This function transfers tokens directly from the buyer's wallet and confirms the payment immediately. + + +```solidity +function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees +) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`buyerAddress`|`address`|The address of the buyer making the payment.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`lineItems`|`ICampaignPaymentTreasury.LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ICampaignPaymentTreasury.ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### cancelPayment + +Cancels an existing payment with the given payment ID. + + +```solidity +function cancelPayment(bytes32 paymentId) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to cancel.| + + +### confirmPayment + +Confirms and finalizes the payment associated with the given payment ID. + + +```solidity +function confirmPayment(bytes32 paymentId, address buyerAddress) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to confirm.| +|`buyerAddress`|`address`|Optional buyer address to mint NFT to. Pass address(0) to skip NFT minting.| + + +### confirmPaymentBatch + +Confirms and finalizes multiple payments in a single transaction. + + +```solidity +function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) + public + override + whenNotPaused + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique payment identifiers to be confirmed.| +|`buyerAddresses`|`address[]`|Array of buyer addresses to mint NFTs to. Must match paymentIds length. Pass address(0) to skip NFT minting for specific payments.| + + +### claimRefund + +Claims a refund for non-NFT payments (payments without minted NFTs). + +Only callable by platform admin. Used for payments confirmed without a buyer address. + + +```solidity +function claimRefund(bytes32 paymentId, address refundAddress) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must NOT have an NFT).| +|`refundAddress`|`address`|The address where the refunded amount should be sent.| + + +### claimRefund + +Claims a refund for non-NFT payments (payments without minted NFTs). + +Only callable by platform admin. Used for payments confirmed without a buyer address. + + +```solidity +function claimRefund(bytes32 paymentId) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must NOT have an NFT).| + + +### claimExpiredFunds + +Allows platform admin to claim all remaining funds once the claim window has opened. + + +```solidity +function claimExpiredFunds() public override whenNotPaused; +``` + +### disburseFees + +Disburses fees collected by the treasury. + + +```solidity +function disburseFees() public override whenNotPaused; +``` + +### claimNonGoalLineItems + +Allows platform admin to claim non-goal line items that are available for claiming. + + +```solidity +function claimNonGoalLineItems(address token) public override whenNotPaused; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address to claim.| + + +### withdraw + +Withdraws funds from the treasury. + + +```solidity +function withdraw() public override whenNotPaused whenNotCancelled; +``` + +### cancelTreasury + +This function is overridden to allow the platform admin and the campaign owner to cancel a treasury. + + +```solidity +function cancelTreasury(bytes32 message) public override; +``` + +### _checkSuccessCondition + +Internal function to check the success condition for fee disbursement. + + +```solidity +function _checkSuccessCondition() internal view virtual override returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|Whether the success condition is met.| + + +## Errors +### PaymentTreasuryUnAuthorized +Emitted when an unauthorized action is attempted. + + +```solidity +error PaymentTreasuryUnAuthorized(); +``` + diff --git a/docs/src/src/treasuries/README.md b/docs/src/src/treasuries/README.md index c01d8d76..6babb651 100644 --- a/docs/src/src/treasuries/README.md +++ b/docs/src/src/treasuries/README.md @@ -2,3 +2,6 @@ # Contents - [AllOrNothing](AllOrNothing.sol/contract.AllOrNothing.md) +- [KeepWhatsRaised](KeepWhatsRaised.sol/contract.KeepWhatsRaised.md) +- [PaymentTreasury](PaymentTreasury.sol/contract.PaymentTreasury.md) +- [TimeConstrainedPaymentTreasury](TimeConstrainedPaymentTreasury.sol/contract.TimeConstrainedPaymentTreasury.md) diff --git a/docs/src/src/treasuries/TimeConstrainedPaymentTreasury.sol/contract.TimeConstrainedPaymentTreasury.md b/docs/src/src/treasuries/TimeConstrainedPaymentTreasury.sol/contract.TimeConstrainedPaymentTreasury.md new file mode 100644 index 00000000..ce4fb893 --- /dev/null +++ b/docs/src/src/treasuries/TimeConstrainedPaymentTreasury.sol/contract.TimeConstrainedPaymentTreasury.md @@ -0,0 +1,296 @@ +# TimeConstrainedPaymentTreasury +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/treasuries/TimeConstrainedPaymentTreasury.sol) + +**Inherits:** +[BasePaymentTreasury](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/BasePaymentTreasury.sol/abstract.BasePaymentTreasury.md), [TimestampChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md) + + +## Functions +### constructor + +Constructor for the TimeConstrainedPaymentTreasury contract. + + +```solidity +constructor() ; +``` + +### initialize + + +```solidity +function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer; +``` + +### _checkTimeWithinRange + +Internal function to check if current time is within the allowed range. + + +```solidity +function _checkTimeWithinRange() internal view; +``` + +### _checkTimeIsGreater + +Internal function to check if current time is greater than launch time. + + +```solidity +function _checkTimeIsGreater() internal view; +``` + +### createPayment + +Creates a new payment entry with the specified details. + + +```solidity +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; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|A unique identifier for the payment.| +|`buyerId`|`bytes32`|The id of the buyer initiating the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`expiration`|`uint256`|The timestamp after which the payment expires.| +|`lineItems`|`ICampaignPaymentTreasury.LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ICampaignPaymentTreasury.ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### createPaymentBatch + +Creates multiple payment entries in a single transaction to prevent nonce conflicts. + + +```solidity +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; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique identifiers for the payments.| +|`buyerIds`|`bytes32[]`|An array of buyer IDs corresponding to each payment.| +|`itemIds`|`bytes32[]`|An array of item identifiers corresponding to each payment.| +|`paymentTokens`|`address[]`|An array of tokens corresponding to each payment.| +|`amounts`|`uint256[]`|An array of amounts corresponding to each payment.| +|`expirations`|`uint256[]`|An array of expiration timestamps corresponding to each payment.| +|`lineItemsArray`|`ICampaignPaymentTreasury.LineItem[][]`|An array of line item arrays, one for each payment.| +|`externalFeesArray`|`ICampaignPaymentTreasury.ExternalFees[][]`|An array of external fee metadata arrays, one for each payment (informational only).| + + +### processCryptoPayment + +Allows a buyer to make a direct crypto payment for an item. + +This function transfers tokens directly from the buyer's wallet and confirms the payment immediately. + + +```solidity +function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees +) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`buyerAddress`|`address`|The address of the buyer making the payment.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`lineItems`|`ICampaignPaymentTreasury.LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ICampaignPaymentTreasury.ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### cancelPayment + +Cancels an existing payment with the given payment ID. + + +```solidity +function cancelPayment(bytes32 paymentId) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to cancel.| + + +### confirmPayment + +Confirms and finalizes the payment associated with the given payment ID. + + +```solidity +function confirmPayment(bytes32 paymentId, address buyerAddress) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to confirm.| +|`buyerAddress`|`address`|Optional buyer address to mint NFT to. Pass address(0) to skip NFT minting.| + + +### confirmPaymentBatch + +Confirms and finalizes multiple payments in a single transaction. + + +```solidity +function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) + public + override + whenNotPaused + whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique payment identifiers to be confirmed.| +|`buyerAddresses`|`address[]`|Array of buyer addresses to mint NFTs to. Must match paymentIds length. Pass address(0) to skip NFT minting for specific payments.| + + +### claimRefund + +Claims a refund for non-NFT payments (payments without minted NFTs). + +Only callable by platform admin. Used for payments confirmed without a buyer address. + + +```solidity +function claimRefund(bytes32 paymentId, address refundAddress) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must NOT have an NFT).| +|`refundAddress`|`address`|The address where the refunded amount should be sent.| + + +### claimRefund + +Claims a refund for non-NFT payments (payments without minted NFTs). + +Only callable by platform admin. Used for payments confirmed without a buyer address. + + +```solidity +function claimRefund(bytes32 paymentId) public override whenNotPaused whenNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must NOT have an NFT).| + + +### claimExpiredFunds + +Allows platform admin to claim all remaining funds once the claim window has opened. + + +```solidity +function claimExpiredFunds() public override whenNotPaused; +``` + +### disburseFees + +Disburses fees collected by the treasury. + + +```solidity +function disburseFees() public override whenNotPaused; +``` + +### claimNonGoalLineItems + +Allows platform admin to claim non-goal line items that are available for claiming. + + +```solidity +function claimNonGoalLineItems(address token) public override whenNotPaused; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address to claim.| + + +### withdraw + +Withdraws funds from the treasury. + + +```solidity +function withdraw() public override whenNotPaused whenNotCancelled; +``` + +### cancelTreasury + +This function is overridden to allow the platform admin and the campaign owner to cancel a treasury. + + +```solidity +function cancelTreasury(bytes32 message) public override; +``` + +### _checkSuccessCondition + +Internal function to check the success condition for fee disbursement. + + +```solidity +function _checkSuccessCondition() internal view virtual override returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|Whether the success condition is met.| + + +## Errors +### TimeConstrainedPaymentTreasuryUnAuthorized +Emitted when an unauthorized action is attempted. + + +```solidity +error TimeConstrainedPaymentTreasuryUnAuthorized(); +``` + diff --git a/docs/src/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md b/docs/src/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md index b8065c6a..886f15d1 100644 --- a/docs/src/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md +++ b/docs/src/src/utils/AdminAccessChecker.sol/abstract.AdminAccessChecker.md @@ -1,44 +1,58 @@ # AdminAccessChecker -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/AdminAccessChecker.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/AdminAccessChecker.sol) -*This abstract contract provides access control mechanisms to restrict the execution of specific functions -to authorized protocol administrators and platform administrators.* +**Inherits:** +Context +This abstract contract provides access control mechanisms to restrict the execution of specific functions +to authorized protocol administrators and platform administrators. -## State Variables -### GLOBAL_PARAMS - -```solidity -IGlobalParams internal GLOBAL_PARAMS; -``` +Updated to use ERC-7201 namespaced storage for upgradeable contracts ## Functions ### __AccessChecker_init +Internal initializer function for AdminAccessChecker + ```solidity function __AccessChecker_init(IGlobalParams globalParams) internal; ``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`globalParams`|`IGlobalParams`|The IGlobalParams contract instance| + + +### _getGlobalParams + +Returns the stored GLOBAL_PARAMS for internal use + + +```solidity +function _getGlobalParams() internal view returns (IGlobalParams); +``` ### onlyProtocolAdmin -*Modifier that restricts function access to protocol administrators only. -Users attempting to execute functions with this modifier must be the protocol admin.* +Modifier that restricts function access to protocol administrators only. +Users attempting to execute functions with this modifier must be the protocol admin. ```solidity -modifier onlyProtocolAdmin(); +modifier onlyProtocolAdmin() ; ``` ### onlyPlatformAdmin -*Modifier that restricts function access to platform administrators of a specific platform. -Users attempting to execute functions with this modifier must be the platform admin for the given platform.* +Modifier that restricts function access to platform administrators of a specific platform. +Users attempting to execute functions with this modifier must be the platform admin for the given platform. ```solidity -modifier onlyPlatformAdmin(bytes32 platformHash); +modifier onlyPlatformAdmin(bytes32 platformHash) ; ``` **Parameters** @@ -49,8 +63,8 @@ modifier onlyPlatformAdmin(bytes32 platformHash); ### _onlyProtocolAdmin -*Internal function to check if the sender is the protocol administrator. -If the sender is not the protocol admin, it reverts with AdminAccessCheckerUnauthorized error.* +Internal function to check if the sender is the protocol administrator. +If the sender is not the protocol admin, it reverts with AdminAccessCheckerUnauthorized error. ```solidity @@ -59,8 +73,8 @@ function _onlyProtocolAdmin() private view; ### _onlyPlatformAdmin -*Internal function to check if the sender is the platform administrator for a specific platform. -If the sender is not the platform admin, it reverts with AdminAccessCheckerUnauthorized error.* +Internal function to check if the sender is the platform administrator for a specific platform. +If the sender is not the platform admin, it reverts with AdminAccessCheckerUnauthorized error. ```solidity @@ -75,7 +89,7 @@ function _onlyPlatformAdmin(bytes32 platformHash) private view; ## Errors ### AdminAccessCheckerUnauthorized -*Throws when the caller is not authorized.* +Throws when the caller is not authorized. ```solidity diff --git a/docs/src/src/utils/BasePaymentTreasury.sol/abstract.BasePaymentTreasury.md b/docs/src/src/utils/BasePaymentTreasury.sol/abstract.BasePaymentTreasury.md new file mode 100644 index 00000000..538cb349 --- /dev/null +++ b/docs/src/src/utils/BasePaymentTreasury.sol/abstract.BasePaymentTreasury.md @@ -0,0 +1,1281 @@ +# BasePaymentTreasury +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/BasePaymentTreasury.sol) + +**Inherits:** +Initializable, [ICampaignPaymentTreasury](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ICampaignPaymentTreasury.sol/interface.ICampaignPaymentTreasury.md), [CampaignAccessChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md), [PausableCancellable](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md), ReentrancyGuard + +Base contract for payment treasury implementations. + +Supports ERC-2771 meta-transactions via adapter contracts for platform admin operations. + + +## State Variables +### ZERO_BYTES + +```solidity +bytes32 internal constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000 +``` + + +### PERCENT_DIVIDER + +```solidity +uint256 internal constant PERCENT_DIVIDER = 10000 +``` + + +### STANDARD_DECIMALS + +```solidity +uint256 internal constant STANDARD_DECIMALS = 18 +``` + + +### ZERO_ADDRESS + +```solidity +address internal constant ZERO_ADDRESS = address(0) +``` + + +### PLATFORM_HASH + +```solidity +bytes32 internal PLATFORM_HASH +``` + + +### PLATFORM_FEE_PERCENT + +```solidity +uint256 internal PLATFORM_FEE_PERCENT +``` + + +### s_paymentIdToToken + +```solidity +mapping(bytes32 => address) internal s_paymentIdToToken +``` + + +### s_platformFeePerToken + +```solidity +mapping(address => uint256) internal s_platformFeePerToken +``` + + +### s_protocolFeePerToken + +```solidity +mapping(address => uint256) internal s_protocolFeePerToken +``` + + +### s_paymentIdToTokenId + +```solidity +mapping(bytes32 => uint256) internal s_paymentIdToTokenId +``` + + +### s_paymentIdToCreator + +```solidity +mapping(bytes32 => address) internal s_paymentIdToCreator +``` + + +### s_payment + +```solidity +mapping(bytes32 => PaymentInfo) internal s_payment +``` + + +### s_paymentLineItems + +```solidity +mapping(bytes32 => ICampaignPaymentTreasury.PaymentLineItem[]) internal s_paymentLineItems +``` + + +### s_paymentExternalFeeMetadata + +```solidity +mapping(bytes32 => ICampaignPaymentTreasury.ExternalFees[]) internal s_paymentExternalFeeMetadata +``` + + +### s_pendingPaymentPerToken + +```solidity +mapping(address => uint256) internal s_pendingPaymentPerToken +``` + + +### s_confirmedPaymentPerToken + +```solidity +mapping(address => uint256) internal s_confirmedPaymentPerToken +``` + + +### s_lifetimeConfirmedPaymentPerToken + +```solidity +mapping(address => uint256) internal s_lifetimeConfirmedPaymentPerToken +``` + + +### s_availableConfirmedPerToken + +```solidity +mapping(address => uint256) internal s_availableConfirmedPerToken +``` + + +### s_nonGoalLineItemPendingPerToken + +```solidity +mapping(address => uint256) internal s_nonGoalLineItemPendingPerToken +``` + + +### s_nonGoalLineItemConfirmedPerToken + +```solidity +mapping(address => uint256) internal s_nonGoalLineItemConfirmedPerToken +``` + + +### s_nonGoalLineItemClaimablePerToken + +```solidity +mapping(address => uint256) internal s_nonGoalLineItemClaimablePerToken +``` + + +### s_refundableNonGoalLineItemPerToken + +```solidity +mapping(address => uint256) internal s_refundableNonGoalLineItemPerToken +``` + + +## Functions +### _scopePaymentIdForOffChain + +Scopes a payment ID for off-chain payments (createPayment/createPaymentBatch). + + +```solidity +function _scopePaymentIdForOffChain(bytes32 paymentId) internal pure returns (bytes32); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The external payment ID.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bytes32`|The scoped internal payment ID.| + + +### _scopePaymentIdForOnChain + +Scopes a payment ID for on-chain crypto payments (processCryptoPayment). + + +```solidity +function _scopePaymentIdForOnChain(bytes32 paymentId) internal view returns (bytes32); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The external payment ID.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bytes32`|The scoped internal payment ID.| + + +### _findPaymentId + +Tries to find a payment by checking both off-chain and on-chain scopes. +- Off-chain payments (createPayment) can be looked up by anyone (scoped with address(0)) +- On-chain payments (processCryptoPayment) can be looked up by anyone using the stored creator address + + +```solidity +function _findPaymentId(bytes32 paymentId) internal view returns (bytes32 internalPaymentId); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The external payment ID.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`internalPaymentId`|`bytes32`|The scoped internal payment ID if found, or ZERO_BYTES if not found.| + + +### _getMaxExpirationDuration + +Retrieves the max expiration duration configured for the current platform or globally. + + +```solidity +function _getMaxExpirationDuration() internal view returns (bool hasLimit, uint256 duration); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`hasLimit`|`bool`|Indicates whether a max expiration duration is configured.| +|`duration`|`uint256`|The max expiration duration in seconds.| + + +### __BaseContract_init + + +```solidity +function __BaseContract_init(bytes32 platformHash, address infoAddress, address trustedForwarder_) internal; +``` + +### _msgSender + +Override _msgSender to support ERC-2771 meta-transactions. +When called by the trusted forwarder (adapter), extracts the actual sender from calldata. + + +```solidity +function _msgSender() internal view virtual override returns (address sender); +``` + +### whenCampaignNotPaused + +Modifier that checks if the campaign is not paused. + + +```solidity +modifier whenCampaignNotPaused() ; +``` + +### whenCampaignNotCancelled + + +```solidity +modifier whenCampaignNotCancelled() ; +``` + +### onlyPlatformAdminOrCampaignOwner + +Restricts access to only the platform admin or the campaign owner. + +Checks if `_msgSender()` is either the platform admin (via `INFO.getPlatformAdminAddress`) +or the campaign owner (via `INFO.owner()`). Reverts with `AccessCheckerUnauthorized` if not authorized. + + +```solidity +modifier onlyPlatformAdminOrCampaignOwner() ; +``` + +### getplatformHash + +Retrieves the platform identifier associated with the treasury. + + +```solidity +function getplatformHash() external view override returns (bytes32); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bytes32`|The platform identifier as a bytes32 value.| + + +### getplatformFeePercent + +Retrieves the platform fee percentage for the treasury. + + +```solidity +function getplatformFeePercent() external view override returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The platform fee percentage as a uint256 value.| + + +### getRaisedAmount + +Retrieves the total raised amount in the treasury. + + +```solidity +function getRaisedAmount() public view virtual override returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total raised amount as a uint256 value.| + + +### getAvailableRaisedAmount + +Retrieves the currently available raised amount in the treasury. + + +```solidity +function getAvailableRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The current available raised amount as a uint256 value.| + + +### getLifetimeRaisedAmount + +Retrieves the lifetime raised amount in the treasury (never decreases with refunds). + + +```solidity +function getLifetimeRaisedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The lifetime raised amount as a uint256 value.| + + +### getRefundedAmount + +Retrieves the total refunded amount in the treasury. + + +```solidity +function getRefundedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total refunded amount as a uint256 value.| + + +### getExpectedAmount + +Retrieves the total expected (pending) amount in the treasury. + +This represents payments that have been created but not yet confirmed. + + +```solidity +function getExpectedAmount() external view returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The total expected amount as a uint256 value.| + + +### _normalizeAmount + +Normalizes token amounts to 18 decimals for consistent comparisons. + + +```solidity +function _normalizeAmount(address token, uint256 amount) internal view returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address.| +|`amount`|`uint256`|The amount to normalize.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The normalized amount (scaled to 18 decimals).| + + +### _validateStoreAndTrackLineItems + +Validates, stores, and tracks line items in a single loop for gas efficiency. + + +```solidity +function _validateStoreAndTrackLineItems( + bytes32 paymentId, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + address paymentToken +) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The payment ID to store line items for.| +|`lineItems`|`ICampaignPaymentTreasury.LineItem[]`|Array of line items to validate, store, and track.| +|`paymentToken`|`address`|The token used for the payment.| + + +### createPayment + +Creates a new payment entry with the specified details. + + +```solidity +function createPayment( + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + address paymentToken, + uint256 amount, + uint256 expiration, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees +) public virtual override onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|A unique identifier for the payment.| +|`buyerId`|`bytes32`|The id of the buyer initiating the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`expiration`|`uint256`|The timestamp after which the payment expires.| +|`lineItems`|`ICampaignPaymentTreasury.LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ICampaignPaymentTreasury.ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### createPaymentBatch + +Creates multiple payment entries in a single transaction to prevent nonce conflicts. + + +```solidity +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 virtual override onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique identifiers for the payments.| +|`buyerIds`|`bytes32[]`|An array of buyer IDs corresponding to each payment.| +|`itemIds`|`bytes32[]`|An array of item identifiers corresponding to each payment.| +|`paymentTokens`|`address[]`|An array of tokens corresponding to each payment.| +|`amounts`|`uint256[]`|An array of amounts corresponding to each payment.| +|`expirations`|`uint256[]`|An array of expiration timestamps corresponding to each payment.| +|`lineItemsArray`|`ICampaignPaymentTreasury.LineItem[][]`|An array of line item arrays, one for each payment.| +|`externalFeesArray`|`ICampaignPaymentTreasury.ExternalFees[][]`|An array of external fee metadata arrays, one for each payment (informational only).| + + +### processCryptoPayment + +Allows a buyer to make a direct crypto payment for an item. + +This function transfers tokens directly from the buyer's wallet and confirms the payment immediately. + + +```solidity +function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees +) public virtual override nonReentrant whenCampaignNotPaused whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`buyerAddress`|`address`|The address of the buyer making the payment.| +|`paymentToken`|`address`|The token to use for the payment.| +|`amount`|`uint256`|The amount to be paid for the item.| +|`lineItems`|`ICampaignPaymentTreasury.LineItem[]`|Array of line items associated with this payment.| +|`externalFees`|`ICampaignPaymentTreasury.ExternalFees[]`|Array of external fee metadata captured for this payment (informational only).| + + +### cancelPayment + +Cancels an existing payment with the given payment ID. + + +```solidity +function cancelPayment(bytes32 paymentId) + public + virtual + override + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to cancel.| + + +### _calculateLineItemTotals + +Calculates line item totals for balance checking and state updates. + + +```solidity +function _calculateLineItemTotals( + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems, + uint256 protocolFeePercent +) internal view returns (LineItemTotals memory totals); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`lineItems`|`ICampaignPaymentTreasury.PaymentLineItem[]`|Array of line items to process.| +|`protocolFeePercent`|`uint256`|Protocol fee percentage.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`totals`|`LineItemTotals`|Struct containing all calculated totals.| + + +### _checkBalanceForConfirmation + +Checks if there's sufficient balance for payment confirmation. + + +```solidity +function _checkBalanceForConfirmation(address paymentToken, uint256 paymentAmount, LineItemTotals memory totals) + internal + view; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentToken`|`address`|The token address.| +|`paymentAmount`|`uint256`|The base payment amount.| +|`totals`|`LineItemTotals`|Line item totals struct.| + + +### _updateLineItemsForConfirmation + +Updates state for line items during payment confirmation. + + +```solidity +function _updateLineItemsForConfirmation( + address paymentToken, + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems, + uint256 protocolFeePercent +) internal returns (uint256 totalInstantTransferAmount); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentToken`|`address`|The token address.| +|`lineItems`|`ICampaignPaymentTreasury.PaymentLineItem[]`|Array of line items to process.| +|`protocolFeePercent`|`uint256`|Protocol fee percentage.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`totalInstantTransferAmount`|`uint256`|Total amount to transfer instantly.| + + +### confirmPayment + +Confirms and finalizes the payment associated with the given payment ID. + + +```solidity +function confirmPayment(bytes32 paymentId, address buyerAddress) + public + virtual + override + nonReentrant + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to confirm.| +|`buyerAddress`|`address`|Optional buyer address to mint NFT to. Pass address(0) to skip NFT minting.| + + +### confirmPaymentBatch + +Confirms and finalizes multiple payments in a single transaction. + + +```solidity +function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) + public + virtual + override + nonReentrant + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique payment identifiers to be confirmed.| +|`buyerAddresses`|`address[]`|Array of buyer addresses to mint NFTs to. Must match paymentIds length. Pass address(0) to skip NFT minting for specific payments.| + + +### claimRefund + +Claims a refund for non-NFT payments (payments without minted NFTs). + +For non-NFT payments only. Verifies that no NFT exists for this payment. + + +```solidity +function claimRefund(bytes32 paymentId, address refundAddress) + public + virtual + override + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must NOT have an NFT).| +|`refundAddress`|`address`|The address where the refunded amount should be sent.| + + +### claimRefund + +Claims a refund for non-NFT payments (payments without minted NFTs). + +For NFT payments only. Requires an NFT exists and burns it. Refund is sent to current NFT owner. + + +```solidity +function claimRefund(bytes32 paymentId) public virtual override whenCampaignNotPaused whenCampaignNotCancelled; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment (must NOT have an NFT).| + + +### disburseFees + +Disburses fees collected by the treasury. + + +```solidity +function disburseFees() public virtual override whenCampaignNotPaused; +``` + +### claimNonGoalLineItems + +Allows platform admin to claim non-goal line items that are available for claiming. + + +```solidity +function claimNonGoalLineItems(address token) + public + virtual + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address to claim.| + + +### claimExpiredFunds + +Allows the platform admin to claim all remaining funds once the claim window has opened. + + +```solidity +function claimExpiredFunds() public virtual onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused; +``` + +### withdraw + +Withdraws funds from the treasury. + + +```solidity +function withdraw() + public + virtual + override + onlyPlatformAdminOrCampaignOwner + whenCampaignNotPaused + whenCampaignNotCancelled; +``` + +### pauseTreasury + +External function to pause the campaign. + + +```solidity +function pauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH); +``` + +### unpauseTreasury + +External function to unpause the campaign. + + +```solidity +function unpauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH); +``` + +### cancelTreasury + +External function to cancel the campaign. + + +```solidity +function cancelTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH); +``` + +### cancelled + +Returns true if the treasury has been cancelled. + + +```solidity +function cancelled() public view virtual override(ICampaignPaymentTreasury, PausableCancellable) returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if cancelled, false otherwise.| + + +### _revertIfCampaignPaused + +Internal function to check if the campaign is paused. +If the campaign is paused, it reverts with PaymentTreasuryCampaignInfoIsPaused error. + + +```solidity +function _revertIfCampaignPaused() internal view; +``` + +### _revertIfCampaignCancelled + + +```solidity +function _revertIfCampaignCancelled() internal view; +``` + +### _validatePaymentForAction + +Validates the given payment ID to ensure it is eligible for further action. +Reverts if: +- The payment does not exist. +- The payment has already been confirmed. +- The payment has already expired. +- The payment is a crypto payment + + +```solidity +function _validatePaymentForAction(bytes32 paymentId) internal view; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment to validate.| + + +### getPaymentData + +Retrieves comprehensive payment data including payment info, token, line items, and external fees. + +This function can look up payments created by anyone: +- Off-chain payments (created via createPayment): Scoped with address(0), anyone can look these up +- On-chain payments (created via processCryptoPayment): Uses stored creator address, anyone can look these up + + +```solidity +function getPaymentData(bytes32 paymentId) + public + view + override + returns (ICampaignPaymentTreasury.PaymentData memory); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`ICampaignPaymentTreasury.PaymentData`|A PaymentData struct containing all payment information.| + + +### _checkSuccessCondition + +Internal function to check the success condition for fee disbursement. + + +```solidity +function _checkSuccessCondition() internal view virtual returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|Whether the success condition is met.| + + +## Events +### PaymentCreated +Emitted when a new payment is created. + + +```solidity +event PaymentCreated( + address buyerAddress, + bytes32 indexed paymentId, + bytes32 buyerId, + bytes32 indexed itemId, + address indexed paymentToken, + uint256 amount, + uint256 expiration, + bool isCryptoPayment +); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`buyerAddress`|`address`|The address of the buyer making the payment.| +|`paymentId`|`bytes32`|The unique identifier of the payment.| +|`buyerId`|`bytes32`|The id of the buyer.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`paymentToken`|`address`|The token used for the payment.| +|`amount`|`uint256`|The amount to be paid for the item (in token's native decimals).| +|`expiration`|`uint256`|The timestamp after which the payment expires.| +|`isCryptoPayment`|`bool`|Boolean indicating whether the payment is made using direct crypto payment.| + +### PaymentCancelled +Emitted when a payment is cancelled and removed from the treasury. + + +```solidity +event PaymentCancelled(bytes32 indexed paymentId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the cancelled payment.| + +### PaymentConfirmed +Emitted when a payment is confirmed. + + +```solidity +event PaymentConfirmed(bytes32 indexed paymentId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the confirmed payment.| + +### PaymentBatchConfirmed +Emitted when multiple payments are confirmed in a single batch operation. + + +```solidity +event PaymentBatchConfirmed(bytes32[] paymentIds); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique identifiers for the confirmed payments.| + +### PaymentBatchCreated +Emitted when multiple payments are created in a single batch operation. + + +```solidity +event PaymentBatchCreated(bytes32[] paymentIds); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentIds`|`bytes32[]`|An array of unique identifiers for the created payments.| + +### FeesDisbursed +Emitted when fees are successfully disbursed. + + +```solidity +event FeesDisbursed(address indexed token, uint256 protocolShare, uint256 platformShare); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token in which fees were disbursed.| +|`protocolShare`|`uint256`|The amount of fees sent to the protocol.| +|`platformShare`|`uint256`|The amount of fees sent to the platform.| + +### WithdrawalWithFeeSuccessful +Emitted when a withdrawal is successfully processed along with the applied fee. + + +```solidity +event WithdrawalWithFeeSuccessful(address indexed token, address indexed to, uint256 amount, uint256 fee); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token that was withdrawn.| +|`to`|`address`|The recipient address receiving the funds.| +|`amount`|`uint256`|The total amount withdrawn (excluding fee).| +|`fee`|`uint256`|The fee amount deducted from the withdrawal.| + +### RefundClaimed +Emitted when a refund is claimed. + + +```solidity +event RefundClaimed(bytes32 indexed paymentId, uint256 refundAmount, address indexed claimer); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the cancelled payment.| +|`refundAmount`|`uint256`|The refund amount claimed.| +|`claimer`|`address`|The address of the claimer.| + +### NonGoalLineItemsClaimed +Emitted when non-goal line items are claimed by the platform admin. + + +```solidity +event NonGoalLineItemsClaimed(address indexed token, uint256 amount, address indexed platformAdmin); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token that was claimed.| +|`amount`|`uint256`|The amount claimed.| +|`platformAdmin`|`address`|The address of the platform admin who claimed.| + +### ExpiredFundsClaimed +Emitted when expired funds are claimed by the platform and protocol admins. + + +```solidity +event ExpiredFundsClaimed(address indexed token, uint256 platformAmount, uint256 protocolAmount); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token that was claimed.| +|`platformAmount`|`uint256`|The amount sent to the platform admin.| +|`protocolAmount`|`uint256`|The amount sent to the protocol admin.| + +## Errors +### PaymentTreasuryInvalidInput +Reverts when one or more provided inputs to the payment treasury are invalid. + + +```solidity +error PaymentTreasuryInvalidInput(); +``` + +### PaymentTreasuryPaymentAlreadyExist +Throws an error indicating that the payment id already exists. + + +```solidity +error PaymentTreasuryPaymentAlreadyExist(bytes32 paymentId); +``` + +### PaymentTreasuryPaymentAlreadyConfirmed +Throws an error indicating that the payment id is already confirmed. + + +```solidity +error PaymentTreasuryPaymentAlreadyConfirmed(bytes32 paymentId); +``` + +### PaymentTreasuryPaymentAlreadyExpired +Throws an error indicating that the payment id is already expired. + + +```solidity +error PaymentTreasuryPaymentAlreadyExpired(bytes32 paymentId); +``` + +### PaymentTreasuryPaymentNotExist +Throws an error indicating that the payment id does not exist. + + +```solidity +error PaymentTreasuryPaymentNotExist(bytes32 paymentId); +``` + +### PaymentTreasuryCampaignInfoIsPaused +Throws an error indicating that the campaign is paused. + + +```solidity +error PaymentTreasuryCampaignInfoIsPaused(); +``` + +### PaymentTreasuryTokenNotAccepted +Emitted when a token is not accepted for the campaign. + + +```solidity +error PaymentTreasuryTokenNotAccepted(address token); +``` + +### PaymentTreasurySuccessConditionNotFulfilled +Throws an error indicating that the success condition was not fulfilled. + + +```solidity +error PaymentTreasurySuccessConditionNotFulfilled(); +``` + +### PaymentTreasuryFeeNotDisbursed +Throws an error indicating that fees have not been disbursed. + + +```solidity +error PaymentTreasuryFeeNotDisbursed(); +``` + +### PaymentTreasuryPaymentNotConfirmed +Throws an error indicating that the payment id is not confirmed. + + +```solidity +error PaymentTreasuryPaymentNotConfirmed(bytes32 paymentId); +``` + +### PaymentTreasuryPaymentNotClaimable +Emitted when claiming an unclaimable refund. + + +```solidity +error PaymentTreasuryPaymentNotClaimable(bytes32 paymentId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the refundable payment.| + +### PaymentTreasuryAlreadyWithdrawn +Emitted when an attempt is made to withdraw funds from the treasury but the payment has already been withdrawn. + + +```solidity +error PaymentTreasuryAlreadyWithdrawn(); +``` + +### PaymentTreasuryCryptoPayment +This error is thrown when an operation is attempted on a crypto payment that is only valid for non-crypto payments. + + +```solidity +error PaymentTreasuryCryptoPayment(bytes32 paymentId); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`paymentId`|`bytes32`|The unique identifier of the payment that caused the error.| + +### PaymentTreasuryInsufficientFundsForFee +Emitted when the fee exceeds the requested withdrawal amount. + + +```solidity +error PaymentTreasuryInsufficientFundsForFee(uint256 withdrawalAmount, uint256 fee); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`withdrawalAmount`|`uint256`|The amount requested for withdrawal.| +|`fee`|`uint256`|The calculated fee, which is greater than the withdrawal amount.| + +### PaymentTreasuryInsufficientBalance +Emitted when there are insufficient unallocated tokens for a payment confirmation. + + +```solidity +error PaymentTreasuryInsufficientBalance(uint256 required, uint256 available); +``` + +### PaymentTreasuryExpirationExceedsMax +Throws an error indicating that the payment expiration exceeds the maximum allowed expiration time. + + +```solidity +error PaymentTreasuryExpirationExceedsMax(uint256 expiration, uint256 maxExpiration); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`expiration`|`uint256`|The requested expiration timestamp.| +|`maxExpiration`|`uint256`|The maximum allowed expiration timestamp.| + +### PaymentTreasuryClaimWindowNotReached +Throws when attempting to claim expired funds before the claim window opens. + + +```solidity +error PaymentTreasuryClaimWindowNotReached(uint256 claimableAt); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`claimableAt`|`uint256`|The timestamp when the claim window opens.| + +### PaymentTreasuryNoFundsToClaim +Throws when there are no funds available to claim. + + +```solidity +error PaymentTreasuryNoFundsToClaim(); +``` + +## Structs +### PaymentInfo +Stores information about a payment in the treasury. + + +```solidity +struct PaymentInfo { + address buyerAddress; + bytes32 buyerId; + bytes32 itemId; + uint256 amount; + uint256 expiration; + bool isConfirmed; + bool isCryptoPayment; + uint256 lineItemCount; +} +``` + +**Properties** + +|Name|Type|Description| +|----|----|-----------| +|`buyerAddress`|`address`|The address of the buyer who made the payment.| +|`buyerId`|`bytes32`|The ID of the buyer.| +|`itemId`|`bytes32`|The identifier of the item being purchased.| +|`amount`|`uint256`|The amount to be paid for the item (in token's native decimals).| +|`expiration`|`uint256`|The timestamp after which the payment expires.| +|`isConfirmed`|`bool`|Boolean indicating whether the payment has been confirmed.| +|`isCryptoPayment`|`bool`|Boolean indicating whether the payment is made using direct crypto payment.| +|`lineItemCount`|`uint256`|The number of line items associated with this payment.| + +### LineItemTotals +Struct to hold line item calculation totals to reduce stack depth. + + +```solidity +struct LineItemTotals { + uint256 totalGoalLineItemAmount; + uint256 totalProtocolFeeFromLineItems; + uint256 totalNonGoalClaimableAmount; + uint256 totalNonGoalRefundableAmount; + uint256 totalInstantTransferAmountForCheck; + uint256 totalInstantTransferAmount; +} +``` + diff --git a/docs/src/src/utils/BaseTreasury.sol/abstract.BaseTreasury.md b/docs/src/src/utils/BaseTreasury.sol/abstract.BaseTreasury.md index 1b437429..a15e56b4 100644 --- a/docs/src/src/utils/BaseTreasury.sol/abstract.BaseTreasury.md +++ b/docs/src/src/utils/BaseTreasury.sol/abstract.BaseTreasury.md @@ -1,63 +1,72 @@ # BaseTreasury -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/BaseTreasury.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/BaseTreasury.sol) **Inherits:** -Initializable, [ICampaignTreasury](/src/interfaces/ICampaignTreasury.sol/interface.ICampaignTreasury.md), [CampaignAccessChecker](/src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md), [PausableCancellable](/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md) +Initializable, [ICampaignTreasury](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/ICampaignTreasury.sol/interface.ICampaignTreasury.md), [CampaignAccessChecker](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md), [PausableCancellable](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md) A base contract for creating and managing treasuries in crowdfunding campaigns. -*This contract defines common functionality and storage for campaign treasuries.* +This contract defines common functionality and storage for campaign treasuries. -*Contracts implementing this base contract should provide specific success conditions.* +Supports ERC-2771 meta-transactions via adapter contracts for platform admin operations. + +Contracts implementing this base contract should provide specific success conditions. ## State Variables ### ZERO_BYTES ```solidity -bytes32 internal constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000; +bytes32 internal constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000 ``` ### PERCENT_DIVIDER ```solidity -uint256 internal constant PERCENT_DIVIDER = 10000; +uint256 internal constant PERCENT_DIVIDER = 10000 +``` + + +### STANDARD_DECIMALS + +```solidity +uint256 internal constant STANDARD_DECIMALS = 18 ``` ### PLATFORM_HASH ```solidity -bytes32 internal PLATFORM_HASH; +bytes32 internal PLATFORM_HASH ``` ### PLATFORM_FEE_PERCENT ```solidity -uint256 internal PLATFORM_FEE_PERCENT; +uint256 internal PLATFORM_FEE_PERCENT ``` -### TOKEN +### s_feesDisbursed ```solidity -IERC20 internal TOKEN; +bool internal s_feesDisbursed ``` -### s_pledgedAmount +### s_tokenRaisedAmounts ```solidity -uint256 internal s_pledgedAmount; +mapping(address => uint256) internal s_tokenRaisedAmounts ``` -### s_feesDisbursed +### s_tokenLifetimeRaisedAmounts ```solidity -bool internal s_feesDisbursed; +mapping(address => uint256) internal s_tokenLifetimeRaisedAmounts ``` @@ -66,23 +75,33 @@ bool internal s_feesDisbursed; ```solidity -function __BaseContract_init(bytes32 platformHash, address infoAddress) internal; +function __BaseContract_init(bytes32 platformHash, address infoAddress, address trustedForwarder_) internal; +``` + +### _msgSender + +Override _msgSender to support ERC-2771 meta-transactions. +When called by the trusted forwarder (adapter), extracts the actual sender from calldata. + + +```solidity +function _msgSender() internal view virtual override returns (address sender); ``` ### whenCampaignNotPaused -*Modifier that checks if the campaign is not paused.* +Modifier that checks if the campaign is not paused. ```solidity -modifier whenCampaignNotPaused(); +modifier whenCampaignNotPaused() ; ``` ### whenCampaignNotCancelled ```solidity -modifier whenCampaignNotCancelled(); +modifier whenCampaignNotCancelled() ; ``` ### getPlatformHash @@ -115,6 +134,50 @@ function getPlatformFeePercent() external view override returns (uint256); |``|`uint256`|The platform fee percentage as a uint256 value.| +### _normalizeAmount + +Normalizes token amount to 18 decimals for consistent comparison. + + +```solidity +function _normalizeAmount(address token, uint256 amount) internal view returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address to normalize.| +|`amount`|`uint256`|The amount to normalize.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The normalized amount in 18 decimals.| + + +### _denormalizeAmount + +Denormalizes an amount from 18 decimals to the token's actual decimals. + + +```solidity +function _denormalizeAmount(address token, uint256 amount) internal view returns (uint256); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`token`|`address`|The token address to denormalize for.| +|`amount`|`uint256`|The amount in 18 decimals to denormalize.| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The denormalized amount in token's native decimals.| + + ### disburseFees Disburses fees collected by the treasury. @@ -135,7 +198,7 @@ function withdraw() public virtual override whenCampaignNotPaused whenCampaignNo ### pauseTreasury -*External function to pause the campaign.* +External function to pause the campaign. ```solidity @@ -144,7 +207,7 @@ function pauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFOR ### unpauseTreasury -*External function to unpause the campaign.* +External function to unpause the campaign. ```solidity @@ -153,17 +216,32 @@ function unpauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATF ### cancelTreasury -*External function to cancel the campaign.* +External function to cancel the campaign. ```solidity function cancelTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH); ``` +### cancelled + +Returns true if the treasury has been cancelled. + + +```solidity +function cancelled() public view virtual override(ICampaignTreasury, PausableCancellable) returns (bool); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if cancelled, false otherwise.| + + ### _revertIfCampaignPaused -*Internal function to check if the campaign is paused. -If the campaign is paused, it reverts with TreasuryCampaignInfoIsPaused error.* +Internal function to check if the campaign is paused. +If the campaign is paused, it reverts with TreasuryCampaignInfoIsPaused error. ```solidity @@ -179,7 +257,7 @@ function _revertIfCampaignCancelled() internal view; ### _checkSuccessCondition -*Internal function to check the success condition for fee disbursement.* +Internal function to check the success condition for fee disbursement. ```solidity @@ -194,32 +272,34 @@ function _checkSuccessCondition() internal view virtual returns (bool); ## Events ### FeesDisbursed -Emitted when fees are successfully disbursed. +Emitted when fees are successfully disbursed for a specific token. ```solidity -event FeesDisbursed(uint256 protocolShare, uint256 platformShare); +event FeesDisbursed(address indexed token, uint256 protocolShare, uint256 platformShare); ``` **Parameters** |Name|Type|Description| |----|----|-----------| +|`token`|`address`|The token address.| |`protocolShare`|`uint256`|The amount of fees sent to the protocol.| |`platformShare`|`uint256`|The amount of fees sent to the platform.| ### WithdrawalSuccessful -Emitted when a withdrawal is successful. +Emitted when a withdrawal is successful for a specific token. ```solidity -event WithdrawalSuccessful(address to, uint256 amount); +event WithdrawalSuccessful(address indexed token, address to, uint256 amount); ``` **Parameters** |Name|Type|Description| |----|----|-----------| +|`token`|`address`|The token address.| |`to`|`address`|The recipient of the withdrawal.| |`amount`|`uint256`|The amount withdrawn.| @@ -233,7 +313,7 @@ event SuccessConditionNotFulfilled(); ## Errors ### TreasuryTransferFailed -*Throws an error indicating a failed treasury transfer.* +Throws an error indicating a failed treasury transfer. ```solidity @@ -241,7 +321,7 @@ error TreasuryTransferFailed(); ``` ### TreasurySuccessConditionNotFulfilled -*Throws an error indicating that the success condition was not fulfilled.* +Throws an error indicating that the success condition was not fulfilled. ```solidity @@ -249,7 +329,7 @@ error TreasurySuccessConditionNotFulfilled(); ``` ### TreasuryFeeNotDisbursed -*Throws an error indicating that fees have not been disbursed.* +Throws an error indicating that fees have not been disbursed. ```solidity @@ -257,7 +337,7 @@ error TreasuryFeeNotDisbursed(); ``` ### TreasuryCampaignInfoIsPaused -*Throws an error indicating that the campaign is paused.* +Throws an error indicating that the campaign is paused. ```solidity diff --git a/docs/src/src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md b/docs/src/src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md index 153e499e..2e8a7c71 100644 --- a/docs/src/src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md +++ b/docs/src/src/utils/CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md @@ -1,22 +1,34 @@ # CampaignAccessChecker -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/CampaignAccessChecker.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/CampaignAccessChecker.sol) -*This abstract contract provides access control mechanisms to restrict the execution of specific functions -to authorized protocol administrators, platform administrators, and campaign owners.* +**Inherits:** +Context + +This abstract contract provides access control mechanisms to restrict the execution of specific functions +to authorized protocol administrators, platform administrators, and campaign owners. ## State Variables ### INFO ```solidity -ICampaignInfo internal INFO; +ICampaignInfo internal INFO +``` + + +### _trustedForwarder +Trusted forwarder address for ERC-2771 meta-transactions (set by derived contracts) + + +```solidity +address internal _trustedForwarder ``` ## Functions ### __CampaignAccessChecker_init -*Constructor to initialize the contract with the address of the campaign information contract.* +Constructor to initialize the contract with the address of the campaign information contract. ```solidity @@ -31,22 +43,22 @@ function __CampaignAccessChecker_init(address campaignInfo) internal; ### onlyProtocolAdmin -*Modifier that restricts function access to protocol administrators only. -Users attempting to execute functions with this modifier must be the protocol admin.* +Modifier that restricts function access to protocol administrators only. +Users attempting to execute functions with this modifier must be the protocol admin. ```solidity -modifier onlyProtocolAdmin(); +modifier onlyProtocolAdmin() ; ``` ### onlyPlatformAdmin -*Modifier that restricts function access to platform administrators of a specific platform. -Users attempting to execute functions with this modifier must be the platform admin for the given platform.* +Modifier that restricts function access to platform administrators of a specific platform. +Users attempting to execute functions with this modifier must be the platform admin for the given platform. ```solidity -modifier onlyPlatformAdmin(bytes32 platformHash); +modifier onlyPlatformAdmin(bytes32 platformHash) ; ``` **Parameters** @@ -57,18 +69,18 @@ modifier onlyPlatformAdmin(bytes32 platformHash); ### onlyCampaignOwner -*Modifier that restricts function access to the owner of the campaign. -Users attempting to execute functions with this modifier must be the owner of the campaign.* +Modifier that restricts function access to the owner of the campaign. +Users attempting to execute functions with this modifier must be the owner of the campaign. ```solidity -modifier onlyCampaignOwner(); +modifier onlyCampaignOwner() ; ``` ### _onlyProtocolAdmin -*Internal function to check if the sender is the protocol administrator. -If the sender is not the protocol admin, it reverts with AccessCheckerUnauthorized error.* +Internal function to check if the sender is the protocol administrator. +If the sender is not the protocol admin, it reverts with AccessCheckerUnauthorized error. ```solidity @@ -77,8 +89,8 @@ function _onlyProtocolAdmin() private view; ### _onlyPlatformAdmin -*Internal function to check if the sender is the platform administrator for a specific platform. -If the sender is not the platform admin, it reverts with AccessCheckerUnauthorized error.* +Internal function to check if the sender is the platform administrator for a specific platform. +If the sender is not the platform admin, it reverts with AccessCheckerUnauthorized error. ```solidity @@ -93,8 +105,8 @@ function _onlyPlatformAdmin(bytes32 platformHash) private view; ### _onlyCampaignOwner -*Internal function to check if the sender is the owner of the campaign. -If the sender is not the owner, it reverts with AccessCheckerUnauthorized error.* +Internal function to check if the sender is the owner of the campaign. +If the sender is not the owner, it reverts with AccessCheckerUnauthorized error. ```solidity @@ -103,7 +115,7 @@ function _onlyCampaignOwner() private view; ## Errors ### AccessCheckerUnauthorized -*Throws when the caller is not authorized.* +Throws when the caller is not authorized. ```solidity diff --git a/docs/src/src/utils/Counters.sol/library.Counters.md b/docs/src/src/utils/Counters.sol/library.Counters.md index 45805f48..8740baec 100644 --- a/docs/src/src/utils/Counters.sol/library.Counters.md +++ b/docs/src/src/utils/Counters.sol/library.Counters.md @@ -1,5 +1,5 @@ # Counters -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/Counters.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/Counters.sol) ## Functions @@ -33,7 +33,7 @@ function reset(Counter storage counter) internal; ## Errors ### CounterDecrementOverflow -*Error thrown when attempting to decrement a counter with value 0.* +Error thrown when attempting to decrement a counter with value 0. ```solidity @@ -45,7 +45,10 @@ error CounterDecrementOverflow(); ```solidity struct Counter { - uint256 _value; + // This variable should never be directly accessed by users of the library: interactions must be restricted to + // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add + // this feature: see https://github.com/ethereum/solidity/issues/4637 + uint256 _value; // default: 0 } ``` diff --git a/docs/src/src/utils/FiatEnabled.sol/abstract.FiatEnabled.md b/docs/src/src/utils/FiatEnabled.sol/abstract.FiatEnabled.md index ee5c2c9d..087d1944 100644 --- a/docs/src/src/utils/FiatEnabled.sol/abstract.FiatEnabled.md +++ b/docs/src/src/utils/FiatEnabled.sol/abstract.FiatEnabled.md @@ -1,5 +1,5 @@ # FiatEnabled -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/FiatEnabled.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/FiatEnabled.sol) A contract that provides functionality for tracking and managing fiat transactions. This contract allows tracking the amount of fiat raised, individual fiat transactions, and the state of fiat fee disbursement. @@ -9,21 +9,21 @@ This contract allows tracking the amount of fiat raised, individual fiat transac ### s_fiatRaisedAmount ```solidity -uint256 internal s_fiatRaisedAmount; +uint256 internal s_fiatRaisedAmount ``` ### s_fiatFeeIsDisbursed ```solidity -bool internal s_fiatFeeIsDisbursed; +bool internal s_fiatFeeIsDisbursed ``` ### s_fiatAmountById ```solidity -mapping(bytes32 => uint256) internal s_fiatAmountById; +mapping(bytes32 => uint256) internal s_fiatAmountById ``` @@ -97,7 +97,7 @@ function _updateFiatTransaction(bytes32 fiatTransactionId, uint256 fiatTransacti ### _updateFiatFeeDisbursementState -*Update the state of fiat fee disbursement.* +Update the state of fiat fee disbursement. ```solidity @@ -147,7 +147,7 @@ event FiatFeeDisbusementStateUpdated(bool isDisbursed, uint256 protocolFeeAmount ## Errors ### FiatEnabledAlreadySet -*Throws an error indicating that the fiat enabled functionality is already set.* +Throws an error indicating that the fiat enabled functionality is already set. ```solidity @@ -155,7 +155,7 @@ error FiatEnabledAlreadySet(); ``` ### FiatEnabledDisallowedState -*Throws an error indicating that the fiat enabled functionality is in an invalid state.* +Throws an error indicating that the fiat enabled functionality is in an invalid state. ```solidity @@ -163,7 +163,7 @@ error FiatEnabledDisallowedState(); ``` ### FiatEnabledInvalidTransaction -*Throws an error indicating that the fiat transaction is invalid.* +Throws an error indicating that the fiat transaction is invalid. ```solidity diff --git a/docs/src/src/utils/ItemRegistry.sol/contract.ItemRegistry.md b/docs/src/src/utils/ItemRegistry.sol/contract.ItemRegistry.md index 2e4d08a6..8f23dd30 100644 --- a/docs/src/src/utils/ItemRegistry.sol/contract.ItemRegistry.md +++ b/docs/src/src/utils/ItemRegistry.sol/contract.ItemRegistry.md @@ -1,17 +1,17 @@ # ItemRegistry -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/ItemRegistry.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/ItemRegistry.sol) **Inherits:** -[IItem](/src/interfaces/IItem.sol/interface.IItem.md), Context +[IItem](/Users/mahabubalahi/Documents/ccp/contracts/docs/src/src/interfaces/IItem.sol/interface.IItem.md), Context -*A contract that manages the registration and retrieval of items.* +A contract that manages the registration and retrieval of items. ## State Variables ### Items ```solidity -mapping(address => mapping(bytes32 => Item)) private Items; +mapping(address => mapping(bytes32 => Item)) private Items ``` @@ -72,7 +72,7 @@ function addItemsBatch(bytes32[] calldata itemIds, Item[] calldata items) extern ## Events ### ItemAdded -*Emitted when a new item is added to the registry.* +Emitted when a new item is added to the registry. ```solidity @@ -89,7 +89,7 @@ event ItemAdded(address indexed owner, bytes32 indexed itemId, Item item); ## Errors ### ItemRegistryMismatchedArraysLength -*Thrown when the input arrays have mismatched lengths.* +Thrown when the input arrays have mismatched lengths. ```solidity diff --git a/docs/src/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md b/docs/src/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md index cd083e89..f1f3cf8f 100644 --- a/docs/src/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md +++ b/docs/src/src/utils/PausableCancellable.sol/abstract.PausableCancellable.md @@ -1,5 +1,8 @@ # PausableCancellable -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/PausableCancellable.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/PausableCancellable.sol) + +**Inherits:** +Context Abstract contract providing pause and cancel state management with events and modifiers @@ -8,14 +11,14 @@ Abstract contract providing pause and cancel state management with events and mo ### _paused ```solidity -bool private _paused; +bool private _paused ``` ### _cancelled ```solidity -bool private _cancelled; +bool private _cancelled ``` @@ -26,7 +29,7 @@ Modifier to allow function only when not paused ```solidity -modifier whenNotPaused(); +modifier whenNotPaused() ; ``` ### whenPaused @@ -35,7 +38,7 @@ Modifier to allow function only when paused ```solidity -modifier whenPaused(); +modifier whenPaused() ; ``` ### whenNotCancelled @@ -44,7 +47,7 @@ Modifier to allow function only when not cancelled ```solidity -modifier whenNotCancelled(); +modifier whenNotCancelled() ; ``` ### whenCancelled @@ -53,7 +56,7 @@ Modifier to allow function only when cancelled ```solidity -modifier whenCancelled(); +modifier whenCancelled() ; ``` ### paused @@ -78,7 +81,7 @@ function cancelled() public view virtual returns (bool); Pauses the contract -*Can only pause if not already paused or cancelled* +Can only pause if not already paused or cancelled ```solidity @@ -95,7 +98,7 @@ function _pause(bytes32 reason) internal virtual whenNotPaused whenNotCancelled; Unpauses the contract -*Can only unpause if currently paused* +Can only unpause if currently paused ```solidity @@ -112,7 +115,7 @@ function _unpause(bytes32 reason) internal virtual whenPaused; Cancels the contract permanently -*Auto-unpauses if paused, and cannot be undone* +Auto-unpauses if paused, and cannot be undone ```solidity @@ -152,7 +155,7 @@ event Cancelled(address indexed account, bytes32 reason); ## Errors ### PausedError -*Reverts if contract is paused* +Reverts if contract is paused ```solidity @@ -160,7 +163,7 @@ error PausedError(); ``` ### NotPausedError -*Reverts if contract is not paused* +Reverts if contract is not paused ```solidity @@ -168,7 +171,7 @@ error NotPausedError(); ``` ### CancelledError -*Reverts if contract is cancelled* +Reverts if contract is cancelled ```solidity @@ -176,7 +179,7 @@ error CancelledError(); ``` ### NotCancelledError -*Reverts if contract is not cancelled* +Reverts if contract is not cancelled ```solidity @@ -184,7 +187,7 @@ error NotCancelledError(); ``` ### CannotCancel -*Reverts if contract is already cancelled* +Reverts if contract is already cancelled ```solidity diff --git a/docs/src/src/utils/PledgeNFT.sol/abstract.PledgeNFT.md b/docs/src/src/utils/PledgeNFT.sol/abstract.PledgeNFT.md new file mode 100644 index 00000000..c91345d5 --- /dev/null +++ b/docs/src/src/utils/PledgeNFT.sol/abstract.PledgeNFT.md @@ -0,0 +1,406 @@ +# PledgeNFT +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/PledgeNFT.sol) + +**Inherits:** +ERC721Burnable, AccessControl + +Abstract contract for NFTs representing pledges with on-chain metadata + +Contains counter logic and NFT metadata storage + + +## State Variables +### MINTER_ROLE + +```solidity +bytes32 public constant MINTER_ROLE = 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6 +``` + + +### s_nftName + +```solidity +string internal s_nftName +``` + + +### s_nftSymbol + +```solidity +string internal s_nftSymbol +``` + + +### s_imageURI + +```solidity +string internal s_imageURI +``` + + +### s_contractURI + +```solidity +string internal s_contractURI +``` + + +### s_tokenIdCounter + +```solidity +Counters.Counter internal s_tokenIdCounter +``` + + +### s_pledgeData + +```solidity +mapping(uint256 => PledgeData) internal s_pledgeData +``` + + +## Functions +### _initializeNFT + +Initialize NFT metadata + +Called by CampaignInfo during initialization + + +```solidity +function _initializeNFT( + string calldata _nftName, + string calldata _nftSymbol, + string calldata _imageURI, + string calldata _contractURI +) internal; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`_nftName`|`string`|NFT collection name| +|`_nftSymbol`|`string`|NFT collection symbol| +|`_imageURI`|`string`|NFT image URI for individual tokens| +|`_contractURI`|`string`|IPFS URI for contract-level metadata| + + +### _validateJsonString + +Validates that a string is safe for JSON embedding + +Reverts if string contains quotes, backslashes, control characters, or non-ASCII + + +```solidity +function _validateJsonString(string calldata str) internal pure; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`str`|`string`|The string to validate| + + +### mintNFTForPledge + +Mints a pledge NFT (auto-increments counter) + +Called by treasuries - returns the new token ID to use as pledge ID + + +```solidity +function mintNFTForPledge( + address backer, + bytes32 reward, + address tokenAddress, + uint256 amount, + uint256 shippingFee, + uint256 tipAmount +) public virtual onlyRole(MINTER_ROLE) returns (uint256 tokenId); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`backer`|`address`|The backer address| +|`reward`|`bytes32`|The reward identifier| +|`tokenAddress`|`address`|The address of the token used for the pledge| +|`amount`|`uint256`|The pledge amount| +|`shippingFee`|`uint256`|The shipping fee| +|`tipAmount`|`uint256`|The tip amount| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The minted token ID (to be used as pledge ID in treasury)| + + +### burn + +Burns a pledge NFT + + +```solidity +function burn(uint256 tokenId) public virtual override; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The token ID to burn| + + +### name + +Override name to return initialized name + + +```solidity +function name() public view virtual override returns (string memory); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`string`|The NFT collection name| + + +### symbol + +Override symbol to return initialized symbol + + +```solidity +function symbol() public view virtual override returns (string memory); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`string`|The NFT collection symbol| + + +### setImageURI + +Sets the image URI for all NFTs + +Must be overridden by inheriting contracts to implement access control + + +```solidity +function setImageURI(string calldata newImageURI) external virtual; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newImageURI`|`string`|The new image URI| + + +### contractURI + +Returns contract-level metadata URI + + +```solidity +function contractURI() external view virtual returns (string memory); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`string`|The contract URI| + + +### updateContractURI + +Update contract-level metadata URI + +Must be overridden by inheriting contracts to implement access control + + +```solidity +function updateContractURI(string calldata newContractURI) external virtual; +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newContractURI`|`string`|The new contract URI| + + +### getPledgeCount + +Gets current total number of pledges + + +```solidity +function getPledgeCount() external view virtual returns (uint256); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`uint256`|The current pledge count| + + +### tokenURI + +Returns the token URI with on-chain metadata + + +```solidity +function tokenURI(uint256 tokenId) public view virtual override returns (string memory); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The token ID| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`string`|The base64 encoded JSON metadata| + + +### getImageURI + +Gets the image URI + + +```solidity +function getImageURI() external view returns (string memory); +``` +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`string`|The current image URI| + + +### getPledgeData + +Gets the pledge data for a token + + +```solidity +function getPledgeData(uint256 tokenId) external view returns (PledgeData memory); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The token ID| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`PledgeData`|The pledge data| + + +### supportsInterface + +Override supportsInterface for multiple inheritance + + +```solidity +function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, AccessControl) returns (bool); +``` +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`interfaceId`|`bytes4`|The interface ID| + +**Returns** + +|Name|Type|Description| +|----|----|-----------| +|``|`bool`|True if the interface is supported| + + +## Events +### ImageURIUpdated +Emitted when the image URI is updated + + +```solidity +event ImageURIUpdated(string newImageURI); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newImageURI`|`string`|The new image URI| + +### ContractURIUpdated +Emitted when the contract URI is updated + + +```solidity +event ContractURIUpdated(string newContractURI); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`newContractURI`|`string`|The new contract URI| + +### PledgeNFTMinted +Emitted when a pledge NFT is minted + + +```solidity +event PledgeNFTMinted(uint256 indexed tokenId, address indexed backer, address indexed treasury, bytes32 reward); +``` + +**Parameters** + +|Name|Type|Description| +|----|----|-----------| +|`tokenId`|`uint256`|The token ID| +|`backer`|`address`|The backer address| +|`treasury`|`address`|The treasury address| +|`reward`|`bytes32`|The reward identifier| + +## Errors +### PledgeNFTUnAuthorized +Emitted when unauthorized access is attempted + + +```solidity +error PledgeNFTUnAuthorized(); +``` + +### PledgeNFTInvalidJsonString +Emitted when a string contains invalid characters for JSON + + +```solidity +error PledgeNFTInvalidJsonString(); +``` + +## Structs +### PledgeData +Struct to store pledge data for each token + + +```solidity +struct PledgeData { + address backer; + bytes32 reward; + address treasury; + address tokenAddress; + uint256 amount; + uint256 shippingFee; + uint256 tipAmount; +} +``` + diff --git a/docs/src/src/utils/README.md b/docs/src/src/utils/README.md index 41bc0ddb..41911c59 100644 --- a/docs/src/src/utils/README.md +++ b/docs/src/src/utils/README.md @@ -2,10 +2,12 @@ # Contents - [AdminAccessChecker](AdminAccessChecker.sol/abstract.AdminAccessChecker.md) +- [BasePaymentTreasury](BasePaymentTreasury.sol/abstract.BasePaymentTreasury.md) - [BaseTreasury](BaseTreasury.sol/abstract.BaseTreasury.md) - [CampaignAccessChecker](CampaignAccessChecker.sol/abstract.CampaignAccessChecker.md) - [Counters](Counters.sol/library.Counters.md) - [FiatEnabled](FiatEnabled.sol/abstract.FiatEnabled.md) - [ItemRegistry](ItemRegistry.sol/contract.ItemRegistry.md) - [PausableCancellable](PausableCancellable.sol/abstract.PausableCancellable.md) +- [PledgeNFT](PledgeNFT.sol/abstract.PledgeNFT.md) - [TimestampChecker](TimestampChecker.sol/abstract.TimestampChecker.md) diff --git a/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md b/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md index ae8c27a2..b0a7c0fb 100644 --- a/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md +++ b/docs/src/src/utils/TimestampChecker.sol/abstract.TimestampChecker.md @@ -1,5 +1,5 @@ # TimestampChecker -[Git Source](https://github.com/ccprotocol/ccprotocol-contracts/blob/b6945e2b533f7d9aacb156ae915f6d1bb6b199de/src/utils/TimestampChecker.sol) +[Git Source](https://github.com/oak-network/contracts/blob/0ce055a8ba31ca09404e9d09ecd2549534cbec61/src/utils/TimestampChecker.sol) A contract that provides timestamp-related checks for contract functions. @@ -11,7 +11,7 @@ Modifier that checks if the current timestamp is greater than a specified time. ```solidity -modifier currentTimeIsGreater(uint256 inputTime); +modifier currentTimeIsGreater(uint256 inputTime) ; ``` **Parameters** @@ -26,7 +26,7 @@ Modifier that checks if the current timestamp is less than a specified time. ```solidity -modifier currentTimeIsLess(uint256 inputTime); +modifier currentTimeIsLess(uint256 inputTime) ; ``` **Parameters** @@ -41,7 +41,7 @@ Modifier that checks if the current timestamp is within a specified time range. ```solidity -modifier currentTimeIsWithinRange(uint256 initialTime, uint256 finalTime); +modifier currentTimeIsWithinRange(uint256 initialTime, uint256 finalTime) ; ``` **Parameters** @@ -53,7 +53,7 @@ modifier currentTimeIsWithinRange(uint256 initialTime, uint256 finalTime); ### _revertIfCurrentTimeIsNotLess -*Internal function to revert if the current timestamp is less than or equal a specified time.* +Internal function to revert if the current timestamp is less than or equal a specified time. ```solidity @@ -68,7 +68,7 @@ function _revertIfCurrentTimeIsNotLess(uint256 inputTime) internal view virtual; ### _revertIfCurrentTimeIsNotGreater -*Internal function to revert if the current timestamp is not greater than or equal a specified time.* +Internal function to revert if the current timestamp is not greater than or equal a specified time. ```solidity @@ -83,7 +83,7 @@ function _revertIfCurrentTimeIsNotGreater(uint256 inputTime) internal view virtu ### _revertIfCurrentTimeIsNotWithinRange -*Internal function to revert if the current timestamp is not within a specified time range.* +Internal function to revert if the current timestamp is not within a specified time range. ```solidity @@ -99,7 +99,7 @@ function _revertIfCurrentTimeIsNotWithinRange(uint256 initialTime, uint256 final ## Errors ### CurrentTimeIsGreater -*Error: The current timestamp is greater than the specified input time.* +Error: The current timestamp is greater than the specified input time. ```solidity @@ -114,7 +114,7 @@ error CurrentTimeIsGreater(uint256 inputTime, uint256 currentTime); |`currentTime`|`uint256`|The current block timestamp.| ### CurrentTimeIsLess -*Error: The current timestamp is less than the specified input time.* +Error: The current timestamp is less than the specified input time. ```solidity @@ -129,7 +129,7 @@ error CurrentTimeIsLess(uint256 inputTime, uint256 currentTime); |`currentTime`|`uint256`|The current block timestamp.| ### CurrentTimeIsNotWithinRange -*Error: The current timestamp is not within the specified range.* +Error: The current timestamp is not within the specified range. ```solidity diff --git a/env.example b/env.example index 9265a035..e2979993 100644 --- a/env.example +++ b/env.example @@ -7,13 +7,13 @@ PRIVATE_KEY= RPC_URL= # Optional: Separate RPC URLs for multiple chains -ALFAJORES_RPC_URL= CELO_RPC_URL= +CELO_SEPOLIA_RPC_URL= # Chain IDs CHAIN_ID= -ALFAJORES_CHAIN_ID= CELO_CHAIN_ID= +CELO_SEPOLIA_CHAIN_ID= # ========================= @@ -41,6 +41,7 @@ PROTOCOL_FEE_PERCENT= PLATFORM_NAME= PLATFORM_ADMIN_ADDRESS= +PLATFORM_ADAPTER_ADDRESS= # Optional: Trusted forwarder address for ERC-2771 meta-transactions (adapter contract) PLATFORM_FEE_PERCENT= @@ -69,3 +70,18 @@ SIMULATE= # ========================= ETHERSCAN_API_KEY= + +# ============================= +# Time Related Constraints +# ============================= +BUFFER_TIME= +CAMPAIGN_LAUNCH_BUFFER= +MINIMUM_CAMPAIGN_DURATION= +MAX_PAYMENT_EXPIRATION= + +# ============================= +# Currency + token matrix +# ============================= + +CURRENCIES=USD,EUR +TOKENS_PER_CURRENCY=0xUsdt...,0xUsdc...,0xDai...;0xEurt...,0xEurc... diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 00000000..e86f427a --- /dev/null +++ b/foundry.lock @@ -0,0 +1,17 @@ +{ + "lib/forge-std": { + "rev": "1d9650e951204a0ddce9ff89c32f1997984cef4d" + }, + "lib/openzeppelin-contracts": { + "tag": { + "name": "v5.3.0", + "rev": "e4f70216d759d8e6a64144a9e1f7bbeed78e7079" + } + }, + "lib/openzeppelin-contracts-upgradeable": { + "tag": { + "name": "v5.3.0", + "rev": "60b305a8f3ff0c7688f02ac470417b6bbf1c4d27" + } + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 95ee243f..4ba7667b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,12 +5,13 @@ libs = ["lib"] via_ir = true optimizer = true optimizer_runs = 200 -solc_version = "0.8.20" +solc_version = "0.8.22" remappings = [ - "@openzeppelin/=lib/openzeppelin-contracts/" + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/" ] [rpc_endpoints] mainnet = "https://forno.celo.org/" -alfajores = "https://alfajores-forno.celo-testnet.org/" +celo_sepolia = "https://forno.celo-sepolia.celo-testnet.org/" diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 00000000..e4f70216 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 00000000..60b305a8 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 60b305a8f3ff0c7688f02ac470417b6bbf1c4d27 diff --git a/script/DeployAll.s.sol b/script/DeployAll.s.sol index f7936213..c1bc4d54 100644 --- a/script/DeployAll.s.sol +++ b/script/DeployAll.s.sol @@ -1,62 +1,154 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; -import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; -import {DeployGlobalParams} from "./DeployGlobalParams.s.sol"; -import {DeployTestToken} from "./DeployTestToken.s.sol"; -import {DeployCampaignInfoFactory} from "./DeployCampaignInfoFactory.s.sol"; -import {DeployTreasuryFactory} from "./DeployTreasuryFactory.s.sol"; - -contract DeployAll is Script { - function deployTestToken() internal returns (address) { - DeployTestToken script = new DeployTestToken(); - return script.deploy(); - } - - function deployGlobalParams(address testToken) internal returns (address) { - DeployGlobalParams script = new DeployGlobalParams(); - return script.deployWithToken(testToken); - } - - function deployTreasuryFactory( - address globalParams - ) internal returns (address) { - DeployTreasuryFactory script = new DeployTreasuryFactory(); - return script.deploy(globalParams); - } - - function deployCampaignFactory( - address globalParams, - address treasuryFactory - ) internal returns (address) { - DeployCampaignInfoFactory script = new DeployCampaignInfoFactory(); - return script.deploy(globalParams, treasuryFactory); - } +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 {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"; +contract DeployAll is DeployBase { function run() external { bool simulate = vm.envOr("SIMULATE", false); uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address deployerAddress = vm.addr(deployerKey); if (!simulate) { vm.startBroadcast(deployerKey); } - address testToken = deployTestToken(); - address globalParams = deployGlobalParams(testToken); - address treasuryFactory = deployTreasuryFactory(globalParams); - address campaignFactory = deployCampaignFactory( - globalParams, - treasuryFactory + // Deploy TestToken only if needed + address testTokenAddress; + bool testTokenDeployed = false; + + if (shouldDeployTestToken()) { + string memory tokenName = vm.envOr("TOKEN_NAME", string("TestToken")); + string memory tokenSymbol = vm.envOr("TOKEN_SYMBOL", string("TST")); + uint8 decimals = uint8(vm.envOr("TOKEN_DECIMALS", uint256(18))); + + TestToken testToken = new TestToken(tokenName, tokenSymbol, decimals); + testTokenAddress = address(testToken); + testTokenDeployed = true; + console2.log("TestToken deployed at:", testTokenAddress); + } else { + console2.log("Skipping TestToken deployment - using custom tokens for currencies"); + } + + // Deploy GlobalParams with UUPS proxy + uint256 protocolFeePercent = vm.envOr("PROTOCOL_FEE_PERCENT", uint256(100)); + + (bytes32[] memory currencies, address[][] memory tokensPerCurrency) = loadCurrenciesAndTokens(testTokenAddress); + + // Deploy GlobalParams implementation + GlobalParams globalParamsImpl = new GlobalParams(); + console2.log("GlobalParams implementation deployed at:", address(globalParamsImpl)); + + // Prepare initialization data for GlobalParams + bytes memory globalParamsInitData = abi.encodeWithSelector( + GlobalParams.initialize.selector, deployerAddress, protocolFeePercent, currencies, tokensPerCurrency + ); + + // Deploy GlobalParams proxy + ERC1967Proxy globalParamsProxy = new ERC1967Proxy(address(globalParamsImpl), globalParamsInitData); + console2.log("GlobalParams proxy deployed at:", address(globalParamsProxy)); + + // Deploy TreasuryFactory with UUPS proxy + TreasuryFactory treasuryFactoryImpl = new TreasuryFactory(); + console2.log("TreasuryFactory implementation deployed at:", address(treasuryFactoryImpl)); + + // Prepare initialization data for TreasuryFactory + bytes memory treasuryFactoryInitData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(address(globalParamsProxy))); + + // Deploy TreasuryFactory proxy + ERC1967Proxy treasuryFactoryProxy = new ERC1967Proxy(address(treasuryFactoryImpl), treasuryFactoryInitData); + console2.log("TreasuryFactory proxy deployed at:", address(treasuryFactoryProxy)); + + // Deploy CampaignInfo implementation + CampaignInfo campaignInfoImplementation = new CampaignInfo(); + console2.log("CampaignInfo implementation deployed at:", address(campaignInfoImplementation)); + + // Deploy CampaignInfoFactory with UUPS proxy + CampaignInfoFactory campaignFactoryImpl = new CampaignInfoFactory(); + console2.log("CampaignInfoFactory implementation deployed at:", address(campaignFactoryImpl)); + + // Prepare initialization data for CampaignInfoFactory + bytes memory campaignFactoryInitData = abi.encodeWithSelector( + CampaignInfoFactory.initialize.selector, + deployerAddress, + IGlobalParams(address(globalParamsProxy)), + address(campaignInfoImplementation), + address(treasuryFactoryProxy) + ); + + // Deploy CampaignInfoFactory proxy + ERC1967Proxy campaignFactoryProxy = new ERC1967Proxy(address(campaignFactoryImpl), campaignFactoryInitData); + console2.log("CampaignInfoFactory proxy deployed at:", address(campaignFactoryProxy)); + + // Configure registry values + uint256 bufferTime = vm.envOr("BUFFER_TIME", uint256(0)); + uint256 campaignLaunchBuffer = vm.envOr("CAMPAIGN_LAUNCH_BUFFER", uint256(0)); + uint256 minimumCampaignDuration = vm.envOr("MINIMUM_CAMPAIGN_DURATION", uint256(0)); + + GlobalParams(address(globalParamsProxy)).addToRegistry(DataRegistryKeys.BUFFER_TIME, bytes32(bufferTime)); + GlobalParams(address(globalParamsProxy)).addToRegistry( + DataRegistryKeys.CAMPAIGN_LAUNCH_BUFFER, bytes32(campaignLaunchBuffer) + ); + GlobalParams(address(globalParamsProxy)).addToRegistry( + DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION, bytes32(minimumCampaignDuration) ); if (!simulate) { vm.stopBroadcast(); } - console2.log("TOKEN_ADDRESS", testToken); - console2.log("GLOBAL_PARAMS_ADDRESS", globalParams); - console2.log("TREASURY_FACTORY_ADDRESS", treasuryFactory); - console2.log("CAMPAIGN_INFO_FACTORY_ADDRESS", campaignFactory); + // Summary + console2.log("\n==========================================="); + console2.log(" Deployment Summary"); + console2.log("==========================================="); + + console2.log("\n--- Core Protocol Contracts (UUPS Proxies) ---"); + console2.log("GLOBAL_PARAMS_PROXY:", address(globalParamsProxy)); + console2.log(" Implementation:", address(globalParamsImpl)); + console2.log("TREASURY_FACTORY_PROXY:", address(treasuryFactoryProxy)); + console2.log(" Implementation:", address(treasuryFactoryImpl)); + console2.log("CAMPAIGN_INFO_FACTORY_PROXY:", address(campaignFactoryProxy)); + console2.log(" Implementation:", address(campaignFactoryImpl)); + + console2.log("\n--- Implementation Contracts ---"); + console2.log("CAMPAIGN_INFO_IMPLEMENTATION:", address(campaignInfoImplementation)); + + 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)"); + if (testTokenDeployed) { + console2.log(" Token:", testTokenAddress); + console2.log(" (TestToken deployed for testing)"); + } + } + + console2.log("\n==========================================="); + console2.log("Deployment completed successfully!"); + console2.log("==========================================="); } } diff --git a/script/DeployAllAndSetupAllOrNothing.s.sol b/script/DeployAllAndSetupAllOrNothing.s.sol index d0dc456c..7f340905 100644 --- a/script/DeployAllAndSetupAllOrNothing.s.sol +++ b/script/DeployAllAndSetupAllOrNothing.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; -import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; import {TestToken} from "../test/mocks/TestToken.sol"; import {GlobalParams} from "src/GlobalParams.sol"; @@ -9,30 +8,41 @@ import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {TreasuryFactory} from "src/TreasuryFactory.sol"; import {AllOrNothing} from "src/treasuries/AllOrNothing.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 */ -contract DeployAllAndSetupAllOrNothing is Script { +contract DeployAllAndSetupAllOrNothing is DeployBase { // Customizable values (set through environment variables) bytes32 platformHash; uint256 protocolFeePercent; uint256 platformFeePercent; uint256 tokenMintAmount; bool simulate; + uint256 bufferTime; + uint256 campaignLaunchBuffer; + uint256 minimumCampaignDuration; // Contract addresses address testToken; address globalParams; + address globalParamsImplementation; address campaignInfoImplementation; address treasuryFactory; + address treasuryFactoryImplementation; address campaignInfoFactory; + address campaignInfoFactoryImplementation; address allOrNothingImplementation; // User addresses address deployerAddress; address finalProtocolAdmin; address finalPlatformAdmin; + address platformAdapter; address backer1; address backer2; @@ -56,30 +66,25 @@ contract DeployAllAndSetupAllOrNothing is Script { // Configure parameters based on environment variables function setupParams() internal { // Get customizable values - string memory platformName = vm.envOr( - "PLATFORM_NAME", - string("MiniFunder") - ); + string memory platformName = vm.envOr("PLATFORM_NAME", string("MiniFunder")); 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)); // 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 - ); + 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)); @@ -87,14 +92,8 @@ contract DeployAllAndSetupAllOrNothing is Script { 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) - ); - allOrNothingImplementation = vm.envOr( - "ALL_OR_NOTHING_IMPLEMENTATION_ADDRESS", - address(0) - ); + campaignInfoFactory = vm.envOr("CAMPAIGN_INFO_FACTORY_ADDRESS", address(0)); + allOrNothingImplementation = vm.envOr("ALL_OR_NOTHING_IMPLEMENTATION_ADDRESS", address(0)); console2.log("Using platform hash for:", platformName); console2.log("Protocol fee percent:", protocolFeePercent); @@ -103,6 +102,33 @@ contract DeployAllAndSetupAllOrNothing is Script { 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); + } + + 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(); + } } // Deploy or reuse contracts @@ -110,96 +136,93 @@ contract DeployAllAndSetupAllOrNothing is Script { 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)) { - testToken = address(new TestToken(tokenName, tokenSymbol)); + 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 { + } 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)) { - globalParams = address( - new GlobalParams( - deployerAddress, // Initially deployer is protocol admin - testToken, - protocolFeePercent - ) + (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 deployed at:", globalParams); + console2.log("GlobalParams proxy deployed at:", globalParams); + console2.log(" Implementation:", globalParamsImplementation); } else { console2.log("Reusing GlobalParams at:", globalParams); } - // We need at least TestToken and GlobalParams to continue - require(testToken != address(0), "TestToken address is required"); + // 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(address(this)) - ); - console2.log( - "CampaignInfo implementation deployed at:", - campaignInfoImplementation - ); + campaignInfoImplementation = address(new CampaignInfo()); + console2.log("CampaignInfo implementation deployed at:", campaignInfoImplementation); } // Deploy or reuse TreasuryFactory if (treasuryFactory == address(0)) { - treasuryFactory = address( - new TreasuryFactory(GlobalParams(globalParams)) - ); + // 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 deployed at:", treasuryFactory); + 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)) { - campaignInfoFactory = address( - new CampaignInfoFactory( - GlobalParams(globalParams), - campaignInfoImplementation - ) - ); - CampaignInfoFactory(campaignInfoFactory)._initialize( - treasuryFactory, - globalParams + // 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 deployed and initialized at:", - campaignInfoFactory - ); + console2.log("CampaignInfoFactory proxy deployed at:", campaignInfoFactory); + console2.log(" Implementation:", campaignInfoFactoryImplementation); } else { - console2.log( - "Reusing CampaignInfoFactory at:", - campaignInfoFactory - ); + console2.log("Reusing CampaignInfoFactory at:", campaignInfoFactory); } // Deploy or reuse AllOrNothing implementation if (allOrNothingImplementation == address(0)) { allOrNothingImplementation = address(new AllOrNothing()); allOrNothingDeployed = true; - console2.log( - "AllOrNothing implementation deployed at:", - allOrNothingImplementation - ); + console2.log("AllOrNothing implementation deployed at:", allOrNothingImplementation); } else { - console2.log( - "Reusing AllOrNothing implementation at:", - allOrNothingImplementation - ); + console2.log("Reusing AllOrNothing implementation at:", allOrNothingImplementation); } } @@ -207,9 +230,7 @@ contract DeployAllAndSetupAllOrNothing is Script { 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" - ); + console2.log("Skipping enlistPlatform - using existing GlobalParams"); platformEnlisted = true; return; } @@ -223,7 +244,8 @@ contract DeployAllAndSetupAllOrNothing is Script { GlobalParams(globalParams).enlistPlatform( platformHash, deployerAddress, // Initially deployer is platform admin - platformFeePercent + platformFeePercent, + platformAdapter // Platform adapter (trusted forwarder) - can be set later with setPlatformAdapter ); if (simulate) { @@ -234,11 +256,9 @@ contract DeployAllAndSetupAllOrNothing is Script { } function registerTreasuryImplementation() internal { - // Skip if we didn't deploy TreasuryFactory (assuming it's already set up) - if (!treasuryFactoryDeployed || !allOrNothingDeployed) { - console2.log( - "Skipping registerTreasuryImplementation - using existing contracts" - ); + // Skip only if both TreasuryFactory and implementation are reused (assuming already set up) + if (!treasuryFactoryDeployed && !allOrNothingDeployed) { + console2.log("Skipping registerTreasuryImplementation - using existing contracts"); implementationRegistered = true; return; } @@ -263,11 +283,9 @@ contract DeployAllAndSetupAllOrNothing is Script { } function approveTreasuryImplementation() internal { - // Skip if we didn't deploy TreasuryFactory (assuming it's already set up) - if (!treasuryFactoryDeployed || !allOrNothingDeployed) { - console2.log( - "Skipping approveTreasuryImplementation - using existing contracts" - ); + // Skip only if both TreasuryFactory and implementation are reused (assuming already set up) + if (!treasuryFactoryDeployed && !allOrNothingDeployed) { + console2.log("Skipping approveTreasuryImplementation - using existing contracts"); implementationApproved = true; return; } @@ -311,35 +329,38 @@ contract DeployAllAndSetupAllOrNothing is Script { 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" - ); + 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 - ); + 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 (finalPlatformAdmin != deployerAddress) { - console2.log( - "Updating platform admin address for platform hash:", - vm.toString(platformHash) - ); - GlobalParams(globalParams).updatePlatformAdminAddress( - platformHash, - finalPlatformAdmin - ); + if (simulate) { + vm.stopPrank(); } adminRightsTransferred = true; @@ -352,11 +373,14 @@ contract DeployAllAndSetupAllOrNothing is Script { uint256 deployerKey = vm.envUint("PRIVATE_KEY"); - // Start broadcast with deployer key - vm.startBroadcast(deployerKey); + // 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 @@ -370,70 +394,83 @@ contract DeployAllAndSetupAllOrNothing is Script { // Finally, transfer admin rights to the final addresses transferAdminRights(); - // Stop broadcast - vm.stopBroadcast(); + // Stop broadcast (skip in simulation mode) + if (!simulate) { + vm.stopBroadcast(); + } // Output summary - console2.log("\n--- Deployment & Setup Summary ---"); - console2.log("Platform Name Hash:", vm.toString(platformHash)); - console2.log("TOKEN_ADDRESS:", testToken); - console2.log("GLOBAL_PARAMS_ADDRESS:", globalParams); + 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_ADDRESS:", - campaignInfoImplementation - ); + console2.log("CAMPAIGN_INFO_IMPLEMENTATION:", campaignInfoImplementation); } - console2.log("TREASURY_FACTORY_ADDRESS:", treasuryFactory); - console2.log("CAMPAIGN_INFO_FACTORY_ADDRESS:", campaignInfoFactory); - console2.log( - "ALL_OR_NOTHING_IMPLEMENTATION_ADDRESS:", - allOrNothingImplementation - ); + console2.log("ALL_OR_NOTHING_IMPLEMENTATION:", allOrNothingImplementation); + + 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("Backer1 (tokens minted):", backer1); - } - if (backer2 != address(0) && backer1 != backer2) { - console2.log("Backer2 (tokens minted):", backer2); + console2.log("\n--- Test Backers (Tokens Minted) ---"); + console2.log("Backer1:", backer1); + if (backer2 != address(0) && backer1 != backer2) { + console2.log("Backer2:", backer2); + } } - console2.log("\nDeployment status:"); - console2.log( - "- TestToken:", - testTokenDeployed ? "Newly deployed" : "Reused existing" - ); - console2.log( - "- GlobalParams:", - globalParamsDeployed ? "Newly deployed" : "Reused existing" - ); - console2.log( - "- TreasuryFactory:", - treasuryFactoryDeployed ? "Newly deployed" : "Reused existing" - ); - console2.log( - "- CampaignInfoFactory:", - campaignInfoFactoryDeployed ? "Newly deployed" : "Reused existing" - ); - console2.log( - "- AllOrNothing Implementation:", - allOrNothingDeployed ? "Newly deployed" : "Reused existing" - ); - - console2.log("\nSetup steps:"); - console2.log("1. Platform enlisted:", platformEnlisted); - console2.log( - "2. Treasury implementation registered:", - implementationRegistered - ); - console2.log( - "3. Treasury implementation approved:", - implementationApproved - ); - console2.log("4. Admin rights transferred:", adminRightsTransferred); + 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("\nDeployment and setup completed successfully!"); + console2.log("\n==========================================="); + console2.log("Deployment and setup completed successfully!"); + console2.log("==========================================="); } } diff --git a/script/DeployAllAndSetupKeepWhatsRaised.s.sol b/script/DeployAllAndSetupKeepWhatsRaised.s.sol new file mode 100644 index 00000000..fe349c06 --- /dev/null +++ b/script/DeployAllAndSetupKeepWhatsRaised.s.sol @@ -0,0 +1,472 @@ +// 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 {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.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 keepWhatsRaised + * @dev Updated for the new KeepWhatsRaised contract that stores fees locally + */ +contract DeployAllAndSetupKeepWhatsRaised is DeployBase { + // Customizable values (set through environment variables) + bytes32 platformHash; + uint256 protocolFeePercent; + uint256 platformFeePercent; + uint256 tokenMintAmount; + bool simulate; + uint256 bufferTime; + uint256 campaignLaunchBuffer; + uint256 minimumCampaignDuration; + + // Contract addresses + address testToken; + address globalParams; + address globalParamsImplementation; + address campaignInfo; + address treasuryFactory; + address treasuryFactoryImplementation; + address campaignInfoFactory; + address campaignInfoFactoryImplementation; + address keepWhatsRaisedImplementation; + + // 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 keepWhatsRaisedDeployed = false; + + // Configure parameters based on environment variables + function setupParams() internal { + // Get customizable values + string memory platformName = vm.envOr("PLATFORM_NAME", string("VAKI")); + platformHash = keccak256(abi.encodePacked(platformName)); + protocolFeePercent = vm.envOr("PROTOCOL_FEE_PERCENT", uint256(100)); // Default 1% + platformFeePercent = vm.envOr("PLATFORM_FEE_PERCENT", uint256(600)); // Default 6% + 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)); + + // 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)); + keepWhatsRaisedImplementation = vm.envOr("KEEP_WHATS_RAISED_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); + } + + 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(); + } + } + + // 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)) { + campaignInfo = address(new CampaignInfo()); + console2.log("CampaignInfo deployed at:", campaignInfo); + } + + // 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), + campaignInfo, + 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 KeepWhatsRaised implementation + if (keepWhatsRaisedImplementation == address(0)) { + keepWhatsRaisedImplementation = address(new KeepWhatsRaised()); + keepWhatsRaisedDeployed = true; + console2.log("KeepWhatsRaised implementation deployed at:", keepWhatsRaisedImplementation); + } else { + console2.log("Reusing KeepWhatsRaised implementation at:", keepWhatsRaisedImplementation); + } + } + + // 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 && !keepWhatsRaisedDeployed) { + 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 + keepWhatsRaisedImplementation + ); + + 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 && !keepWhatsRaisedDeployed) { + 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(); + 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 (campaignInfo != address(0)) { + console2.log("CAMPAIGN_INFO_IMPLEMENTATION:", campaignInfo); + } + console2.log("KEEP_WHATS_RAISED_IMPLEMENTATION:", keepWhatsRaisedImplementation); + + 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/DeployAllAndSetupPaymentTreasury.s.sol b/script/DeployAllAndSetupPaymentTreasury.s.sol new file mode 100644 index 00000000..5810e1fb --- /dev/null +++ b/script/DeployAllAndSetupPaymentTreasury.s.sol @@ -0,0 +1,508 @@ +// 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 {PaymentTreasury} from "src/treasuries/PaymentTreasury.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 + */ +contract DeployAllAndSetupPaymentTreasury 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 paymentTreasuryImplementation; + + // User addresses + address deployerAddress; + address finalProtocolAdmin; + address finalPlatformAdmin; + address platformAdapter; + address backer1; + address backer2; + + // Token details + // string tokenName; + // string tokenSymbol; + + // 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 paymentTreasuryDeployed = 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)); + paymentTreasuryImplementation = vm.envOr("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 PaymentTreasury implementation + if (paymentTreasuryImplementation == address(0)) { + paymentTreasuryImplementation = address(new PaymentTreasury()); + paymentTreasuryDeployed = true; + console2.log("PaymentTreasury implementation deployed at:", paymentTreasuryImplementation); + } else { + console2.log("Reusing PaymentTreasury implementation at:", paymentTreasuryImplementation); + } + } + + // 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 && !paymentTreasuryDeployed) { + 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 + paymentTreasuryImplementation + ); + + 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 && !paymentTreasuryDeployed) { + 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("PAYMENT_TREASURY_IMPLEMENTATION:", paymentTreasuryImplementation); + + 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/DeployAllAndSetupTimeConstrainedPaymentTreasury.s.sol b/script/DeployAllAndSetupTimeConstrainedPaymentTreasury.s.sol new file mode 100644 index 00000000..d15e1393 --- /dev/null +++ b/script/DeployAllAndSetupTimeConstrainedPaymentTreasury.s.sol @@ -0,0 +1,516 @@ +// 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 {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaymentTreasury.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 TimeConstrainedPaymentTreasury + */ +contract DeployAllAndSetupTimeConstrainedPaymentTreasury 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 timeConstrainedPaymentTreasuryImplementation; + + // User addresses + address deployerAddress; + address finalProtocolAdmin; + address finalPlatformAdmin; + address platformAdapter; + address backer1; + address backer2; + + // Token details + // string tokenName; + // string tokenSymbol; + + // 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 timeConstrainedPaymentTreasuryDeployed = 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)); + timeConstrainedPaymentTreasuryImplementation = + vm.envOr("TIME_CONSTRAINED_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 TimeConstrainedPaymentTreasury implementation + if (timeConstrainedPaymentTreasuryImplementation == address(0)) { + timeConstrainedPaymentTreasuryImplementation = address(new TimeConstrainedPaymentTreasury()); + timeConstrainedPaymentTreasuryDeployed = true; + console2.log( + "TimeConstrainedPaymentTreasury implementation deployed at:", + timeConstrainedPaymentTreasuryImplementation + ); + } else { + console2.log( + "Reusing TimeConstrainedPaymentTreasury implementation at:", + timeConstrainedPaymentTreasuryImplementation + ); + } + } + + // 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 && !timeConstrainedPaymentTreasuryDeployed) { + 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 + timeConstrainedPaymentTreasuryImplementation + ); + + 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 && !timeConstrainedPaymentTreasuryDeployed) { + 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("TIME_CONSTRAINED_PAYMENT_TREASURY_IMPLEMENTATION:", timeConstrainedPaymentTreasuryImplementation); + + 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/DeployAllOrNothingImplementation.s.sol b/script/DeployAllOrNothingImplementation.s.sol index 7c017a50..2b046be0 100644 --- a/script/DeployAllOrNothingImplementation.s.sol +++ b/script/DeployAllOrNothingImplementation.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; @@ -9,10 +9,7 @@ contract DeployAllOrNothingImplementation is Script { function deploy() public returns (address) { console2.log("Deploying AllOrNothingImplementation..."); AllOrNothing allOrNothingImplementation = new AllOrNothing(); - console2.log( - "AllOrNothingImplementation deployed at:", - address(allOrNothingImplementation) - ); + console2.log("AllOrNothingImplementation deployed at:", address(allOrNothingImplementation)); return address(allOrNothingImplementation); } @@ -30,9 +27,6 @@ contract DeployAllOrNothingImplementation is Script { vm.stopBroadcast(); } - console2.log( - "ALL_OR_NOTHING_IMPLEMENTATION_ADDRESS", - implementationAddress - ); + console2.log("ALL_OR_NOTHING_IMPLEMENTATION_ADDRESS", implementationAddress); } } diff --git a/script/DeployCampaignInfoFactory.s.sol b/script/DeployCampaignInfoFactory.s.sol index cbf00748..c2c5b0d0 100644 --- a/script/DeployCampaignInfoFactory.s.sol +++ b/script/DeployCampaignInfoFactory.s.sol @@ -1,38 +1,44 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {GlobalParams} from "src/GlobalParams.sol"; +import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {DeployBase} from "./lib/DeployBase.s.sol"; contract DeployCampaignInfoFactory is DeployBase { - function deploy( - address globalParams, - address treasuryFactory - ) public returns (address) { + function deploy(address globalParams, address treasuryFactory) public returns (address) { console2.log("Deploying CampaignInfoFactory..."); - // Properly deploy CampaignInfo with direct instantiation - CampaignInfo campaignInfoImpl = new CampaignInfo(address(this)); + address deployer = vm.addr(vm.envUint("PRIVATE_KEY")); + + // Deploy CampaignInfo implementation + CampaignInfo campaignInfoImpl = new CampaignInfo(); address campaignInfo = address(campaignInfoImpl); console2.log("CampaignInfo implementation deployed at:", campaignInfo); - // Create and initialize the factory - CampaignInfoFactory campaignInfoFactory = new CampaignInfoFactory( - GlobalParams(globalParams), - campaignInfo + // Deploy CampaignInfoFactory implementation + CampaignInfoFactory factoryImplementation = new CampaignInfoFactory(); + console2.log("CampaignInfoFactory implementation deployed at:", address(factoryImplementation)); + + // Prepare initialization data + bytes memory initData = abi.encodeWithSelector( + CampaignInfoFactory.initialize.selector, + deployer, + IGlobalParams(globalParams), + campaignInfo, + treasuryFactory ); - campaignInfoFactory._initialize(treasuryFactory, globalParams); + // Deploy proxy + ERC1967Proxy proxy = new ERC1967Proxy(address(factoryImplementation), initData); - console2.log( - "CampaignInfoFactory deployed and initialized at:", - address(campaignInfoFactory) - ); - return address(campaignInfoFactory); + console2.log("CampaignInfoFactory proxy deployed and initialized at:", address(proxy)); + return address(proxy); } function run() external { diff --git a/script/DeployCampaignInfoImplementation.s.sol b/script/DeployCampaignInfoImplementation.s.sol index dd4f4dc6..85b44fc3 100644 --- a/script/DeployCampaignInfoImplementation.s.sol +++ b/script/DeployCampaignInfoImplementation.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; @@ -10,11 +10,8 @@ contract DeployCampaignInfoImplementation is Script { console2.log("Deploying CampaignInfo implementation..."); // Implementation will use the script address as admin, but this will be replaced // when the factory creates new instances - CampaignInfo campaignInfo = new CampaignInfo(address(this)); - console2.log( - "CampaignInfo implementation deployed at:", - address(campaignInfo) - ); + CampaignInfo campaignInfo = new CampaignInfo(); + console2.log("CampaignInfo implementation deployed at:", address(campaignInfo)); return address(campaignInfo); } diff --git a/script/DeployGlobalParams.s.sol b/script/DeployGlobalParams.s.sol index 185d3856..525558db 100644 --- a/script/DeployGlobalParams.s.sol +++ b/script/DeployGlobalParams.s.sol @@ -1,13 +1,27 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {GlobalParams} from "../src/GlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {DeployBase} from "./lib/DeployBase.s.sol"; contract DeployGlobalParams is DeployBase { function deployWithToken(address token) public returns (address) { address deployer = vm.addr(vm.envUint("PRIVATE_KEY")); - return address(new GlobalParams(deployer, token, 200)); + + (bytes32[] memory currencies, address[][] memory tokensPerCurrency) = loadCurrenciesAndTokens(token); + + // Deploy implementation + GlobalParams implementation = new GlobalParams(); + + // Prepare initialization data + bytes memory initData = + abi.encodeWithSelector(GlobalParams.initialize.selector, deployer, 200, currencies, tokensPerCurrency); + + // Deploy proxy + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); + + return address(proxy); } function deploy() public returns (address) { @@ -18,7 +32,20 @@ contract DeployGlobalParams is DeployBase { address deployer = vm.addr(vm.envUint("PRIVATE_KEY")); address token = vm.envOr("TOKEN_ADDRESS", address(0)); require(token != address(0), "TestToken address must be set"); - return address(new GlobalParams(deployer, token, 200)); + + (bytes32[] memory currencies, address[][] memory tokensPerCurrency) = loadCurrenciesAndTokens(token); + + // Deploy implementation + GlobalParams implementation = new GlobalParams(); + + // Prepare initialization data + bytes memory initData = + abi.encodeWithSelector(GlobalParams.initialize.selector, deployer, 200, currencies, tokensPerCurrency); + + // Deploy proxy + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); + + return address(proxy); } function run() external { diff --git a/script/DeployKeepWhatsRaisedImplementation.s.sol b/script/DeployKeepWhatsRaisedImplementation.s.sol new file mode 100644 index 00000000..79f4593d --- /dev/null +++ b/script/DeployKeepWhatsRaisedImplementation.s.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; + +contract DeployKeepWhatsRaisedImplementation is Script { + function deploy() public returns (address) { + console2.log("Deploying KeepWhatsRaisedImplementation..."); + KeepWhatsRaised keepWhatsRaisedImplementation = new KeepWhatsRaised(); + console2.log("KeepWhatsRaisedImplementation deployed at:", address(keepWhatsRaisedImplementation)); + return address(keepWhatsRaisedImplementation); + } + + 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("KEEP_WHATS_RAISED_IMPLEMENTATION_ADDRESS", implementationAddress); + } +} diff --git a/script/DeployPaymentTreasuryImplementation.s.sol b/script/DeployPaymentTreasuryImplementation.s.sol new file mode 100644 index 00000000..75457978 --- /dev/null +++ b/script/DeployPaymentTreasuryImplementation.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 {PaymentTreasury} from "src/treasuries/PaymentTreasury.sol"; + +contract DeployPaymentTreasuryImplementation is Script { + function deploy() public returns (address) { + console2.log("Deploying PaymentTreasuryImplementation..."); + PaymentTreasury paymentTreasuryImplementation = new PaymentTreasury(); + console2.log("PaymentTreasuryImplementation deployed at:", address(paymentTreasuryImplementation)); + return address(paymentTreasuryImplementation); + } + + 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("PAYMENT_TREASURY_IMPLEMENTATION_ADDRESS", implementationAddress); + } +} + diff --git a/script/DeployTestToken.s.sol b/script/DeployTestToken.s.sol index 93d6d072..7628c808 100644 --- a/script/DeployTestToken.s.sol +++ b/script/DeployTestToken.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {TestToken} from "../test/mocks/TestToken.sol"; import {DeployBase} from "./lib/DeployBase.s.sol"; @@ -12,7 +12,8 @@ contract DeployTestToken is DeployBase { function _deploy() internal returns (address) { string memory tokenName = vm.envOr("TOKEN_NAME", string("TestToken")); string memory tokenSymbol = vm.envOr("TOKEN_SYMBOL", string("TST")); - return address(new TestToken(tokenName, tokenSymbol)); + uint8 decimals = uint8(vm.envOr("TOKEN_DECIMALS", uint256(18))); + return address(new TestToken(tokenName, tokenSymbol, decimals)); } function run() external { diff --git a/script/DeployTimeConstrainedPaymentTreasuryImplementation.s.sol b/script/DeployTimeConstrainedPaymentTreasuryImplementation.s.sol new file mode 100644 index 00000000..d49bd851 --- /dev/null +++ b/script/DeployTimeConstrainedPaymentTreasuryImplementation.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 {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaymentTreasury.sol"; + +contract DeployTimeConstrainedPaymentTreasuryImplementation is Script { + function deploy() public returns (address) { + console2.log("Deploying TimeConstrainedPaymentTreasuryImplementation..."); + TimeConstrainedPaymentTreasury timeConstrainedPaymentTreasuryImplementation = new TimeConstrainedPaymentTreasury(); + console2.log("TimeConstrainedPaymentTreasuryImplementation deployed at:", address(timeConstrainedPaymentTreasuryImplementation)); + return address(timeConstrainedPaymentTreasuryImplementation); + } + + 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("TIME_CONSTRAINED_PAYMENT_TREASURY_IMPLEMENTATION_ADDRESS", implementationAddress); + } +} + diff --git a/script/DeployTreasuryFactory.s.sol b/script/DeployTreasuryFactory.s.sol index 4689147f..d62142b9 100644 --- a/script/DeployTreasuryFactory.s.sol +++ b/script/DeployTreasuryFactory.s.sol @@ -1,14 +1,27 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {TreasuryFactory} from "../src/TreasuryFactory.sol"; import {GlobalParams} from "../src/GlobalParams.sol"; +import {IGlobalParams} from "../src/interfaces/IGlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {DeployBase} from "./lib/DeployBase.s.sol"; contract DeployTreasuryFactory is DeployBase { function deploy(address _globalParams) public returns (address) { require(_globalParams != address(0), "GlobalParams not set"); - return address(new TreasuryFactory(GlobalParams(_globalParams))); + + // Deploy implementation + TreasuryFactory implementation = new TreasuryFactory(); + + // Prepare initialization data + bytes memory initData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(_globalParams)); + + // Deploy proxy + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); + + return address(proxy); } function run() external { diff --git a/script/UpgradeCampaignInfoFactory.s.sol b/script/UpgradeCampaignInfoFactory.s.sol new file mode 100644 index 00000000..7fa95100 --- /dev/null +++ b/script/UpgradeCampaignInfoFactory.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {CampaignInfoFactory} from "../src/CampaignInfoFactory.sol"; + +/** + * @title UpgradeCampaignInfoFactory + * @notice Script to upgrade the CampaignInfoFactory implementation contract + * @dev Uses UUPS upgrade pattern + */ +contract UpgradeCampaignInfoFactory is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address proxyAddress = vm.envAddress("CAMPAIGN_INFO_FACTORY_ADDRESS"); + + require(proxyAddress != address(0), "Proxy address must be set"); + + vm.startBroadcast(deployerKey); + + // Deploy new implementation + CampaignInfoFactory newImplementation = new CampaignInfoFactory(); + console2.log("New CampaignInfoFactory implementation deployed at:", address(newImplementation)); + + // Upgrade the proxy to point to the new implementation + CampaignInfoFactory proxy = CampaignInfoFactory(proxyAddress); + proxy.upgradeToAndCall(address(newImplementation), ""); + + console2.log("CampaignInfoFactory proxy upgraded successfully"); + console2.log("Proxy address:", proxyAddress); + console2.log("New implementation address:", address(newImplementation)); + + vm.stopBroadcast(); + } +} diff --git a/script/UpgradeGlobalParams.s.sol b/script/UpgradeGlobalParams.s.sol new file mode 100644 index 00000000..0400f5fe --- /dev/null +++ b/script/UpgradeGlobalParams.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {GlobalParams} from "../src/GlobalParams.sol"; + +/** + * @title UpgradeGlobalParams + * @notice Script to upgrade the GlobalParams implementation contract + * @dev Uses UUPS upgrade pattern + */ +contract UpgradeGlobalParams is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address proxyAddress = vm.envAddress("GLOBAL_PARAMS_ADDRESS"); + + require(proxyAddress != address(0), "Proxy address must be set"); + + vm.startBroadcast(deployerKey); + + // Deploy new implementation + GlobalParams newImplementation = new GlobalParams(); + console2.log("New GlobalParams implementation deployed at:", address(newImplementation)); + + // Upgrade the proxy to point to the new implementation + GlobalParams proxy = GlobalParams(proxyAddress); + proxy.upgradeToAndCall(address(newImplementation), ""); + + console2.log("GlobalParams proxy upgraded successfully"); + console2.log("Proxy address:", proxyAddress); + console2.log("New implementation address:", address(newImplementation)); + + vm.stopBroadcast(); + } +} diff --git a/script/UpgradeTreasuryFactory.s.sol b/script/UpgradeTreasuryFactory.s.sol new file mode 100644 index 00000000..f8859700 --- /dev/null +++ b/script/UpgradeTreasuryFactory.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {TreasuryFactory} from "../src/TreasuryFactory.sol"; + +/** + * @title UpgradeTreasuryFactory + * @notice Script to upgrade the TreasuryFactory implementation contract + * @dev Uses UUPS upgrade pattern + */ +contract UpgradeTreasuryFactory is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address proxyAddress = vm.envAddress("TREASURY_FACTORY_ADDRESS"); + + require(proxyAddress != address(0), "Proxy address must be set"); + + vm.startBroadcast(deployerKey); + + // Deploy new implementation + TreasuryFactory newImplementation = new TreasuryFactory(); + console2.log("New TreasuryFactory implementation deployed at:", address(newImplementation)); + + // Upgrade the proxy to point to the new implementation + TreasuryFactory proxy = TreasuryFactory(proxyAddress); + proxy.upgradeToAndCall(address(newImplementation), ""); + + console2.log("TreasuryFactory proxy upgraded successfully"); + console2.log("Proxy address:", proxyAddress); + console2.log("New implementation address:", address(newImplementation)); + + vm.stopBroadcast(); + } +} diff --git a/script/lib/DeployBase.s.sol b/script/lib/DeployBase.s.sol index e3b38fc7..a61ebe50 100644 --- a/script/lib/DeployBase.s.sol +++ b/script/lib/DeployBase.s.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; contract DeployBase is Script { - function deployOrUse( - string memory envVar, - function() internal returns (address) deployFn - ) internal returns (address deployedOrExisting) { + function deployOrUse(string memory envVar, function() internal returns (address) deployFn) + internal + returns (address deployedOrExisting) + { address existing = vm.envOr(envVar, address(0)); if (existing != address(0)) { console2.log(envVar, "Using existing contract at:", existing); @@ -18,4 +18,146 @@ contract DeployBase is Script { deployedOrExisting = deployFn(); console2.log(envVar, "Deployed new contract at:", deployedOrExisting); } + + /** + * @notice Checks if TestToken deployment should be skipped + * @dev TestToken is only needed when CURRENCIES is not provided (defaults to USD) + * @return true if TestToken should be deployed, false otherwise + */ + function shouldDeployTestToken() internal returns (bool) { + string memory currenciesConfig = vm.envOr("CURRENCIES", string("")); + return bytes(currenciesConfig).length == 0; + } + + function loadCurrenciesAndTokens(address defaultToken) + internal + returns (bytes32[] memory currencies, address[][] memory tokensPerCurrency) + { + string memory currenciesConfig = vm.envOr("CURRENCIES", string("")); + if (bytes(currenciesConfig).length == 0) { + currencies = new bytes32[](1); + currencies[0] = _toCurrencyKey("USD"); + + tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](1); + tokensPerCurrency[0][0] = defaultToken; + return (currencies, tokensPerCurrency); + } + + string memory tokensConfig = vm.envOr("TOKENS_PER_CURRENCY", string("")); + require(bytes(tokensConfig).length != 0, "TOKENS_PER_CURRENCY env must be set"); + + string[] memory currencyStrings = _split(currenciesConfig, ","); + string[] memory perCurrencyConfigs = _split(tokensConfig, ";"); + require(currencyStrings.length == perCurrencyConfigs.length, "TOKENS_PER_CURRENCY length mismatch"); + + currencies = new bytes32[](currencyStrings.length); + for (uint256 i = 0; i < currencyStrings.length; i++) { + string memory currency = _trimWhitespace(currencyStrings[i]); + require(bytes(currency).length != 0, "Currency value empty"); + currencies[i] = _toCurrencyKey(currency); + } + + tokensPerCurrency = new address[][](perCurrencyConfigs.length); + for (uint256 i = 0; i < perCurrencyConfigs.length; i++) { + string[] memory tokenStrings = _split(perCurrencyConfigs[i], ","); + require(tokenStrings.length > 0, "Currency must have at least one token"); + + tokensPerCurrency[i] = new address[](tokenStrings.length); + for (uint256 j = 0; j < tokenStrings.length; j++) { + string memory tokenString = _trimWhitespace(tokenStrings[j]); + require(bytes(tokenString).length != 0, "Token address string empty"); + + address tokenAddress = vm.parseAddress(tokenString); + require(tokenAddress != address(0), "Token address cannot be zero"); + tokensPerCurrency[i][j] = tokenAddress; + } + } + } + + function _split(string memory input, string memory delimiter) internal pure returns (string[] memory) { + bytes memory inputBytes = bytes(input); + bytes memory delimiterBytes = bytes(delimiter); + require(delimiterBytes.length == 1, "Delimiter must be a single character"); + + if (inputBytes.length == 0) { + string[] memory empty = new string[](1); + empty[0] = ""; + return empty; + } + + uint256 parts = 1; + for (uint256 i = 0; i < inputBytes.length; i++) { + if (inputBytes[i] == delimiterBytes[0]) { + unchecked { + ++parts; + } + } + } + + string[] memory output = new string[](parts); + uint256 lastIndex = 0; + uint256 partIndex = 0; + + for (uint256 i = 0; i <= inputBytes.length; i++) { + if (i == inputBytes.length || inputBytes[i] == delimiterBytes[0]) { + output[partIndex] = _substring(inputBytes, lastIndex, i); + unchecked { + ++partIndex; + } + lastIndex = i + 1; + } + } + + return output; + } + + function _substring(bytes memory input, uint256 start, uint256 end) internal pure returns (string memory) { + require(end >= start && end <= input.length, "Invalid substring range"); + + bytes memory result = new bytes(end - start); + for (uint256 i = start; i < end; i++) { + result[i - start] = input[i]; + } + return string(result); + } + + function _trimWhitespace(string memory input) internal pure returns (string memory) { + bytes memory inputBytes = bytes(input); + uint256 start = 0; + uint256 end = inputBytes.length; + + while (start < end && _isWhitespace(inputBytes[start])) { + unchecked { + ++start; + } + } + + while (end > start && _isWhitespace(inputBytes[end - 1])) { + unchecked { + --end; + } + } + + if (start == 0 && end == inputBytes.length) { + return input; + } + + return _substring(inputBytes, start, end); + } + + function _isWhitespace(bytes1 char) private pure returns (bool) { + return char == 0x20 /* space */ || char == 0x09 /* tab */ || char == 0x0A /* line feed */ || char == 0x0D; /* carriage return */ + } + + function _toCurrencyKey(string memory currency) internal pure returns (bytes32) { + bytes memory currencyBytes = bytes(currency); + require(currencyBytes.length <= 32, "Currency too long"); + + bytes32 key; + assembly { + key := mload(add(currency, 32)) + } + return key; + } } diff --git a/src/CampaignInfo.sol b/src/CampaignInfo.sol index 9dc4e9af..cd650eb1 100644 --- a/src/CampaignInfo.sol +++ b/src/CampaignInfo.sol @@ -1,17 +1,22 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {ICampaignInfo} from "./interfaces/ICampaignInfo.sol"; import {ICampaignData} from "./interfaces/ICampaignData.sol"; import {ICampaignTreasury} from "./interfaces/ICampaignTreasury.sol"; +import {ICampaignPaymentTreasury} from "./interfaces/ICampaignPaymentTreasury.sol"; import {IGlobalParams} from "./interfaces/IGlobalParams.sol"; import {TimestampChecker} from "./utils/TimestampChecker.sol"; import {AdminAccessChecker} from "./utils/AdminAccessChecker.sol"; import {PausableCancellable} from "./utils/PausableCancellable.sol"; +import {PledgeNFT} from "./utils/PledgeNFT.sol"; +import {Counters} from "./utils/Counters.sol"; +import {DataRegistryKeys} from "./constants/DataRegistryKeys.sol"; /** * @title CampaignInfo @@ -24,8 +29,11 @@ contract CampaignInfo is PausableCancellable, TimestampChecker, AdminAccessChecker, + PledgeNFT, Initializable { + using Counters for Counters.Counter; + CampaignData private s_campaignData; mapping(bytes32 => address) private s_platformTreasuryAddress; @@ -36,14 +44,25 @@ contract CampaignInfo is bytes32[] private s_approvedPlatformHashes; - function getApprovedPlatformHashes() - external - view - returns (bytes32[] memory) - { + // Multi-token support + address[] private s_acceptedTokens; // Accepted tokens for this campaign + mapping(address => bool) private s_isAcceptedToken; // O(1) token validation + + // Lock mechanism - prevents certain operations after treasury deployment + bool private s_isLocked; + + function getApprovedPlatformHashes() external view returns (bytes32[] memory) { return s_approvedPlatformHashes; } + /** + * @dev Returns whether the campaign is locked (after treasury deployment). + * @return True if the campaign is locked, false otherwise. + */ + function isLocked() external view override returns (bool) { + return s_isLocked; + } + /** * @dev Emitted when the launch time of the campaign is updated. * @param newLaunchTime The new launch time. @@ -67,30 +86,21 @@ contract CampaignInfo is * @param platformHash The bytes32 identifier of the platform. * @param selection The new selection state. */ - event CampaignInfoSelectedPlatformUpdated( - bytes32 indexed platformHash, - bool selection - ); + event CampaignInfoSelectedPlatformUpdated(bytes32 indexed platformHash, bool selection); /** * @dev Emitted when platform information is updated for the campaign. * @param platformHash The bytes32 identifier of the platform. * @param platformTreasury The address of the platform's treasury. */ - event CampaignInfoPlatformInfoUpdated( - bytes32 indexed platformHash, - address indexed platformTreasury - ); + event CampaignInfoPlatformInfoUpdated(bytes32 indexed platformHash, address indexed platformTreasury); /** * @dev Emitted when an invalid platform update is attempted. * @param platformHash The bytes32 identifier of the platform. * @param selection The selection state (true/false). */ - error CampaignInfoInvalidPlatformUpdate( - bytes32 platformHash, - bool selection - ); + error CampaignInfoInvalidPlatformUpdate(bytes32 platformHash, bool selection); /** * @dev Emitted when an unauthorized action is attempted. @@ -114,7 +124,27 @@ contract CampaignInfo is */ error CampaignInfoPlatformAlreadyApproved(bytes32 platformHash); - constructor(address creator) Ownable(creator) {} + /** + * @dev Emitted when an operation is attempted on a locked campaign. + */ + error CampaignInfoIsLocked(); + + /** + * @dev Modifier that checks if the campaign is not locked. + */ + modifier whenNotLocked() { + if (s_isLocked) { + revert CampaignInfoIsLocked(); + } + _; + } + + /** + * @notice Constructor passes empty strings to ERC721 + */ + constructor() Ownable(_msgSender()) ERC721("", "") { + _disableInitializers(); + } function initialize( address creator, @@ -122,53 +152,56 @@ contract CampaignInfo is bytes32[] calldata selectedPlatformHash, bytes32[] calldata platformDataKey, bytes32[] calldata platformDataValue, - CampaignData calldata campaignData + CampaignData calldata campaignData, + address[] calldata acceptedTokens, + string calldata nftName, + string calldata nftSymbol, + string calldata nftImageURI, + string calldata nftContractURI ) external initializer { __AccessChecker_init(globalParams); _transferOwnership(creator); s_campaignData = campaignData; + + // Store accepted tokens + uint256 tokenLen = acceptedTokens.length; + for (uint256 i = 0; i < tokenLen; ++i) { + address token = acceptedTokens[i]; + s_acceptedTokens.push(token); + s_isAcceptedToken[token] = true; + } + uint256 len = selectedPlatformHash.length; for (uint256 i = 0; i < len; ++i) { - s_platformFeePercent[selectedPlatformHash[i]] = GLOBAL_PARAMS - .getPlatformFeePercent(selectedPlatformHash[i]); + s_platformFeePercent[selectedPlatformHash[i]] = + _getGlobalParams().getPlatformFeePercent(selectedPlatformHash[i]); s_isSelectedPlatform[selectedPlatformHash[i]] = true; } len = platformDataKey.length; - bool isValid; for (uint256 i = 0; i < len; ++i) { - isValid = GLOBAL_PARAMS.checkIfPlatformDataKeyValid( - platformDataKey[i] - ); - if (!isValid) { - revert CampaignInfoInvalidInput(); - } s_platformData[platformDataKey[i]] = platformDataValue[i]; } + + // Initialize NFT metadata + _initializeNFT(nftName, nftSymbol, nftImageURI, nftContractURI); } struct Config { address treasuryFactory; - address token; uint256 protocolFeePercent; bytes32 identifierHash; } function getCampaignConfig() public view returns (Config memory config) { bytes memory args = Clones.fetchCloneArgs(address(this)); - ( - config.treasuryFactory, - config.token, - config.protocolFeePercent, - config.identifierHash - ) = abi.decode(args, (address, address, uint256, bytes32)); + (config.treasuryFactory, config.protocolFeePercent, config.identifierHash) = + abi.decode(args, (address, uint256, bytes32)); } /** * @inheritdoc ICampaignInfo */ - function checkIfPlatformSelected( - bytes32 platformHash - ) public view override returns (bool) { + function checkIfPlatformSelected(bytes32 platformHash) public view override returns (bool) { return s_isSelectedPlatform[platformHash]; } @@ -177,21 +210,14 @@ contract CampaignInfo is * @param platformHash The bytes32 identifier of the platform. * @return True if the platform is already approved, false otherwise. */ - function checkIfPlatformApproved( - bytes32 platformHash - ) public view returns (bool) { + function checkIfPlatformApproved(bytes32 platformHash) public view returns (bool) { return s_isApprovedPlatform[platformHash]; } /** * @inheritdoc ICampaignInfo */ - function owner() - public - view - override(ICampaignInfo, Ownable) - returns (address account) - { + function owner() public view override(ICampaignInfo, Ownable) returns (address account) { account = super.owner(); } @@ -199,13 +225,61 @@ contract CampaignInfo is * @inheritdoc ICampaignInfo */ function getProtocolAdminAddress() public view override returns (address) { - return GLOBAL_PARAMS.getProtocolAdminAddress(); + return _getGlobalParams().getProtocolAdminAddress(); } /** * @inheritdoc ICampaignInfo */ function getTotalRaisedAmount() external view override returns (uint256) { + bytes32[] memory tempPlatforms = s_approvedPlatformHashes; + uint256 length = s_approvedPlatformHashes.length; + uint256 amount; + address tempTreasury; + for (uint256 i = 0; i < length; i++) { + tempTreasury = s_platformTreasuryAddress[tempPlatforms[i]]; + // Skip cancelled treasuries + if (!ICampaignTreasury(tempTreasury).cancelled()) { + amount += ICampaignTreasury(tempTreasury).getRaisedAmount(); + } + } + return amount; + } + + /** + * @inheritdoc ICampaignInfo + */ + function getTotalLifetimeRaisedAmount() external view returns (uint256) { + bytes32[] memory tempPlatforms = s_approvedPlatformHashes; + uint256 length = s_approvedPlatformHashes.length; + uint256 amount; + address tempTreasury; + for (uint256 i = 0; i < length; i++) { + tempTreasury = s_platformTreasuryAddress[tempPlatforms[i]]; + amount += ICampaignTreasury(tempTreasury).getLifetimeRaisedAmount(); + } + return amount; + } + + /** + * @inheritdoc ICampaignInfo + */ + function getTotalRefundedAmount() external view returns (uint256) { + bytes32[] memory tempPlatforms = s_approvedPlatformHashes; + uint256 length = s_approvedPlatformHashes.length; + uint256 amount; + address tempTreasury; + for (uint256 i = 0; i < length; i++) { + tempTreasury = s_platformTreasuryAddress[tempPlatforms[i]]; + amount += ICampaignTreasury(tempTreasury).getRefundedAmount(); + } + return amount; + } + + /** + * @inheritdoc ICampaignInfo + */ + function getTotalAvailableRaisedAmount() external view returns (uint256) { bytes32[] memory tempPlatforms = s_approvedPlatformHashes; uint256 length = s_approvedPlatformHashes.length; uint256 amount; @@ -220,10 +294,46 @@ contract CampaignInfo is /** * @inheritdoc ICampaignInfo */ - function getPlatformAdminAddress( - bytes32 platformHash - ) external view override returns (address) { - return GLOBAL_PARAMS.getPlatformAdminAddress(platformHash); + function getTotalCancelledAmount() external view returns (uint256) { + bytes32[] memory tempPlatforms = s_approvedPlatformHashes; + uint256 length = s_approvedPlatformHashes.length; + uint256 amount; + address tempTreasury; + for (uint256 i = 0; i < length; i++) { + tempTreasury = s_platformTreasuryAddress[tempPlatforms[i]]; + // Only include cancelled treasuries + if (ICampaignTreasury(tempTreasury).cancelled()) { + amount += ICampaignTreasury(tempTreasury).getRaisedAmount(); + } + } + return amount; + } + + /** + * @inheritdoc ICampaignInfo + */ + function getTotalExpectedAmount() external view returns (uint256) { + bytes32[] memory tempPlatforms = s_approvedPlatformHashes; + uint256 length = s_approvedPlatformHashes.length; + uint256 amount; + address tempTreasury; + for (uint256 i = 0; i < length; i++) { + tempTreasury = s_platformTreasuryAddress[tempPlatforms[i]]; + // Try to call getExpectedAmount - will only work for payment treasuries + try ICampaignPaymentTreasury(tempTreasury).getExpectedAmount() returns (uint256 expectedAmount) { + amount += expectedAmount; + } catch { + // Not a payment treasury or call failed, skip + } + } + return amount; + } + + /** + * @inheritdoc ICampaignInfo + */ + function getPlatformAdminAddress(bytes32 platformHash) external view override returns (address) { + return _getGlobalParams().getPlatformAdminAddress(platformHash); } /** @@ -250,58 +360,64 @@ contract CampaignInfo is /** * @inheritdoc ICampaignInfo */ - function getTokenAddress() external view override returns (address) { + function getProtocolFeePercent() external view override returns (uint256) { Config memory config = getCampaignConfig(); - return config.token; + return config.protocolFeePercent; } /** * @inheritdoc ICampaignInfo */ - function getProtocolFeePercent() external view override returns (uint256) { - Config memory config = getCampaignConfig(); - return config.protocolFeePercent; + function getCampaignCurrency() external view override returns (bytes32) { + return s_campaignData.currency; } /** * @inheritdoc ICampaignInfo */ - function paused() - public - view - override(ICampaignInfo, PausableCancellable) - returns (bool) - { + function getAcceptedTokens() external view override returns (address[] memory) { + return s_acceptedTokens; + } + + /** + * @inheritdoc ICampaignInfo + */ + function isTokenAccepted(address token) external view override returns (bool) { + return s_isAcceptedToken[token]; + } + + /** + * @inheritdoc ICampaignInfo + */ + function paused() public view override(ICampaignInfo, PausableCancellable) returns (bool) { return super.paused(); } /** * @inheritdoc ICampaignInfo */ - function cancelled() - public - view - override(ICampaignInfo, PausableCancellable) - returns (bool) - { + function cancelled() public view override(ICampaignInfo, PausableCancellable) returns (bool) { return super.cancelled(); } /** * @inheritdoc ICampaignInfo */ - function getPlatformFeePercent( - bytes32 platformHash - ) external view override returns (uint256) { + function getPlatformFeePercent(bytes32 platformHash) external view override returns (uint256) { return s_platformFeePercent[platformHash]; } /** * @inheritdoc ICampaignInfo */ - function getPlatformData( - bytes32 platformDataKey - ) external view override returns (bytes32) { + function getPlatformClaimDelay(bytes32 platformHash) external view override returns (uint256) { + return _getGlobalParams().getPlatformClaimDelay(platformHash); + } + + /** + * @inheritdoc ICampaignInfo + */ + function getPlatformData(bytes32 platformDataKey) external view override returns (bytes32) { bytes32 platformDataValue = s_platformData[platformDataKey]; if (platformDataValue == bytes32(0)) { revert CampaignInfoInvalidInput(); @@ -317,12 +433,44 @@ contract CampaignInfo is return config.identifierHash; } + /** + * @inheritdoc ICampaignInfo + */ + function getDataFromRegistry(bytes32 key) external view override returns (bytes32 value) { + return _getGlobalParams().getFromRegistry(key); + } + + /** + * @inheritdoc ICampaignInfo + */ + function getBufferTime() external view override returns (uint256 bufferTime) { + bytes32 valueBytes = _getGlobalParams().getFromRegistry(DataRegistryKeys.BUFFER_TIME); + bufferTime = uint256(valueBytes); + } + + /** + * @inheritdoc ICampaignInfo + */ + function getLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + override + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ) + { + return _getGlobalParams().getPlatformLineItemType(platformHash, typeId); + } + /** * @inheritdoc Ownable */ - function transferOwnership( - address newOwner - ) + function transferOwnership(address newOwner) public override(ICampaignInfo, Ownable) onlyOwner @@ -335,19 +483,24 @@ contract CampaignInfo is /** * @inheritdoc ICampaignInfo */ - function updateLaunchTime( - uint256 launchTime - ) + function updateLaunchTime(uint256 launchTime) external override onlyOwner - currentTimeIsLess(getLaunchTime()) whenNotPaused whenNotCancelled + whenNotLocked { - if (launchTime < block.timestamp || getDeadline() <= launchTime) { + uint256 deadline = getDeadline(); + uint256 minimumCampaignDuration = + uint256(_getGlobalParams().getFromRegistry(DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION)); + + // Ensure launch time is not in the past and deadline still meets minimum duration requirement + // Allow moving launch time closer to current time as long as minimum duration is maintained + if (launchTime < block.timestamp || deadline <= launchTime || deadline < launchTime + minimumCampaignDuration) { revert CampaignInfoInvalidInput(); } + s_campaignData.launchTime = launchTime; emit CampaignInfoLaunchTimeUpdated(launchTime); } @@ -355,17 +508,19 @@ contract CampaignInfo is /** * @inheritdoc ICampaignInfo */ - function updateDeadline( - uint256 deadline - ) + function updateDeadline(uint256 deadline) external override onlyOwner - currentTimeIsLess(getLaunchTime()) whenNotPaused whenNotCancelled + whenNotLocked { - if (deadline <= getLaunchTime()) { + uint256 launchTime = getLaunchTime(); + uint256 minimumCampaignDuration = + uint256(_getGlobalParams().getFromRegistry(DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION)); + + if (deadline <= launchTime || deadline < launchTime + minimumCampaignDuration) { revert CampaignInfoInvalidInput(); } @@ -376,15 +531,13 @@ contract CampaignInfo is /** * @inheritdoc ICampaignInfo */ - function updateGoalAmount( - uint256 goalAmount - ) + function updateGoalAmount(uint256 goalAmount) external override onlyOwner - currentTimeIsLess(getLaunchTime()) whenNotPaused whenNotCancelled + whenNotLocked { if (goalAmount == 0) { revert CampaignInfoInvalidInput(); @@ -398,32 +551,48 @@ contract CampaignInfo is */ function updateSelectedPlatform( bytes32 platformHash, - bool selection - ) - external - override - onlyOwner - currentTimeIsLess(getLaunchTime()) - whenNotPaused - whenNotCancelled - { + bool selection, + bytes32[] calldata platformDataKey, + bytes32[] calldata platformDataValue + ) external override onlyOwner currentTimeIsLess(getLaunchTime()) whenNotPaused whenNotCancelled { if (checkIfPlatformSelected(platformHash) == selection) { revert CampaignInfoInvalidInput(); } - if (!GLOBAL_PARAMS.checkIfPlatformIsListed(platformHash)) { + + IGlobalParams globalParams = _getGlobalParams(); + + if (!globalParams.checkIfPlatformIsListed(platformHash)) { revert CampaignInfoInvalidPlatformUpdate(platformHash, selection); } - if (!selection && checkIfPlatformApproved(platformHash)) { revert CampaignInfoPlatformAlreadyApproved(platformHash); } + if (platformDataKey.length != platformDataValue.length) { + revert CampaignInfoInvalidInput(); + } + + if (selection) { + bool isValid; + for (uint256 i = 0; i < platformDataKey.length; i++) { + isValid = globalParams.checkIfPlatformDataKeyValid(platformDataKey[i]); + if (!isValid) { + revert CampaignInfoInvalidInput(); + } + if (platformDataValue[i] == bytes32(0)) { + revert CampaignInfoInvalidInput(); + } + + s_platformData[platformDataKey[i]] = platformDataValue[i]; + } + } + s_isSelectedPlatform[platformHash] = selection; if (selection) { - s_platformFeePercent[platformHash] = GLOBAL_PARAMS - .getPlatformFeePercent(platformHash); + s_platformFeePercent[platformHash] = globalParams.getPlatformFeePercent(platformHash); } else { s_platformFeePercent[platformHash] = 0; } + emit CampaignInfoSelectedPlatformUpdated(platformHash, selection); } @@ -445,23 +614,65 @@ contract CampaignInfo is * @dev External function to cancel the campaign. */ function _cancelCampaign(bytes32 message) external { - if (msg.sender != getProtocolAdminAddress() && msg.sender != owner()) { + if (_msgSender() != getProtocolAdminAddress() && _msgSender() != owner()) { revert CampaignInfoUnauthorized(); } _cancel(message); } /** - * @dev Sets platform information for the campaign. + * @notice Sets the image URI for NFT metadata + * @dev Can only be updated before campaign launch + * @param newImageURI The new image URI + */ + function setImageURI(string calldata newImageURI) + external + override(ICampaignInfo, PledgeNFT) + onlyOwner + currentTimeIsLess(getLaunchTime()) + { + s_imageURI = newImageURI; + emit ImageURIUpdated(newImageURI); + } + + /** + * @notice Updates the contract-level metadata URI + * @dev Can only be updated before campaign launch + * @param newContractURI The new contract URI + */ + function updateContractURI(string calldata newContractURI) + external + override(ICampaignInfo, PledgeNFT) + onlyOwner + currentTimeIsLess(getLaunchTime()) + { + s_contractURI = newContractURI; + emit ContractURIUpdated(newContractURI); + } + + function mintNFTForPledge( + address backer, + bytes32 reward, + address tokenAddress, + uint256 amount, + uint256 shippingFee, + uint256 tipAmount + ) public override(ICampaignInfo, PledgeNFT) returns (uint256 tokenId) { + return super.mintNFTForPledge(backer, reward, tokenAddress, amount, shippingFee, tipAmount); + } + + function burn(uint256 tokenId) public override(ICampaignInfo, PledgeNFT) { + super.burn(tokenId); + } + + /** + * @dev Sets platform information for the campaign and grants treasury role. * @param platformHash The bytes32 identifier of the platform. * @param platformTreasuryAddress The address of the platform's treasury. */ - function _setPlatformInfo( - bytes32 platformHash, - address platformTreasuryAddress - ) external whenNotPaused { + function _setPlatformInfo(bytes32 platformHash, address platformTreasuryAddress) external whenNotPaused { Config memory config = getCampaignConfig(); - if (msg.sender != config.treasuryFactory) { + if (_msgSender() != config.treasuryFactory) { revert CampaignInfoUnauthorized(); } bool selected = checkIfPlatformSelected(platformHash); @@ -475,9 +686,13 @@ contract CampaignInfo is s_approvedPlatformHashes.push(platformHash); s_isApprovedPlatform[platformHash] = true; - emit CampaignInfoPlatformInfoUpdated( - platformHash, - platformTreasuryAddress - ); + // Grant MINTER_ROLE to allow treasury to mint pledge NFTs + _grantRole(MINTER_ROLE, platformTreasuryAddress); + // Lock the campaign after the first treasury deployment + if (!s_isLocked) { + s_isLocked = true; + } + + emit CampaignInfoPlatformInfoUpdated(platformHash, platformTreasuryAddress); } } diff --git a/src/CampaignInfoFactory.sol b/src/CampaignInfoFactory.sol index 8342956c..c29acd1a 100644 --- a/src/CampaignInfoFactory.sol +++ b/src/CampaignInfoFactory.sol @@ -1,30 +1,22 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IGlobalParams} from "./interfaces/IGlobalParams.sol"; import {ICampaignInfoFactory} from "./interfaces/ICampaignInfoFactory.sol"; +import {CampaignInfoFactoryStorage} from "./storage/CampaignInfoFactoryStorage.sol"; +import {DataRegistryKeys} from "./constants/DataRegistryKeys.sol"; /** * @title CampaignInfoFactory * @notice Factory contract for creating campaign information contracts. + * @dev UUPS Upgradeable contract with ERC-7201 namespaced storage */ -contract CampaignInfoFactory is Initializable, ICampaignInfoFactory, Ownable { - IGlobalParams private GLOBAL_PARAMS; - address private s_treasuryFactoryAddress; - bool private s_initialized; - address private s_implementation; - mapping(address => bool) public isValidCampaignInfo; - mapping(bytes32 => address) public identifierToCampaignInfo; - - /** - * @dev Emitted when the factory is initialized. - */ - error CampaignInfoFactoryAlreadyInitialized(); - +contract CampaignInfoFactory is Initializable, ICampaignInfoFactory, OwnableUpgradeable, UUPSUpgradeable { /** * @dev Emitted when invalid input is provided. */ @@ -35,43 +27,68 @@ contract CampaignInfoFactory is Initializable, ICampaignInfoFactory, Ownable { */ error CampaignInfoFactoryCampaignInitializationFailed(); error CampaignInfoFactoryPlatformNotListed(bytes32 platformHash); - error CampaignInfoFactoryCampaignWithSameIdentifierExists( - bytes32 identifierHash, - address cloneExists - ); + error CampaignInfoFactoryCampaignWithSameIdentifierExists(bytes32 identifierHash, address cloneExists); /** - * @param globalParams The address of the global parameters contract. + * @dev Emitted when the campaign currency has no tokens. */ - constructor( - IGlobalParams globalParams, - address campaignImplementation - ) Ownable(msg.sender) { - GLOBAL_PARAMS = globalParams; - s_implementation = campaignImplementation; + error CampaignInfoInvalidTokenList(); + + /** + * @dev Constructor that disables initializers to prevent implementation contract initialization + */ + constructor() { + _disableInitializers(); } /** - * @dev Initializes the factory with treasury factory address. - * @param treasuryFactoryAddress The address of the treasury factory contract. + * @notice Initializes the CampaignInfoFactory contract. + * @param initialOwner The address that will own the factory * @param globalParams The address of the global parameters contract. + * @param campaignImplementation The address of the campaign implementation contract. + * @param treasuryFactoryAddress The address of the treasury factory contract. */ - function _initialize( - address treasuryFactoryAddress, - address globalParams - ) external onlyOwner initializer { + function initialize( + address initialOwner, + IGlobalParams globalParams, + address campaignImplementation, + address treasuryFactoryAddress + ) public initializer { if ( - treasuryFactoryAddress == address(0) || globalParams == address(0) + address(globalParams) == address(0) || campaignImplementation == address(0) + || treasuryFactoryAddress == address(0) || initialOwner == address(0) ) { revert CampaignInfoFactoryInvalidInput(); } - GLOBAL_PARAMS = IGlobalParams(globalParams); - s_treasuryFactoryAddress = treasuryFactoryAddress; - s_initialized = true; + + __Ownable_init(initialOwner); + __UUPSUpgradeable_init(); + + CampaignInfoFactoryStorage.Storage storage $ = CampaignInfoFactoryStorage._getCampaignInfoFactoryStorage(); + $.globalParams = globalParams; + $.implementation = campaignImplementation; + $.treasuryFactoryAddress = treasuryFactoryAddress; } + /** + * @dev Function that authorizes an upgrade to a new implementation + * @param newImplementation Address of the new implementation + */ + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + /** * @inheritdoc ICampaignInfoFactory + * @notice Creates a new campaign with NFT + * @param creator The campaign creator address + * @param identifierHash The unique identifier hash for the campaign + * @param selectedPlatformHash Array of selected platform hashes + * @param platformDataKey Array of platform data keys + * @param platformDataValue Array of platform data values + * @param campaignData The campaign data + * @param nftName NFT collection name + * @param nftSymbol NFT collection symbol + * @param nftImageURI NFT image URI for individual tokens + * @param contractURI IPFS URI for contract-level metadata (constructed off-chain) */ function createCampaign( address creator, @@ -79,57 +96,92 @@ contract CampaignInfoFactory is Initializable, ICampaignInfoFactory, Ownable { bytes32[] calldata selectedPlatformHash, bytes32[] calldata platformDataKey, bytes32[] calldata platformDataValue, - CampaignData calldata campaignData + CampaignData calldata campaignData, + string calldata nftName, + string calldata nftSymbol, + string calldata nftImageURI, + string calldata contractURI ) external override { - if ( - campaignData.launchTime < block.timestamp || - campaignData.deadline <= campaignData.launchTime - ) { + if (creator == address(0)) { revert CampaignInfoFactoryInvalidInput(); } if (platformDataKey.length != platformDataValue.length) { revert CampaignInfoFactoryInvalidInput(); } - address cloneExists = identifierToCampaignInfo[identifierHash]; + + CampaignInfoFactoryStorage.Storage storage $ = CampaignInfoFactoryStorage._getCampaignInfoFactoryStorage(); + + // Cache globalParams to save gas on repeated storage reads + IGlobalParams globalParams = $.globalParams; + + // Retrieve time constraints from GlobalParams dataRegistry + uint256 campaignLaunchBuffer = uint256(globalParams.getFromRegistry(DataRegistryKeys.CAMPAIGN_LAUNCH_BUFFER)); + uint256 minimumCampaignDuration = + uint256(globalParams.getFromRegistry(DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION)); + + // Validate campaign timing constraints + if (campaignData.launchTime < block.timestamp + campaignLaunchBuffer) { + revert CampaignInfoFactoryInvalidInput(); + } + if (campaignData.deadline < campaignData.launchTime + minimumCampaignDuration) { + revert CampaignInfoFactoryInvalidInput(); + } + + bool isValid; + for (uint256 i = 0; i < platformDataKey.length; i++) { + isValid = globalParams.checkIfPlatformDataKeyValid(platformDataKey[i]); + if (!isValid) { + revert CampaignInfoFactoryInvalidInput(); + } + if (platformDataValue[i] == bytes32(0)) { + revert CampaignInfoFactoryInvalidInput(); + } + } + address cloneExists = $.identifierToCampaignInfo[identifierHash]; if (cloneExists != address(0)) { - revert CampaignInfoFactoryCampaignWithSameIdentifierExists( - identifierHash, - cloneExists - ); + revert CampaignInfoFactoryCampaignWithSameIdentifierExists(identifierHash, cloneExists); } bool isListed; bytes32 platformHash; for (uint256 i = 0; i < selectedPlatformHash.length; i++) { platformHash = selectedPlatformHash[i]; - isListed = GLOBAL_PARAMS.checkIfPlatformIsListed(platformHash); + isListed = globalParams.checkIfPlatformIsListed(platformHash); if (!isListed) { revert CampaignInfoFactoryPlatformNotListed(platformHash); } } - bytes memory args = abi.encode( - s_treasuryFactoryAddress, - GLOBAL_PARAMS.getTokenAddress(), - GLOBAL_PARAMS.getProtocolFeePercent(), - identifierHash - ); - address clone = Clones.cloneWithImmutableArgs(s_implementation, args); - (bool success, ) = clone.call( + // Get accepted tokens for the campaign currency + address[] memory acceptedTokens = globalParams.getTokensForCurrency(campaignData.currency); + if (acceptedTokens.length == 0) { + revert CampaignInfoInvalidTokenList(); + } + + bytes memory args = abi.encode($.treasuryFactoryAddress, globalParams.getProtocolFeePercent(), identifierHash); + address clone = Clones.cloneWithImmutableArgs($.implementation, args); + + // Initialize with all parameters including NFT metadata + (bool success,) = clone.call( abi.encodeWithSignature( - "initialize(address,address,bytes32[],bytes32[],bytes32[],(uint256,uint256,uint256))", + "initialize(address,address,bytes32[],bytes32[],bytes32[],(uint256,uint256,uint256,bytes32),address[],string,string,string,string)", creator, - address(GLOBAL_PARAMS), + address(globalParams), selectedPlatformHash, platformDataKey, platformDataValue, - campaignData + campaignData, + acceptedTokens, + nftName, + nftSymbol, + nftImageURI, + contractURI ) ); if (!success) { revert CampaignInfoFactoryCampaignInitializationFailed(); } - identifierToCampaignInfo[identifierHash] = clone; - isValidCampaignInfo[clone] = true; + $.identifierToCampaignInfo[identifierHash] = clone; + $.isValidCampaignInfo[clone] = true; emit CampaignInfoFactoryCampaignCreated(identifierHash, clone); emit CampaignInfoFactoryCampaignInitialized(); } @@ -137,12 +189,31 @@ contract CampaignInfoFactory is Initializable, ICampaignInfoFactory, Ownable { /** * @inheritdoc ICampaignInfoFactory */ - function updateImplementation( - address newImplementation - ) external override onlyOwner { + function updateImplementation(address newImplementation) external override onlyOwner { if (newImplementation == address(0)) { revert CampaignInfoFactoryInvalidInput(); } - s_implementation = newImplementation; + CampaignInfoFactoryStorage.Storage storage $ = CampaignInfoFactoryStorage._getCampaignInfoFactoryStorage(); + $.implementation = newImplementation; + } + + /** + * @notice Check if a campaign info address is valid + * @param campaignInfo The campaign info address to check + * @return bool True if valid, false otherwise + */ + function isValidCampaignInfo(address campaignInfo) external view returns (bool) { + CampaignInfoFactoryStorage.Storage storage $ = CampaignInfoFactoryStorage._getCampaignInfoFactoryStorage(); + return $.isValidCampaignInfo[campaignInfo]; + } + + /** + * @notice Get campaign info address from identifier + * @param identifierHash The identifier hash + * @return address The campaign info address + */ + function identifierToCampaignInfo(bytes32 identifierHash) external view returns (address) { + CampaignInfoFactoryStorage.Storage storage $ = CampaignInfoFactoryStorage._getCampaignInfoFactoryStorage(); + return $.identifierToCampaignInfo[identifierHash]; } } diff --git a/src/GlobalParams.sol b/src/GlobalParams.sol index 202951c4..8c6078ab 100644 --- a/src/GlobalParams.sol +++ b/src/GlobalParams.sol @@ -1,30 +1,23 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IGlobalParams} from "./interfaces/IGlobalParams.sol"; import {Counters} from "./utils/Counters.sol"; +import {GlobalParamsStorage} from "./storage/GlobalParamsStorage.sol"; /** * @title GlobalParams * @notice Manages global parameters and platform information. + * @dev UUPS Upgradeable contract with ERC-7201 namespaced storage */ -contract GlobalParams is IGlobalParams, Ownable { +contract GlobalParams is Initializable, IGlobalParams, OwnableUpgradeable, UUPSUpgradeable { using Counters for Counters.Counter; - bytes32 private constant ZERO_BYTES = - 0x0000000000000000000000000000000000000000000000000000000000000000; - address private s_protocolAdminAddress; - address private s_tokenAddress; - uint256 private s_protocolFeePercent; - mapping(bytes32 => bool) private s_platformIsListed; - mapping(bytes32 => address) private s_platformAdminAddress; - mapping(bytes32 => uint256) private s_platformFeePercent; - mapping(bytes32 => bytes32) private s_platformDataOwner; - mapping(bytes32 => bool) private s_platformData; - - Counters.Counter private s_numberOfListedPlatforms; + bytes32 private constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000; /** * @dev Emitted when a platform is enlisted. @@ -33,9 +26,7 @@ contract GlobalParams is IGlobalParams, Ownable { * @param platformFeePercent The fee percentage of the enlisted platform. */ event PlatformEnlisted( - bytes32 indexed platformHash, - address indexed platformAdminAddress, - uint256 platformFeePercent + bytes32 indexed platformHash, address indexed platformAdminAddress, uint256 platformFeePercent ); /** @@ -51,10 +42,18 @@ contract GlobalParams is IGlobalParams, Ownable { event ProtocolAdminAddressUpdated(address indexed newAdminAddress); /** - * @dev Emitted when the token address is updated. - * @param newTokenAddress The new token address. + * @dev Emitted when a token is added to a currency. + * @param currency The currency identifier. + * @param token The token address added. + */ + event TokenAddedToCurrency(bytes32 indexed currency, address indexed token); + + /** + * @dev Emitted when a token is removed from a currency. + * @param currency The currency identifier. + * @param token The token address removed. */ - event TokenAddressUpdated(address indexed newTokenAddress); + event TokenRemovedFromCurrency(bytes32 indexed currency, address indexed token); /** * @dev Emitted when the protocol fee percent is updated. @@ -67,30 +66,63 @@ contract GlobalParams is IGlobalParams, Ownable { * @param platformHash The identifier of the platform. * @param newAdminAddress The new admin address of the platform. */ - event PlatformAdminAddressUpdated( - bytes32 indexed platformHash, - address indexed newAdminAddress - ); + event PlatformAdminAddressUpdated(bytes32 indexed platformHash, address indexed newAdminAddress); /** * @dev Emitted when platform data is added. * @param platformHash The identifier of the platform. * @param platformDataKey The data key added to the platform. */ - event PlatformDataAdded( - bytes32 indexed platformHash, - bytes32 indexed platformDataKey - ); + event PlatformDataAdded(bytes32 indexed platformHash, bytes32 indexed platformDataKey); /** * @dev Emitted when platform data is removed. * @param platformHash The identifier of the platform. * @param platformDataKey The data key removed from the platform. */ - event PlatformDataRemoved( + event PlatformDataRemoved(bytes32 indexed platformHash, bytes32 platformDataKey); + + /** + * @dev Emitted when data is added to the registry. + * @param key The registry key. + * @param value The registry value. + */ + event DataAddedToRegistry(bytes32 indexed key, bytes32 value); + + /** + * @dev Emitted when a platform-specific line item type is set or updated. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type. + * @param label The label of the line item type. + * @param countsTowardGoal Whether this line item counts toward the campaign goal. + * @param applyProtocolFee Whether this line item is included in protocol fee calculation. + * @param canRefund Whether this line item can be refunded. + * @param instantTransfer Whether this line item amount can be instantly transferred to platform admin after payment confirmation. + */ + event PlatformLineItemTypeSet( bytes32 indexed platformHash, - bytes32 platformDataKey + bytes32 indexed typeId, + string label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer ); + event PlatformClaimDelayUpdated(bytes32 indexed platformHash, uint256 claimDelay); + + /** + * @dev Emitted when a platform adapter (trusted forwarder) is set. + * @param platformHash The identifier of the platform. + * @param adapter The address of the adapter contract. + */ + event PlatformAdapterSet(bytes32 indexed platformHash, address indexed adapter); + + /** + * @dev Emitted when a platform-specific line item type is removed. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the removed line item type. + */ + event PlatformLineItemTypeRemoved(bytes32 indexed platformHash, bytes32 indexed typeId); /** * @dev Throws when the input address is zero. @@ -141,6 +173,31 @@ contract GlobalParams is IGlobalParams, Ownable { */ error GlobalParamsUnauthorized(); + /** + * @dev Throws when currency and token arrays length mismatch. + */ + error GlobalParamsCurrencyTokenLengthMismatch(); + + /** + * @dev Throws when a currency has no tokens registered. + * @param currency The currency identifier. + */ + error GlobalParamsCurrencyHasNoTokens(bytes32 currency); + + /** + * @dev Throws when a token is not found in a currency. + * @param currency The currency identifier. + * @param token The token address. + */ + error GlobalParamsTokenNotInCurrency(bytes32 currency, address token); + + /** + * @dev Throws when a platform-specific line item type is not found. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type. + */ + error GlobalParamsPlatformLineItemTypeNotFound(bytes32 platformHash, bytes32 typeId); + /** * @dev Reverts if the input address is zero. */ @@ -167,33 +224,97 @@ contract GlobalParams is IGlobalParams, Ownable { } /** + * @dev Constructor that disables initializers to prevent implementation contract initialization + */ + constructor() { + _disableInitializers(); + } + + /** + * @dev Initializer function (replaces constructor) * @param protocolAdminAddress The address of the protocol admin. - * @param tokenAddress The address of the token contract. * @param protocolFeePercent The protocol fee percentage. + * @param currencies The array of currency identifiers. + * @param tokensPerCurrency The array of token arrays for each currency. */ - constructor( + function initialize( address protocolAdminAddress, - address tokenAddress, - uint256 protocolFeePercent - ) Ownable(protocolAdminAddress) { - s_protocolAdminAddress = protocolAdminAddress; - s_tokenAddress = tokenAddress; - s_protocolFeePercent = protocolFeePercent; + uint256 protocolFeePercent, + bytes32[] memory currencies, + address[][] memory tokensPerCurrency + ) public initializer { + __Ownable_init(protocolAdminAddress); + __UUPSUpgradeable_init(); + + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.protocolAdminAddress = protocolAdminAddress; + $.protocolFeePercent = protocolFeePercent; + + uint256 currencyLength = currencies.length; + + if (currencyLength != tokensPerCurrency.length) { + revert GlobalParamsCurrencyTokenLengthMismatch(); + } + + for (uint256 i = 0; i < currencyLength;) { + for (uint256 j = 0; j < tokensPerCurrency[i].length;) { + address token = tokensPerCurrency[i][j]; + if (token == address(0)) { + revert GlobalParamsInvalidInput(); + } + $.currencyToTokens[currencies[i]].push(token); + unchecked { + ++j; + } + } + unchecked { + ++i; + } + } + } + + /** + * @dev Function that authorizes an upgrade to a new implementation + * @param newImplementation Address of the new implementation + */ + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + /** + * @notice Adds a key-value pair to the data registry. + * @param key The registry key. + * @param value The registry value. + */ + function addToRegistry(bytes32 key, bytes32 value) external onlyOwner { + if (key == ZERO_BYTES) { + revert GlobalParamsInvalidInput(); + } + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.dataRegistry[key] = value; + emit DataAddedToRegistry(key, value); + } + + /** + * @notice Retrieves a value from the data registry. + * @param key The registry key. + * @return value The registry value. + */ + function getFromRegistry(bytes32 key) external view returns (bytes32 value) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + value = $.dataRegistry[key]; } /** * @inheritdoc IGlobalParams */ - function getPlatformAdminAddress( - bytes32 platformHash - ) + function getPlatformAdminAddress(bytes32 platformHash) external view override platformIsListed(platformHash) returns (address account) { - account = s_platformAdminAddress[platformHash]; + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + account = $.platformAdminAddress[platformHash]; if (account == address(0)) { revert GlobalParamsPlatformAdminNotSet(platformHash); } @@ -202,110 +323,109 @@ contract GlobalParams is IGlobalParams, Ownable { /** * @inheritdoc IGlobalParams */ - function getNumberOfListedPlatforms() - external - view - override - returns (uint256) - { - return s_numberOfListedPlatforms.current(); + function getNumberOfListedPlatforms() external view override returns (uint256) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + return $.numberOfListedPlatforms.current(); } /** * @inheritdoc IGlobalParams */ - function getProtocolAdminAddress() - external - view - override - returns (address) - { - return s_protocolAdminAddress; + function getProtocolAdminAddress() external view override returns (address) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + return $.protocolAdminAddress; } /** * @inheritdoc IGlobalParams */ - function getTokenAddress() external view override returns (address) { - return s_tokenAddress; + function getProtocolFeePercent() external view override returns (uint256) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + return $.protocolFeePercent; } /** * @inheritdoc IGlobalParams */ - function getProtocolFeePercent() external view override returns (uint256) { - return s_protocolFeePercent; + function getPlatformFeePercent(bytes32 platformHash) + external + view + override + platformIsListed(platformHash) + returns (uint256 platformFeePercent) + { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + platformFeePercent = $.platformFeePercent[platformHash]; } /** * @inheritdoc IGlobalParams */ - function getPlatformFeePercent( - bytes32 platformHash - ) + function getPlatformClaimDelay(bytes32 platformHash) external view override platformIsListed(platformHash) - returns (uint256 platformFeePercent) + returns (uint256 claimDelay) { - platformFeePercent = s_platformFeePercent[platformHash]; + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + claimDelay = $.platformClaimDelay[platformHash]; } /** * @inheritdoc IGlobalParams */ - function getPlatformDataOwner( - bytes32 platformDataKey - ) external view override returns (bytes32 platformHash) { - platformHash = s_platformDataOwner[platformDataKey]; + function getPlatformDataOwner(bytes32 platformDataKey) external view override returns (bytes32 platformHash) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + platformHash = $.platformDataOwner[platformDataKey]; } /** * @inheritdoc IGlobalParams */ - function checkIfPlatformIsListed( - bytes32 platformHash - ) public view override returns (bool) { - return s_platformIsListed[platformHash]; + function checkIfPlatformIsListed(bytes32 platformHash) public view override returns (bool) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + return $.platformIsListed[platformHash]; } /** * @inheritdoc IGlobalParams */ - function checkIfPlatformDataKeyValid( - bytes32 platformDataKey - ) external view override returns (bool isValid) { - isValid = s_platformData[platformDataKey]; + function checkIfPlatformDataKeyValid(bytes32 platformDataKey) external view override returns (bool isValid) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + isValid = $.platformData[platformDataKey]; } /** - * @notice Enlists a platform with its admin address and fee percentage. + * @notice Enlists a platform with its admin address, fee percentage, and optional adapter. * @dev The platformFeePercent can be any value including zero. * @param platformHash The platform's identifier. * @param platformAdminAddress The platform's admin address. * @param platformFeePercent The platform's fee percentage. + * @param platformAdapter The platform's adapter (trusted forwarder) address for ERC-2771 meta-transactions. Can be address(0) if not needed. */ function enlistPlatform( bytes32 platformHash, address platformAdminAddress, - uint256 platformFeePercent + uint256 platformFeePercent, + address platformAdapter ) external onlyOwner notAddressZero(platformAdminAddress) { if (platformHash == ZERO_BYTES) { revert GlobalParamsInvalidInput(); } - if (s_platformIsListed[platformHash]) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + if ($.platformIsListed[platformHash]) { revert GlobalParamsPlatformAlreadyListed(platformHash); } else { - s_platformIsListed[platformHash] = true; - s_platformAdminAddress[platformHash] = platformAdminAddress; - s_platformFeePercent[platformHash] = platformFeePercent; - s_numberOfListedPlatforms.increment(); - emit PlatformEnlisted( - platformHash, - platformAdminAddress, - platformFeePercent - ); + $.platformIsListed[platformHash] = true; + $.platformAdminAddress[platformHash] = platformAdminAddress; + $.platformFeePercent[platformHash] = platformFeePercent; + $.platformAdapter[platformHash] = platformAdapter; + $.numberOfListedPlatforms.increment(); + emit PlatformEnlisted(platformHash, platformAdminAddress, platformFeePercent); + if (platformAdapter != address(0)) { + emit PlatformAdapterSet(platformHash, platformAdapter); + } } } @@ -313,13 +433,12 @@ contract GlobalParams is IGlobalParams, Ownable { * @notice Delists a platform. * @param platformHash The platform's identifier. */ - function delistPlatform( - bytes32 platformHash - ) external onlyOwner platformIsListed(platformHash) { - s_platformIsListed[platformHash] = false; - s_platformAdminAddress[platformHash] = address(0); - s_platformFeePercent[platformHash] = 0; - s_numberOfListedPlatforms.decrement(); + function delistPlatform(bytes32 platformHash) external onlyOwner platformIsListed(platformHash) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.platformIsListed[platformHash] = false; + $.platformAdminAddress[platformHash] = address(0); + $.platformFeePercent[platformHash] = 0; + $.numberOfListedPlatforms.decrement(); emit PlatformDelisted(platformHash); } @@ -328,18 +447,20 @@ contract GlobalParams is IGlobalParams, Ownable { * @param platformHash The platform's identifier. * @param platformDataKey The platform data key. */ - function addPlatformData( - bytes32 platformHash, - bytes32 platformDataKey - ) external platformIsListed(platformHash) onlyPlatformAdmin(platformHash) { + function addPlatformData(bytes32 platformHash, bytes32 platformDataKey) + external + platformIsListed(platformHash) + onlyPlatformAdmin(platformHash) + { if (platformDataKey == ZERO_BYTES) { revert GlobalParamsInvalidInput(); } - if (s_platformData[platformDataKey]) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + if ($.platformData[platformDataKey]) { revert GlobalParamsPlatformDataAlreadySet(); } - s_platformData[platformDataKey] = true; - s_platformDataOwner[platformDataKey] = platformHash; + $.platformData[platformDataKey] = true; + $.platformDataOwner[platformDataKey] = platformHash; emit PlatformDataAdded(platformHash, platformDataKey); } @@ -348,66 +469,270 @@ contract GlobalParams is IGlobalParams, Ownable { * @param platformHash The platform's identifier. * @param platformDataKey The platform data key. */ - function removePlatformData( - bytes32 platformHash, - bytes32 platformDataKey - ) external platformIsListed(platformHash) onlyPlatformAdmin(platformHash) { + function removePlatformData(bytes32 platformHash, bytes32 platformDataKey) + external + platformIsListed(platformHash) + onlyPlatformAdmin(platformHash) + { if (platformDataKey == ZERO_BYTES) { revert GlobalParamsInvalidInput(); } - if (!s_platformData[platformDataKey]) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + if (!$.platformData[platformDataKey]) { revert GlobalParamsPlatformDataNotSet(); } - s_platformData[platformDataKey] = false; - s_platformDataOwner[platformDataKey] = ZERO_BYTES; + $.platformData[platformDataKey] = false; + $.platformDataOwner[platformDataKey] = ZERO_BYTES; emit PlatformDataRemoved(platformHash, platformDataKey); } /** * @inheritdoc IGlobalParams */ - function updateProtocolAdminAddress( - address protocolAdminAddress - ) external override onlyOwner notAddressZero(protocolAdminAddress) { - s_protocolAdminAddress = protocolAdminAddress; + function updateProtocolAdminAddress(address protocolAdminAddress) + external + override + onlyOwner + notAddressZero(protocolAdminAddress) + { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.protocolAdminAddress = protocolAdminAddress; emit ProtocolAdminAddressUpdated(protocolAdminAddress); } /** * @inheritdoc IGlobalParams */ - function updateTokenAddress( - address tokenAddress - ) external override onlyOwner notAddressZero(tokenAddress) { - s_tokenAddress = tokenAddress; - emit TokenAddressUpdated(tokenAddress); + function updateProtocolFeePercent(uint256 protocolFeePercent) external override onlyOwner { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.protocolFeePercent = protocolFeePercent; + emit ProtocolFeePercentUpdated(protocolFeePercent); } /** * @inheritdoc IGlobalParams */ - function updateProtocolFeePercent( - uint256 protocolFeePercent - ) external override onlyOwner { - s_protocolFeePercent = protocolFeePercent; - emit ProtocolFeePercentUpdated(protocolFeePercent); + function updatePlatformAdminAddress(bytes32 platformHash, address platformAdminAddress) + external + override + onlyOwner + platformIsListed(platformHash) + notAddressZero(platformAdminAddress) + { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.platformAdminAddress[platformHash] = platformAdminAddress; + emit PlatformAdminAddressUpdated(platformHash, platformAdminAddress); } /** * @inheritdoc IGlobalParams */ - function updatePlatformAdminAddress( - bytes32 platformHash, - address platformAdminAddress - ) + function updatePlatformClaimDelay(bytes32 platformHash, uint256 claimDelay) + external + override + platformIsListed(platformHash) + onlyPlatformAdmin(platformHash) + { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.platformClaimDelay[platformHash] = claimDelay; + emit PlatformClaimDelayUpdated(platformHash, claimDelay); + } + + /** + * @inheritdoc IGlobalParams + */ + function getPlatformAdapter(bytes32 platformHash) + external + view + override + platformIsListed(platformHash) + returns (address) + { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + return $.platformAdapter[platformHash]; + } + + /** + * @inheritdoc IGlobalParams + */ + function setPlatformAdapter(bytes32 platformHash, address adapter) external override onlyOwner platformIsListed(platformHash) - notAddressZero(platformAdminAddress) { - s_platformAdminAddress[platformHash] = platformAdminAddress; - emit PlatformAdminAddressUpdated(platformHash, platformAdminAddress); + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.platformAdapter[platformHash] = adapter; + emit PlatformAdapterSet(platformHash, adapter); + } + + /** + * @inheritdoc IGlobalParams + */ + function addTokenToCurrency(bytes32 currency, address token) external override onlyOwner notAddressZero(token) { + if (currency == ZERO_BYTES) { + revert GlobalParamsInvalidInput(); + } + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.currencyToTokens[currency].push(token); + emit TokenAddedToCurrency(currency, token); + } + + /** + * @inheritdoc IGlobalParams + */ + function removeTokenFromCurrency(bytes32 currency, address token) + external + override + onlyOwner + notAddressZero(token) + { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + address[] storage tokens = $.currencyToTokens[currency]; + uint256 length = tokens.length; + bool found = false; + + for (uint256 i = 0; i < length;) { + if (tokens[i] == token) { + tokens[i] = tokens[length - 1]; + tokens.pop(); + found = true; + break; + } + unchecked { + ++i; + } + } + + if (!found) { + revert GlobalParamsTokenNotInCurrency(currency, token); + } + emit TokenRemovedFromCurrency(currency, token); + } + + /** + * @inheritdoc IGlobalParams + */ + function getTokensForCurrency(bytes32 currency) external view override returns (address[] memory) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + return $.currencyToTokens[currency]; + } + + /** + * @notice Sets or updates a platform-specific line item type configuration. + * @dev Only callable by the platform admin. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type. + * @param label The label identifier for the line item type. + * @param countsTowardGoal Whether this line item counts toward the campaign goal. + * @param applyProtocolFee Whether this line item is included in protocol fee calculation. + * @param canRefund Whether this line item can be refunded. + * @param instantTransfer Whether this line item amount can be instantly transferred. + * + * Constraints: + * - If countsTowardGoal is true, then applyProtocolFee must be false, canRefund must be true, and instantTransfer must be false. + * - Non-goal instant transfer items cannot be refundable. + */ + function setPlatformLineItemType( + bytes32 platformHash, + bytes32 typeId, + string calldata label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ) external platformIsListed(platformHash) onlyPlatformAdmin(platformHash) { + if (typeId == ZERO_BYTES) { + revert GlobalParamsInvalidInput(); + } + + // Validation constraint 1: If countsTowardGoal is true, then applyProtocolFee must be false, canRefund must be true, and instantTransfer must be false + if (countsTowardGoal) { + if (applyProtocolFee) { + revert GlobalParamsInvalidInput(); + } + if (!canRefund) { + revert GlobalParamsInvalidInput(); + } + if (instantTransfer) { + revert GlobalParamsInvalidInput(); + } + } + + // Validation constraint 2: Non-goal instant transfer items cannot be refundable + if (!countsTowardGoal && instantTransfer && canRefund) { + revert GlobalParamsInvalidInput(); + } + + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + $.platformLineItemTypes[platformHash][typeId] = GlobalParamsStorage.LineItemType({ + exists: true, + label: label, + countsTowardGoal: countsTowardGoal, + applyProtocolFee: applyProtocolFee, + canRefund: canRefund, + instantTransfer: instantTransfer + }); + emit PlatformLineItemTypeSet( + platformHash, typeId, label, countsTowardGoal, applyProtocolFee, canRefund, instantTransfer + ); + } + + /** + * @notice Removes a platform-specific line item type by setting its exists flag to false. + * @dev Only callable by the platform admin. This prevents the type from being used in new pledges. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type to remove. + */ + function removePlatformLineItemType(bytes32 platformHash, bytes32 typeId) + external + platformIsListed(platformHash) + onlyPlatformAdmin(platformHash) + { + if (typeId == ZERO_BYTES) { + revert GlobalParamsInvalidInput(); + } + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + if (!$.platformLineItemTypes[platformHash][typeId].exists) { + revert GlobalParamsPlatformLineItemTypeNotFound(platformHash, typeId); + } + $.platformLineItemTypes[platformHash][typeId].exists = false; + emit PlatformLineItemTypeRemoved(platformHash, typeId); + } + + /** + * @notice Retrieves a platform-specific line item type configuration. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type. + * @return exists Whether this line item type exists and is active. + * @return label The label identifier for the line item type. + * @return countsTowardGoal Whether this line item counts toward the campaign goal. + * @return applyProtocolFee Whether this line item is included in protocol fee calculation. + * @return canRefund Whether this line item can be refunded. + * @return instantTransfer Whether this line item amount can be instantly transferred to platform admin after payment confirmation. + */ + function getPlatformLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ) + { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + GlobalParamsStorage.LineItemType storage lineItemType = $.platformLineItemTypes[platformHash][typeId]; + return ( + lineItemType.exists, + lineItemType.label, + lineItemType.countsTowardGoal, + lineItemType.applyProtocolFee, + lineItemType.canRefund, + lineItemType.instantTransfer + ); } /** @@ -421,11 +746,12 @@ contract GlobalParams is IGlobalParams, Ownable { /** * @dev Internal function to check if the sender is the platform administrator for a specific platform. - * If the sender is not the platform admin, it reverts with AdminAccessCheckerUnauthorized error. + * If the sender is not the platform admin, it reverts with GlobalParamsUnauthorized error. * @param platformHash The unique identifier of the platform. */ function _onlyPlatformAdmin(bytes32 platformHash) private view { - if (msg.sender != s_platformAdminAddress[platformHash]) { + GlobalParamsStorage.Storage storage $ = GlobalParamsStorage._getGlobalParamsStorage(); + if (_msgSender() != $.platformAdminAddress[platformHash]) { revert GlobalParamsUnauthorized(); } } diff --git a/src/TreasuryFactory.sol b/src/TreasuryFactory.sol index 44f05280..f2afad17 100644 --- a/src/TreasuryFactory.sol +++ b/src/TreasuryFactory.sol @@ -1,15 +1,20 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ITreasuryFactory} from "./interfaces/ITreasuryFactory.sol"; import {IGlobalParams, AdminAccessChecker} from "./utils/AdminAccessChecker.sol"; +import {TreasuryFactoryStorage} from "./storage/TreasuryFactoryStorage.sol"; -contract TreasuryFactory is ITreasuryFactory, AdminAccessChecker { - mapping(bytes32 => mapping(uint256 => address)) private implementationMap; - mapping(address => bool) private approvedImplementations; - +/** + * @title TreasuryFactory + * @notice Factory contract for creating treasury contracts + * @dev UUPS Upgradeable contract with ERC-7201 namespaced storage + */ +contract TreasuryFactory is Initializable, ITreasuryFactory, AdminAccessChecker, UUPSUpgradeable { error TreasuryFactoryUnauthorized(); error TreasuryFactoryInvalidKey(); error TreasuryFactoryTreasuryCreationFailed(); @@ -19,114 +24,113 @@ contract TreasuryFactory is ITreasuryFactory, AdminAccessChecker { error TreasuryFactoryTreasuryInitializationFailed(); error TreasuryFactorySettingPlatformInfoFailed(); + /** + * @dev Constructor that disables initializers to prevent implementation contract initialization + */ + constructor() { + _disableInitializers(); + } + /** * @notice Initializes the TreasuryFactory contract. - * @dev This constructor sets the address of the GlobalParams contract as the admin. + * @param globalParams The address of the GlobalParams contract */ - constructor(IGlobalParams globalParams) { + function initialize(IGlobalParams globalParams) public initializer { __AccessChecker_init(globalParams); + __UUPSUpgradeable_init(); } + /** + * @dev Function that authorizes an upgrade to a new implementation + * @param newImplementation Address of the new implementation + */ + function _authorizeUpgrade(address newImplementation) internal override onlyProtocolAdmin {} + /** * @inheritdoc ITreasuryFactory */ - function registerTreasuryImplementation( - bytes32 platformHash, - uint256 implementationId, - address implementation - ) external override onlyPlatformAdmin(platformHash) { + function registerTreasuryImplementation(bytes32 platformHash, uint256 implementationId, address implementation) + external + override + onlyPlatformAdmin(platformHash) + { if (implementation == address(0)) { revert TreasuryFactoryInvalidAddress(); } - implementationMap[platformHash][implementationId] = implementation; + TreasuryFactoryStorage.Storage storage $ = TreasuryFactoryStorage._getTreasuryFactoryStorage(); + $.implementationMap[platformHash][implementationId] = implementation; + emit TreasuryImplementationRegistered(platformHash, implementationId, implementation); } /** * @inheritdoc ITreasuryFactory */ - function approveTreasuryImplementation( - bytes32 platformHash, - uint256 implementationId - ) external override onlyProtocolAdmin { - address implementation = implementationMap[platformHash][ - implementationId - ]; + function approveTreasuryImplementation(bytes32 platformHash, uint256 implementationId) + external + override + onlyProtocolAdmin + { + TreasuryFactoryStorage.Storage storage $ = TreasuryFactoryStorage._getTreasuryFactoryStorage(); + address implementation = $.implementationMap[platformHash][implementationId]; if (implementation == address(0)) { revert TreasuryFactoryImplementationNotSet(); } - approvedImplementations[implementation] = true; + $.approvedImplementations[implementation] = true; + emit TreasuryImplementationApproval(implementation, true); } /** * @inheritdoc ITreasuryFactory */ - function disapproveTreasuryImplementation( - address implementation - ) external override onlyProtocolAdmin { - approvedImplementations[implementation] = false; + function disapproveTreasuryImplementation(address implementation) external override onlyProtocolAdmin { + TreasuryFactoryStorage.Storage storage $ = TreasuryFactoryStorage._getTreasuryFactoryStorage(); + $.approvedImplementations[implementation] = false; + emit TreasuryImplementationApproval(implementation, false); } /** * @inheritdoc ITreasuryFactory */ - function removeTreasuryImplementation( - bytes32 platformHash, - uint256 implementationId - ) external override onlyPlatformAdmin(platformHash) { - delete implementationMap[platformHash][implementationId]; + function removeTreasuryImplementation(bytes32 platformHash, uint256 implementationId) + external + override + onlyPlatformAdmin(platformHash) + { + TreasuryFactoryStorage.Storage storage $ = TreasuryFactoryStorage._getTreasuryFactoryStorage(); + delete $.implementationMap[platformHash][implementationId]; + emit TreasuryImplementationRemoved(platformHash, implementationId); } /** * @inheritdoc ITreasuryFactory */ - function deploy( - bytes32 platformHash, - address infoAddress, - uint256 implementationId, - string calldata name, - string calldata symbol - ) + function deploy(bytes32 platformHash, address infoAddress, uint256 implementationId) external override onlyPlatformAdmin(platformHash) returns (address clone) { - address implementation = implementationMap[platformHash][ - implementationId - ]; - if (!approvedImplementations[implementation]) { + TreasuryFactoryStorage.Storage storage $ = TreasuryFactoryStorage._getTreasuryFactoryStorage(); + address implementation = $.implementationMap[platformHash][implementationId]; + if (!$.approvedImplementations[implementation]) { revert TreasuryFactoryImplementationNotSetOrApproved(); } clone = Clones.clone(implementation); - (bool success, ) = clone.call( - abi.encodeWithSignature( - "initialize(bytes32,address,string,string)", - platformHash, - infoAddress, - name, - symbol - ) + // Fetch the platform adapter (trusted forwarder) from GlobalParams + address platformAdapter = _getGlobalParams().getPlatformAdapter(platformHash); + + (bool success,) = clone.call( + abi.encodeWithSignature("initialize(bytes32,address,address)", platformHash, infoAddress, platformAdapter) ); if (!success) { revert TreasuryFactoryTreasuryInitializationFailed(); } - (success, ) = infoAddress.call( - abi.encodeWithSignature( - "_setPlatformInfo(bytes32,address)", - platformHash, - clone - ) - ); + (success,) = infoAddress.call(abi.encodeWithSignature("_setPlatformInfo(bytes32,address)", platformHash, clone)); if (!success) { revert TreasuryFactorySettingPlatformInfoFailed(); } - emit TreasuryFactoryTreasuryDeployed( - platformHash, - implementationId, - infoAddress, - clone - ); + emit TreasuryFactoryTreasuryDeployed(platformHash, implementationId, infoAddress, clone); } } diff --git a/src/constants/DataRegistryKeys.sol b/src/constants/DataRegistryKeys.sol new file mode 100644 index 00000000..f3dbe571 --- /dev/null +++ b/src/constants/DataRegistryKeys.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +/** + * @title DataRegistryKeys + * @notice Centralized storage for all dataRegistry keys used in GlobalParams + * @dev This library provides a single source of truth for all dataRegistry keys + * to ensure consistency across contracts and prevent key collisions. + */ +library DataRegistryKeys { + // Time-related keys + bytes32 public constant BUFFER_TIME = keccak256("bufferTime"); + bytes32 public constant MAX_PAYMENT_EXPIRATION = keccak256("maxPaymentExpiration"); + bytes32 public constant CAMPAIGN_LAUNCH_BUFFER = keccak256("campaignLaunchBuffer"); + bytes32 public constant MINIMUM_CAMPAIGN_DURATION = keccak256("minimumCampaignDuration"); + + /** + * @notice Generates a namespaced registry key scoped to a specific platform. + * @param baseKey The base registry key. + * @param platformHash The identifier of the platform. + * @return platformKey The platform-scoped registry key. + */ + function scopedToPlatform(bytes32 baseKey, bytes32 platformHash) internal pure returns (bytes32 platformKey) { + platformKey = keccak256(abi.encode(baseKey, platformHash)); + } +} diff --git a/src/interfaces/ICampaignData.sol b/src/interfaces/ICampaignData.sol index abf5e0ac..bb8a5095 100644 --- a/src/interfaces/ICampaignData.sol +++ b/src/interfaces/ICampaignData.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title ICampaignData @@ -7,11 +7,12 @@ pragma solidity ^0.8.20; */ interface ICampaignData { /** - * @dev Struct to represent campaign data, including launch time, deadline, and goal amount. + * @dev Struct to represent campaign data, including launch time, deadline, goal amount, and currency. */ struct CampaignData { uint256 launchTime; // Timestamp when the campaign is launched. uint256 deadline; // Timestamp or block number when the campaign ends. uint256 goalAmount; // Funding goal amount that the campaign aims to achieve. + bytes32 currency; // Currency identifier for the campaign (e.g., bytes32("USD")). } } diff --git a/src/interfaces/ICampaignInfo.sol b/src/interfaces/ICampaignInfo.sol index 43771b79..9fc9854a 100644 --- a/src/interfaces/ICampaignInfo.sol +++ b/src/interfaces/ICampaignInfo.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /** * @title ICampaignInfo * @notice An interface for managing campaign information in a crowdfunding system. + * @dev Inherits from IERC721 as CampaignInfo is an ERC721 NFT collection */ -interface ICampaignInfo { +interface ICampaignInfo is IERC721 { /** * @notice Returns the owner of the contract. * @return The address of the contract owner. @@ -17,16 +20,59 @@ interface ICampaignInfo { * @param platformHash The bytes32 identifier of the platform to check. * @return True if the platform is selected, false otherwise. */ - function checkIfPlatformSelected( - bytes32 platformHash - ) external view returns (bool); + function checkIfPlatformSelected(bytes32 platformHash) external view returns (bool); /** - * @notice Retrieves the total amount raised in the campaign. + * @notice Retrieves the total amount raised across non-cancelled treasuries. + * @dev This excludes cancelled treasuries and is affected by refunds. * @return The total amount raised in the campaign. */ function getTotalRaisedAmount() external view returns (uint256); + /** + * @notice Retrieves the total lifetime raised amount across all treasuries. + * @dev This amount never decreases even when refunds are processed. + * It represents the sum of all pledges/payments ever made to the campaign, + * regardless of cancellations or refunds. + * @return The total lifetime raised amount as a uint256 value. + */ + function getTotalLifetimeRaisedAmount() external view returns (uint256); + + /** + * @notice Retrieves the total refunded amount across all treasuries. + * @dev This is calculated as the difference between lifetime raised amount + * and current raised amount. It represents the sum of all refunds + * that have been processed across all treasuries. + * @return The total refunded amount as a uint256 value. + */ + function getTotalRefundedAmount() external view returns (uint256); + + /** + * @notice Retrieves the total available raised amount across all treasuries. + * @dev This includes funds from both active and cancelled treasuries, + * and is affected by refunds. It represents the actual current + * balance of funds across all treasuries. + * @return The total available raised amount as a uint256 value. + */ + function getTotalAvailableRaisedAmount() external view returns (uint256); + + /** + * @notice Retrieves the total raised amount from cancelled treasuries only. + * @dev This is the opposite of getTotalRaisedAmount(), which only includes + * non-cancelled treasuries. This function only sums up raised amounts + * from treasuries that have been cancelled. + * @return The total raised amount from cancelled treasuries as a uint256 value. + */ + function getTotalCancelledAmount() external view returns (uint256); + + /** + * @notice Retrieves the total expected (pending) amount across payment treasuries. + * @dev This only applies to payment treasuries and represents payments that + * have been created but not yet confirmed. Regular treasuries are skipped. + * @return The total expected amount as a uint256 value. + */ + function getTotalExpectedAmount() external view returns (uint256); + /** * @notice Retrieves the address of the protocol administrator. * @return The address of the protocol administrator. @@ -38,9 +84,7 @@ interface ICampaignInfo { * @param platformHash The bytes32 identifier of the platform. * @return The address of the platform administrator. */ - function getPlatformAdminAddress( - bytes32 platformHash - ) external view returns (address); + function getPlatformAdminAddress(bytes32 platformHash) external view returns (address); /** * @notice Retrieves the campaign's launch time. @@ -60,35 +104,51 @@ interface ICampaignInfo { */ function getGoalAmount() external view returns (uint256); - /** - * @notice Retrieves the address of the token used in the campaign. - * @return The address of the campaign's token. - */ - function getTokenAddress() external view returns (address); - /** * @notice Retrieves the protocol fee percentage for the campaign. * @return The protocol fee percentage applied to the campaign. */ function getProtocolFeePercent() external view returns (uint256); + /** + * @notice Retrieves the campaign's currency identifier. + * @return The bytes32 currency identifier for the campaign. + */ + function getCampaignCurrency() external view returns (bytes32); + + /** + * @notice Retrieves the cached accepted tokens for the campaign. + * @return An array of token addresses accepted for the campaign. + */ + function getAcceptedTokens() external view returns (address[] memory); + + /** + * @notice Checks if a token is accepted for the campaign. + * @param token The token address to check. + * @return True if the token is accepted; otherwise, false. + */ + function isTokenAccepted(address token) external view returns (bool); + /** * @notice Retrieves the platform fee percentage for a specific platform. * @param platformHash The bytes32 identifier of the platform. * @return The platform fee percentage applied to the campaign on the platform. */ - function getPlatformFeePercent( - bytes32 platformHash - ) external view returns (uint256); + function getPlatformFeePercent(bytes32 platformHash) external view returns (uint256); + + /** + * @notice Retrieves the claim delay (in seconds) configured for the given platform. + * @param platformHash The identifier of the platform. + * @return The claim delay in seconds. + */ + function getPlatformClaimDelay(bytes32 platformHash) external view returns (uint256); /** * @notice Retrieves platform-specific data for the campaign. * @param platformDataKey The bytes32 identifier of the platform-specific data. * @return The platform-specific data associated with the given key. */ - function getPlatformData( - bytes32 platformDataKey - ) external view returns (bytes32); + function getPlatformData(bytes32 platformDataKey) external view returns (bytes32); /** * @notice Retrieves the unique identifier hash of the campaign. @@ -125,10 +185,14 @@ interface ICampaignInfo { * @dev It can only be called for a platform if its not approved i.e. the platform treasury is not deployed * @param platformHash The bytes32 identifier of the platform. * @param selection The new selection status (true or false). + * @param platformDataKey An array of platform-specific data keys. + * @param platformDataValue An array of platform-specific data values. */ function updateSelectedPlatform( bytes32 platformHash, - bool selection + bool selection, + bytes32[] calldata platformDataKey, + bytes32[] calldata platformDataValue ) external; /** @@ -140,4 +204,83 @@ interface ICampaignInfo { * @dev Returns true if the campaign is cancelled, and false otherwise. */ function cancelled() external view returns (bool); + + /** + * @notice Retrieves a value from the GlobalParams data registry. + * @param key The registry key. + * @return value The registry value. + */ + function getDataFromRegistry(bytes32 key) external view returns (bytes32 value); + + /** + * @notice Retrieves the buffer time from the GlobalParams data registry. + * @return bufferTime The buffer time value. + */ + function getBufferTime() external view returns (uint256 bufferTime); + + /** + * @notice Retrieves a platform-specific line item type configuration from GlobalParams. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type. + * @return exists Whether this line item type exists and is active. + * @return label The label identifier for the line item type. + * @return countsTowardGoal Whether this line item counts toward the campaign goal. + * @return applyProtocolFee Whether this line item is included in protocol fee calculation. + * @return canRefund Whether this line item can be refunded. + * @return instantTransfer Whether this line item amount can be instantly transferred. + */ + function getLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ); + + /** + * @notice Mints a pledge NFT for a backer + * @dev Can only be called by treasuries with MINTER_ROLE + * @param backer The backer address + * @param reward The reward identifier + * @param tokenAddress The address of the token used for the pledge + * @param amount The pledge amount + * @param shippingFee The shipping fee + * @param tipAmount The tip amount + * @return tokenId The minted token ID (pledge ID) + */ + function mintNFTForPledge( + address backer, + bytes32 reward, + address tokenAddress, + uint256 amount, + uint256 shippingFee, + uint256 tipAmount + ) external returns (uint256 tokenId); + + /** + * @notice Sets the image URI for NFT metadata + * @param newImageURI The new image URI + */ + function setImageURI(string calldata newImageURI) external; + + /** + * @notice Updates the contract-level metadata URI + * @param newContractURI The new contract URI + */ + function updateContractURI(string calldata newContractURI) external; + + /** + * @notice Burns a pledge NFT + * @param tokenId The token ID to burn + */ + function burn(uint256 tokenId) external; + + /** + * @dev Returns true if the campaign is locked (after treasury deployment), and false otherwise. + */ + function isLocked() external view returns (bool); } diff --git a/src/interfaces/ICampaignInfoFactory.sol b/src/interfaces/ICampaignInfoFactory.sol index f8b53c35..6e366b9b 100644 --- a/src/interfaces/ICampaignInfoFactory.sol +++ b/src/interfaces/ICampaignInfoFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {ICampaignData} from "./ICampaignData.sol"; @@ -13,10 +13,7 @@ interface ICampaignInfoFactory is ICampaignData { * @param identifierHash The unique identifier hash of the campaign. * @param campaignInfoAddress The address of the created campaign information contract. */ - event CampaignInfoFactoryCampaignCreated( - bytes32 indexed identifierHash, - address indexed campaignInfoAddress - ); + event CampaignInfoFactoryCampaignCreated(bytes32 indexed identifierHash, address indexed campaignInfoAddress); /** * @notice Emitted when the campaign after creation is initialized. @@ -24,13 +21,22 @@ interface ICampaignInfoFactory is ICampaignData { event CampaignInfoFactoryCampaignInitialized(); /** - * @notice Creates a new campaign information contract. + * @notice Creates a new campaign information contract with NFT. + * @dev IMPORTANT: Protocol and platform fees are retrieved at execution time and locked + * permanently in the campaign contract. Users should verify current fees before + * calling this function or using intermediate contracts that check fees haven't + * changed from expected values. The protocol fee is stored as immutable in the cloned + * contract and platform fees are stored during initialization. * @param creator The address of the creator of the campaign. * @param identifierHash The unique identifier hash of the campaign. * @param selectedPlatformHash An array of platform identifiers selected for the campaign. * @param platformDataKey An array of platform-specific data keys. * @param platformDataValue An array of platform-specific data values. - * @param campaignData The struct containing campaign launch details. + * @param campaignData The struct containing campaign launch details (including currency). + * @param nftName NFT collection name + * @param nftSymbol NFT collection symbol + * @param nftImageURI NFT image URI for individual tokens + * @param contractURI IPFS URI for contract-level metadata */ function createCampaign( address creator, @@ -38,7 +44,11 @@ interface ICampaignInfoFactory is ICampaignData { bytes32[] calldata selectedPlatformHash, bytes32[] calldata platformDataKey, bytes32[] calldata platformDataValue, - CampaignData calldata campaignData + CampaignData calldata campaignData, + string calldata nftName, + string calldata nftSymbol, + string calldata nftImageURI, + string calldata contractURI ) external; /** diff --git a/src/interfaces/ICampaignPaymentTreasury.sol b/src/interfaces/ICampaignPaymentTreasury.sol new file mode 100644 index 00000000..48bbde4c --- /dev/null +++ b/src/interfaces/ICampaignPaymentTreasury.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +/** + * @title ICampaignPaymentTreasury + * @notice An interface for managing campaign payment treasury contracts. + */ +interface ICampaignPaymentTreasury { + /** + * @notice Represents a stored line item with its configuration snapshot. + * @param typeId The type identifier of the line item. + * @param amount The amount of the line item. + * @param label The human-readable label of the line item type. + * @param countsTowardGoal Whether this line item counts toward the campaign goal. + * @param applyProtocolFee Whether protocol fee applies to this line item. + * @param canRefund Whether this line item can be refunded. + * @param instantTransfer Whether this line item is transferred instantly. + */ + struct PaymentLineItem { + bytes32 typeId; + uint256 amount; + string label; + bool countsTowardGoal; + bool applyProtocolFee; + bool canRefund; + bool instantTransfer; + } + + /** + * @notice Comprehensive payment data structure containing all payment information. + * @param buyerAddress The address of the buyer who made the payment. + * @param buyerId The ID of the buyer. + * @param itemId The identifier of the item being purchased. + * @param amount The amount to be paid for the item (in token's native decimals). + * @param expiration The timestamp after which the payment expires. + * @param isConfirmed Boolean indicating whether the payment has been confirmed. + * @param isCryptoPayment Boolean indicating whether the payment is made using direct crypto payment. + * @param lineItemCount The number of line items associated with this payment. + * @param paymentToken The token address used for this payment. + * @param lineItems Array of stored line items with their configuration snapshots. + * @param externalFees Array of external fee metadata associated with this payment (informational only). + */ + struct PaymentData { + address buyerAddress; + bytes32 buyerId; + bytes32 itemId; + uint256 amount; + uint256 expiration; + bool isConfirmed; + bool isCryptoPayment; + uint256 lineItemCount; + address paymentToken; + PaymentLineItem[] lineItems; + ExternalFees[] externalFees; + } + + /** + * @notice Represents a line item in a payment. + * @param typeId The type identifier of the line item (must exist in GlobalParams). + * @param amount The amount of the line item (denominated in pledge token). + */ + struct LineItem { + bytes32 typeId; + uint256 amount; + } + + /** + * @notice Represents metadata about external fees associated with a payment. + * @dev These values are informational only and do not affect treasury balances or transfers. + * @param feeType The type identifier of the external fee. + * @param feeAmount The amount of the external fee. + */ + struct ExternalFees { + bytes32 feeType; + uint256 feeAmount; + } + + /** + * @notice Creates a new payment entry with the specified details. + * @param paymentId A unique identifier for the payment. + * @param buyerId The id of the buyer initiating the payment. + * @param itemId The identifier of the item being purchased. + * @param paymentToken The token to use for the payment. + * @param amount The amount to be paid for the item. + * @param expiration The timestamp after which the payment expires. + * @param lineItems Array of line items associated with this payment. + * @param externalFees Array of external fee metadata captured for this payment (informational only). + */ + function createPayment( + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + address paymentToken, + uint256 amount, + uint256 expiration, + LineItem[] calldata lineItems, + ExternalFees[] calldata externalFees + ) external; + + /** + * @notice Creates multiple payment entries in a single transaction to prevent nonce conflicts. + * @param paymentIds An array of unique identifiers for the payments. + * @param buyerIds An array of buyer IDs corresponding to each payment. + * @param itemIds An array of item identifiers corresponding to each payment. + * @param paymentTokens An array of tokens corresponding to each payment. + * @param amounts An array of amounts corresponding to each payment. + * @param expirations An array of expiration timestamps corresponding to each payment. + * @param lineItemsArray An array of line item arrays, one for each payment. + * @param externalFeesArray An array of external fee metadata arrays, one for each payment (informational only). + */ + function createPaymentBatch( + bytes32[] calldata paymentIds, + bytes32[] calldata buyerIds, + bytes32[] calldata itemIds, + address[] calldata paymentTokens, + uint256[] calldata amounts, + uint256[] calldata expirations, + LineItem[][] calldata lineItemsArray, + ExternalFees[][] calldata externalFeesArray + ) external; + + /** + * @notice Allows a buyer to make a direct crypto payment for an item. + * @dev This function transfers tokens directly from the buyer's wallet and confirms the payment immediately. + * @param paymentId The unique identifier of the payment. + * @param itemId The identifier of the item being purchased. + * @param buyerAddress The address of the buyer making the payment. + * @param paymentToken The token to use for the payment. + * @param amount The amount to be paid for the item. + * @param lineItems Array of line items associated with this payment. + * @param externalFees Array of external fee metadata captured for this payment (informational only). + */ + function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + LineItem[] calldata lineItems, + ExternalFees[] calldata externalFees + ) external; + + /** + * @notice Cancels an existing payment with the given payment ID. + * @param paymentId The unique identifier of the payment to cancel. + */ + function cancelPayment(bytes32 paymentId) external; + + /** + * @notice Confirms and finalizes the payment associated with the given payment ID. + * @param paymentId The unique identifier of the payment to confirm. + * @param buyerAddress Optional buyer address to mint NFT to. Pass address(0) to skip NFT minting. + */ + function confirmPayment(bytes32 paymentId, address buyerAddress) external; + + /** + * @notice Confirms and finalizes multiple payments in a single transaction. + * @param paymentIds An array of unique payment identifiers to be confirmed. + * @param buyerAddresses Array of buyer addresses to mint NFTs to. Must match paymentIds length. Pass address(0) to skip NFT minting for specific payments. + */ + function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) external; + + /** + * @notice Disburses fees collected by the treasury. + */ + function disburseFees() external; + + /** + * @notice Withdraws funds from the treasury. + */ + function withdraw() external; + + /** + * @notice Claims a refund for non-NFT payments (payments without minted NFTs). + * @dev Only callable by platform admin. Used for payments confirmed without a buyer address. + * @param paymentId The unique identifier of the refundable payment (must NOT have an NFT). + * @param refundAddress The address where the refunded amount should be sent. + */ + function claimRefund(bytes32 paymentId, address refundAddress) external; + + /** + * @notice Claims a refund for NFT payments (payments with minted NFTs). + * @dev Burns the NFT associated with the payment. Caller must have approved the treasury for the NFT. + * Used for processCryptoPayment and confirmPayment (with buyer address) transactions. + * @param paymentId The unique identifier of the refundable payment (must have an NFT). + */ + function claimRefund(bytes32 paymentId) external; + + /** + * @notice Allows platform admin to claim all remaining funds once the claim window has opened. + */ + function claimExpiredFunds() external; + + /** + * @notice Retrieves the platform identifier associated with the treasury. + * @return The platform identifier as a bytes32 value. + */ + function getplatformHash() external view returns (bytes32); + + /** + * @notice Retrieves the platform fee percentage for the treasury. + * @return The platform fee percentage as a uint256 value. + */ + function getplatformFeePercent() external view returns (uint256); + + /** + * @notice Retrieves the total raised amount in the treasury. + * @return The total raised amount as a uint256 value. + */ + function getRaisedAmount() external view returns (uint256); + + /** + * @notice Retrieves the currently available raised amount in the treasury. + * @return The current available raised amount as a uint256 value. + */ + function getAvailableRaisedAmount() external view returns (uint256); + + /** + * @notice Retrieves comprehensive payment data including payment info, token, line items, and external fees. + * @param paymentId The unique identifier of the payment. + * @return A PaymentData struct containing all payment information. + */ + function getPaymentData(bytes32 paymentId) external view returns (PaymentData memory); + + /** + * @notice Retrieves the lifetime raised amount in the treasury (never decreases with refunds). + * @return The lifetime raised amount as a uint256 value. + */ + function getLifetimeRaisedAmount() external view returns (uint256); + + /** + * @notice Retrieves the total refunded amount in the treasury. + * @return The total refunded amount as a uint256 value. + */ + function getRefundedAmount() external view returns (uint256); + + /** + * @notice Retrieves the total expected (pending) amount in the treasury. + * @dev This represents payments that have been created but not yet confirmed. + * @return The total expected amount as a uint256 value. + */ + function getExpectedAmount() external view returns (uint256); + + /** + * @notice Checks if the treasury has been cancelled. + * @return True if the treasury is cancelled, false otherwise. + */ + function cancelled() external view returns (bool); +} diff --git a/src/interfaces/ICampaignTreasury.sol b/src/interfaces/ICampaignTreasury.sol index 2b6c2b67..ea7ef0cc 100644 --- a/src/interfaces/ICampaignTreasury.sol +++ b/src/interfaces/ICampaignTreasury.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title ICampaignTreasury @@ -39,4 +39,22 @@ interface ICampaignTreasury { * @return The total raised amount as a uint256 value. */ function getRaisedAmount() external view returns (uint256); + + /** + * @notice Retrieves the lifetime raised amount in the treasury (never decreases with refunds). + * @return The lifetime raised amount as a uint256 value. + */ + function getLifetimeRaisedAmount() external view returns (uint256); + + /** + * @notice Retrieves the total refunded amount in the treasury. + * @return The total refunded amount as a uint256 value. + */ + function getRefundedAmount() external view returns (uint256); + + /** + * @notice Checks if the treasury has been cancelled. + * @return True if the treasury is cancelled, false otherwise. + */ + function cancelled() external view returns (bool); } diff --git a/src/interfaces/IGlobalParams.sol b/src/interfaces/IGlobalParams.sol index 4bc1f7dc..0f155ebc 100644 --- a/src/interfaces/IGlobalParams.sol +++ b/src/interfaces/IGlobalParams.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title IGlobalParams @@ -11,18 +11,14 @@ interface IGlobalParams { * @param _platformHash The unique identifier of the platform. * @return True if the platform is listed; otherwise, false. */ - function checkIfPlatformIsListed( - bytes32 _platformHash - ) external view returns (bool); + function checkIfPlatformIsListed(bytes32 _platformHash) external view returns (bool); /** * @notice Retrieves the admin address of a platform. * @param _platformHash The unique identifier of the platform. * @return The admin address of the platform. */ - function getPlatformAdminAddress( - bytes32 _platformHash - ) external view returns (address); + function getPlatformAdminAddress(bytes32 _platformHash) external view returns (address); /** * @notice Retrieves the number of listed platforms in the protocol. @@ -36,12 +32,6 @@ interface IGlobalParams { */ function getProtocolAdminAddress() external view returns (address); - /** - * @notice Retrieves the address of the protocol's native token. - * @return The address of the native token. - */ - function getTokenAddress() external view returns (address); - /** * @notice Retrieves the protocol fee percentage. * @return The protocol fee percentage as a uint256 value. @@ -53,27 +43,28 @@ interface IGlobalParams { * @param platformDataKey The key of the platform-specific data. * @return platformHash The platform identifier associated with the data. */ - function getPlatformDataOwner( - bytes32 platformDataKey - ) external view returns (bytes32 platformHash); + function getPlatformDataOwner(bytes32 platformDataKey) external view returns (bytes32 platformHash); /** * @notice Retrieves the platform fee percentage for a specific platform. * @param platformHash The unique identifier of the platform. * @return The platform fee percentage as a uint256 value. */ - function getPlatformFeePercent( - bytes32 platformHash - ) external view returns (uint256); + function getPlatformFeePercent(bytes32 platformHash) external view returns (uint256); + + /** + * @notice Retrieves the claim delay (in seconds) for a specific platform. + * @param platformHash The unique identifier of the platform. + * @return The claim delay in seconds. + */ + function getPlatformClaimDelay(bytes32 platformHash) external view returns (uint256); /** * @notice Checks if a platform-specific data key is valid. * @param platformDataKey The key of the platform-specific data. * @return isValid True if the data key is valid; otherwise, false. */ - function checkIfPlatformDataKeyValid( - bytes32 platformDataKey - ) external view returns (bool isValid); + function checkIfPlatformDataKeyValid(bytes32 platformDataKey) external view returns (bool isValid); /** * @notice Updates the admin address of the protocol. @@ -81,12 +72,6 @@ interface IGlobalParams { */ function updateProtocolAdminAddress(address _protocolAdminAddress) external; - /** - * @notice Updates the address of the protocol's native token. - * @param _tokenAddress The new address of the native token. - */ - function updateTokenAddress(address _tokenAddress) external; - /** * @notice Updates the protocol fee percentage. * @param _protocolFeePercent The new protocol fee percentage as a uint256 value. @@ -98,8 +83,105 @@ interface IGlobalParams { * @param _platformHash The unique identifier of the platform. * @param _platformAdminAddress The new admin address of the platform. */ - function updatePlatformAdminAddress( - bytes32 _platformHash, - address _platformAdminAddress + function updatePlatformAdminAddress(bytes32 _platformHash, address _platformAdminAddress) external; + + /** + * @notice Updates the claim delay for a specific platform. + * @param platformHash The unique identifier of the platform. + * @param claimDelay The claim delay in seconds. + */ + function updatePlatformClaimDelay(bytes32 platformHash, uint256 claimDelay) external; + + /** + * @notice Retrieves the adapter (trusted forwarder) address for a platform. + * @param platformHash The unique identifier of the platform. + * @return The adapter address for ERC-2771 meta-transactions. + */ + function getPlatformAdapter(bytes32 platformHash) external view returns (address); + + /** + * @notice Sets the adapter (trusted forwarder) address for a platform. + * @dev Only callable by the protocol admin (owner). + * @param platformHash The unique identifier of the platform. + * @param adapter The address of the adapter contract. + */ + function setPlatformAdapter(bytes32 platformHash, address adapter) external; + + /** + * @notice Adds a token to a currency. + * @param currency The currency identifier. + * @param token The token address to add. + */ + function addTokenToCurrency(bytes32 currency, address token) external; + + /** + * @notice Removes a token from a currency. + * @param currency The currency identifier. + * @param token The token address to remove. + */ + function removeTokenFromCurrency(bytes32 currency, address token) external; + + /** + * @notice Retrieves all tokens accepted for a specific currency. + * @param currency The currency identifier. + * @return An array of token addresses accepted for the currency. + */ + function getTokensForCurrency(bytes32 currency) external view returns (address[] memory); + + /** + * @notice Retrieves a value from the data registry. + * @param key The registry key. + * @return value The registry value. + */ + function getFromRegistry(bytes32 key) external view returns (bytes32 value); + + /** + * @notice Sets or updates a platform-specific line item type configuration. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type. + * @param label The label identifier for the line item type. + * @param countsTowardGoal Whether this line item counts toward the campaign goal. + * @param applyProtocolFee Whether this line item is included in protocol fee calculation. + * @param canRefund Whether this line item can be refunded. + * @param instantTransfer Whether this line item amount can be instantly transferred. + */ + function setPlatformLineItemType( + bytes32 platformHash, + bytes32 typeId, + string calldata label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer ) external; + + /** + * @notice Removes a platform-specific line item type by setting its exists flag to false. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type to remove. + */ + function removePlatformLineItemType(bytes32 platformHash, bytes32 typeId) external; + + /** + * @notice Retrieves a platform-specific line item type configuration. + * @param platformHash The identifier of the platform. + * @param typeId The identifier of the line item type. + * @return exists Whether this line item type exists and is active. + * @return label The label identifier for the line item type. + * @return countsTowardGoal Whether this line item counts toward the campaign goal. + * @return applyProtocolFee Whether this line item is included in protocol fee calculation. + * @return canRefund Whether this line item can be refunded. + * @return instantTransfer Whether this line item amount can be instantly transferred. + */ + function getPlatformLineItemType(bytes32 platformHash, bytes32 typeId) + external + view + returns ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ); } diff --git a/src/interfaces/IItem.sol b/src/interfaces/IItem.sol index 95a1aad8..d85a537b 100644 --- a/src/interfaces/IItem.sol +++ b/src/interfaces/IItem.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title IItem @@ -24,10 +24,7 @@ interface IItem { * @param itemId The unique identifier of the item. * @return item The attributes of the item as an `Item` struct. */ - function getItem( - address owner, - bytes32 itemId - ) external view returns (Item memory item); + function getItem(address owner, bytes32 itemId) external view returns (Item memory item); /** * @notice Adds a new item with the given attributes. diff --git a/src/interfaces/IReward.sol b/src/interfaces/IReward.sol index 92a3212f..0c570869 100644 --- a/src/interfaces/IReward.sol +++ b/src/interfaces/IReward.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title IReward * @notice An interface for managing rewards in a campaign. diff --git a/src/interfaces/ITreasuryFactory.sol b/src/interfaces/ITreasuryFactory.sol index ca3f4b74..9729e9df 100644 --- a/src/interfaces/ITreasuryFactory.sol +++ b/src/interfaces/ITreasuryFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title ITreasuryFactory @@ -20,6 +20,30 @@ interface ITreasuryFactory { address treasuryAddress ); + /** + * @dev Emitted when a treasury implementation is registered for a platform. + * @param platformHash The platform identifier. + * @param implementationId The ID of the implementation. + * @param implementation The contract address of the implementation. + */ + event TreasuryImplementationRegistered( + bytes32 indexed platformHash, uint256 indexed implementationId, address indexed implementation + ); + + /** + * @dev Emitted when a treasury implementation is removed from a platform. + * @param platformHash The platform identifier. + * @param implementationId The ID of the implementation. + */ + event TreasuryImplementationRemoved(bytes32 indexed platformHash, uint256 indexed implementationId); + + /** + * @dev Emitted when a treasury implementation is approved or disapproved by the protocol admin. + * @param implementation The contract address of the implementation. + * @param isApproved True if approved, false if disapproved. + */ + event TreasuryImplementationApproval(address indexed implementation, bool isApproved); + /** * @notice Registers a treasury implementation for a given platform. * @dev Callable only by the platform admin. @@ -27,11 +51,8 @@ interface ITreasuryFactory { * @param implementationId The ID to assign to the implementation. * @param implementation The contract address of the implementation. */ - function registerTreasuryImplementation( - bytes32 platformHash, - uint256 implementationId, - address implementation - ) external; + function registerTreasuryImplementation(bytes32 platformHash, uint256 implementationId, address implementation) + external; /** * @notice Approves a previously registered implementation. @@ -39,10 +60,7 @@ interface ITreasuryFactory { * @param platformHash The platform identifier. * @param implementationId The ID of the implementation to approve. */ - function approveTreasuryImplementation( - bytes32 platformHash, - uint256 implementationId - ) external; + function approveTreasuryImplementation(bytes32 platformHash, uint256 implementationId) external; /** * @notice Disapproves a previously approved treasury implementation. @@ -55,10 +73,7 @@ interface ITreasuryFactory { * @param platformHash The platform identifier. * @param implementationId The implementation ID to remove. */ - function removeTreasuryImplementation( - bytes32 platformHash, - uint256 implementationId - ) external; + function removeTreasuryImplementation(bytes32 platformHash, uint256 implementationId) external; /** * @notice Deploys a treasury clone using an approved implementation. @@ -66,15 +81,9 @@ interface ITreasuryFactory { * @param platformHash The platform identifier. * @param infoAddress The address of the campaign info contract. * @param implementationId The ID of the implementation to use. - * @param name The name of the treasury token. - * @param symbol The symbol of the treasury token. * @return clone The address of the deployed treasury contract. */ - function deploy( - bytes32 platformHash, - address infoAddress, - uint256 implementationId, - string calldata name, - string calldata symbol - ) external returns (address clone); + function deploy(bytes32 platformHash, address infoAddress, uint256 implementationId) + external + returns (address clone); } diff --git a/src/storage/AdminAccessCheckerStorage.sol b/src/storage/AdminAccessCheckerStorage.sol new file mode 100644 index 00000000..455904b2 --- /dev/null +++ b/src/storage/AdminAccessCheckerStorage.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IGlobalParams} from "../interfaces/IGlobalParams.sol"; + +/** + * @title AdminAccessCheckerStorage + * @notice Storage contract for AdminAccessChecker using ERC-7201 namespaced storage + * @dev This contract contains the storage layout and accessor functions for AdminAccessChecker + */ +library AdminAccessCheckerStorage { + /// @custom:storage-location erc7201:ccprotocol.storage.AdminAccessChecker + struct Storage { + IGlobalParams globalParams; + } + + // keccak256(abi.encode(uint256(keccak256("ccprotocol.storage.AdminAccessChecker")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ADMIN_ACCESS_CHECKER_STORAGE_LOCATION = + 0x7c2f08fa04c2c7c7ab255a45dbf913d4c236b91c59858917e818398e997f8800; + + function _getAdminAccessCheckerStorage() internal pure returns (Storage storage $) { + assembly { + $.slot := ADMIN_ACCESS_CHECKER_STORAGE_LOCATION + } + } +} diff --git a/src/storage/CampaignInfoFactoryStorage.sol b/src/storage/CampaignInfoFactoryStorage.sol new file mode 100644 index 00000000..d96d66db --- /dev/null +++ b/src/storage/CampaignInfoFactoryStorage.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IGlobalParams} from "../interfaces/IGlobalParams.sol"; + +/** + * @title CampaignInfoFactoryStorage + * @notice Storage contract for CampaignInfoFactory using ERC-7201 namespaced storage + * @dev This contract contains the storage layout and accessor functions for CampaignInfoFactory + */ +library CampaignInfoFactoryStorage { + /// @custom:storage-location erc7201:ccprotocol.storage.CampaignInfoFactory + struct Storage { + IGlobalParams globalParams; + address treasuryFactoryAddress; + address implementation; + mapping(address => bool) isValidCampaignInfo; + mapping(bytes32 => address) identifierToCampaignInfo; + } + + // keccak256(abi.encode(uint256(keccak256("ccprotocol.storage.CampaignInfoFactory")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant CAMPAIGN_INFO_FACTORY_STORAGE_LOCATION = + 0x2857858a392b093e1f8b3f368c2276ce911f27cef445605a2932ebe945968d00; + + function _getCampaignInfoFactoryStorage() internal pure returns (Storage storage $) { + assembly { + $.slot := CAMPAIGN_INFO_FACTORY_STORAGE_LOCATION + } + } +} diff --git a/src/storage/GlobalParamsStorage.sol b/src/storage/GlobalParamsStorage.sol new file mode 100644 index 00000000..22988f0a --- /dev/null +++ b/src/storage/GlobalParamsStorage.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Counters} from "../utils/Counters.sol"; + +/** + * @title GlobalParamsStorage + * @notice Storage contract for GlobalParams using ERC-7201 namespaced storage + * @dev This contract contains the storage layout and accessor functions for GlobalParams + */ +library GlobalParamsStorage { + using Counters for Counters.Counter; + + /** + * @notice Line item type configuration + * @param exists Whether this line item type exists and is active + * @param label The label identifier for the line item type (e.g., "shipping_fee") + * @param countsTowardGoal Whether this line item counts toward the campaign goal + * @param applyProtocolFee Whether this line item is included in protocol fee calculation + * @param canRefund Whether this line item can be refunded + * @param instantTransfer Whether this line item amount can be instantly transferred + */ + struct LineItemType { + bool exists; + string label; + bool countsTowardGoal; + bool applyProtocolFee; + bool canRefund; + bool instantTransfer; + } + + /// @custom:storage-location erc7201:ccprotocol.storage.GlobalParams + struct Storage { + address protocolAdminAddress; + uint256 protocolFeePercent; + mapping(bytes32 => bool) platformIsListed; + mapping(bytes32 => address) platformAdminAddress; + mapping(bytes32 => uint256) platformFeePercent; + mapping(bytes32 => bytes32) platformDataOwner; + mapping(bytes32 => bool) platformData; + mapping(bytes32 => bytes32) dataRegistry; + mapping(bytes32 => address[]) currencyToTokens; + // Platform-specific line item types: mapping(platformHash => mapping(typeId => LineItemType)) + mapping(bytes32 => mapping(bytes32 => LineItemType)) platformLineItemTypes; + mapping(bytes32 => uint256) platformClaimDelay; + // Platform adapter (trusted forwarder) for ERC-2771 meta-transactions: mapping(platformHash => adapterAddress) + mapping(bytes32 => address) platformAdapter; + Counters.Counter numberOfListedPlatforms; + } + + // keccak256(abi.encode(uint256(keccak256("ccprotocol.storage.GlobalParams")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant GLOBAL_PARAMS_STORAGE_LOCATION = + 0x83d0145f7c1378f10048390769ec94f999b3ba6d94904b8fd7251512962b1c00; + + function _getGlobalParamsStorage() internal pure returns (Storage storage $) { + assembly { + $.slot := GLOBAL_PARAMS_STORAGE_LOCATION + } + } +} diff --git a/src/storage/TreasuryFactoryStorage.sol b/src/storage/TreasuryFactoryStorage.sol new file mode 100644 index 00000000..67337c1a --- /dev/null +++ b/src/storage/TreasuryFactoryStorage.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +/** + * @title TreasuryFactoryStorage + * @notice Storage contract for TreasuryFactory using ERC-7201 namespaced storage + * @dev This contract contains the storage layout and accessor functions for TreasuryFactory + */ +library TreasuryFactoryStorage { + /// @custom:storage-location erc7201:ccprotocol.storage.TreasuryFactory + struct Storage { + mapping(bytes32 => mapping(uint256 => address)) implementationMap; + mapping(address => bool) approvedImplementations; + } + + // keccak256(abi.encode(uint256(keccak256("ccprotocol.storage.TreasuryFactory")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant TREASURY_FACTORY_STORAGE_LOCATION = + 0x96b7de8c171ef460648aea35787d043e89feb6b6de2623a1e6f17a91b9c9e900; + + function _getTreasuryFactoryStorage() internal pure returns (Storage storage $) { + assembly { + $.slot := TREASURY_FACTORY_STORAGE_LOCATION + } + } +} diff --git a/src/treasuries/AllOrNothing.sol b/src/treasuries/AllOrNothing.sol index 2a400a9e..fb9391da 100644 --- a/src/treasuries/AllOrNothing.sol +++ b/src/treasuries/AllOrNothing.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Counters} from "../utils/Counters.sol"; import {TimestampChecker} from "../utils/TimestampChecker.sol"; import {ICampaignTreasury} from "../interfaces/ICampaignTreasury.sol"; +import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; import {BaseTreasury} from "../utils/BaseTreasury.sol"; import {IReward} from "../interfaces/IReward.sol"; @@ -15,12 +15,7 @@ import {IReward} from "../interfaces/IReward.sol"; * @title AllOrNothing * @notice A contract for handling crowdfunding campaigns with rewards. */ -contract AllOrNothing is - IReward, - BaseTreasury, - TimestampChecker, - ERC721Burnable -{ +contract AllOrNothing is IReward, BaseTreasury, TimestampChecker, ReentrancyGuard { using Counters for Counters.Counter; using SafeERC20 for IERC20; @@ -30,17 +25,16 @@ contract AllOrNothing is mapping(uint256 => uint256) private s_tokenToPledgedAmount; // Mapping to store reward details by name mapping(bytes32 => Reward) private s_reward; + // Mapping to store the token used for each pledge + mapping(uint256 => address) private s_tokenIdToPledgeToken; - // Counters for token IDs and rewards - Counters.Counter private s_tokenIdCounter; + // Counter for reward tiers Counters.Counter private s_rewardCounter; - string private s_name; - string private s_symbol; - /** * @dev Emitted when a backer makes a pledge. * @param backer The address of the backer making the pledge. + * @param pledgeToken The token used for the pledge. * @param reward The name of the reward. * @param pledgeAmount The amount pledged. * @param tokenId The ID of the token representing the pledge. @@ -48,7 +42,8 @@ contract AllOrNothing is */ event Receipt( address indexed backer, - bytes32 indexed reward, + address indexed pledgeToken, + bytes32 reward, uint256 pledgeAmount, uint256 shippingFee, uint256 tokenId, @@ -110,6 +105,11 @@ contract AllOrNothing is */ error AllOrNothingRewardExists(); + /** + * @dev Emitted when a token is not accepted for the campaign. + */ + error AllOrNothingTokenNotAccepted(address token); + /** * @dev Emitted when claiming an unclaimable refund. * @param tokenId The ID of the token representing the pledge. @@ -119,25 +119,10 @@ contract AllOrNothing is /** * @dev Constructor for the AllOrNothing contract. */ - constructor() ERC721("", "") {} - - function initialize( - bytes32 _platformHash, - address _infoAddress, - string calldata _name, - string calldata _symbol - ) external initializer { - __BaseContract_init(_platformHash, _infoAddress); - s_name = _name; - s_symbol = _symbol; - } - - function name() public view override returns (string memory) { - return s_name; - } + constructor() {} - function symbol() public view override returns (string memory) { - return s_symbol; + function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer { + __BaseContract_init(_platformHash, _infoAddress, _trustedForwarder); } /** @@ -145,9 +130,7 @@ contract AllOrNothing is * @param rewardName The name of the reward. * @return reward The details of the reward as a `Reward` struct. */ - function getReward( - bytes32 rewardName - ) external view returns (Reward memory reward) { + function getReward(bytes32 rewardName) external view returns (Reward memory reward) { if (s_reward[rewardName].rewardValue == 0) { revert AllOrNothingInvalidInput(); } @@ -158,7 +141,56 @@ contract AllOrNothing is * @inheritdoc ICampaignTreasury */ function getRaisedAmount() external view override returns (uint256) { - return s_pledgedAmount; + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_tokenRaisedAmounts[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignTreasury + */ + function getLifetimeRaisedAmount() external view override returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_tokenLifetimeRaisedAmounts[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignTreasury + */ + function getRefundedAmount() external view override returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 lifetimeAmount = s_tokenLifetimeRaisedAmounts[token]; + uint256 currentAmount = s_tokenRaisedAmounts[token]; + uint256 refundedAmount = lifetimeAmount - currentAmount; + if (refundedAmount > 0) { + totalNormalized += _normalizeAmount(token, refundedAmount); + } + } + + return totalNormalized; } /** @@ -170,10 +202,7 @@ contract AllOrNothing is * @param rewardNames An array of reward names. * @param rewards An array of `Reward` structs containing reward details. */ - function addRewards( - bytes32[] calldata rewardNames, - Reward[] calldata rewards - ) + function addRewards(bytes32[] calldata rewardNames, Reward[] calldata rewards) external onlyCampaignOwner whenCampaignNotPaused @@ -196,8 +225,8 @@ contract AllOrNothing is // If there are any items, their arrays must match in length if ( - (reward.itemId.length != reward.itemValue.length) || - (reward.itemId.length != reward.itemQuantity.length) + (reward.itemId.length != reward.itemValue.length) + || (reward.itemId.length != reward.itemQuantity.length) ) { revert AllOrNothingInvalidInput(); } @@ -217,9 +246,7 @@ contract AllOrNothing is * @notice Removes a reward from the campaign. * @param rewardName The name of the reward. */ - function removeReward( - bytes32 rewardName - ) + function removeReward(bytes32 rewardName) external onlyCampaignOwner whenCampaignNotPaused @@ -240,28 +267,24 @@ contract AllOrNothing is * @dev The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. * The non-reward tiers cannot be pledged for without a reward. * @param backer The address of the backer making the pledge. + * @param pledgeToken The token address to use for the pledge. + * @param shippingFee The shipping fee amount. * @param reward An array of reward names. */ - function pledgeForAReward( - address backer, - uint256 shippingFee, - bytes32[] calldata reward - ) + function pledgeForAReward(address backer, address pledgeToken, uint256 shippingFee, bytes32[] calldata reward) external + nonReentrant currentTimeIsWithinRange(INFO.getLaunchTime(), INFO.getDeadline()) whenCampaignNotPaused whenNotPaused whenCampaignNotCancelled whenNotCancelled { - uint256 tokenId = s_tokenIdCounter.current(); uint256 rewardLen = reward.length; Reward storage tempReward = s_reward[reward[0]]; if ( - backer == address(0) || - rewardLen > s_rewardCounter.current() || - reward[0] == ZERO_BYTES || - !tempReward.isRewardTier + backer == address(0) || rewardLen > s_rewardCounter.current() || reward[0] == ZERO_BYTES + || !tempReward.isRewardTier ) { revert AllOrNothingInvalidInput(); } @@ -270,40 +293,40 @@ contract AllOrNothing is if (reward[i] == ZERO_BYTES) { revert AllOrNothingInvalidInput(); } - pledgeAmount += s_reward[reward[i]].rewardValue; + tempReward = s_reward[reward[i]]; + if (tempReward.rewardValue == 0) { + revert AllOrNothingInvalidInput(); + } + pledgeAmount += tempReward.rewardValue; } - _pledge(backer, reward[0], pledgeAmount, shippingFee, tokenId, reward); + _pledge(backer, pledgeToken, reward[0], pledgeAmount, shippingFee, reward); } /** * @notice Allows a backer to pledge without selecting a reward. * @param backer The address of the backer making the pledge. + * @param pledgeToken The token address to use for the pledge. * @param pledgeAmount The amount of the pledge. */ - function pledgeWithoutAReward( - address backer, - uint256 pledgeAmount - ) + function pledgeWithoutAReward(address backer, address pledgeToken, uint256 pledgeAmount) external + nonReentrant currentTimeIsWithinRange(INFO.getLaunchTime(), INFO.getDeadline()) whenCampaignNotPaused whenNotPaused whenCampaignNotCancelled whenNotCancelled { - uint256 tokenId = s_tokenIdCounter.current(); bytes32[] memory emptyByteArray = new bytes32[](0); - _pledge(backer, ZERO_BYTES, pledgeAmount, 0, tokenId, emptyByteArray); + _pledge(backer, pledgeToken, ZERO_BYTES, pledgeAmount, 0, emptyByteArray); } /** * @notice Allows a backer to claim a refund. * @param tokenId The ID of the token representing the pledge. */ - function claimRefund( - uint256 tokenId - ) + function claimRefund(uint256 tokenId) external currentTimeIsGreater(INFO.getLaunchTime()) whenCampaignNotPaused @@ -312,29 +335,34 @@ contract AllOrNothing is if (block.timestamp >= INFO.getDeadline() && _checkSuccessCondition()) { revert AllOrNothingNotClaimable(tokenId); } + + // Get NFT owner before burning + address nftOwner = INFO.ownerOf(tokenId); + uint256 amountToRefund = s_tokenToTotalCollectedAmount[tokenId]; uint256 pledgedAmount = s_tokenToPledgedAmount[tokenId]; + address pledgeToken = s_tokenIdToPledgeToken[tokenId]; + if (amountToRefund == 0) { revert AllOrNothingNotClaimable(tokenId); } + s_tokenToTotalCollectedAmount[tokenId] = 0; s_tokenToPledgedAmount[tokenId] = 0; - s_pledgedAmount -= pledgedAmount; - burn(tokenId); - TOKEN.safeTransfer(msg.sender, amountToRefund); - emit RefundClaimed(tokenId, amountToRefund, msg.sender); + s_tokenRaisedAmounts[pledgeToken] -= pledgedAmount; + delete s_tokenIdToPledgeToken[tokenId]; + + // Burn the NFT (requires treasury approval from owner) + INFO.burn(tokenId); + + IERC20(pledgeToken).safeTransfer(nftOwner, amountToRefund); + emit RefundClaimed(tokenId, amountToRefund, nftOwner); } /** * @inheritdoc ICampaignTreasury */ - function disburseFees() - public - override - currentTimeIsGreater(INFO.getDeadline()) - whenNotPaused - whenNotCancelled - { + function disburseFees() public override currentTimeIsGreater(INFO.getDeadline()) whenNotPaused whenNotCancelled { if (s_feesDisbursed) { revert AllOrNothingFeeAlreadyDisbursed(); } @@ -353,10 +381,7 @@ contract AllOrNothing is * @dev This function is overridden to allow the platform admin and the campaign owner to cancel a treasury. */ function cancelTreasury(bytes32 message) public override { - if ( - msg.sender != INFO.getPlatformAdminAddress(PLATFORM_HASH) && - msg.sender != INFO.owner() - ) { + if (_msgSender() != INFO.getPlatformAdminAddress(PLATFORM_HASH) && _msgSender() != INFO.owner()) { revert AllOrNothingUnAuthorized(); } _cancel(message); @@ -365,45 +390,52 @@ contract AllOrNothing is /** * @inheritdoc BaseTreasury */ - function _checkSuccessCondition() - internal - view - virtual - override - returns (bool) - { + function _checkSuccessCondition() internal view virtual override returns (bool) { return INFO.getTotalRaisedAmount() >= INFO.getGoalAmount(); } function _pledge( address backer, + address pledgeToken, bytes32 reward, uint256 pledgeAmount, uint256 shippingFee, - uint256 tokenId, bytes32[] memory rewards ) private { - uint256 totalAmount = pledgeAmount + shippingFee; - TOKEN.safeTransferFrom(backer, address(this), totalAmount); - s_tokenIdCounter.increment(); - s_tokenToPledgedAmount[tokenId] = pledgeAmount; - s_tokenToTotalCollectedAmount[tokenId] = totalAmount; - s_pledgedAmount += pledgeAmount; - _safeMint(backer, tokenId, abi.encodePacked(backer, reward, rewards)); - emit Receipt( - backer, - reward, - pledgeAmount, - shippingFee, - tokenId, - rewards + // Validate token is accepted + if (!INFO.isTokenAccepted(pledgeToken)) { + revert AllOrNothingTokenNotAccepted(pledgeToken); + } + + // If this is for a reward, pledgeAmount and shippingFee are in 18 decimals + // If not for a reward, amounts are already in token decimals + uint256 pledgeAmountInTokenDecimals; + uint256 shippingFeeInTokenDecimals; + + if (reward != ZERO_BYTES) { + // Reward pledge: denormalize from 18 decimals to token decimals + pledgeAmountInTokenDecimals = _denormalizeAmount(pledgeToken, pledgeAmount); + shippingFeeInTokenDecimals = _denormalizeAmount(pledgeToken, shippingFee); + } else { + // Non-reward pledge: already in token decimals + pledgeAmountInTokenDecimals = pledgeAmount; + shippingFeeInTokenDecimals = shippingFee; + } + + uint256 totalAmount = pledgeAmountInTokenDecimals + shippingFeeInTokenDecimals; + + IERC20(pledgeToken).safeTransferFrom(backer, address(this), totalAmount); + + uint256 tokenId = INFO.mintNFTForPledge( + backer, reward, pledgeToken, pledgeAmountInTokenDecimals, shippingFeeInTokenDecimals, 0 ); - } - // The following functions are overrides required by Solidity. - function supportsInterface( - bytes4 interfaceId - ) public view override returns (bool) { - return super.supportsInterface(interfaceId); + s_tokenToPledgedAmount[tokenId] = pledgeAmountInTokenDecimals; + s_tokenToTotalCollectedAmount[tokenId] = totalAmount; + s_tokenIdToPledgeToken[tokenId] = pledgeToken; + s_tokenRaisedAmounts[pledgeToken] += pledgeAmountInTokenDecimals; + s_tokenLifetimeRaisedAmounts[pledgeToken] += pledgeAmountInTokenDecimals; + + emit Receipt(backer, pledgeToken, reward, pledgeAmount, shippingFee, tokenId, rewards); } } diff --git a/src/treasuries/KeepWhatsRaised.sol b/src/treasuries/KeepWhatsRaised.sol new file mode 100644 index 00000000..673e039d --- /dev/null +++ b/src/treasuries/KeepWhatsRaised.sol @@ -0,0 +1,1240 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import {Counters} from "../utils/Counters.sol"; +import {TimestampChecker} from "../utils/TimestampChecker.sol"; +import {BaseTreasury} from "../utils/BaseTreasury.sol"; +import {ICampaignTreasury} from "../interfaces/ICampaignTreasury.sol"; +import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; +import {IReward} from "../interfaces/IReward.sol"; +import {ICampaignData} from "../interfaces/ICampaignData.sol"; + +/** + * @title KeepWhatsRaised + * @notice A contract that keeps all the funds raised, regardless of the success condition. + */ +contract KeepWhatsRaised is IReward, BaseTreasury, TimestampChecker, ICampaignData, ReentrancyGuard { + using Counters for Counters.Counter; + using SafeERC20 for IERC20; + + // Mapping to store the pledged amount per token ID + mapping(uint256 => uint256) private s_tokenToPledgedAmount; + // Mapping to store the tipped amount per token ID + mapping(uint256 => uint256) private s_tokenToTippedAmount; + // Mapping to store the payment fee per token ID + mapping(uint256 => uint256) private s_tokenToPaymentFee; + // Mapping to store reward details by name + mapping(bytes32 => Reward) private s_reward; + /// Tracks whether a pledge with a specific ID has already been processed + mapping(bytes32 => bool) public s_processedPledges; + /// Mapping to store payment gateway fees by unique pledge ID + mapping(bytes32 => uint256) public s_paymentGatewayFees; + /// Mapping that stores fee values indexed by their corresponding fee keys. + mapping(bytes32 => uint256) private s_feeValues; + + // Multi-token support + mapping(uint256 => address) private s_tokenIdToPledgeToken; // Token used for each pledge + mapping(address => uint256) private s_protocolFeePerToken; // Protocol fees per token + mapping(address => uint256) private s_platformFeePerToken; // Platform fees per token + mapping(address => uint256) private s_tipPerToken; // Tips per token + mapping(address => uint256) private s_availablePerToken; // Available amount per token + + // Counter for reward tiers + Counters.Counter private s_rewardCounter; + + /** + * @dev Represents keys used to reference different fee configurations. + * These keys are typically used to look up fee values stored in `s_platformData`. + */ + struct FeeKeys { + /// @dev Key for a flat fee applied to an operation. + bytes32 flatFeeKey; + /// @dev Key for a cumulative flat fee, potentially across multiple actions. + bytes32 cumulativeFlatFeeKey; + /// @dev Keys for gross percentage-based fees (calculated before deductions). + bytes32[] grossPercentageFeeKeys; + } + + /** + * @dev Represents the complete fee structure values for treasury operations. + * These values correspond to the fees that will be applied to transactions + * and are typically retrieved using keys from `FeeKeys` struct. + */ + struct FeeValues { + /// @dev Value for a flat fee applied to an operation. + uint256 flatFeeValue; + /// @dev Value for a cumulative flat fee, potentially across multiple actions. + uint256 cumulativeFlatFeeValue; + /// @dev Values for gross percentage-based fees (calculated before deductions). + uint256[] grossPercentageFeeValues; + } + /** + * @dev System configuration parameters related to withdrawal and refund behavior. + */ + + struct Config { + /// @dev The minimum withdrawal amount required to qualify for fee exemption. + uint256 minimumWithdrawalForFeeExemption; + /// @dev Time delay (in timestamp) enforced before a withdrawal can be completed. + uint256 withdrawalDelay; + /// @dev Time delay (in timestamp) before a refund becomes claimable or processed. + uint256 refundDelay; + /// @dev Duration (in timestamp) for which config changes are locked to prevent immediate updates. + uint256 configLockPeriod; + /// @dev True if the creator is Colombian, false otherwise. + bool isColombianCreator; + } + + uint256 private s_cancellationTime; + bool private s_isWithdrawalApproved; + bool private s_tipClaimed; + bool private s_fundClaimed; + FeeKeys private s_feeKeys; + Config private s_config; + CampaignData private s_campaignData; + + /** + * @dev Emitted when a backer makes a pledge. + * @param backer The address of the backer making the pledge. + * @param pledgeToken The token used for the pledge. + * @param reward The name of the reward. + * @param pledgeAmount The amount pledged. + * @param tip An optional tip can be added during the process. + * @param tokenId The ID of the token representing the pledge. + * @param rewards An array of reward names. + */ + event Receipt( + address indexed backer, + address indexed pledgeToken, + bytes32 reward, + uint256 pledgeAmount, + uint256 tip, + uint256 tokenId, + bytes32[] rewards + ); + + /** + * @dev Emitted when rewards are added to the campaign. + * @param rewardNames The names of the rewards. + * @param rewards The details of the rewards. + */ + event RewardsAdded(bytes32[] rewardNames, Reward[] rewards); + + /** + * @dev Emitted when a reward is removed from the campaign. + * @param rewardName The name of the reward. + */ + event RewardRemoved(bytes32 indexed rewardName); + + /// @dev Emitted when withdrawal functionality has been approved by the platform admin. + event WithdrawalApproved(); + + /** + * @dev Emitted when the treasury configuration is updated. + * @param config The updated configuration parameters (e.g., delays, exemptions). + * @param campaignData The campaign-related data associated with the treasury setup. + * @param feeKeys The set of keys used to determine applicable fees. + * @param feeValues The fee values corresponding to the fee keys. + */ + event TreasuryConfigured(Config config, CampaignData campaignData, FeeKeys feeKeys, FeeValues feeValues); + + /** + * @dev Emitted when a withdrawal is successfully processed along with the applied fee. + * @param to The recipient address receiving the funds. + * @param amount The total amount withdrawn (excluding fee). + * @param fee The fee amount deducted from the withdrawal. + */ + event WithdrawalWithFeeSuccessful(address indexed to, uint256 amount, uint256 fee); + + /** + * @dev Emitted when a tip is claimed from the contract. + * @param amount The amount of tip claimed. + * @param claimer The address that claimed the tip. + */ + event TipClaimed(uint256 amount, address indexed claimer); + + /** + * @dev Emitted when campaign or user's remaining funds are successfully claimed by the platform admin. + * @param amount The amount of funds claimed. + * @param claimer The address that claimed the funds. + */ + event FundClaimed(uint256 amount, address indexed claimer); + + /** + * @dev Emitted when a refund is claimed. + * @param tokenId The ID of the token representing the pledge. + * @param refundAmount The refund amount claimed. + * @param claimer The address of the claimer. + */ + event RefundClaimed(uint256 indexed tokenId, uint256 refundAmount, address indexed claimer); + + /** + * @dev Emitted when the deadline of the campaign is updated. + * @param newDeadline The new deadline. + */ + event KeepWhatsRaisedDeadlineUpdated(uint256 newDeadline); + + /** + * @dev Emitted when the goal amount for a campaign is updated. + * @param newGoalAmount The new goal amount set for the campaign. + */ + event KeepWhatsRaisedGoalAmountUpdated(uint256 newGoalAmount); + + /** + * @dev Emitted when a gateway fee is set for a specific pledge. + * @param pledgeId The unique identifier of the pledge. + * @param fee The amount of the payment gateway fee set. + */ + event KeepWhatsRaisedPaymentGatewayFeeSet(bytes32 indexed pledgeId, uint256 fee); + + /** + * @dev Emitted when an unauthorized action is attempted. + */ + error KeepWhatsRaisedUnAuthorized(); + + /** + * @dev Emitted when an invalid input is detected. + */ + error KeepWhatsRaisedInvalidInput(); + + /** + * @dev Emitted when a token is not accepted for the campaign. + */ + error KeepWhatsRaisedTokenNotAccepted(address token); + + /** + * @dev Emitted when a `Reward` already exists for given input. + */ + error KeepWhatsRaisedRewardExists(); + + /** + * @dev Emitted when anyone called a disabled function. + */ + error KeepWhatsRaisedDisabled(); + + /** + * @dev Emitted when any functionality is already enabled and cannot be re-enabled. + */ + error KeepWhatsRaisedAlreadyEnabled(); + + /** + * @dev Emitted when a withdrawal attempt exceeds the available funds after accounting for the fee. + * @param availableAmount The maximum amount that can be withdrawn. + * @param withdrawalAmount The attempted withdrawal amount. + * @param fee The fee that would be applied to the withdrawal. + */ + error KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee( + uint256 availableAmount, uint256 withdrawalAmount, uint256 fee + ); + + /** + * @notice Emitted when the fee exceeds the requested withdrawal amount. + * + * @param withdrawalAmount The amount requested for withdrawal. + * @param fee The calculated fee, which is greater than the withdrawal amount. + */ + error KeepWhatsRaisedInsufficientFundsForFee(uint256 withdrawalAmount, uint256 fee); + + /** + * @dev Emitted when a withdrawal has already been made and cannot be repeated. + */ + error KeepWhatsRaisedAlreadyWithdrawn(); + + /** + * @dev Emitted when funds or rewards have already been claimed for the given context. + */ + error KeepWhatsRaisedAlreadyClaimed(); + + /** + * @dev Emitted when a token or pledge is not eligible for claiming (e.g., claim period not reached or not valid). + * @param tokenId The ID of the token that was attempted to be claimed. + */ + error KeepWhatsRaisedNotClaimable(uint256 tokenId); + + /** + * @dev Emitted when an admin attempts to claim funds that are not yet claimable according to the rules. + */ + error KeepWhatsRaisedNotClaimableAdmin(); + + /** + * @dev Emitted when a configuration change is attempted during the lock period. + */ + error KeepWhatsRaisedConfigLocked(); + + /** + * @dev Emitted when a disbursement is attempted before the refund period has ended. + */ + error KeepWhatsRaisedDisbursementBlocked(); + + /** + * @dev Emitted when a pledge is submitted using a pledgeId that has already been processed. + * @param pledgeId The unique identifier of the pledge that was already used. + */ + error KeepWhatsRaisedPledgeAlreadyProcessed(bytes32 pledgeId); + + /** + * @dev Ensures that withdrawals are currently enabled. + * Reverts with `KeepWhatsRaisedDisabled` if the withdrawal approval flag is not set. + */ + modifier withdrawalEnabled() { + if (!s_isWithdrawalApproved) { + revert KeepWhatsRaisedDisabled(); + } + _; + } + + /** + * @dev Restricts execution to only occur before the configuration lock period. + * Reverts with `KeepWhatsRaisedConfigLocked` if called too close to or after the campaign deadline. + * The lock period is defined as the duration before the deadline during which configuration changes are not allowed. + */ + modifier onlyBeforeConfigLock() { + if (block.timestamp > s_campaignData.deadline - s_config.configLockPeriod) { + revert KeepWhatsRaisedConfigLocked(); + } + _; + } + + /// @notice Restricts access to only the platform admin or the campaign owner. + /// @dev Checks if `_msgSender()` is either the platform admin (via `INFO.getPlatformAdminAddress`) + /// or the campaign owner (via `INFO.owner()`). Reverts with `KeepWhatsRaisedUnAuthorized` if not authorized. + modifier onlyPlatformAdminOrCampaignOwner() { + if (_msgSender() != INFO.getPlatformAdminAddress(PLATFORM_HASH) && _msgSender() != INFO.owner()) { + revert KeepWhatsRaisedUnAuthorized(); + } + _; + } + + /** + * @dev Constructor for the KeepWhatsRaised contract. + */ + constructor() {} + + function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer { + __BaseContract_init(_platformHash, _infoAddress, _trustedForwarder); + } + + /** + * @notice Retrieves the withdrawal approval status. + */ + function getWithdrawalApprovalStatus() public view returns (bool) { + return s_isWithdrawalApproved; + } + + /** + * @notice Retrieves the details of a reward. + * @param rewardName The name of the reward. + * @return reward The details of the reward as a `Reward` struct. + */ + function getReward(bytes32 rewardName) external view returns (Reward memory reward) { + if (s_reward[rewardName].rewardValue == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + return s_reward[rewardName]; + } + + /** + * @inheritdoc ICampaignTreasury + */ + function getRaisedAmount() external view override returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_tokenRaisedAmounts[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignTreasury + */ + function getLifetimeRaisedAmount() external view override returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_tokenLifetimeRaisedAmounts[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignTreasury + */ + function getRefundedAmount() external view override returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 lifetimeAmount = s_tokenLifetimeRaisedAmounts[token]; + uint256 currentAmount = s_tokenRaisedAmounts[token]; + uint256 refundedAmount = lifetimeAmount - currentAmount; + if (refundedAmount > 0) { + totalNormalized += _normalizeAmount(token, refundedAmount); + } + } + + return totalNormalized; + } + + /** + * @notice Retrieves the currently available raised amount in the treasury. + * @return The current available raised amount as a uint256 value. + */ + function getAvailableRaisedAmount() external view returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_availablePerToken[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @notice Retrieves the campaign's launch time. + * @return The timestamp when the campaign was launched. + */ + function getLaunchTime() public view returns (uint256) { + return s_campaignData.launchTime; + } + + /** + * @notice Retrieves the campaign's deadline. + * @return The timestamp when the campaign ends. + */ + function getDeadline() public view returns (uint256) { + return s_campaignData.deadline; + } + + /** + * @notice Retrieves the campaign's funding goal amount. + * @return The funding goal amount of the campaign. + */ + function getGoalAmount() external view returns (uint256) { + return s_campaignData.goalAmount; + } + + /** + * @notice Retrieves the payment gateway fee for a given pledge ID. + * @param pledgeId The unique identifier of the pledge. + * @return The fixed gateway fee amount associated with the pledge ID. + */ + function getPaymentGatewayFee(bytes32 pledgeId) public view returns (uint256) { + return s_paymentGatewayFees[pledgeId]; + } + + /** + * @dev Retrieves the fee value associated with a specific fee key from storage. + * @param {bytes32} feeKey - The unique identifier key used to reference a specific fee type. + * + * @return {uint256} The fee value corresponding to the provided fee key. + */ + function getFeeValue(bytes32 feeKey) public view returns (uint256) { + return s_feeValues[feeKey]; + } + + /** + * @notice Sets the fixed payment gateway fee for a specific pledge. + * @param pledgeId The unique identifier of the pledge. + * @param fee The gateway fee amount to be associated with the given pledge ID. + */ + function setPaymentGatewayFee(bytes32 pledgeId, uint256 fee) + public + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + s_paymentGatewayFees[pledgeId] = fee; + + emit KeepWhatsRaisedPaymentGatewayFeeSet(pledgeId, fee); + } + + /** + * @notice Approves the withdrawal of the treasury by the platform admin. + */ + function approveWithdrawal() + external + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + if (s_isWithdrawalApproved) { + revert KeepWhatsRaisedAlreadyEnabled(); + } + + s_isWithdrawalApproved = true; + + emit WithdrawalApproved(); + } + + /** + * @dev Configures the treasury for a campaign by setting the system parameters, + * campaign-specific data, and fee configuration keys. + * + * @param config The configuration settings including withdrawal delay, refund delay, + * fee exemption threshold, and configuration lock period. + * @param campaignData The campaign-related metadata such as deadlines and funding goals. + * @param feeKeys The set of keys used to reference applicable flat and percentage-based fees. + * @param feeValues The fee values corresponding to the fee keys. + */ + function configureTreasury( + Config memory config, + CampaignData memory campaignData, + FeeKeys memory feeKeys, + FeeValues memory feeValues + ) + external + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + if (campaignData.launchTime < block.timestamp || campaignData.deadline <= campaignData.launchTime) { + revert KeepWhatsRaisedInvalidInput(); + } + if (feeKeys.grossPercentageFeeKeys.length != feeValues.grossPercentageFeeValues.length) { + revert KeepWhatsRaisedInvalidInput(); + } + + s_config = config; + s_feeKeys = feeKeys; + s_campaignData = campaignData; + + s_feeValues[feeKeys.flatFeeKey] = feeValues.flatFeeValue; + s_feeValues[feeKeys.cumulativeFlatFeeKey] = feeValues.cumulativeFlatFeeValue; + + for (uint256 i = 0; i < feeKeys.grossPercentageFeeKeys.length; i++) { + s_feeValues[feeKeys.grossPercentageFeeKeys[i]] = feeValues.grossPercentageFeeValues[i]; + } + + emit TreasuryConfigured(config, campaignData, feeKeys, feeValues); + } + + /** + * @dev Updates the campaign's deadline. + * + * @param deadline The new deadline timestamp for the campaign. + * + * Requirements: + * - Must be called before the configuration lock period (see `onlyBeforeConfigLock`). + * - The new deadline must be a future timestamp. + */ + function updateDeadline(uint256 deadline) + external + onlyPlatformAdminOrCampaignOwner + onlyBeforeConfigLock + whenNotPaused + whenNotCancelled + { + if (deadline <= getLaunchTime() || deadline <= block.timestamp) { + revert KeepWhatsRaisedInvalidInput(); + } + + s_campaignData.deadline = deadline; + emit KeepWhatsRaisedDeadlineUpdated(deadline); + } + + /** + * @dev Updates the funding goal amount for the campaign. + * + * @param goalAmount The new goal amount. + * + * Requirements: + * - Must be called before the configuration lock period (see `onlyBeforeConfigLock`). + */ + function updateGoalAmount(uint256 goalAmount) + external + onlyPlatformAdminOrCampaignOwner + onlyBeforeConfigLock + whenNotPaused + whenNotCancelled + { + if (goalAmount == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + s_campaignData.goalAmount = goalAmount; + emit KeepWhatsRaisedGoalAmountUpdated(goalAmount); + } + + /** + * @notice Adds multiple rewards in a batch. + * @dev This function allows for both reward tiers and non-reward tiers. + * For both types, rewards must have non-zero value. + * If items are specified (non-empty arrays), the itemId, itemValue, and itemQuantity arrays must match in length. + * Empty arrays are allowed for both reward tiers and non-reward tiers. + * @param rewardNames An array of reward names. + * @param rewards An array of `Reward` structs containing reward details. + */ + function addRewards(bytes32[] calldata rewardNames, Reward[] calldata rewards) + external + onlyCampaignOwner + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + if (rewardNames.length != rewards.length) { + revert KeepWhatsRaisedInvalidInput(); + } + + for (uint256 i = 0; i < rewardNames.length; i++) { + bytes32 rewardName = rewardNames[i]; + Reward calldata reward = rewards[i]; + + // Reward name must not be zero bytes and reward value must be non-zero + if (rewardName == ZERO_BYTES || reward.rewardValue == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + + // If there are any items, their arrays must match in length + if ( + (reward.itemId.length != reward.itemValue.length) + || (reward.itemId.length != reward.itemQuantity.length) + ) { + revert KeepWhatsRaisedInvalidInput(); + } + + // Check for duplicate reward + if (s_reward[rewardName].rewardValue != 0) { + revert KeepWhatsRaisedRewardExists(); + } + + s_reward[rewardName] = reward; + s_rewardCounter.increment(); + } + emit RewardsAdded(rewardNames, rewards); + } + + /** + * @notice Removes a reward from the campaign. + * @param rewardName The name of the reward. + */ + function removeReward(bytes32 rewardName) + external + onlyCampaignOwner + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + if (s_reward[rewardName].rewardValue == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + delete s_reward[rewardName]; + s_rewardCounter.decrement(); + emit RewardRemoved(rewardName); + } + + /** + * @notice Sets the payment gateway fee and executes a pledge in a single transaction. + * @param pledgeId The unique identifier of the pledge. + * @param backer The address of the backer making the pledge. + * @param pledgeAmount The amount of the pledge. + * @param tip An optional tip can be added during the process. + * @param fee The payment gateway fee to associate with this pledge. + * @param reward An array of reward names. + * @param isPledgeForAReward A boolean indicating whether this pledge is for a reward or without.. + */ + function setFeeAndPledge( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 pledgeAmount, + uint256 tip, + uint256 fee, + bytes32[] calldata reward, + bool isPledgeForAReward + ) + external + nonReentrant + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + //Set Payment Gateway Fee + setPaymentGatewayFee(pledgeId, fee); + + if (isPledgeForAReward) { + _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, _msgSender()); // Pass admin as token source + } else { + _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, _msgSender()); // Pass admin as token source + } + } + + /** + * @notice Allows a backer to pledge for a reward. + * @dev The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. + * The non-reward tiers cannot be pledged for without a reward. + * @param pledgeId The unique identifier of the pledge. + * @param backer The address of the backer making the pledge. + * @param pledgeToken The token to use for the pledge. + * @param tip An optional tip can be added during the process. + * @param reward An array of reward names. + */ + function pledgeForAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 tip, + bytes32[] calldata reward + ) + public + nonReentrant + currentTimeIsWithinRange(getLaunchTime(), getDeadline()) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + _pledgeForAReward(pledgeId, backer, pledgeToken, tip, reward, backer); // Pass backer as token source for direct calls + } + + /** + * @notice Internal function that allows a backer to pledge for a reward with tokens transferred from a specified source. + * @dev The first element of the `reward` array must be a reward tier and the other elements can be either reward tiers or non-reward tiers. + * The non-reward tiers cannot be pledged for without a reward. + * This function is called internally by both public pledgeForAReward (with backer as token source) and + * setFeeAndPledge (with admin as token source). + * @param pledgeId The unique identifier of the pledge. + * @param backer The address of the backer making the pledge (receives the NFT). + * @param pledgeToken The token to use for the pledge. + * @param tip An optional tip can be added during the process. + * @param reward An array of reward names. + * @param tokenSource The address from which tokens will be transferred (either backer for direct calls or admin for setFeeAndPledge calls). + */ + function _pledgeForAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 tip, + bytes32[] calldata reward, + address tokenSource + ) internal { + bytes32 internalPledgeId = keccak256(abi.encodePacked(pledgeId, _msgSender())); + + if (s_processedPledges[internalPledgeId]) { + revert KeepWhatsRaisedPledgeAlreadyProcessed(internalPledgeId); + } + s_processedPledges[internalPledgeId] = true; + + uint256 rewardLen = reward.length; + Reward memory tempReward = s_reward[reward[0]]; + if ( + backer == address(0) || rewardLen > s_rewardCounter.current() || reward[0] == ZERO_BYTES + || !tempReward.isRewardTier + ) { + revert KeepWhatsRaisedInvalidInput(); + } + uint256 pledgeAmount = tempReward.rewardValue; + for (uint256 i = 1; i < rewardLen; i++) { + if (reward[i] == ZERO_BYTES) { + revert KeepWhatsRaisedInvalidInput(); + } + tempReward = s_reward[reward[i]]; + if (tempReward.rewardValue == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + pledgeAmount += tempReward.rewardValue; + } + _pledge(pledgeId, backer, pledgeToken, reward[0], pledgeAmount, tip, reward, tokenSource); + } + + /** + * @notice Allows a backer to pledge without selecting a reward. + * @param pledgeId The unique identifier of the pledge. + * @param backer The address of the backer making the pledge. + * @param pledgeToken The token to use for the pledge. + * @param pledgeAmount The amount of the pledge. + * @param tip An optional tip can be added during the process. + */ + function pledgeWithoutAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 pledgeAmount, + uint256 tip + ) + public + nonReentrant + currentTimeIsWithinRange(getLaunchTime(), getDeadline()) + whenCampaignNotPaused + whenNotPaused + whenCampaignNotCancelled + whenNotCancelled + { + _pledgeWithoutAReward(pledgeId, backer, pledgeToken, pledgeAmount, tip, backer); // Pass backer as token source for direct calls + } + + /** + * @notice Internal function that allows a backer to pledge without selecting a reward with tokens transferred from a specified source. + * @dev This function is called internally by both public pledgeWithoutAReward (with backer as token source) and + * setFeeAndPledge (with admin as token source). + * @param pledgeId The unique identifier of the pledge. + * @param backer The address of the backer making the pledge (receives the NFT). + * @param pledgeToken The token to use for the pledge. + * @param pledgeAmount The amount of the pledge. + * @param tip An optional tip can be added during the process. + * @param tokenSource The address from which tokens will be transferred (either backer for direct calls or admin for setFeeAndPledge calls). + */ + function _pledgeWithoutAReward( + bytes32 pledgeId, + address backer, + address pledgeToken, + uint256 pledgeAmount, + uint256 tip, + address tokenSource + ) internal { + bytes32 internalPledgeId = keccak256(abi.encodePacked(pledgeId, _msgSender())); + + if (s_processedPledges[internalPledgeId]) { + revert KeepWhatsRaisedPledgeAlreadyProcessed(internalPledgeId); + } + s_processedPledges[internalPledgeId] = true; + + bytes32[] memory emptyByteArray = new bytes32[](0); + + _pledge(pledgeId, backer, pledgeToken, ZERO_BYTES, pledgeAmount, tip, emptyByteArray, tokenSource); + } + + /** + * @inheritdoc ICampaignTreasury + */ + function withdraw() public view override whenNotPaused whenNotCancelled { + revert KeepWhatsRaisedDisabled(); + } + + /** + * @dev Allows the campaign owner or platform admin to withdraw funds, applying required fees and taxes. + * + * @param token The token to withdraw. + * @param amount The withdrawal amount (ignored for final withdrawals). + * + * Requirements: + * - Caller must be authorized. + * - Withdrawals must be enabled, not paused, and within the allowed time. + * - Token must be accepted for the campaign. + * - For partial withdrawals: + * - `amount` > 0 and `amount + fees` ≤ available balance. + * - For final withdrawals: + * - Available balance > 0 and fees ≤ available balance. + * + * Effects: + * - Deducts fees (flat, cumulative, and Colombian tax if applicable). + * - Updates available balance per token. + * - Transfers net funds to the recipient. + * + * Reverts: + * - If insufficient funds or invalid input. + * + * Emits: + * - `WithdrawalWithFeeSuccessful`. + */ + function withdraw(address token, uint256 amount) + public + onlyPlatformAdminOrCampaignOwner + currentTimeIsLess(getDeadline() + s_config.withdrawalDelay) + whenNotPaused + whenNotCancelled + withdrawalEnabled + { + if (!INFO.isTokenAccepted(token)) { + revert KeepWhatsRaisedTokenNotAccepted(token); + } + + // Fee config values are in 18 decimals, denormalize for comparison/calculation + uint256 flatFee = _denormalizeAmount(token, getFeeValue(s_feeKeys.flatFeeKey)); + uint256 cumulativeFee = _denormalizeAmount(token, getFeeValue(s_feeKeys.cumulativeFlatFeeKey)); + uint256 minimumWithdrawalForFeeExemption = _denormalizeAmount(token, s_config.minimumWithdrawalForFeeExemption); + + uint256 currentTime = block.timestamp; + uint256 withdrawalAmount = s_availablePerToken[token]; + uint256 totalFee = 0; + address recipient = INFO.owner(); + bool isFinalWithdrawal = (currentTime > getDeadline()); + + //Main Fees + if (isFinalWithdrawal) { + if (withdrawalAmount == 0) { + revert KeepWhatsRaisedAlreadyWithdrawn(); + } + if (withdrawalAmount < minimumWithdrawalForFeeExemption) { + s_platformFeePerToken[token] += flatFee; + totalFee += flatFee; + } + } else { + withdrawalAmount = amount; + if (withdrawalAmount == 0) { + revert KeepWhatsRaisedInvalidInput(); + } + if (withdrawalAmount > s_availablePerToken[token]) { + revert KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee( + s_availablePerToken[token], withdrawalAmount, totalFee + ); + } + + if (withdrawalAmount < minimumWithdrawalForFeeExemption) { + s_platformFeePerToken[token] += cumulativeFee; + totalFee += cumulativeFee; + } else { + s_platformFeePerToken[token] += flatFee; + totalFee += flatFee; + } + } + + uint256 availableBeforeTax = withdrawalAmount; //The tax implemented is on the withdrawal amount + + // Colombian creator tax + if (s_config.isColombianCreator) { + // Formula: (availableBeforeTax * 0.004) / 1.004 ≈ ((availableBeforeTax * 40) / 10040) + uint256 scaled = availableBeforeTax * PERCENT_DIVIDER; + uint256 numerator = scaled * 40; + uint256 denominator = 10040; + uint256 columbianCreatorTax = numerator / (denominator * PERCENT_DIVIDER); + + s_platformFeePerToken[token] += columbianCreatorTax; + totalFee += columbianCreatorTax; + } + + if (isFinalWithdrawal) { + if (withdrawalAmount < totalFee) { + revert KeepWhatsRaisedInsufficientFundsForFee(withdrawalAmount, totalFee); + } + + s_availablePerToken[token] = 0; + IERC20(token).safeTransfer(recipient, withdrawalAmount - totalFee); + } else { + if (s_availablePerToken[token] < (withdrawalAmount + totalFee)) { + revert KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee( + s_availablePerToken[token], withdrawalAmount, totalFee + ); + } + + s_availablePerToken[token] -= (withdrawalAmount + totalFee); + IERC20(token).safeTransfer(recipient, withdrawalAmount); + } + + emit WithdrawalWithFeeSuccessful( + recipient, isFinalWithdrawal ? withdrawalAmount - totalFee : withdrawalAmount, totalFee + ); + } + + /** + * @dev Allows a backer to claim a refund associated with a specific pledge (token ID). + * + * @param tokenId The ID of the token representing the backer's pledge. + * + * Requirements: + * - Refund delay must have passed. + * - The token must be eligible for a refund and not previously claimed. + */ + function claimRefund(uint256 tokenId) + external + currentTimeIsGreater(getLaunchTime()) + whenCampaignNotPaused + whenNotPaused + { + if (!_checkRefundPeriodStatus(false)) { + revert KeepWhatsRaisedNotClaimable(tokenId); + } + + // Get NFT owner before burning + address nftOwner = INFO.ownerOf(tokenId); + + address pledgeToken = s_tokenIdToPledgeToken[tokenId]; + uint256 amountToRefund = s_tokenToPledgedAmount[tokenId]; + uint256 paymentFee = s_tokenToPaymentFee[tokenId]; + uint256 netRefundAmount = amountToRefund - paymentFee; + + if (netRefundAmount == 0 || s_availablePerToken[pledgeToken] < netRefundAmount) { + revert KeepWhatsRaisedNotClaimable(tokenId); + } + + s_tokenToPledgedAmount[tokenId] = 0; + s_tokenRaisedAmounts[pledgeToken] -= amountToRefund; + s_availablePerToken[pledgeToken] -= netRefundAmount; + s_tokenToPaymentFee[tokenId] = 0; + + // Burn the NFT (requires treasury approval from owner) + INFO.burn(tokenId); + + IERC20(pledgeToken).safeTransfer(nftOwner, netRefundAmount); + emit RefundClaimed(tokenId, netRefundAmount, nftOwner); + } + + /** + * @dev Disburses all accumulated fees to the appropriate fee collector or treasury. + * + * Requirements: + * - Only callable when fees are available. + */ + function disburseFees() public override whenNotPaused whenNotCancelled { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + address protocolAdmin = INFO.getProtocolAdminAddress(); + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 protocolShare = s_protocolFeePerToken[token]; + uint256 platformShare = s_platformFeePerToken[token]; + + if (protocolShare > 0 || platformShare > 0) { + s_protocolFeePerToken[token] = 0; + s_platformFeePerToken[token] = 0; + + if (protocolShare > 0) { + IERC20(token).safeTransfer(protocolAdmin, protocolShare); + } + + if (platformShare > 0) { + IERC20(token).safeTransfer(platformAdmin, platformShare); + } + + emit FeesDisbursed(token, protocolShare, platformShare); + } + } + } + + /** + * @dev Allows an authorized claimer to collect tips contributed during the campaign. + * + * Requirements: + * - Caller must be authorized to claim tips. + * - Tip amount must be non-zero. + */ + function claimTip() external onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenNotPaused { + if (s_cancellationTime == 0 && block.timestamp <= getDeadline()) { + revert KeepWhatsRaisedNotClaimableAdmin(); + } + + if (s_tipClaimed) { + revert KeepWhatsRaisedAlreadyClaimed(); + } + + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + s_tipClaimed = true; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 tip = s_tipPerToken[token]; + + if (tip > 0) { + s_tipPerToken[token] = 0; + IERC20(token).safeTransfer(platformAdmin, tip); + emit TipClaimed(tip, platformAdmin); + } + } + } + + /** + * @dev Allows the platform admin to claim the remaining funds from a campaign. + * + * Requirements: + * - Claim period must have started and funds must be available. + * - Cannot be previously claimed. + */ + function claimFund() external onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenNotPaused { + bool isCancelled = s_cancellationTime > 0; + uint256 cancelLimit = s_cancellationTime + s_config.refundDelay; + uint256 deadlineLimit = getDeadline() + s_config.withdrawalDelay; + + if ((isCancelled && block.timestamp <= cancelLimit) || (!isCancelled && block.timestamp <= deadlineLimit)) { + revert KeepWhatsRaisedNotClaimableAdmin(); + } + + if (s_fundClaimed) { + revert KeepWhatsRaisedAlreadyClaimed(); + } + + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + s_fundClaimed = true; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amountToClaim = s_availablePerToken[token]; + + if (amountToClaim > 0) { + s_availablePerToken[token] = 0; + IERC20(token).safeTransfer(platformAdmin, amountToClaim); + emit FundClaimed(amountToClaim, platformAdmin); + } + } + } + + /** + * @inheritdoc BaseTreasury + * @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 { + s_cancellationTime = block.timestamp; + _cancel(message); + } + + /** + * @inheritdoc BaseTreasury + */ + function _checkSuccessCondition() internal view virtual override returns (bool) { + return true; + } + + function _pledge( + bytes32 pledgeId, + address backer, + address pledgeToken, + bytes32 reward, + uint256 pledgeAmount, + uint256 tip, + bytes32[] memory rewards, + address tokenSource + ) private { + // Validate token is accepted + if (!INFO.isTokenAccepted(pledgeToken)) { + revert KeepWhatsRaisedTokenNotAccepted(pledgeToken); + } + + // If this is for a reward, pledgeAmount is in 18 decimals and needs to be denormalized + // If not for a reward (pledgeWithoutAReward), pledgeAmount is already in token decimals + // Tip is always in the pledgeToken's decimals (same token used for payment) + uint256 pledgeAmountInTokenDecimals; + if (reward != ZERO_BYTES) { + // Reward pledge: denormalize from 18 decimals to token decimals + pledgeAmountInTokenDecimals = _denormalizeAmount(pledgeToken, pledgeAmount); + } else { + // Non-reward pledge: already in token decimals + pledgeAmountInTokenDecimals = pledgeAmount; + } + + uint256 totalAmount = pledgeAmountInTokenDecimals + tip; + + IERC20(pledgeToken).safeTransferFrom(tokenSource, address(this), totalAmount); + + uint256 tokenId = INFO.mintNFTForPledge(backer, reward, pledgeToken, pledgeAmountInTokenDecimals, 0, tip); + + s_tokenToPledgedAmount[tokenId] = pledgeAmountInTokenDecimals; + s_tokenToTippedAmount[tokenId] = tip; + s_tokenIdToPledgeToken[tokenId] = pledgeToken; + s_tipPerToken[pledgeToken] += tip; + s_tokenRaisedAmounts[pledgeToken] += pledgeAmountInTokenDecimals; + s_tokenLifetimeRaisedAmounts[pledgeToken] += pledgeAmountInTokenDecimals; + + uint256 netAvailable = _calculateNetAvailable(pledgeId, pledgeToken, tokenId, pledgeAmountInTokenDecimals); + s_availablePerToken[pledgeToken] += netAvailable; + + emit Receipt(backer, pledgeToken, reward, pledgeAmount, tip, tokenId, rewards); + } + + /** + * @notice Calculates the net amount available from a pledge after deducting + * all applicable fees. + * + * @dev The function performs the following: + * - Applies all configured gross percentage-based fees + * - Applies payment gateway fee for the given pledge + * - Applies protocol fee based on protocol configuration + * - Accumulates total platform and protocol fees per token + * - Records the total deducted fee for the token + * + * @param pledgeId The unique identifier of the pledge + * @param pledgeToken The token used for the pledge + * @param tokenId The token ID representing the pledge + * @param pledgeAmount The original pledged amount before deductions + * + * @return The net available amount after all fees are deducted + */ + function _calculateNetAvailable(bytes32 pledgeId, address pledgeToken, uint256 tokenId, uint256 pledgeAmount) + internal + returns (uint256) + { + uint256 totalFee = 0; + + // Gross Percentage Fee Calculation (correct as-is) + uint256 len = s_feeKeys.grossPercentageFeeKeys.length; + for (uint256 i = 0; i < len; i++) { + uint256 fee = (pledgeAmount * getFeeValue(s_feeKeys.grossPercentageFeeKeys[i])) / PERCENT_DIVIDER; + s_platformFeePerToken[pledgeToken] += fee; + totalFee += fee; + } + + // Payment Gateway Fee Calculation - MUST DENORMALIZE + uint256 paymentGatewayFeeNormalized = getPaymentGatewayFee(pledgeId); + uint256 paymentGatewayFee = _denormalizeAmount(pledgeToken, paymentGatewayFeeNormalized); + s_platformFeePerToken[pledgeToken] += paymentGatewayFee; + totalFee += paymentGatewayFee; + + // Protocol Fee Calculation (correct as-is) + uint256 protocolFee = (pledgeAmount * INFO.getProtocolFeePercent()) / PERCENT_DIVIDER; + s_protocolFeePerToken[pledgeToken] += protocolFee; + totalFee += protocolFee; + + s_tokenToPaymentFee[tokenId] = totalFee; + + return pledgeAmount - totalFee; + } + + /** + * @dev Checks the refund period status based on campaign state + * @param checkIfOver If true, returns whether refund period is over; if false, returns whether currently within refund period + * @return bool Status based on checkIfOver parameter + * + * @notice Refund period logic: + * - If campaign is cancelled: refund period is active until s_cancellationTime + s_config.refundDelay + * - If campaign is not cancelled: refund period is active until deadline + s_config.refundDelay + * - Before deadline (non-cancelled): not in refund period + * + * @dev This function handles both cancelled and non-cancelled campaign scenarios + */ + function _checkRefundPeriodStatus(bool checkIfOver) internal view returns (bool) { + uint256 deadline = getDeadline(); + bool isCancelled = s_cancellationTime > 0; + + bool refundPeriodOver; + + if (isCancelled) { + // If cancelled, refund period ends after s_config.refundDelay from cancellation time + refundPeriodOver = block.timestamp > s_cancellationTime + s_config.refundDelay; + } else { + // If not cancelled, refund period ends after s_config.refundDelay from deadline + refundPeriodOver = block.timestamp > deadline + s_config.refundDelay; + } + + if (checkIfOver) { + return refundPeriodOver; + } else { + // For non-cancelled campaigns, also check if we're after deadline + if (!isCancelled) { + return block.timestamp > deadline && !refundPeriodOver; + } + return !refundPeriodOver; + } + } +} diff --git a/src/treasuries/PaymentTreasury.sol b/src/treasuries/PaymentTreasury.sol new file mode 100644 index 00000000..4063fced --- /dev/null +++ b/src/treasuries/PaymentTreasury.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {BasePaymentTreasury} from "../utils/BasePaymentTreasury.sol"; +import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; + +contract PaymentTreasury is BasePaymentTreasury { + using SafeERC20 for IERC20; + + /** + * @dev Emitted when an unauthorized action is attempted. + */ + error PaymentTreasuryUnAuthorized(); + + /** + * @dev Constructor for the PaymentTreasury contract. + */ + constructor() {} + + function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer { + __BaseContract_init(_platformHash, _infoAddress, _trustedForwarder); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + 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 { + super.createPayment(paymentId, buyerId, itemId, paymentToken, amount, expiration, lineItems, externalFees); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + 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 { + super.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, lineItemsArray, externalFeesArray + ); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ) public override whenNotPaused whenNotCancelled { + super.processCryptoPayment(paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function cancelPayment(bytes32 paymentId) public override whenNotPaused whenNotCancelled { + super.cancelPayment(paymentId); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function confirmPayment(bytes32 paymentId, address buyerAddress) public override whenNotPaused whenNotCancelled { + super.confirmPayment(paymentId, buyerAddress); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) + public + override + whenNotPaused + whenNotCancelled + { + super.confirmPaymentBatch(paymentIds, buyerAddresses); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function claimRefund(bytes32 paymentId, address refundAddress) public override whenNotPaused whenNotCancelled { + super.claimRefund(paymentId, refundAddress); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function claimRefund(bytes32 paymentId) public override whenNotPaused whenNotCancelled { + super.claimRefund(paymentId); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function claimExpiredFunds() public override whenNotPaused { + super.claimExpiredFunds(); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function disburseFees() public override whenNotPaused { + super.disburseFees(); + } + + /** + * @inheritdoc BasePaymentTreasury + */ + function claimNonGoalLineItems(address token) public override whenNotPaused { + super.claimNonGoalLineItems(token); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function withdraw() public override whenNotPaused whenNotCancelled { + 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 { + if (_msgSender() != INFO.getPlatformAdminAddress(PLATFORM_HASH) && _msgSender() != INFO.owner()) { + revert PaymentTreasuryUnAuthorized(); + } + _cancel(message); + } + + /** + * @inheritdoc BasePaymentTreasury + */ + function _checkSuccessCondition() internal view virtual override returns (bool) { + return true; + } +} diff --git a/src/treasuries/TimeConstrainedPaymentTreasury.sol b/src/treasuries/TimeConstrainedPaymentTreasury.sol new file mode 100644 index 00000000..cf2f1820 --- /dev/null +++ b/src/treasuries/TimeConstrainedPaymentTreasury.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {BasePaymentTreasury} from "../utils/BasePaymentTreasury.sol"; +import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; +import {TimestampChecker} from "../utils/TimestampChecker.sol"; + +contract TimeConstrainedPaymentTreasury is BasePaymentTreasury, TimestampChecker { + using SafeERC20 for IERC20; + + /** + * @dev Emitted when an unauthorized action is attempted. + */ + error TimeConstrainedPaymentTreasuryUnAuthorized(); + + /** + * @dev Constructor for the TimeConstrainedPaymentTreasury contract. + */ + constructor() {} + + function initialize(bytes32 _platformHash, address _infoAddress, address _trustedForwarder) external initializer { + __BaseContract_init(_platformHash, _infoAddress, _trustedForwarder); + } + + /** + * @dev Internal function to check if current time is within the allowed range. + */ + function _checkTimeWithinRange() internal view { + uint256 launchTime = INFO.getLaunchTime(); + uint256 deadline = INFO.getDeadline(); + uint256 bufferTime = INFO.getBufferTime(); + _revertIfCurrentTimeIsNotWithinRange(launchTime, deadline + bufferTime); + } + + /** + * @dev Internal function to check if current time is greater than launch time. + */ + function _checkTimeIsGreater() internal view { + uint256 launchTime = INFO.getLaunchTime(); + _revertIfCurrentTimeIsNotGreater(launchTime); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + 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 { + _checkTimeWithinRange(); + super.createPayment(paymentId, buyerId, itemId, paymentToken, amount, expiration, lineItems, externalFees); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + 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 { + _checkTimeWithinRange(); + super.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, lineItemsArray, externalFeesArray + ); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ) public override whenNotPaused whenNotCancelled { + _checkTimeWithinRange(); + super.processCryptoPayment(paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function cancelPayment(bytes32 paymentId) public override whenNotPaused whenNotCancelled { + _checkTimeWithinRange(); + super.cancelPayment(paymentId); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function confirmPayment(bytes32 paymentId, address buyerAddress) public override whenNotPaused whenNotCancelled { + _checkTimeWithinRange(); + super.confirmPayment(paymentId, buyerAddress); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) + public + override + whenNotPaused + whenNotCancelled + { + _checkTimeWithinRange(); + super.confirmPaymentBatch(paymentIds, buyerAddresses); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function claimRefund(bytes32 paymentId, address refundAddress) public override whenNotPaused whenNotCancelled { + _checkTimeIsGreater(); + super.claimRefund(paymentId, refundAddress); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function claimRefund(bytes32 paymentId) public override whenNotPaused whenNotCancelled { + _checkTimeIsGreater(); + super.claimRefund(paymentId); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function claimExpiredFunds() public override whenNotPaused { + _checkTimeIsGreater(); + super.claimExpiredFunds(); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function disburseFees() public override whenNotPaused { + _checkTimeIsGreater(); + super.disburseFees(); + } + + /** + * @inheritdoc BasePaymentTreasury + */ + function claimNonGoalLineItems(address token) public override whenNotPaused { + _checkTimeIsGreater(); + super.claimNonGoalLineItems(token); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function withdraw() public override whenNotPaused whenNotCancelled { + _checkTimeIsGreater(); + 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 { + if (_msgSender() != INFO.getPlatformAdminAddress(PLATFORM_HASH) && _msgSender() != INFO.owner()) { + revert TimeConstrainedPaymentTreasuryUnAuthorized(); + } + _cancel(message); + } + + /** + * @inheritdoc BasePaymentTreasury + */ + function _checkSuccessCondition() internal view virtual override returns (bool) { + return true; + } +} diff --git a/src/utils/AdminAccessChecker.sol b/src/utils/AdminAccessChecker.sol index 7c025282..d65d2e58 100644 --- a/src/utils/AdminAccessChecker.sol +++ b/src/utils/AdminAccessChecker.sol @@ -1,24 +1,37 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {IGlobalParams} from "../interfaces/IGlobalParams.sol"; +import {AdminAccessCheckerStorage} from "../storage/AdminAccessCheckerStorage.sol"; /** * @title AdminAccessChecker * @dev This abstract contract provides access control mechanisms to restrict the execution of specific functions * to authorized protocol administrators and platform administrators. + * @dev Updated to use ERC-7201 namespaced storage for upgradeable contracts */ -abstract contract AdminAccessChecker { - // Immutable reference to the IGlobalParams contract, which manages global parameters and admin addresses. - IGlobalParams internal GLOBAL_PARAMS; - +abstract contract AdminAccessChecker is Context { /** * @dev Throws when the caller is not authorized. */ error AdminAccessCheckerUnauthorized(); + /** + * @dev Internal initializer function for AdminAccessChecker + * @param globalParams The IGlobalParams contract instance + */ function __AccessChecker_init(IGlobalParams globalParams) internal { - GLOBAL_PARAMS = globalParams; + AdminAccessCheckerStorage.Storage storage $ = AdminAccessCheckerStorage._getAdminAccessCheckerStorage(); + $.globalParams = globalParams; + } + + /** + * @dev Returns the stored GLOBAL_PARAMS for internal use + */ + function _getGlobalParams() internal view returns (IGlobalParams) { + AdminAccessCheckerStorage.Storage storage $ = AdminAccessCheckerStorage._getAdminAccessCheckerStorage(); + return $.globalParams; } /** @@ -45,7 +58,8 @@ abstract contract AdminAccessChecker { * If the sender is not the protocol admin, it reverts with AdminAccessCheckerUnauthorized error. */ function _onlyProtocolAdmin() private view { - if (msg.sender != GLOBAL_PARAMS.getProtocolAdminAddress()) { + AdminAccessCheckerStorage.Storage storage $ = AdminAccessCheckerStorage._getAdminAccessCheckerStorage(); + if (_msgSender() != $.globalParams.getProtocolAdminAddress()) { revert AdminAccessCheckerUnauthorized(); } } @@ -56,7 +70,8 @@ abstract contract AdminAccessChecker { * @param platformHash The unique identifier of the platform. */ function _onlyPlatformAdmin(bytes32 platformHash) private view { - if (msg.sender != GLOBAL_PARAMS.getPlatformAdminAddress(platformHash)) { + AdminAccessCheckerStorage.Storage storage $ = AdminAccessCheckerStorage._getAdminAccessCheckerStorage(); + if (_msgSender() != $.globalParams.getPlatformAdminAddress(platformHash)) { revert AdminAccessCheckerUnauthorized(); } } diff --git a/src/utils/BasePaymentTreasury.sol b/src/utils/BasePaymentTreasury.sol new file mode 100644 index 00000000..f7e45f44 --- /dev/null +++ b/src/utils/BasePaymentTreasury.sol @@ -0,0 +1,1851 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import {ICampaignPaymentTreasury} from "../interfaces/ICampaignPaymentTreasury.sol"; +import {CampaignAccessChecker} from "./CampaignAccessChecker.sol"; +import {PausableCancellable} from "./PausableCancellable.sol"; +import {DataRegistryKeys} from "../constants/DataRegistryKeys.sol"; + +/** + * @title BasePaymentTreasury + * @notice Base contract for payment treasury implementations. + * @dev Supports ERC-2771 meta-transactions via adapter contracts for platform admin operations. + */ +abstract contract BasePaymentTreasury is + Initializable, + ICampaignPaymentTreasury, + CampaignAccessChecker, + PausableCancellable, + ReentrancyGuard +{ + using SafeERC20 for IERC20; + + bytes32 internal constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000; + uint256 internal constant PERCENT_DIVIDER = 10000; + uint256 internal constant STANDARD_DECIMALS = 18; + address internal constant ZERO_ADDRESS = address(0); + + bytes32 internal PLATFORM_HASH; + uint256 internal PLATFORM_FEE_PERCENT; + + // Multi-token support + mapping(bytes32 => address) internal s_paymentIdToToken; // Track token used for each payment + mapping(address => uint256) internal s_platformFeePerToken; // Platform fees per token + mapping(address => uint256) internal s_protocolFeePerToken; // Protocol fees per token + mapping(bytes32 => uint256) internal s_paymentIdToTokenId; // Track NFT token ID for each payment (0 means no NFT) + mapping(bytes32 => address) internal s_paymentIdToCreator; // Track creator address for on-chain payments (for getPaymentData lookup) + + /** + * @dev Stores information about a payment in the treasury. + * @param buyerAddress The address of the buyer who made the payment. + * @param buyerId The ID of the buyer. + * @param itemId The identifier of the item being purchased. + * @param amount The amount to be paid for the item (in token's native decimals). + * @param expiration The timestamp after which the payment expires. + * @param isConfirmed Boolean indicating whether the payment has been confirmed. + * @param isCryptoPayment Boolean indicating whether the payment is made using direct crypto payment. + * @param lineItemCount The number of line items associated with this payment. + */ + struct PaymentInfo { + address buyerAddress; + bytes32 buyerId; + bytes32 itemId; + uint256 amount; + uint256 expiration; + bool isConfirmed; + bool isCryptoPayment; + uint256 lineItemCount; + } + + mapping(bytes32 => PaymentInfo) internal s_payment; + + // Combined line items with their configuration snapshots per payment ID + mapping(bytes32 => ICampaignPaymentTreasury.PaymentLineItem[]) internal s_paymentLineItems; // paymentId => array of stored line items + + // External fee metadata per payment ID (information only, no financial impact) + mapping(bytes32 => ICampaignPaymentTreasury.ExternalFees[]) internal s_paymentExternalFeeMetadata; // paymentId => array of external fee metadata + + // Multi-token balances (all in token's native decimals) + mapping(address => uint256) internal s_pendingPaymentPerToken; // Pending payment amounts per token + mapping(address => uint256) internal s_confirmedPaymentPerToken; // Confirmed payment amounts per token (decreases on refunds) + mapping(address => uint256) internal s_lifetimeConfirmedPaymentPerToken; // Lifetime confirmed payment amounts per token (never decreases) + mapping(address => uint256) internal s_availableConfirmedPerToken; // Available confirmed amounts per token + + // Tracking for non-goal line items (countTowardsGoal = False) per token + mapping(address => uint256) internal s_nonGoalLineItemPendingPerToken; // Pending non-goal line items per token + mapping(address => uint256) internal s_nonGoalLineItemConfirmedPerToken; // Confirmed non-goal line items per token + mapping(address => uint256) internal s_nonGoalLineItemClaimablePerToken; // Claimable non-goal line items per token (after fees) + mapping(address => uint256) internal s_refundableNonGoalLineItemPerToken; // Refundable non-goal line items per token (after fees) + + /** + * @dev Emitted when a new payment is created. + * @param buyerAddress The address of the buyer making the payment. + * @param paymentId The unique identifier of the payment. + * @param buyerId The id of the buyer. + * @param itemId The identifier of the item being purchased. + * @param paymentToken The token used for the payment. + * @param amount The amount to be paid for the item (in token's native decimals). + * @param expiration The timestamp after which the payment expires. + * @param isCryptoPayment Boolean indicating whether the payment is made using direct crypto payment. + */ + event PaymentCreated( + address buyerAddress, + bytes32 indexed paymentId, + bytes32 buyerId, + bytes32 indexed itemId, + address indexed paymentToken, + uint256 amount, + uint256 expiration, + bool isCryptoPayment + ); + + /** + * @dev Emitted when a payment is cancelled and removed from the treasury. + * @param paymentId The unique identifier of the cancelled payment. + */ + event PaymentCancelled(bytes32 indexed paymentId); + + /** + * @dev Emitted when a payment is confirmed. + * @param paymentId The unique identifier of the confirmed payment. + */ + event PaymentConfirmed(bytes32 indexed paymentId); + + /** + * @dev Emitted when multiple payments are confirmed in a single batch operation. + * @param paymentIds An array of unique identifiers for the confirmed payments. + */ + event PaymentBatchConfirmed(bytes32[] paymentIds); + + /** + * @dev Emitted when multiple payments are created in a single batch operation. + * @param paymentIds An array of unique identifiers for the created payments. + */ + event PaymentBatchCreated(bytes32[] paymentIds); + + /** + * @notice Emitted when fees are successfully disbursed. + * @param token The token in which fees were disbursed. + * @param protocolShare The amount of fees sent to the protocol. + * @param platformShare The amount of fees sent to the platform. + */ + event FeesDisbursed(address indexed token, uint256 protocolShare, uint256 platformShare); + + /** + * @dev Emitted when a withdrawal is successfully processed along with the applied fee. + * @param token The token that was withdrawn. + * @param to The recipient address receiving the funds. + * @param amount The total amount withdrawn (excluding fee). + * @param fee The fee amount deducted from the withdrawal. + */ + event WithdrawalWithFeeSuccessful(address indexed token, address indexed to, uint256 amount, uint256 fee); + + /** + * @dev Emitted when a refund is claimed. + * @param paymentId The unique identifier of the cancelled payment. + * @param refundAmount The refund amount claimed. + * @param claimer The address of the claimer. + */ + event RefundClaimed(bytes32 indexed paymentId, uint256 refundAmount, address indexed claimer); + + /** + * @dev Emitted when non-goal line items are claimed by the platform admin. + * @param token The token that was claimed. + * @param amount The amount claimed. + * @param platformAdmin The address of the platform admin who claimed. + */ + event NonGoalLineItemsClaimed(address indexed token, uint256 amount, address indexed platformAdmin); + + /** + * @dev Emitted when expired funds are claimed by the platform and protocol admins. + * @param token The token that was claimed. + * @param platformAmount The amount sent to the platform admin. + * @param protocolAmount The amount sent to the protocol admin. + */ + event ExpiredFundsClaimed(address indexed token, uint256 platformAmount, uint256 protocolAmount); + + /** + * @dev Reverts when one or more provided inputs to the payment treasury are invalid. + */ + error PaymentTreasuryInvalidInput(); + + /** + * @dev Throws an error indicating that the payment id already exists. + */ + error PaymentTreasuryPaymentAlreadyExist(bytes32 paymentId); + + /** + * @dev Throws an error indicating that the payment id is already confirmed. + */ + error PaymentTreasuryPaymentAlreadyConfirmed(bytes32 paymentId); + + /** + * @dev Throws an error indicating that the payment id is already expired. + */ + error PaymentTreasuryPaymentAlreadyExpired(bytes32 paymentId); + + /** + * @dev Throws an error indicating that the payment id does not exist. + */ + error PaymentTreasuryPaymentNotExist(bytes32 paymentId); + + /** + * @dev Throws an error indicating that the campaign is paused. + */ + error PaymentTreasuryCampaignInfoIsPaused(); + + /** + * @dev Emitted when a token is not accepted for the campaign. + */ + error PaymentTreasuryTokenNotAccepted(address token); + + /** + * @dev Throws an error indicating that the success condition was not fulfilled. + */ + error PaymentTreasurySuccessConditionNotFulfilled(); + + /** + * @dev Throws an error indicating that fees have not been disbursed. + */ + error PaymentTreasuryFeeNotDisbursed(); + + /** + * @dev Throws an error indicating that the payment id is not confirmed. + */ + error PaymentTreasuryPaymentNotConfirmed(bytes32 paymentId); + + /** + * @dev Emitted when claiming an unclaimable refund. + * @param paymentId The unique identifier of the refundable payment. + */ + error PaymentTreasuryPaymentNotClaimable(bytes32 paymentId); + + /** + * @dev Emitted when an attempt is made to withdraw funds from the treasury but the payment has already been withdrawn. + */ + error PaymentTreasuryAlreadyWithdrawn(); + + /** + * @dev This error is thrown when an operation is attempted on a crypto payment that is only valid for non-crypto payments. + * @param paymentId The unique identifier of the payment that caused the error. + */ + error PaymentTreasuryCryptoPayment(bytes32 paymentId); + + /** + * @notice Emitted when the fee exceeds the requested withdrawal amount. + * + * @param withdrawalAmount The amount requested for withdrawal. + * @param fee The calculated fee, which is greater than the withdrawal amount. + */ + error PaymentTreasuryInsufficientFundsForFee(uint256 withdrawalAmount, uint256 fee); + + /** + * @dev Emitted when there are insufficient unallocated tokens for a payment confirmation. + */ + error PaymentTreasuryInsufficientBalance(uint256 required, uint256 available); + + /** + * @dev Throws an error indicating that the payment expiration exceeds the maximum allowed expiration time. + * @param expiration The requested expiration timestamp. + * @param maxExpiration The maximum allowed expiration timestamp. + */ + error PaymentTreasuryExpirationExceedsMax(uint256 expiration, uint256 maxExpiration); + + /** + * @dev Throws when attempting to claim expired funds before the claim window opens. + * @param claimableAt The timestamp when the claim window opens. + */ + error PaymentTreasuryClaimWindowNotReached(uint256 claimableAt); + + /** + * @dev Throws when there are no funds available to claim. + */ + error PaymentTreasuryNoFundsToClaim(); + + /** + * @dev Scopes a payment ID for off-chain payments (createPayment/createPaymentBatch). + * @param paymentId The external payment ID. + * @return The scoped internal payment ID. + */ + function _scopePaymentIdForOffChain(bytes32 paymentId) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(paymentId, ZERO_ADDRESS)); + } + + /** + * @dev Scopes a payment ID for on-chain crypto payments (processCryptoPayment). + * @param paymentId The external payment ID. + * @return The scoped internal payment ID. + */ + function _scopePaymentIdForOnChain(bytes32 paymentId) internal view returns (bytes32) { + return keccak256(abi.encodePacked(paymentId, _msgSender())); + } + + /** + * @dev Tries to find a payment by checking both off-chain and on-chain scopes. + * - Off-chain payments (createPayment) can be looked up by anyone (scoped with address(0)) + * - On-chain payments (processCryptoPayment) can be looked up by anyone using the stored creator address + * @param paymentId The external payment ID. + * @return internalPaymentId The scoped internal payment ID if found, or ZERO_BYTES if not found. + */ + function _findPaymentId(bytes32 paymentId) internal view returns (bytes32 internalPaymentId) { + // Try off-chain scope first (for createPayment) - anyone can look these up + internalPaymentId = _scopePaymentIdForOffChain(paymentId); + if ( + s_payment[internalPaymentId].buyerId != ZERO_BYTES + || s_payment[internalPaymentId].buyerAddress != address(0) + ) { + return internalPaymentId; + } + + // Try on-chain scope (for processCryptoPayment) - use stored creator address + // Since paymentIds are globally unique, there's only one creator per paymentId + address creatorAddress = s_paymentIdToCreator[paymentId]; + if (creatorAddress != address(0)) { + internalPaymentId = keccak256(abi.encodePacked(paymentId, creatorAddress)); + if ( + s_payment[internalPaymentId].buyerId != ZERO_BYTES + || s_payment[internalPaymentId].buyerAddress != address(0) + ) { + return internalPaymentId; + } + } + + // Not found in either scope + return ZERO_BYTES; + } + + /** + * @dev Retrieves the max expiration duration configured for the current platform or globally. + * @return hasLimit Indicates whether a max expiration duration is configured. + * @return duration The max expiration duration in seconds. + */ + function _getMaxExpirationDuration() internal view returns (bool hasLimit, uint256 duration) { + bytes32 platformScopedKey = + DataRegistryKeys.scopedToPlatform(DataRegistryKeys.MAX_PAYMENT_EXPIRATION, PLATFORM_HASH); + + // Prefer platform-specific value stored in GlobalParams via registry. + bytes32 maxExpirationBytes = INFO.getDataFromRegistry(platformScopedKey); + + if (maxExpirationBytes == ZERO_BYTES) { + maxExpirationBytes = INFO.getDataFromRegistry(DataRegistryKeys.MAX_PAYMENT_EXPIRATION); + } + + if (maxExpirationBytes == ZERO_BYTES) { + return (false, 0); + } + + duration = uint256(maxExpirationBytes); + + if (duration == 0) { + return (false, 0); + } + + hasLimit = true; + } + + function __BaseContract_init(bytes32 platformHash, address infoAddress, address trustedForwarder_) internal { + __CampaignAccessChecker_init(infoAddress); + PLATFORM_HASH = platformHash; + PLATFORM_FEE_PERCENT = INFO.getPlatformFeePercent(platformHash); + _trustedForwarder = trustedForwarder_; + } + + /** + * @dev Override _msgSender to support ERC-2771 meta-transactions. + * When called by the trusted forwarder (adapter), extracts the actual sender from calldata. + */ + function _msgSender() internal view virtual override returns (address sender) { + if (msg.sender == _trustedForwarder && msg.data.length >= 20) { + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + sender = msg.sender; + } + } + + /** + * @dev Modifier that checks if the campaign is not paused. + */ + modifier whenCampaignNotPaused() { + _revertIfCampaignPaused(); + _; + } + + modifier whenCampaignNotCancelled() { + _revertIfCampaignCancelled(); + _; + } + + /** + * @dev Restricts access to only the platform admin or the campaign owner. + * @dev Checks if `_msgSender()` is either the platform admin (via `INFO.getPlatformAdminAddress`) + * or the campaign owner (via `INFO.owner()`). Reverts with `AccessCheckerUnauthorized` if not authorized. + */ + modifier onlyPlatformAdminOrCampaignOwner() { + if (_msgSender() != INFO.getPlatformAdminAddress(PLATFORM_HASH) && _msgSender() != INFO.owner()) { + revert AccessCheckerUnauthorized(); + } + _; + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function getplatformHash() external view override returns (bytes32) { + return PLATFORM_HASH; + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function getplatformFeePercent() external view override returns (uint256) { + return PLATFORM_FEE_PERCENT; + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function getRaisedAmount() public view virtual override returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_confirmedPaymentPerToken[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function getAvailableRaisedAmount() external view returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_availableConfirmedPerToken[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function getLifetimeRaisedAmount() external view returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_lifetimeConfirmedPaymentPerToken[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function getRefundedAmount() external view returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 lifetimeAmount = s_lifetimeConfirmedPaymentPerToken[token]; + uint256 currentAmount = s_confirmedPaymentPerToken[token]; + uint256 refundedAmount = lifetimeAmount - currentAmount; + if (refundedAmount > 0) { + totalNormalized += _normalizeAmount(token, refundedAmount); + } + } + + return totalNormalized; + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function getExpectedAmount() external view returns (uint256) { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 totalNormalized = 0; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 amount = s_pendingPaymentPerToken[token]; + if (amount > 0) { + totalNormalized += _normalizeAmount(token, amount); + } + } + + return totalNormalized; + } + + /** + * @dev Normalizes token amounts to 18 decimals for consistent comparisons. + * @param token The token address. + * @param amount The amount to normalize. + * @return The normalized amount (scaled to 18 decimals). + */ + function _normalizeAmount(address token, uint256 amount) internal view returns (uint256) { + uint8 decimals = IERC20Metadata(token).decimals(); + + if (decimals == STANDARD_DECIMALS) { + return amount; + } else if (decimals < STANDARD_DECIMALS) { + return amount * (10 ** (STANDARD_DECIMALS - decimals)); + } else { + return amount / (10 ** (decimals - STANDARD_DECIMALS)); + } + } + + /** + * @dev Struct to hold line item calculation totals to reduce stack depth. + */ + struct LineItemTotals { + uint256 totalGoalLineItemAmount; + uint256 totalProtocolFeeFromLineItems; + uint256 totalNonGoalClaimableAmount; + uint256 totalNonGoalRefundableAmount; + uint256 totalInstantTransferAmountForCheck; + uint256 totalInstantTransferAmount; + } + + /** + * @dev Validates, stores, and tracks line items in a single loop for gas efficiency. + * @param paymentId The payment ID to store line items for. + * @param lineItems Array of line items to validate, store, and track. + * @param paymentToken The token used for the payment. + */ + function _validateStoreAndTrackLineItems( + bytes32 paymentId, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + address paymentToken + ) internal { + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.LineItem calldata item = lineItems[i]; + + // Validate line item + if (item.typeId == ZERO_BYTES || item.amount == 0) { + revert PaymentTreasuryInvalidInput(); + } + + // Get line item type configuration (single call per item) + ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ) = INFO.getLineItemType(PLATFORM_HASH, item.typeId); + + if (!exists) { + revert PaymentTreasuryInvalidInput(); + } + + // Store line item with configuration snapshot + s_paymentLineItems[paymentId].push( + ICampaignPaymentTreasury.PaymentLineItem({ + typeId: item.typeId, + amount: item.amount, + label: label, + countsTowardGoal: countsTowardGoal, + applyProtocolFee: applyProtocolFee, + canRefund: canRefund, + instantTransfer: instantTransfer + }) + ); + + // Track pending amounts based on whether it counts toward goal + if (countsTowardGoal) { + s_pendingPaymentPerToken[paymentToken] += item.amount; + } else { + s_nonGoalLineItemPendingPerToken[paymentToken] += item.amount; + } + } + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function createPayment( + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + address paymentToken, + uint256 amount, + uint256 expiration, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ) public virtual override onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenCampaignNotCancelled { + if ( + buyerId == ZERO_BYTES || amount == 0 || expiration <= block.timestamp || paymentId == ZERO_BYTES + || itemId == ZERO_BYTES || paymentToken == address(0) + ) { + revert PaymentTreasuryInvalidInput(); + } + + // Validate expiration does not exceed maximum allowed expiration time (platform-specific or global) + (bool hasMaxExpiration, uint256 maxExpirationDuration) = _getMaxExpirationDuration(); + if (hasMaxExpiration) { + uint256 maxAllowedExpiration = block.timestamp + maxExpirationDuration; + if (expiration > maxAllowedExpiration) { + revert PaymentTreasuryExpirationExceedsMax(expiration, maxAllowedExpiration); + } + } + + // Validate token is accepted + if (!INFO.isTokenAccepted(paymentToken)) { + revert PaymentTreasuryTokenNotAccepted(paymentToken); + } + + // Check if an on-chain payment with the same paymentId already exists + address creatorAddress = s_paymentIdToCreator[paymentId]; + if (creatorAddress != address(0)) { + bytes32 onChainPaymentId = keccak256(abi.encodePacked(paymentId, creatorAddress)); + revert PaymentTreasuryPaymentAlreadyExist(onChainPaymentId); + } + + bytes32 internalPaymentId = _scopePaymentIdForOffChain(paymentId); + if ( + s_payment[internalPaymentId].buyerId != ZERO_BYTES + || s_payment[internalPaymentId].buyerAddress != address(0) + ) { + revert PaymentTreasuryPaymentAlreadyExist(internalPaymentId); + } + + s_payment[internalPaymentId] = PaymentInfo({ + buyerId: buyerId, + buyerAddress: address(0), + itemId: itemId, + amount: amount, // Amount in token's native decimals + expiration: expiration, + isConfirmed: false, + isCryptoPayment: false, + lineItemCount: lineItems.length + }); + + // Validate, store, and track line items + _validateStoreAndTrackLineItems(internalPaymentId, lineItems, paymentToken); + + // Store external fee metadata for informational purposes only + ICampaignPaymentTreasury.ExternalFees[] storage externalFeeMetadata = + s_paymentExternalFeeMetadata[internalPaymentId]; + for (uint256 i = 0; i < externalFees.length;) { + externalFeeMetadata.push(externalFees[i]); + unchecked { + ++i; + } + } + + s_paymentIdToToken[internalPaymentId] = paymentToken; + s_pendingPaymentPerToken[paymentToken] += amount; + + emit PaymentCreated(address(0), paymentId, buyerId, itemId, paymentToken, amount, expiration, false); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + 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 virtual override onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused whenCampaignNotCancelled { + // Validate array lengths are consistent + uint256 length = paymentIds.length; + if ( + length == 0 || length != buyerIds.length || length != itemIds.length || length != paymentTokens.length + || length != amounts.length || length != expirations.length || length != lineItemsArray.length + || length != externalFeesArray.length + ) { + revert PaymentTreasuryInvalidInput(); + } + + // Get max expiration duration once outside the loop for efficiency (platform-specific or global) + (bool hasMaxExpiration, uint256 maxExpirationDuration) = _getMaxExpirationDuration(); + uint256 maxAllowedExpiration = 0; + if (hasMaxExpiration) { + maxAllowedExpiration = block.timestamp + maxExpirationDuration; + } + + // Process each payment in the batch + for (uint256 i = 0; i < length;) { + bytes32 paymentId = paymentIds[i]; + bytes32 buyerId = buyerIds[i]; + bytes32 itemId = itemIds[i]; + address paymentToken = paymentTokens[i]; + uint256 amount = amounts[i]; + uint256 expiration = expirations[i]; + ICampaignPaymentTreasury.LineItem[] calldata lineItems = lineItemsArray[i]; + + // Validate individual payment parameters + if ( + buyerId == ZERO_BYTES || amount == 0 || expiration <= block.timestamp || paymentId == ZERO_BYTES + || itemId == ZERO_BYTES || paymentToken == address(0) + ) { + revert PaymentTreasuryInvalidInput(); + } + + // Validate expiration does not exceed maximum allowed expiration time + if (hasMaxExpiration && expiration > maxAllowedExpiration) { + revert PaymentTreasuryExpirationExceedsMax(expiration, maxAllowedExpiration); + } + + // Validate token is accepted + if (!INFO.isTokenAccepted(paymentToken)) { + revert PaymentTreasuryTokenNotAccepted(paymentToken); + } + + // Check if an on-chain payment with the same paymentId already exists + address creatorAddress = s_paymentIdToCreator[paymentId]; + if (creatorAddress != address(0)) { + bytes32 onChainPaymentId = keccak256(abi.encodePacked(paymentId, creatorAddress)); + revert PaymentTreasuryPaymentAlreadyExist(onChainPaymentId); + } + + bytes32 internalPaymentId = _scopePaymentIdForOffChain(paymentId); + // Check if payment already exists + if ( + s_payment[internalPaymentId].buyerId != ZERO_BYTES + || s_payment[internalPaymentId].buyerAddress != address(0) + ) { + revert PaymentTreasuryPaymentAlreadyExist(internalPaymentId); + } + + // Create the payment + s_payment[internalPaymentId] = PaymentInfo({ + buyerId: buyerId, + buyerAddress: address(0), + itemId: itemId, + amount: amount, // Amount in token's native decimals + expiration: expiration, + isConfirmed: false, + isCryptoPayment: false, + lineItemCount: lineItems.length + }); + + // Validate, store, and track line items in a single loop + _validateStoreAndTrackLineItems(internalPaymentId, lineItems, paymentToken); + + // Store external fee metadata for informational purposes only + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees = externalFeesArray[i]; + ICampaignPaymentTreasury.ExternalFees[] storage externalFeeMetadata = + s_paymentExternalFeeMetadata[internalPaymentId]; + for (uint256 j = 0; j < externalFees.length;) { + externalFeeMetadata.push(externalFees[j]); + unchecked { + ++j; + } + } + + s_paymentIdToToken[internalPaymentId] = paymentToken; + s_pendingPaymentPerToken[paymentToken] += amount; + + emit PaymentCreated(address(0), paymentId, buyerId, itemId, paymentToken, amount, expiration, false); + + unchecked { + ++i; + } + } + + emit PaymentBatchCreated(paymentIds); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function processCryptoPayment( + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] calldata lineItems, + ICampaignPaymentTreasury.ExternalFees[] calldata externalFees + ) public virtual override nonReentrant whenCampaignNotPaused whenCampaignNotCancelled { + if ( + buyerAddress == address(0) || amount == 0 || paymentId == ZERO_BYTES || itemId == ZERO_BYTES + || paymentToken == address(0) + ) { + revert PaymentTreasuryInvalidInput(); + } + + // Validate token is accepted + if (!INFO.isTokenAccepted(paymentToken)) { + revert PaymentTreasuryTokenNotAccepted(paymentToken); + } + + // Check if an off-chain payment with the same paymentId already exists + bytes32 offChainPaymentId = _scopePaymentIdForOffChain(paymentId); + if ( + s_payment[offChainPaymentId].buyerId != ZERO_BYTES + || s_payment[offChainPaymentId].buyerAddress != address(0) + ) { + revert PaymentTreasuryPaymentAlreadyExist(offChainPaymentId); + } + + // Check if any on-chain payment with the same paymentId already exists (globally unique) + if (s_paymentIdToCreator[paymentId] != address(0)) { + address existingCreator = s_paymentIdToCreator[paymentId]; + bytes32 existingPaymentId = keccak256(abi.encodePacked(paymentId, existingCreator)); + revert PaymentTreasuryPaymentAlreadyExist(existingPaymentId); + } + + // Check if an on-chain payment with the same paymentId already exists for this caller + bytes32 internalPaymentId = _scopePaymentIdForOnChain(paymentId); + if ( + s_payment[internalPaymentId].buyerAddress != address(0) + || s_payment[internalPaymentId].buyerId != ZERO_BYTES + ) { + revert PaymentTreasuryPaymentAlreadyExist(internalPaymentId); + } + + // Validate, calculate total, store, and process line items + uint256 totalAmount = amount; + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + uint256 protocolFeePercent = INFO.getProtocolFeePercent(); + uint256 totalInstantTransferAmount = 0; + + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.LineItem calldata item = lineItems[i]; + + // Validate line item + if (item.typeId == ZERO_BYTES || item.amount == 0) { + revert PaymentTreasuryInvalidInput(); + } + + // Get line item type configuration (single call per item) + ( + bool exists, + string memory label, + bool countsTowardGoal, + bool applyProtocolFee, + bool canRefund, + bool instantTransfer + ) = INFO.getLineItemType(PLATFORM_HASH, item.typeId); + + if (!exists) { + revert PaymentTreasuryInvalidInput(); + } + + // Accumulate total amount + totalAmount += item.amount; + + // Store line item with configuration snapshot + s_paymentLineItems[internalPaymentId].push( + ICampaignPaymentTreasury.PaymentLineItem({ + typeId: item.typeId, + amount: item.amount, + label: label, + countsTowardGoal: countsTowardGoal, + applyProtocolFee: applyProtocolFee, + canRefund: canRefund, + instantTransfer: instantTransfer + }) + ); + + // Process line items immediately since crypto payment is confirmed + if (countsTowardGoal) { + // Line items that count toward goal use existing tracking variables + s_confirmedPaymentPerToken[paymentToken] += item.amount; + s_lifetimeConfirmedPaymentPerToken[paymentToken] += item.amount; + s_availableConfirmedPerToken[paymentToken] += item.amount; + } else { + // Apply protocol fee if applicable + uint256 feeAmount = 0; + if (applyProtocolFee) { + uint256 protocolFee = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; + feeAmount += protocolFee; + s_protocolFeePerToken[paymentToken] += protocolFee; + } + uint256 netAmount = item.amount - feeAmount; + + if (instantTransfer) { + // Accumulate for batch transfer after loop + totalInstantTransferAmount += netAmount; + } else { + // Track outstanding non-goal balances using net amounts (after fees) + s_nonGoalLineItemConfirmedPerToken[paymentToken] += netAmount; + + if (canRefund) { + s_refundableNonGoalLineItemPerToken[paymentToken] += netAmount; + } else { + s_nonGoalLineItemClaimablePerToken[paymentToken] += netAmount; + } + } + } + } + + // Store external fee metadata for informational purposes only + ICampaignPaymentTreasury.ExternalFees[] storage externalFeeMetadata = + s_paymentExternalFeeMetadata[internalPaymentId]; + for (uint256 i = 0; i < externalFees.length;) { + externalFeeMetadata.push(externalFees[i]); + unchecked { + ++i; + } + } + + IERC20(paymentToken).safeTransferFrom(buyerAddress, address(this), totalAmount); + + s_payment[internalPaymentId] = PaymentInfo({ + buyerId: ZERO_BYTES, + buyerAddress: buyerAddress, + itemId: itemId, + amount: amount, // Amount in token's native decimals + expiration: 0, + isConfirmed: true, + isCryptoPayment: true, + lineItemCount: lineItems.length + }); + + s_paymentIdToToken[internalPaymentId] = paymentToken; + s_paymentIdToCreator[paymentId] = _msgSender(); // Store creator address for getPaymentData lookup + s_confirmedPaymentPerToken[paymentToken] += amount; + s_lifetimeConfirmedPaymentPerToken[paymentToken] += amount; + s_availableConfirmedPerToken[paymentToken] += amount; + + // Perform single batch transfer if there are any instant transfer amounts + if (totalInstantTransferAmount > 0) { + IERC20(paymentToken).safeTransfer(platformAdmin, totalInstantTransferAmount); + } + // Mint NFT for crypto payment + uint256 tokenId = INFO.mintNFTForPledge( + buyerAddress, + itemId, // Using itemId as the reward identifier + paymentToken, + amount, + 0, // shippingFee (0 for payment treasuries) + 0 // tipAmount (0 for payment treasuries) + ); + s_paymentIdToTokenId[internalPaymentId] = tokenId; + + emit PaymentCreated(buyerAddress, paymentId, ZERO_BYTES, itemId, paymentToken, amount, 0, true); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function cancelPayment(bytes32 paymentId) + public + virtual + override + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled + { + bytes32 internalPaymentId = _scopePaymentIdForOffChain(paymentId); + _validatePaymentForAction(internalPaymentId); + + address paymentToken = s_paymentIdToToken[internalPaymentId]; + uint256 amount = s_payment[internalPaymentId].amount; + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems = s_paymentLineItems[internalPaymentId]; + + // Remove pending tracking for line items using snapshot from payment creation + // This prevents issues if line item type configuration changed after payment creation + for (uint256 i = 0; i < lineItems.length; i++) { + // Use snapshot instead of current configuration to ensure consistency + if (lineItems[i].countsTowardGoal) { + s_pendingPaymentPerToken[paymentToken] -= lineItems[i].amount; + } else { + s_nonGoalLineItemPendingPerToken[paymentToken] -= lineItems[i].amount; + } + } + + delete s_payment[internalPaymentId]; + delete s_paymentIdToToken[internalPaymentId]; + delete s_paymentLineItems[internalPaymentId]; + delete s_paymentExternalFeeMetadata[internalPaymentId]; + + s_pendingPaymentPerToken[paymentToken] -= amount; + + emit PaymentCancelled(paymentId); + } + + /** + * @dev Calculates line item totals for balance checking and state updates. + * @param lineItems Array of line items to process. + * @param protocolFeePercent Protocol fee percentage. + * @return totals Struct containing all calculated totals. + */ + function _calculateLineItemTotals( + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems, + uint256 protocolFeePercent + ) internal view returns (LineItemTotals memory totals) { + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; + + bool countsTowardGoal = item.countsTowardGoal; + bool applyProtocolFee = item.applyProtocolFee; + bool instantTransfer = item.instantTransfer; + + if (countsTowardGoal) { + totals.totalGoalLineItemAmount += item.amount; + } else { + uint256 feeAmount = 0; + if (applyProtocolFee) { + uint256 protocolFee = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; + totals.totalProtocolFeeFromLineItems += protocolFee; + feeAmount += protocolFee; + } + + uint256 netAmount = item.amount - feeAmount; + + if (instantTransfer) { + totals.totalInstantTransferAmountForCheck += netAmount; + } else if (item.canRefund) { + totals.totalNonGoalRefundableAmount += netAmount; + } else { + totals.totalNonGoalClaimableAmount += netAmount; + } + } + } + } + + /** + * @dev Checks if there's sufficient balance for payment confirmation. + * @param paymentToken The token address. + * @param paymentAmount The base payment amount. + * @param totals Line item totals struct. + */ + function _checkBalanceForConfirmation(address paymentToken, uint256 paymentAmount, LineItemTotals memory totals) + internal + view + { + uint256 actualBalance = IERC20(paymentToken).balanceOf(address(this)); + uint256 currentlyCommitted = s_availableConfirmedPerToken[paymentToken] + s_protocolFeePerToken[paymentToken] + + s_platformFeePerToken[paymentToken] + s_nonGoalLineItemClaimablePerToken[paymentToken] + + s_refundableNonGoalLineItemPerToken[paymentToken]; + + uint256 newCommitted = currentlyCommitted + paymentAmount + totals.totalGoalLineItemAmount + + totals.totalProtocolFeeFromLineItems + totals.totalNonGoalClaimableAmount + + totals.totalNonGoalRefundableAmount; + + if (newCommitted + totals.totalInstantTransferAmountForCheck > actualBalance) { + revert PaymentTreasuryInsufficientBalance( + newCommitted + totals.totalInstantTransferAmountForCheck, actualBalance + ); + } + } + + /** + * @dev Updates state for line items during payment confirmation. + * @param paymentToken The token address. + * @param lineItems Array of line items to process. + * @param protocolFeePercent Protocol fee percentage. + * @return totalInstantTransferAmount Total amount to transfer instantly. + */ + function _updateLineItemsForConfirmation( + address paymentToken, + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems, + uint256 protocolFeePercent + ) internal returns (uint256 totalInstantTransferAmount) { + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; + + bool countsTowardGoal = item.countsTowardGoal; + bool applyProtocolFee = item.applyProtocolFee; + bool canRefund = item.canRefund; + bool instantTransfer = item.instantTransfer; + + if (countsTowardGoal) { + s_pendingPaymentPerToken[paymentToken] -= item.amount; + s_confirmedPaymentPerToken[paymentToken] += item.amount; + s_lifetimeConfirmedPaymentPerToken[paymentToken] += item.amount; + s_availableConfirmedPerToken[paymentToken] += item.amount; + } else { + s_nonGoalLineItemPendingPerToken[paymentToken] -= item.amount; + + uint256 feeAmount = 0; + if (applyProtocolFee) { + uint256 protocolFee = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; + feeAmount += protocolFee; + s_protocolFeePerToken[paymentToken] += protocolFee; + } + + uint256 netAmount = item.amount - feeAmount; + + if (instantTransfer) { + totalInstantTransferAmount += netAmount; + // Instant transfer items are not tracked in s_nonGoalLineItemConfirmedPerToken + } else { + // Track outstanding non-goal balances using net amounts (after fees) + s_nonGoalLineItemConfirmedPerToken[paymentToken] += netAmount; + + if (canRefund) { + s_refundableNonGoalLineItemPerToken[paymentToken] += netAmount; + } else { + s_nonGoalLineItemClaimablePerToken[paymentToken] += netAmount; + } + } + } + } + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function confirmPayment(bytes32 paymentId, address buyerAddress) + public + virtual + override + nonReentrant + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled + { + bytes32 internalPaymentId = _scopePaymentIdForOffChain(paymentId); + _validatePaymentForAction(internalPaymentId); + + address paymentToken = s_paymentIdToToken[internalPaymentId]; + uint256 paymentAmount = s_payment[internalPaymentId].amount; + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems = s_paymentLineItems[internalPaymentId]; + + uint256 protocolFeePercent = INFO.getProtocolFeePercent(); + LineItemTotals memory totals = _calculateLineItemTotals(lineItems, protocolFeePercent); + + _checkBalanceForConfirmation(paymentToken, paymentAmount, totals); + + totals.totalInstantTransferAmount = _updateLineItemsForConfirmation(paymentToken, lineItems, protocolFeePercent); + + s_payment[internalPaymentId].isConfirmed = true; + + s_pendingPaymentPerToken[paymentToken] -= paymentAmount; + s_confirmedPaymentPerToken[paymentToken] += paymentAmount; + s_lifetimeConfirmedPaymentPerToken[paymentToken] += paymentAmount; + s_availableConfirmedPerToken[paymentToken] += paymentAmount; + + if (totals.totalInstantTransferAmount > 0) { + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + IERC20(paymentToken).safeTransfer(platformAdmin, totals.totalInstantTransferAmount); + } + + if (buyerAddress != address(0)) { + s_payment[internalPaymentId].buyerAddress = buyerAddress; + bytes32 itemId = s_payment[internalPaymentId].itemId; + uint256 tokenId = INFO.mintNFTForPledge(buyerAddress, itemId, paymentToken, paymentAmount, 0, 0); + s_paymentIdToTokenId[internalPaymentId] = tokenId; + } + + emit PaymentConfirmed(paymentId); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function confirmPaymentBatch(bytes32[] calldata paymentIds, address[] calldata buyerAddresses) + public + virtual + override + nonReentrant + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled + { + // Validate array lengths must match + if (buyerAddresses.length != paymentIds.length) { + revert PaymentTreasuryInvalidInput(); + } + + bytes32 currentPaymentId; + address currentToken; + + uint256 protocolFeePercent = INFO.getProtocolFeePercent(); + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + + for (uint256 i = 0; i < paymentIds.length;) { + currentPaymentId = paymentIds[i]; + bytes32 internalPaymentId = _scopePaymentIdForOffChain(currentPaymentId); + + _validatePaymentForAction(internalPaymentId); + + currentToken = s_paymentIdToToken[internalPaymentId]; + uint256 amount = s_payment[internalPaymentId].amount; + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems = s_paymentLineItems[internalPaymentId]; + + LineItemTotals memory totals = _calculateLineItemTotals(lineItems, protocolFeePercent); + _checkBalanceForConfirmation(currentToken, amount, totals); + + totals.totalInstantTransferAmount = + _updateLineItemsForConfirmation(currentToken, lineItems, protocolFeePercent); + + s_payment[internalPaymentId].isConfirmed = true; + + s_pendingPaymentPerToken[currentToken] -= amount; + s_confirmedPaymentPerToken[currentToken] += amount; + s_lifetimeConfirmedPaymentPerToken[currentToken] += amount; + s_availableConfirmedPerToken[currentToken] += amount; + + if (totals.totalInstantTransferAmount > 0) { + IERC20(currentToken).safeTransfer(platformAdmin, totals.totalInstantTransferAmount); + } + + if (buyerAddresses[i] != address(0)) { + address buyerAddress = buyerAddresses[i]; + s_payment[internalPaymentId].buyerAddress = buyerAddress; + bytes32 itemId = s_payment[internalPaymentId].itemId; + uint256 tokenId = INFO.mintNFTForPledge(buyerAddress, itemId, currentToken, amount, 0, 0); + s_paymentIdToTokenId[internalPaymentId] = tokenId; + } + + unchecked { + ++i; + } + } + + emit PaymentBatchConfirmed(paymentIds); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev For non-NFT payments only. Verifies that no NFT exists for this payment. + */ + function claimRefund(bytes32 paymentId, address refundAddress) + public + virtual + override + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + whenCampaignNotCancelled + { + if (refundAddress == address(0)) { + revert PaymentTreasuryInvalidInput(); + } + bytes32 internalPaymentId = _scopePaymentIdForOffChain(paymentId); + PaymentInfo memory payment = s_payment[internalPaymentId]; + address paymentToken = s_paymentIdToToken[internalPaymentId]; + uint256 amountToRefund = payment.amount; + uint256 availablePaymentAmount = s_availableConfirmedPerToken[paymentToken]; + uint256 tokenId = s_paymentIdToTokenId[internalPaymentId]; + + if (payment.buyerId == ZERO_BYTES) { + revert PaymentTreasuryPaymentNotExist(internalPaymentId); + } + if (!payment.isConfirmed) { + revert PaymentTreasuryPaymentNotConfirmed(internalPaymentId); + } + if (amountToRefund == 0 || availablePaymentAmount < amountToRefund) { + revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + } + // This function is for non-NFT payments only + if (tokenId != 0) { + revert PaymentTreasuryCryptoPayment(internalPaymentId); + } + + // Use snapshots of line item type configuration from payment creation time + // This prevents issues if line item type configuration changed after payment creation/confirmation + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems = s_paymentLineItems[internalPaymentId]; + uint256 protocolFeePercent = INFO.getProtocolFeePercent(); + + // Calculate total line item refund amount using snapshots + uint256 totalGoalLineItemRefundAmount = 0; + uint256 totalNonGoalLineItemRefundAmount = 0; + + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; + + // Use snapshot flags instead of current configuration + if (!item.canRefund) { + continue; // Skip non-refundable line items (based on snapshot at creation time) + } + + if (item.countsTowardGoal) { + // Goal line items: full amount is refundable from goal tracking + totalGoalLineItemRefundAmount += item.amount; + } else { + // Non-goal line items: handle fees and instant transfers + // For instant transfer items, the net amount was already sent to platform admin - don't refund + // For non-instant items, only refund the net amount (after fees), not the fees themselves + if (item.instantTransfer) { + // Skip instant transfer items - they were already sent to platform admin + continue; + } + + uint256 feeAmount = 0; + if (item.applyProtocolFee) { + feeAmount = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; + } + uint256 netAmount = item.amount - feeAmount; + + // Only refund the net amount (fees are not refundable) + totalNonGoalLineItemRefundAmount += netAmount; + } + } + + // Check that we have enough available balance for the total refund (BEFORE modifying state) + // Goal line items are in availableConfirmedPerToken, non-goal items need separate check + uint256 totalRefundAmount = amountToRefund + totalGoalLineItemRefundAmount + totalNonGoalLineItemRefundAmount; + + // For goal line items and base payment, check availableConfirmedPerToken + if (availablePaymentAmount < (amountToRefund + totalGoalLineItemRefundAmount)) { + revert PaymentTreasuryPaymentNotClaimable(paymentId); + } + + // For non-goal line items, check that we have enough claimable balance + // (only non-instant transfer items are refundable, and only their net amounts after fees) + if (totalNonGoalLineItemRefundAmount > 0) { + uint256 availableRefundable = s_refundableNonGoalLineItemPerToken[paymentToken]; + if (availableRefundable < totalNonGoalLineItemRefundAmount) { + revert PaymentTreasuryPaymentNotClaimable(paymentId); + } + } + + // Check that contract has enough actual balance to perform the transfer + uint256 contractBalance = IERC20(paymentToken).balanceOf(address(this)); + if (contractBalance < totalRefundAmount) { + revert PaymentTreasuryPaymentNotClaimable(paymentId); + } + + // Update state: remove tracking for refundable line items using snapshots + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; + + // Use snapshot flags instead of current configuration + if (!item.canRefund) { + continue; // Skip non-refundable line items (based on snapshot at creation time) + } + + if (item.countsTowardGoal) { + // Goal line items: remove from goal tracking + s_confirmedPaymentPerToken[paymentToken] -= item.amount; + s_availableConfirmedPerToken[paymentToken] -= item.amount; + } else { + // Non-goal line items: remove from non-goal tracking + // Note: instantTransfer items are skipped in the refund calculation above + if (item.instantTransfer) { + // Instant transfer items were already sent to platform admin; nothing tracked + continue; + } + + // Calculate fees and net amount using snapshot + uint256 feeAmount = 0; + if (item.applyProtocolFee) { + feeAmount = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; + // Fees are NOT refunded - they remain in the protocol fee pool + } + + uint256 netAmount = item.amount - feeAmount; + + // Remove net amount from outstanding non-goal tracking + s_nonGoalLineItemConfirmedPerToken[paymentToken] -= netAmount; + + // Remove from refundable tracking (only net amount is refundable) + s_refundableNonGoalLineItemPerToken[paymentToken] -= netAmount; + } + } + + delete s_payment[internalPaymentId]; + delete s_paymentIdToToken[internalPaymentId]; + delete s_paymentLineItems[internalPaymentId]; + delete s_paymentExternalFeeMetadata[internalPaymentId]; + + s_confirmedPaymentPerToken[paymentToken] -= amountToRefund; + s_availableConfirmedPerToken[paymentToken] -= amountToRefund; + + IERC20(paymentToken).safeTransfer(refundAddress, totalRefundAmount); + emit RefundClaimed(paymentId, totalRefundAmount, refundAddress); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev For NFT payments only. Requires an NFT exists and burns it. Refund is sent to current NFT owner. + */ + function claimRefund(bytes32 paymentId) public virtual override whenCampaignNotPaused whenCampaignNotCancelled { + // Use _findPaymentId to look up the payment using stored creator address + bytes32 internalPaymentId = _findPaymentId(paymentId); + if (internalPaymentId == ZERO_BYTES) { + revert PaymentTreasuryPaymentNotExist(paymentId); + } + PaymentInfo memory payment = s_payment[internalPaymentId]; + address paymentToken = s_paymentIdToToken[internalPaymentId]; + address buyerAddress = payment.buyerAddress; + uint256 amountToRefund = payment.amount; + uint256 availablePaymentAmount = s_availableConfirmedPerToken[paymentToken]; + uint256 tokenId = s_paymentIdToTokenId[internalPaymentId]; + + if (buyerAddress == address(0)) { + revert PaymentTreasuryPaymentNotExist(internalPaymentId); + } + if (amountToRefund == 0 || availablePaymentAmount < amountToRefund) { + revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + } + // NFT must exist for crypto payments + if (tokenId == 0) { + revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + } + + // Get NFT owner before burning + address nftOwner = INFO.ownerOf(tokenId); + + // Use snapshots of line item type configuration from payment creation time + // This prevents issues if line item type configuration changed after payment creation/confirmation + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItems = s_paymentLineItems[internalPaymentId]; + uint256 protocolFeePercent = INFO.getProtocolFeePercent(); + + // Calculate total line item refund amount using snapshots + uint256 totalGoalLineItemRefundAmount = 0; + uint256 totalNonGoalLineItemRefundAmount = 0; + + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; + + // Use snapshot flags instead of current configuration + if (!item.canRefund) { + continue; // Skip non-refundable line items (based on snapshot at creation time) + } + + if (item.countsTowardGoal) { + // Goal line items: full amount is refundable from goal tracking + totalGoalLineItemRefundAmount += item.amount; + } else { + // Non-goal line items: handle fees and instant transfers + // For instant transfer items, the net amount was already sent to platform admin - don't refund + // For non-instant items, only refund the net amount (after fees), not the fees themselves + if (item.instantTransfer) { + // Skip instant transfer items - they were already sent to platform admin + continue; + } + + uint256 feeAmount = 0; + if (item.applyProtocolFee) { + feeAmount = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; + } + uint256 netAmount = item.amount - feeAmount; + + // Only refund the net amount (fees are not refundable) + totalNonGoalLineItemRefundAmount += netAmount; + } + } + + // Check that we have enough available balance for the total refund (BEFORE modifying state) + // Goal line items are in availableConfirmedPerToken, non-goal items need separate check + uint256 totalRefundAmount = amountToRefund + totalGoalLineItemRefundAmount + totalNonGoalLineItemRefundAmount; + + // For goal line items and base payment, check availableConfirmedPerToken + if (availablePaymentAmount < (amountToRefund + totalGoalLineItemRefundAmount)) { + revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + } + + // For non-goal line items, check that we have enough claimable balance + // (only non-instant transfer items are refundable, and only their net amounts after fees) + if (totalNonGoalLineItemRefundAmount > 0) { + uint256 availableRefundable = s_refundableNonGoalLineItemPerToken[paymentToken]; + if (availableRefundable < totalNonGoalLineItemRefundAmount) { + revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + } + } + + // Check that contract has enough actual balance to perform the transfer + uint256 contractBalance = IERC20(paymentToken).balanceOf(address(this)); + if (contractBalance < totalRefundAmount) { + revert PaymentTreasuryPaymentNotClaimable(internalPaymentId); + } + + // Update state: remove tracking for refundable line items using snapshots + for (uint256 i = 0; i < lineItems.length; i++) { + ICampaignPaymentTreasury.PaymentLineItem memory item = lineItems[i]; + + // Use snapshot flags instead of current configuration + if (!item.canRefund) { + continue; // Skip non-refundable line items (based on snapshot at creation time) + } + + if (item.countsTowardGoal) { + // Goal line items: remove from goal tracking + s_confirmedPaymentPerToken[paymentToken] -= item.amount; + s_availableConfirmedPerToken[paymentToken] -= item.amount; + } else { + // Non-goal line items: remove from non-goal tracking + // Note: instantTransfer items are skipped in the refund calculation above + if (item.instantTransfer) { + // Instant transfer items were already sent to platform admin; nothing tracked + continue; + } + + // Calculate fees and net amount using snapshot + uint256 feeAmount = 0; + if (item.applyProtocolFee) { + feeAmount = (item.amount * protocolFeePercent) / PERCENT_DIVIDER; + // Fees are NOT refunded - they remain in the protocol fee pool + } + + uint256 netAmount = item.amount - feeAmount; + + // Remove net amount from outstanding non-goal tracking + s_nonGoalLineItemConfirmedPerToken[paymentToken] -= netAmount; + + // Remove from refundable tracking (only net amount is refundable) + s_refundableNonGoalLineItemPerToken[paymentToken] -= netAmount; + } + } + + delete s_payment[internalPaymentId]; + delete s_paymentIdToToken[internalPaymentId]; + delete s_paymentLineItems[internalPaymentId]; + delete s_paymentExternalFeeMetadata[internalPaymentId]; + delete s_paymentIdToTokenId[internalPaymentId]; + delete s_paymentIdToCreator[paymentId]; // Clean up creator mapping for on-chain payments + + s_confirmedPaymentPerToken[paymentToken] -= amountToRefund; + s_availableConfirmedPerToken[paymentToken] -= amountToRefund; + + // Burn NFT (requires treasury approval from owner) + INFO.burn(tokenId); + + IERC20(paymentToken).safeTransfer(nftOwner, totalRefundAmount); + emit RefundClaimed(paymentId, totalRefundAmount, nftOwner); + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function disburseFees() public virtual override whenCampaignNotPaused { + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + address protocolAdmin = INFO.getProtocolAdminAddress(); + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 protocolShare = s_protocolFeePerToken[token]; + uint256 platformShare = s_platformFeePerToken[token]; + + if (protocolShare > 0 || platformShare > 0) { + s_protocolFeePerToken[token] = 0; + s_platformFeePerToken[token] = 0; + + if (protocolShare > 0) { + IERC20(token).safeTransfer(protocolAdmin, protocolShare); + } + + if (platformShare > 0) { + IERC20(token).safeTransfer(platformAdmin, platformShare); + } + + emit FeesDisbursed(token, protocolShare, platformShare); + } + } + } + + /** + * @notice Allows platform admin to claim non-goal line items that are available for claiming. + * @param token The token address to claim. + */ + function claimNonGoalLineItems(address token) + public + virtual + onlyPlatformAdmin(PLATFORM_HASH) + whenCampaignNotPaused + { + if (!INFO.isTokenAccepted(token)) { + revert PaymentTreasuryTokenNotAccepted(token); + } + + uint256 claimableAmount = s_nonGoalLineItemClaimablePerToken[token]; + if (claimableAmount == 0) { + revert PaymentTreasuryInvalidInput(); + } + + s_nonGoalLineItemClaimablePerToken[token] = 0; + uint256 currentNonGoalConfirmed = s_nonGoalLineItemConfirmedPerToken[token]; + s_nonGoalLineItemConfirmedPerToken[token] = + currentNonGoalConfirmed > claimableAmount ? currentNonGoalConfirmed - claimableAmount : 0; + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + + IERC20(token).safeTransfer(platformAdmin, claimableAmount); + + emit NonGoalLineItemsClaimed(token, claimableAmount, platformAdmin); + } + + /** + * @notice Allows the platform admin to claim all remaining funds once the claim window has opened. + */ + function claimExpiredFunds() public virtual onlyPlatformAdmin(PLATFORM_HASH) whenCampaignNotPaused { + uint256 claimDelay = INFO.getPlatformClaimDelay(PLATFORM_HASH); + uint256 claimableAt = INFO.getDeadline(); + claimableAt += claimDelay; + + if (block.timestamp < claimableAt) { + revert PaymentTreasuryClaimWindowNotReached(claimableAt); + } + + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + address platformAdmin = INFO.getPlatformAdminAddress(PLATFORM_HASH); + address protocolAdmin = INFO.getProtocolAdminAddress(); + + bool claimedAny; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + + uint256 availableConfirmed = s_availableConfirmedPerToken[token]; + uint256 claimableAmount = s_nonGoalLineItemClaimablePerToken[token]; + uint256 refundableAmount = s_refundableNonGoalLineItemPerToken[token]; + uint256 platformFeeAmount = s_platformFeePerToken[token]; + uint256 protocolFeeAmount = s_protocolFeePerToken[token]; + + uint256 platformAmount = availableConfirmed + claimableAmount + refundableAmount + platformFeeAmount; + uint256 protocolAmount = protocolFeeAmount; + + if (platformAmount == 0 && protocolAmount == 0) { + continue; + } + + if (availableConfirmed > 0) { + uint256 currentConfirmed = s_confirmedPaymentPerToken[token]; + s_confirmedPaymentPerToken[token] = + currentConfirmed > availableConfirmed ? currentConfirmed - availableConfirmed : 0; + s_availableConfirmedPerToken[token] = 0; + } + + if (claimableAmount > 0 || refundableAmount > 0) { + uint256 reduction = claimableAmount + refundableAmount; + uint256 currentNonGoalConfirmed = s_nonGoalLineItemConfirmedPerToken[token]; + s_nonGoalLineItemConfirmedPerToken[token] = + currentNonGoalConfirmed > reduction ? currentNonGoalConfirmed - reduction : 0; + s_nonGoalLineItemClaimablePerToken[token] = 0; + s_refundableNonGoalLineItemPerToken[token] = 0; + } + + if (platformFeeAmount > 0) { + s_platformFeePerToken[token] = 0; + } + + if (protocolFeeAmount > 0) { + s_protocolFeePerToken[token] = 0; + } + + // transfer funds after state has been cleared + if (platformAmount > 0) { + IERC20(token).safeTransfer(platformAdmin, platformAmount); + claimedAny = true; + } + + if (protocolAmount > 0) { + IERC20(token).safeTransfer(protocolAdmin, protocolAmount); + claimedAny = true; + } + + emit ExpiredFundsClaimed(token, platformAmount, protocolAmount); + } + + if (!claimedAny) { + revert PaymentTreasuryNoFundsToClaim(); + } + } + + /** + * @inheritdoc ICampaignPaymentTreasury + */ + function withdraw() + public + virtual + override + onlyPlatformAdminOrCampaignOwner + whenCampaignNotPaused + whenCampaignNotCancelled + { + if (!_checkSuccessCondition()) { + revert PaymentTreasurySuccessConditionNotFulfilled(); + } + + address recipient = INFO.owner(); + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + uint256 protocolFeePercent = INFO.getProtocolFeePercent(); + uint256 platformFeePercent = INFO.getPlatformFeePercent(PLATFORM_HASH); + + bool hasWithdrawn = false; + + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 balance = s_availableConfirmedPerToken[token]; + + if (balance > 0) { + hasWithdrawn = true; + + // Calculate fees + uint256 protocolShare = (balance * protocolFeePercent) / PERCENT_DIVIDER; + uint256 platformShare = (balance * platformFeePercent) / PERCENT_DIVIDER; + + s_protocolFeePerToken[token] += protocolShare; + s_platformFeePerToken[token] += platformShare; + + uint256 totalFee = protocolShare + platformShare; + + if (balance < totalFee) { + revert PaymentTreasuryInsufficientFundsForFee(balance, totalFee); + } + uint256 withdrawalAmount = balance - totalFee; + + // Reset balance + s_availableConfirmedPerToken[token] = 0; + + IERC20(token).safeTransfer(recipient, withdrawalAmount); + + emit WithdrawalWithFeeSuccessful(token, recipient, withdrawalAmount, totalFee); + } + } + + if (!hasWithdrawn) { + revert PaymentTreasuryAlreadyWithdrawn(); + } + } + + /** + * @dev External function to pause the campaign. + */ + function pauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH) { + _pause(message); + } + + /** + * @dev External function to unpause the campaign. + */ + function unpauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH) { + _unpause(message); + } + + /** + * @dev External function to cancel the campaign. + */ + function cancelTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH) { + _cancel(message); + } + + /** + * @notice Returns true if the treasury has been cancelled. + * @return True if cancelled, false otherwise. + */ + function cancelled() public view virtual override(ICampaignPaymentTreasury, PausableCancellable) returns (bool) { + return super.cancelled(); + } + + /** + * @dev Internal function to check if the campaign is paused. + * If the campaign is paused, it reverts with PaymentTreasuryCampaignInfoIsPaused error. + */ + function _revertIfCampaignPaused() internal view { + if (INFO.paused()) { + revert PaymentTreasuryCampaignInfoIsPaused(); + } + } + + function _revertIfCampaignCancelled() internal view { + if (INFO.cancelled()) { + revert PaymentTreasuryCampaignInfoIsPaused(); + } + } + + /** + * @dev Validates the given payment ID to ensure it is eligible for further action. + * Reverts if: + * - The payment does not exist. + * - The payment has already been confirmed. + * - The payment has already expired. + * - The payment is a crypto payment + * @param paymentId The unique identifier of the payment to validate. + */ + function _validatePaymentForAction(bytes32 paymentId) internal view { + PaymentInfo memory payment = s_payment[paymentId]; + + if (payment.buyerId == ZERO_BYTES) { + revert PaymentTreasuryPaymentNotExist(paymentId); + } + + if (payment.isConfirmed) { + revert PaymentTreasuryPaymentAlreadyConfirmed(paymentId); + } + + if (payment.expiration <= block.timestamp) { + revert PaymentTreasuryPaymentAlreadyExpired(paymentId); + } + + if (payment.isCryptoPayment) { + revert PaymentTreasuryCryptoPayment(paymentId); + } + } + + /** + * @inheritdoc ICampaignPaymentTreasury + * @dev This function can look up payments created by anyone: + * - Off-chain payments (created via createPayment): Scoped with address(0), anyone can look these up + * - On-chain payments (created via processCryptoPayment): Uses stored creator address, anyone can look these up + */ + function getPaymentData(bytes32 paymentId) + public + view + override + returns (ICampaignPaymentTreasury.PaymentData memory) + { + // Try off-chain scope first (address(0)) - works for createPayment + // Then try on-chain scope using stored creator address - works for processCryptoPayment + bytes32 internalPaymentId = _findPaymentId(paymentId); + if (internalPaymentId == ZERO_BYTES) { + revert PaymentTreasuryPaymentNotExist(paymentId); + } + PaymentInfo memory payment = s_payment[internalPaymentId]; + address paymentToken = s_paymentIdToToken[internalPaymentId]; + ICampaignPaymentTreasury.PaymentLineItem[] storage lineItemsStorage = s_paymentLineItems[internalPaymentId]; + ICampaignPaymentTreasury.ExternalFees[] storage externalFeesStorage = + s_paymentExternalFeeMetadata[internalPaymentId]; + + // Copy line items from storage to memory (required: cannot directly assign storage array to memory array) + ICampaignPaymentTreasury.PaymentLineItem[] memory lineItems = + new ICampaignPaymentTreasury.PaymentLineItem[](lineItemsStorage.length); + for (uint256 i = 0; i < lineItemsStorage.length; i++) { + lineItems[i] = lineItemsStorage[i]; + } + + // Copy external fees from storage to memory (same reason as line items) + ICampaignPaymentTreasury.ExternalFees[] memory externalFees = + new ICampaignPaymentTreasury.ExternalFees[](externalFeesStorage.length); + for (uint256 i = 0; i < externalFeesStorage.length; i++) { + externalFees[i] = externalFeesStorage[i]; + } + + return ICampaignPaymentTreasury.PaymentData({ + buyerAddress: payment.buyerAddress, + buyerId: payment.buyerId, + itemId: payment.itemId, + amount: payment.amount, + expiration: payment.expiration, + isConfirmed: payment.isConfirmed, + isCryptoPayment: payment.isCryptoPayment, + lineItemCount: payment.lineItemCount, + paymentToken: paymentToken, + lineItems: lineItems, + externalFees: externalFees + }); + } + + /** + * @dev Internal function to check the success condition for fee disbursement. + * @return Whether the success condition is met. + */ + function _checkSuccessCondition() internal view virtual returns (bool); +} diff --git a/src/utils/BaseTreasury.sol b/src/utils/BaseTreasury.sol index 78abe289..dc224685 100644 --- a/src/utils/BaseTreasury.sol +++ b/src/utils/BaseTreasury.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {ICampaignTreasury} from "../interfaces/ICampaignTreasury.sol"; @@ -12,40 +13,40 @@ import {PausableCancellable} from "./PausableCancellable.sol"; * @title BaseTreasury * @notice A base contract for creating and managing treasuries in crowdfunding campaigns. * @dev This contract defines common functionality and storage for campaign treasuries. + * @dev Supports ERC-2771 meta-transactions via adapter contracts for platform admin operations. * @dev Contracts implementing this base contract should provide specific success conditions. */ -abstract contract BaseTreasury is - Initializable, - ICampaignTreasury, - CampaignAccessChecker, - PausableCancellable -{ +abstract contract BaseTreasury is Initializable, ICampaignTreasury, CampaignAccessChecker, PausableCancellable { using SafeERC20 for IERC20; - bytes32 internal constant ZERO_BYTES = - 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant ZERO_BYTES = 0x0000000000000000000000000000000000000000000000000000000000000000; uint256 internal constant PERCENT_DIVIDER = 10000; + uint256 internal constant STANDARD_DECIMALS = 18; bytes32 internal PLATFORM_HASH; uint256 internal PLATFORM_FEE_PERCENT; - IERC20 internal TOKEN; - uint256 internal s_pledgedAmount; bool internal s_feesDisbursed; + // Multi-token support + mapping(address => uint256) internal s_tokenRaisedAmounts; // Amount raised per token (decreases on refunds) + mapping(address => uint256) internal s_tokenLifetimeRaisedAmounts; // Lifetime raised amount per token (never decreases) + /** - * @notice Emitted when fees are successfully disbursed. + * @notice Emitted when fees are successfully disbursed for a specific token. + * @param token The token address. * @param protocolShare The amount of fees sent to the protocol. * @param platformShare The amount of fees sent to the platform. */ - event FeesDisbursed(uint256 protocolShare, uint256 platformShare); + event FeesDisbursed(address indexed token, uint256 protocolShare, uint256 platformShare); /** - * @notice Emitted when a withdrawal is successful. + * @notice Emitted when a withdrawal is successful for a specific token. + * @param token The token address. * @param to The recipient of the withdrawal. * @param amount The amount withdrawn. */ - event WithdrawalSuccessful(address to, uint256 amount); + event WithdrawalSuccessful(address indexed token, address to, uint256 amount); /** * @notice Emitted when the success condition is not fulfilled during fee disbursement. @@ -72,14 +73,25 @@ abstract contract BaseTreasury is */ error TreasuryCampaignInfoIsPaused(); - function __BaseContract_init( - bytes32 platformHash, - address infoAddress - ) internal { + function __BaseContract_init(bytes32 platformHash, address infoAddress, address trustedForwarder_) internal { __CampaignAccessChecker_init(infoAddress); PLATFORM_HASH = platformHash; - TOKEN = IERC20(INFO.getTokenAddress()); PLATFORM_FEE_PERCENT = INFO.getPlatformFeePercent(platformHash); + _trustedForwarder = trustedForwarder_; + } + + /** + * @dev Override _msgSender to support ERC-2771 meta-transactions. + * When called by the trusted forwarder (adapter), extracts the actual sender from calldata. + */ + function _msgSender() internal view virtual override returns (address sender) { + if (msg.sender == _trustedForwarder && msg.data.length >= 20) { + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + sender = msg.sender; + } } /** @@ -109,82 +121,133 @@ abstract contract BaseTreasury is return PLATFORM_FEE_PERCENT; } + /** + * @dev Normalizes token amount to 18 decimals for consistent comparison. + * @param token The token address to normalize. + * @param amount The amount to normalize. + * @return The normalized amount in 18 decimals. + */ + function _normalizeAmount(address token, uint256 amount) internal view returns (uint256) { + uint8 decimals = IERC20Metadata(token).decimals(); + + if (decimals == STANDARD_DECIMALS) { + return amount; + } else if (decimals < STANDARD_DECIMALS) { + // Scale up for tokens with fewer decimals + return amount * (10 ** (STANDARD_DECIMALS - decimals)); + } else { + // Scale down for tokens with more decimals (rare but possible) + return amount / (10 ** (decimals - STANDARD_DECIMALS)); + } + } + + /** + * @dev Denormalizes an amount from 18 decimals to the token's actual decimals. + * @param token The token address to denormalize for. + * @param amount The amount in 18 decimals to denormalize. + * @return The denormalized amount in token's native decimals. + */ + function _denormalizeAmount(address token, uint256 amount) internal view returns (uint256) { + uint8 decimals = IERC20Metadata(token).decimals(); + + if (decimals == STANDARD_DECIMALS) { + return amount; + } else if (decimals < STANDARD_DECIMALS) { + // Scale down for tokens with fewer decimals (e.g., USDC 6 decimals) + uint256 divisor = 10 ** (STANDARD_DECIMALS - decimals); + return (amount + divisor - 1) / divisor; + } else { + // Scale up for tokens with more decimals (rare but possible) + return amount * (10 ** (decimals - STANDARD_DECIMALS)); + } + } + /** * @inheritdoc ICampaignTreasury */ - function disburseFees() - public - virtual - override - whenCampaignNotPaused - whenCampaignNotCancelled - { + function disburseFees() public virtual override whenCampaignNotPaused whenCampaignNotCancelled { if (!_checkSuccessCondition()) { revert TreasurySuccessConditionNotFulfilled(); } - uint256 balance = s_pledgedAmount; - uint256 protocolShare = (balance * INFO.getProtocolFeePercent()) / - PERCENT_DIVIDER; - uint256 platformShare = (balance * - INFO.getPlatformFeePercent(PLATFORM_HASH)) / PERCENT_DIVIDER; - TOKEN.safeTransfer(INFO.getProtocolAdminAddress(), protocolShare); - - TOKEN.safeTransfer( - INFO.getPlatformAdminAddress(PLATFORM_HASH), - platformShare - ); + + address[] memory acceptedTokens = INFO.getAcceptedTokens(); + + // Disburse fees for each token + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 balance = s_tokenRaisedAmounts[token]; + + if (balance > 0) { + uint256 protocolShare = (balance * INFO.getProtocolFeePercent()) / PERCENT_DIVIDER; + uint256 platformShare = (balance * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + + if (protocolShare > 0) { + IERC20(token).safeTransfer(INFO.getProtocolAdminAddress(), protocolShare); + } + + if (platformShare > 0) { + IERC20(token).safeTransfer(INFO.getPlatformAdminAddress(PLATFORM_HASH), platformShare); + } + + emit FeesDisbursed(token, protocolShare, platformShare); + } + } s_feesDisbursed = true; - emit FeesDisbursed(protocolShare, platformShare); } /** * @inheritdoc ICampaignTreasury */ - function withdraw() - public - virtual - override - whenCampaignNotPaused - whenCampaignNotCancelled - { + function withdraw() public virtual override whenCampaignNotPaused whenCampaignNotCancelled { if (!s_feesDisbursed) { revert TreasuryFeeNotDisbursed(); } - uint256 balance = TOKEN.balanceOf(address(this)); + + address[] memory acceptedTokens = INFO.getAcceptedTokens(); address recipient = INFO.owner(); - TOKEN.safeTransfer(recipient, balance); - emit WithdrawalSuccessful(recipient, balance); + // Withdraw remaining balance for each token + for (uint256 i = 0; i < acceptedTokens.length; i++) { + address token = acceptedTokens[i]; + uint256 balance = IERC20(token).balanceOf(address(this)); + + if (balance > 0) { + IERC20(token).safeTransfer(recipient, balance); + emit WithdrawalSuccessful(token, recipient, balance); + } + } } /** * @dev External function to pause the campaign. */ - function pauseTreasury( - bytes32 message - ) public virtual onlyPlatformAdmin(PLATFORM_HASH) { + function pauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH) { _pause(message); } /** * @dev External function to unpause the campaign. */ - function unpauseTreasury( - bytes32 message - ) public virtual onlyPlatformAdmin(PLATFORM_HASH) { + function unpauseTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH) { _unpause(message); } /** * @dev External function to cancel the campaign. */ - function cancelTreasury( - bytes32 message - ) public virtual onlyPlatformAdmin(PLATFORM_HASH) { + function cancelTreasury(bytes32 message) public virtual onlyPlatformAdmin(PLATFORM_HASH) { _cancel(message); } + /** + * @notice Returns true if the treasury has been cancelled. + * @return True if cancelled, false otherwise. + */ + function cancelled() public view virtual override(ICampaignTreasury, PausableCancellable) returns (bool) { + return super.cancelled(); + } + /** * @dev Internal function to check if the campaign is paused. * If the campaign is paused, it reverts with TreasuryCampaignInfoIsPaused error. diff --git a/src/utils/CampaignAccessChecker.sol b/src/utils/CampaignAccessChecker.sol index 060cca68..dd0869ed 100644 --- a/src/utils/CampaignAccessChecker.sol +++ b/src/utils/CampaignAccessChecker.sol @@ -1,18 +1,21 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {ICampaignInfo} from "../interfaces/ICampaignInfo.sol"; -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; /** * @title CampaignAccessChecker * @dev This abstract contract provides access control mechanisms to restrict the execution of specific functions * to authorized protocol administrators, platform administrators, and campaign owners. */ -abstract contract CampaignAccessChecker { +abstract contract CampaignAccessChecker is Context { // Immutable reference to the ICampaignInfo contract, which provides campaign-related information and admin addresses. ICampaignInfo internal INFO; + /// @dev Trusted forwarder address for ERC-2771 meta-transactions (set by derived contracts) + address internal _trustedForwarder; + /** * @dev Throws when the caller is not authorized. */ @@ -59,7 +62,7 @@ abstract contract CampaignAccessChecker { * If the sender is not the protocol admin, it reverts with AccessCheckerUnauthorized error. */ function _onlyProtocolAdmin() private view { - if (msg.sender != INFO.getProtocolAdminAddress()) { + if (_msgSender() != INFO.getProtocolAdminAddress()) { revert AccessCheckerUnauthorized(); } } @@ -70,7 +73,7 @@ abstract contract CampaignAccessChecker { * @param platformHash The unique identifier of the platform. */ function _onlyPlatformAdmin(bytes32 platformHash) private view { - if (msg.sender != INFO.getPlatformAdminAddress(platformHash)) { + if (_msgSender() != INFO.getPlatformAdminAddress(platformHash)) { revert AccessCheckerUnauthorized(); } } @@ -80,7 +83,7 @@ abstract contract CampaignAccessChecker { * If the sender is not the owner, it reverts with AccessCheckerUnauthorized error. */ function _onlyCampaignOwner() private view { - if (INFO.owner() != msg.sender) { + if (INFO.owner() != _msgSender()) { revert AccessCheckerUnauthorized(); } } diff --git a/src/utils/Counters.sol b/src/utils/Counters.sol index cacf1388..549f2f36 100644 --- a/src/utils/Counters.sol +++ b/src/utils/Counters.sol @@ -7,7 +7,7 @@ // The updates in this version are to ensure compatibility with Solidity v0.8.20 and to be consistent in style of other contracts used in this repository. -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; library Counters { /** diff --git a/src/utils/FiatEnabled.sol b/src/utils/FiatEnabled.sol index 85d98f62..378f7d1a 100644 --- a/src/utils/FiatEnabled.sol +++ b/src/utils/FiatEnabled.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title FiatEnabled @@ -16,10 +16,7 @@ abstract contract FiatEnabled { * @param fiatTransactionId The unique identifier of the fiat transaction. * @param fiatTransactionAmount The updated amount of the fiat transaction. */ - event FiatTransactionUpdated( - bytes32 indexed fiatTransactionId, - uint256 fiatTransactionAmount - ); + event FiatTransactionUpdated(bytes32 indexed fiatTransactionId, uint256 fiatTransactionAmount); /** * @notice Emitted when the state of fiat fee disbursement is updated. @@ -27,11 +24,7 @@ abstract contract FiatEnabled { * @param protocolFeeAmount The protocol fee amount. * @param platformFeeAmount The platform fee amount. */ - event FiatFeeDisbusementStateUpdated( - bool isDisbursed, - uint256 protocolFeeAmount, - uint256 platformFeeAmount - ); + event FiatFeeDisbusementStateUpdated(bool isDisbursed, uint256 protocolFeeAmount, uint256 platformFeeAmount); /** * @dev Throws an error indicating that the fiat enabled functionality is already set. @@ -59,9 +52,7 @@ abstract contract FiatEnabled { * @param fiatTransactionId The unique identifier of the fiat transaction. * @return amount The amount of the specified fiat transaction. */ - function getFiatTransactionAmount( - bytes32 fiatTransactionId - ) external view returns (uint256 amount) { + function getFiatTransactionAmount(bytes32 fiatTransactionId) external view returns (uint256 amount) { amount = s_fiatAmountById[fiatTransactionId]; if (amount == 0) { revert FiatEnabledInvalidTransaction(); @@ -81,10 +72,7 @@ abstract contract FiatEnabled { * @param fiatTransactionId The unique identifier of the fiat transaction. * @param fiatTransactionAmount The amount of the fiat transaction. */ - function _updateFiatTransaction( - bytes32 fiatTransactionId, - uint256 fiatTransactionAmount - ) internal { + function _updateFiatTransaction(bytes32 fiatTransactionId, uint256 fiatTransactionAmount) internal { s_fiatAmountById[fiatTransactionId] = fiatTransactionAmount; s_fiatRaisedAmount += fiatTransactionAmount; emit FiatTransactionUpdated(fiatTransactionId, fiatTransactionAmount); @@ -96,11 +84,9 @@ abstract contract FiatEnabled { * @param protocolFeeAmount The protocol fee amount. * @param platformFeeAmount The platform fee amount. */ - function _updateFiatFeeDisbursementState( - bool isDisbursed, - uint256 protocolFeeAmount, - uint256 platformFeeAmount - ) internal { + function _updateFiatFeeDisbursementState(bool isDisbursed, uint256 protocolFeeAmount, uint256 platformFeeAmount) + internal + { if (s_fiatFeeIsDisbursed == true) { revert FiatEnabledAlreadySet(); } @@ -108,10 +94,6 @@ abstract contract FiatEnabled { revert FiatEnabledDisallowedState(); } s_fiatFeeIsDisbursed = true; - emit FiatFeeDisbusementStateUpdated( - isDisbursed, - protocolFeeAmount, - platformFeeAmount - ); + emit FiatFeeDisbusementStateUpdated(isDisbursed, protocolFeeAmount, platformFeeAmount); } } diff --git a/src/utils/ItemRegistry.sol b/src/utils/ItemRegistry.sol index 0e6f4282..e1931739 100644 --- a/src/utils/ItemRegistry.sol +++ b/src/utils/ItemRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; @@ -28,10 +28,7 @@ contract ItemRegistry is IItem, Context { /** * @inheritdoc IItem */ - function getItem( - address owner, - bytes32 itemId - ) external view override returns (Item memory) { + function getItem(address owner, bytes32 itemId) external view override returns (Item memory) { return Items[owner][itemId]; } @@ -48,10 +45,7 @@ contract ItemRegistry is IItem, Context { * @param itemIds An array of unique item identifiers. * @param items An array of `Item` structs containing item attributes. */ - function addItemsBatch( - bytes32[] calldata itemIds, - Item[] calldata items - ) external { + function addItemsBatch(bytes32[] calldata itemIds, Item[] calldata items) external { if (itemIds.length != items.length) { revert ItemRegistryMismatchedArraysLength(); } diff --git a/src/utils/PausableCancellable.sol b/src/utils/PausableCancellable.sol index db0994d0..441a69f9 100644 --- a/src/utils/PausableCancellable.sol +++ b/src/utils/PausableCancellable.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; + +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; /// @title PausableCancellable /// @notice Abstract contract providing pause and cancel state management with events and modifiers -abstract contract PausableCancellable { +abstract contract PausableCancellable is Context { bool private _paused; bool private _cancelled; @@ -98,11 +100,9 @@ abstract contract PausableCancellable { * @param reason A short reason for pausing * @dev Can only pause if not already paused or cancelled */ - function _pause( - bytes32 reason - ) internal virtual whenNotPaused whenNotCancelled { + function _pause(bytes32 reason) internal virtual whenNotPaused whenNotCancelled { _paused = true; - emit Paused(msg.sender, reason); + emit Paused(_msgSender(), reason); } /** @@ -112,7 +112,7 @@ abstract contract PausableCancellable { */ function _unpause(bytes32 reason) internal virtual whenPaused { _paused = false; - emit Unpaused(msg.sender, reason); + emit Unpaused(_msgSender(), reason); } /** @@ -124,11 +124,9 @@ abstract contract PausableCancellable { if (_cancelled) revert CannotCancel(); /// @dev keccak256 Hash of `Auto-unpaused during cancellation` is passed as a reason if (_paused) { - _unpause( - 0x231da0eace2a459b43889b78bbd1fc88a89e3192ee6cbcda7015c539d577e2cd - ); + _unpause(0x231da0eace2a459b43889b78bbd1fc88a89e3192ee6cbcda7015c539d577e2cd); } _cancelled = true; - emit Cancelled(msg.sender, reason); + emit Cancelled(_msgSender(), reason); } } diff --git a/src/utils/PledgeNFT.sol b/src/utils/PledgeNFT.sol new file mode 100644 index 00000000..7d854806 --- /dev/null +++ b/src/utils/PledgeNFT.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; +import {Counters} from "./Counters.sol"; + +/** + * @title PledgeNFT + * @notice Abstract contract for NFTs representing pledges with on-chain metadata + * @dev Contains counter logic and NFT metadata storage + */ +abstract contract PledgeNFT is ERC721Burnable, AccessControl { + using Strings for uint256; + using Strings for address; + using Counters for Counters.Counter; + + bytes32 public constant MINTER_ROLE = 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6; + + /** + * @dev Struct to store pledge data for each token + */ + struct PledgeData { + address backer; + bytes32 reward; + address treasury; + address tokenAddress; + uint256 amount; + uint256 shippingFee; + uint256 tipAmount; + } + + // NFT metadata storage + string internal s_nftName; + string internal s_nftSymbol; + string internal s_imageURI; + string internal s_contractURI; + + // Token ID counter (also serves as pledge ID counter) + Counters.Counter internal s_tokenIdCounter; + + // Mapping from token ID to pledge data + mapping(uint256 => PledgeData) internal s_pledgeData; + + /** + * @dev Emitted when the image URI is updated + * @param newImageURI The new image URI + */ + event ImageURIUpdated(string newImageURI); + + /** + * @dev Emitted when the contract URI is updated + * @param newContractURI The new contract URI + */ + event ContractURIUpdated(string newContractURI); + + /** + * @dev Emitted when a pledge NFT is minted + * @param tokenId The token ID + * @param backer The backer address + * @param treasury The treasury address + * @param reward The reward identifier + */ + event PledgeNFTMinted(uint256 indexed tokenId, address indexed backer, address indexed treasury, bytes32 reward); + + /** + * @dev Emitted when unauthorized access is attempted + */ + error PledgeNFTUnAuthorized(); + + /** + * @dev Emitted when a string contains invalid characters for JSON + */ + error PledgeNFTInvalidJsonString(); + + /** + * @notice Initialize NFT metadata + * @dev Called by CampaignInfo during initialization + * @param _nftName NFT collection name + * @param _nftSymbol NFT collection symbol + * @param _imageURI NFT image URI for individual tokens + * @param _contractURI IPFS URI for contract-level metadata + */ + function _initializeNFT( + string calldata _nftName, + string calldata _nftSymbol, + string calldata _imageURI, + string calldata _contractURI + ) internal { + _validateJsonString(_nftName); + s_nftName = _nftName; + s_nftSymbol = _nftSymbol; + s_imageURI = _imageURI; + s_contractURI = _contractURI; + } + + /** + * @notice Validates that a string is safe for JSON embedding + * @dev Reverts if string contains quotes, backslashes, control characters, or non-ASCII + * @param str The string to validate + */ + function _validateJsonString(string calldata str) internal pure { + bytes memory b = bytes(str); + for (uint256 i = 0; i < b.length; i++) { + bytes1 char = b[i]; + // Reject: control characters (0x00-0x1F), double quote ("), backslash (\), or non-ASCII (>0x7E) + if (char < 0x20 || char == 0x22 || char == 0x5C || char > 0x7E) { + revert PledgeNFTInvalidJsonString(); + } + } + } + + /** + * @notice Mints a pledge NFT (auto-increments counter) + * @dev Called by treasuries - returns the new token ID to use as pledge ID + * @param backer The backer address + * @param reward The reward identifier + * @param tokenAddress The address of the token used for the pledge + * @param amount The pledge amount + * @param shippingFee The shipping fee + * @param tipAmount The tip amount + * @return tokenId The minted token ID (to be used as pledge ID in treasury) + */ + function mintNFTForPledge( + address backer, + bytes32 reward, + address tokenAddress, + uint256 amount, + uint256 shippingFee, + uint256 tipAmount + ) public virtual onlyRole(MINTER_ROLE) returns (uint256 tokenId) { + // Increment counter and get new token ID + s_tokenIdCounter.increment(); + tokenId = s_tokenIdCounter.current(); + + // Set pledge data + s_pledgeData[tokenId] = PledgeData({ + backer: backer, + reward: reward, + treasury: _msgSender(), + tokenAddress: tokenAddress, + amount: amount, + shippingFee: shippingFee, + tipAmount: tipAmount + }); + + emit PledgeNFTMinted(tokenId, backer, msg.sender, reward); + + // Mint NFT + _safeMint(backer, tokenId); + + return tokenId; + } + + /** + * @notice Burns a pledge NFT + * @param tokenId The token ID to burn + */ + function burn(uint256 tokenId) public virtual override { + delete s_pledgeData[tokenId]; + super.burn(tokenId); + } + + /** + * @notice Override name to return initialized name + * @return The NFT collection name + */ + function name() public view virtual override returns (string memory) { + return s_nftName; + } + + /** + * @notice Override symbol to return initialized symbol + * @return The NFT collection symbol + */ + function symbol() public view virtual override returns (string memory) { + return s_nftSymbol; + } + + /** + * @notice Sets the image URI for all NFTs + * @dev Must be overridden by inheriting contracts to implement access control + * @param newImageURI The new image URI + */ + function setImageURI(string calldata newImageURI) external virtual; + + /** + * @notice Returns contract-level metadata URI + * @return The contract URI + */ + function contractURI() external view virtual returns (string memory) { + return s_contractURI; + } + + /** + * @notice Update contract-level metadata URI + * @dev Must be overridden by inheriting contracts to implement access control + * @param newContractURI The new contract URI + */ + function updateContractURI(string calldata newContractURI) external virtual; + + /** + * @notice Gets current total number of pledges + * @return The current pledge count + */ + function getPledgeCount() external view virtual returns (uint256) { + return s_tokenIdCounter.current(); + } + + /** + * @notice Returns the token URI with on-chain metadata + * @param tokenId The token ID + * @return The base64 encoded JSON metadata + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + _requireOwned(tokenId); + + PledgeData memory data = s_pledgeData[tokenId]; + + string memory json = string( + abi.encodePacked( + '{"name":"', + name(), + " #", + tokenId.toString(), + '","image":"', + s_imageURI, + '","attributes":[', + '{"trait_type":"Backer","value":"', + Strings.toHexString(uint160(data.backer), 20), + '"},', + '{"trait_type":"Reward","value":"', + Strings.toHexString(uint256(data.reward), 32), + '"},', + '{"trait_type":"Treasury","value":"', + Strings.toHexString(uint160(data.treasury), 20), + '"},', + '{"trait_type":"Campaign","value":"', + Strings.toHexString(uint160(address(this)), 20), + '"},', + '{"trait_type":"PledgeToken","value":"', + Strings.toHexString(uint160(data.tokenAddress), 20), + '"},', + '{"trait_type":"PledgeAmount","value":"', + data.amount.toString(), + '"},', + '{"trait_type":"ShippingFee","value":"', + data.shippingFee.toString(), + '"},', + '{"trait_type":"TipAmount","value":"', + data.tipAmount.toString(), + '"}', + "]}" + ) + ); + + return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json)))); + } + + /** + * @notice Gets the image URI + * @return The current image URI + */ + function getImageURI() external view returns (string memory) { + return s_imageURI; + } + + /** + * @notice Gets the pledge data for a token + * @param tokenId The token ID + * @return The pledge data + */ + function getPledgeData(uint256 tokenId) external view returns (PledgeData memory) { + return s_pledgeData[tokenId]; + } + + /** + * @notice Override supportsInterface for multiple inheritance + * @param interfaceId The interface ID + * @return True if the interface is supported + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, AccessControl) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/src/utils/TimestampChecker.sol b/src/utils/TimestampChecker.sol index 7ae687f7..02e26c40 100644 --- a/src/utils/TimestampChecker.sol +++ b/src/utils/TimestampChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; /** * @title TimestampChecker @@ -59,9 +59,7 @@ abstract contract TimestampChecker { * @dev Internal function to revert if the current timestamp is less than or equal a specified time. * @param inputTime The timestamp being checked against. */ - function _revertIfCurrentTimeIsNotLess( - uint256 inputTime - ) internal view virtual { + function _revertIfCurrentTimeIsNotLess(uint256 inputTime) internal view virtual { uint256 currentTime = block.timestamp; if (currentTime >= inputTime) { revert CurrentTimeIsGreater(inputTime, currentTime); @@ -72,9 +70,7 @@ abstract contract TimestampChecker { * @dev Internal function to revert if the current timestamp is not greater than or equal a specified time. * @param inputTime The timestamp being checked against. */ - function _revertIfCurrentTimeIsNotGreater( - uint256 inputTime - ) internal view virtual { + function _revertIfCurrentTimeIsNotGreater(uint256 inputTime) internal view virtual { uint256 currentTime = block.timestamp; if (currentTime <= inputTime) { revert CurrentTimeIsLess(inputTime, currentTime); @@ -86,10 +82,7 @@ abstract contract TimestampChecker { * @param initialTime The initial timestamp of the range. * @param finalTime The final timestamp of the range. */ - function _revertIfCurrentTimeIsNotWithinRange( - uint256 initialTime, - uint256 finalTime - ) internal view virtual { + function _revertIfCurrentTimeIsNotWithinRange(uint256 initialTime, uint256 finalTime) internal view virtual { uint256 currentTime = block.timestamp; if (currentTime < initialTime || currentTime > finalTime) { revert CurrentTimeIsNotWithinRange(initialTime, finalTime); diff --git a/test/foundry/Base.t.sol b/test/foundry/Base.t.sol index ad771340..55b79115 100644 --- a/test/foundry/Base.t.sol +++ b/test/foundry/Base.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import "forge-std/Test.sol"; import {Users} from "./utils/Types.sol"; @@ -10,18 +10,29 @@ import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; import {TreasuryFactory} from "src/TreasuryFactory.sol"; import {AllOrNothing} from "src/treasuries/AllOrNothing.sol"; +import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; +import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; /// @notice Base test contract with common logic needed by all tests. abstract contract Base_Test is Test, Defaults { //Variables Users internal users; - //Test Contracts + //Test Contracts - Multiple tokens for multi-token testing + TestToken internal usdtToken; // 6 decimals - Tether + TestToken internal usdcToken; // 6 decimals - USD Coin + TestToken internal cUSDToken; // 18 decimals - Celo Dollar + + // Legacy support - points to cUSDToken for backward compatibility TestToken internal testToken; + GlobalParams internal globalParams; CampaignInfoFactory internal campaignInfoFactory; TreasuryFactory internal treasuryFactory; AllOrNothing internal allOrNothingImplementation; + KeepWhatsRaised internal keepWhatsRaisedImplementation; CampaignInfo internal campaignInfo; function setUp() public virtual { @@ -39,49 +50,106 @@ abstract contract Base_Test is Test, Defaults { vm.startPrank(users.contractOwner); - // Deploy the base test contracts. - testToken = new TestToken(tokenName, tokenSymbol); - globalParams = new GlobalParams( + // Deploy multiple test tokens with different decimals + usdtToken = new TestToken("Tether USD", "USDT", 6); + usdcToken = new TestToken("USD Coin", "USDC", 6); + cUSDToken = new TestToken("Celo Dollar", "cUSD", 18); + + // Backward compatibility + testToken = cUSDToken; + + // Setup currencies and tokens for multi-token support + bytes32[] memory currencies = new bytes32[](1); + currencies[0] = bytes32("USD"); + + address[][] memory tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](3); + tokensPerCurrency[0][0] = address(usdtToken); + tokensPerCurrency[0][1] = address(usdcToken); + tokensPerCurrency[0][2] = address(cUSDToken); + + // Deploy GlobalParams with UUPS proxy + GlobalParams globalParamsImpl = new GlobalParams(); + bytes memory globalParamsInitData = abi.encodeWithSelector( + GlobalParams.initialize.selector, users.protocolAdminAddress, - address(testToken), - PROTOCOL_FEE_PERCENT + PROTOCOL_FEE_PERCENT, + currencies, + tokensPerCurrency ); + ERC1967Proxy globalParamsProxy = new ERC1967Proxy(address(globalParamsImpl), globalParamsInitData); + globalParams = GlobalParams(address(globalParamsProxy)); - campaignInfo = new CampaignInfo(address(this)); + // Deploy CampaignInfo implementation + campaignInfo = new CampaignInfo(); console.log("CampaignInfo address: ", address(campaignInfo)); - campaignInfoFactory = new CampaignInfoFactory( - globalParams, - address(campaignInfo) - ); - treasuryFactory = new TreasuryFactory(globalParams); - //Initialize campaignInfoFactory - campaignInfoFactory._initialize( - address(treasuryFactory), - address(globalParams) + // Deploy TreasuryFactory with UUPS proxy + TreasuryFactory treasuryFactoryImpl = new TreasuryFactory(); + bytes memory treasuryFactoryInitData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(address(globalParams))); + ERC1967Proxy treasuryFactoryProxy = new ERC1967Proxy(address(treasuryFactoryImpl), treasuryFactoryInitData); + treasuryFactory = TreasuryFactory(address(treasuryFactoryProxy)); + + // Deploy CampaignInfoFactory with UUPS proxy + CampaignInfoFactory campaignFactoryImpl = new CampaignInfoFactory(); + bytes memory campaignFactoryInitData = abi.encodeWithSelector( + CampaignInfoFactory.initialize.selector, + users.contractOwner, + IGlobalParams(address(globalParams)), + address(campaignInfo), + address(treasuryFactory) ); + ERC1967Proxy campaignFactoryProxy = new ERC1967Proxy(address(campaignFactoryImpl), campaignFactoryInitData); + campaignInfoFactory = CampaignInfoFactory(address(campaignFactoryProxy)); allOrNothingImplementation = new AllOrNothing(); - //Mint token to the backer - testToken.mint(users.backer1Address, TOKEN_MINT_AMOUNT); - testToken.mint(users.backer2Address, TOKEN_MINT_AMOUNT); + keepWhatsRaisedImplementation = new KeepWhatsRaised(); + + vm.stopPrank(); + + // Set time constraints in dataRegistry (requires protocol admin) + vm.startPrank(users.protocolAdminAddress); + globalParams.addToRegistry( + DataRegistryKeys.CAMPAIGN_LAUNCH_BUFFER, + bytes32(uint256(0)) // No buffer for most tests + ); + globalParams.addToRegistry( + DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION, + bytes32(uint256(0)) // No minimum duration for most tests + ); + vm.stopPrank(); + + vm.startPrank(users.contractOwner); + //Mint tokens to backers - all three token types + usdtToken.mint(users.backer1Address, TOKEN_MINT_AMOUNT / 1e12); // Adjust for 6 decimals + usdtToken.mint(users.backer2Address, TOKEN_MINT_AMOUNT / 1e12); + + usdcToken.mint(users.backer1Address, TOKEN_MINT_AMOUNT / 1e12); + usdcToken.mint(users.backer2Address, TOKEN_MINT_AMOUNT / 1e12); + + cUSDToken.mint(users.backer1Address, TOKEN_MINT_AMOUNT); + cUSDToken.mint(users.backer2Address, TOKEN_MINT_AMOUNT); + + // Also mint to platform admins for setFeeAndPledge tests + usdtToken.mint(users.platform1AdminAddress, TOKEN_MINT_AMOUNT / 1e12); + usdcToken.mint(users.platform1AdminAddress, TOKEN_MINT_AMOUNT / 1e12); + cUSDToken.mint(users.platform1AdminAddress, TOKEN_MINT_AMOUNT); + + usdtToken.mint(users.platform2AdminAddress, TOKEN_MINT_AMOUNT / 1e12); + usdcToken.mint(users.platform2AdminAddress, TOKEN_MINT_AMOUNT / 1e12); + cUSDToken.mint(users.platform2AdminAddress, TOKEN_MINT_AMOUNT); vm.stopPrank(); // Label the base test contracts. - vm.label({account: address(testToken), newLabel: "TestToken"}); - vm.label({ - account: address(globalParams), - newLabel: "Global Parameter" - }); - vm.label({ - account: address(campaignInfoFactory), - newLabel: "Campaign Info Factory" - }); - vm.label({ - account: address(treasuryFactory), - newLabel: "Treasury Factory" - }); + vm.label({account: address(usdtToken), newLabel: "USDT"}); + vm.label({account: address(usdcToken), newLabel: "USDC"}); + vm.label({account: address(cUSDToken), newLabel: "cUSD"}); + vm.label({account: address(testToken), newLabel: "TestToken(cUSD)"}); + vm.label({account: address(globalParams), newLabel: "Global Parameter"}); + vm.label({account: address(campaignInfoFactory), newLabel: "Campaign Info Factory"}); + vm.label({account: address(treasuryFactory), newLabel: "Treasury Factory"}); // Warp to October 1, 2023 at 00:00 GMT to provide a more realistic testing environment. vm.warp(OCTOBER_1_2023); @@ -93,4 +161,17 @@ abstract contract Base_Test is Test, Defaults { vm.deal({account: user, newBalance: 100 ether}); return user; } + + /// @dev Helper to get token amount adjusted for decimals + function getTokenAmount(address token, uint256 baseAmount) internal view returns (uint256) { + if (token == address(usdtToken) || token == address(usdcToken)) { + return baseAmount / 1e12; // Convert 18 decimal amount to 6 decimal + } + return baseAmount; // 18 decimals (cUSD) + } + + /// @dev Helper to create an array filled with address(0) + function _createZeroAddressArray(uint256 length) internal pure returns (address[] memory) { + return new address[](length); + } } diff --git a/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol b/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol index 2b12aebb..e1e7a909 100644 --- a/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol +++ b/test/foundry/integration/AllOrNothing/AllOrNothing.t.sol @@ -10,35 +10,15 @@ import {CampaignInfo} from "src/CampaignInfo.sol"; import {IReward} from "src/interfaces/IReward.sol"; import {LogDecoder} from "../../utils/LogDecoder.sol"; -/** - * @title AllOrNothing Integration Test Shared Contract - * @notice Common testing logic needed by all AllOrNothing integration tests. - * @dev Abstract contract that provides shared setup and helper functions for AllOrNothing treasury testing. - * Handles platform enrollment, treasury implementation registration, campaign creation, and treasury deployment. - * Also provides utility functions for pledging, refunding, fee disbursement, and withdrawals. - */ -abstract contract AllOrNothing_Integration_Shared_Test is - IReward, - LogDecoder, - Base_Test -{ - /// @dev Address of the created campaign contract +/// @notice Common testing logic needed by all AllOrNothing integration tests. +abstract contract AllOrNothing_Integration_Shared_Test is IReward, LogDecoder, Base_Test { address campaignAddress; - - /// @dev Address of the deployed treasury contract address treasuryAddress; - - /// @dev Instance of the AllOrNothing treasury contract AllOrNothing internal allOrNothing; - /// @dev Token ID for pledges that include rewards uint256 pledgeForARewardTokenId; - /** - * @notice Initial setup for AllOrNothing integration tests - * @dev Performs the complete setup sequence: platform enrollment, treasury registration, - * campaign creation, and treasury deployment. Called by inheriting test contracts. - */ + /// @dev Initial dependent functions setup included for AllOrNothing Integration Tests. function setUp() public virtual override { super.setUp(); console.log("setUp: enlistPlatform"); @@ -47,11 +27,9 @@ abstract contract AllOrNothing_Integration_Shared_Test is enlistPlatform(PLATFORM_1_HASH); console.log("enlisted platform"); - //Register Treasury Implementation registerTreasuryImplementation(PLATFORM_1_HASH); console.log("registered treasury"); - //Approve Treasury Implementation approveTreasuryImplementation(PLATFORM_1_HASH); console.log("approved treasury"); @@ -65,40 +43,26 @@ abstract contract AllOrNothing_Integration_Shared_Test is } /** - * @notice Enlists a platform in the protocol - * @dev Called by protocol admin to register a new platform with specified fee structure - * @param platformHash The unique identifier hash for the platform + * @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 + PLATFORM_FEE_PERCENT, + address(0) // Platform adapter - can be set later with setPlatformAdapter ); vm.stopPrank(); } - /** - * @notice Registers a treasury implementation for a platform - * @dev Called by platform admin to register AllOrNothing treasury implementation - * @param platformHash The platform identifier to register the treasury for - */ function registerTreasuryImplementation(bytes32 platformHash) internal { vm.startPrank(users.platform1AdminAddress); - treasuryFactory.registerTreasuryImplementation( - platformHash, - 0, - address(allOrNothingImplementation) - ); + treasuryFactory.registerTreasuryImplementation(platformHash, 0, address(allOrNothingImplementation)); vm.stopPrank(); } - /** - * @notice Approves a registered treasury implementation - * @dev Called by protocol admin to approve a platform's treasury implementation - * @param platformHash The platform identifier whose treasury implementation to approve - */ function approveTreasuryImplementation(bytes32 platformHash) internal { vm.startPrank(users.protocolAdminAddress); treasuryFactory.approveTreasuryImplementation(platformHash, 0); @@ -106,9 +70,8 @@ abstract contract AllOrNothing_Integration_Shared_Test is } /** - * @notice Creates a new campaign for testing - * @dev Creates a campaign info contract and extracts the campaign address from emitted events - * @param platformHash The platform identifier to create the campaign on + * @notice Implements createCampaign helper function. It creates new campaign info contract + * @param platformHash The platform bytes. */ function createCampaign(bytes32 platformHash) internal { bytes32 identifierHash = keccak256(abi.encodePacked(platformHash)); @@ -126,16 +89,18 @@ abstract contract AllOrNothing_Integration_Shared_Test is selectedPlatformHash, platformDataKey, platformDataValue, - CAMPAIGN_DATA + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" ); Vm.Log[] memory entries = vm.getRecordedLogs(); vm.stopPrank(); - (bytes32[] memory topics, ) = decodeTopicsAndData( - entries, - "CampaignInfoFactoryCampaignCreated(bytes32,address)", - address(campaignInfoFactory) + (bytes32[] memory topics,) = decodeTopicsAndData( + entries, "CampaignInfoFactoryCampaignCreated(bytes32,address)", address(campaignInfoFactory) ); require(topics.length == 3, "Unexpected topic length for event"); @@ -144,25 +109,21 @@ abstract contract AllOrNothing_Integration_Shared_Test is } /** - * @notice Deploys a treasury contract for the created campaign - * @dev Deploys AllOrNothing treasury and extracts the treasury address from emitted events - * @param platformHash The platform identifier to deploy the treasury for + * @notice Implements deploy helper function. It deploys treasury contract. */ function deploy(bytes32 platformHash) internal { vm.startPrank(users.platform1AdminAddress); vm.recordLogs(); // Deploy the treasury contract - treasuryFactory.deploy(platformHash, campaignAddress, 0, NAME, SYMBOL); + treasuryFactory.deploy(platformHash, campaignAddress, 0); Vm.Log[] memory entries = vm.getRecordedLogs(); vm.stopPrank(); // Decode the TreasuryDeployed event (bytes32[] memory topics, bytes memory data) = decodeTopicsAndData( - entries, - "TreasuryFactoryTreasuryDeployed(bytes32,uint256,address,address)", - address(treasuryFactory) + entries, "TreasuryFactoryTreasuryDeployed(bytes32,uint256,address,address)", address(treasuryFactory) ); require(topics.length >= 3, "Expected indexed params missing"); @@ -173,194 +134,113 @@ abstract contract AllOrNothing_Integration_Shared_Test is allOrNothing = AllOrNothing(treasuryAddress); } - /** - * @notice Adds rewards to a treasury contract - * @dev Helper function to add reward tiers to an AllOrNothing treasury - * @param caller The address that will call the addRewards function - * @param treasury The treasury contract address - * @param rewardNames Array of reward names/identifiers - * @param rewards Array of reward structs containing reward details - */ - function addRewards( - address caller, - address treasury, - bytes32[] memory rewardNames, - Reward[] memory rewards - ) internal { + function addRewards(address caller, address treasury, bytes32[] memory rewardNames, Reward[] memory rewards) + internal + { vm.startPrank(caller); AllOrNothing(treasury).addRewards(rewardNames, rewards); vm.stopPrank(); } /** - * @notice Simulates pledging for a specific reward - * @dev Creates a pledge with reward selection and captures the receipt event - * @param caller The address making the pledge - * @param warpTime The block timestamp to warp to - * @param allOrNothingAddress The treasury contract address - * @param pledgeAmount The amount to pledge (automatically calculated from reward) - * @param shippingFee The shipping fee for the reward - * @param rewardName The identifier of the reward being pledged for - * @return logs The transaction logs - * @return tokenId The NFT token ID representing the pledge - * @return rewards Array of reward names associated with the pledge + * @notice Implements pledgeForAReward helper function. */ function pledgeForAReward( address caller, - uint256 warpTime, + address token, address allOrNothingAddress, uint256 pledgeAmount, uint256 shippingFee, + uint256 launchTime, bytes32 rewardName - ) - internal - returns ( - Vm.Log[] memory logs, - uint256 tokenId, - bytes32[] memory rewards - ) - { + ) internal returns (Vm.Log[] memory logs, uint256 tokenId, bytes32[] memory rewards) { vm.startPrank(caller); - vm.warp(warpTime); vm.recordLogs(); testToken.approve(allOrNothingAddress, pledgeAmount + shippingFee); + vm.warp(launchTime); bytes32[] memory reward = new bytes32[](1); reward[0] = rewardName; - AllOrNothing(allOrNothingAddress).pledgeForAReward( - caller, - shippingFee, - reward - ); + AllOrNothing(allOrNothingAddress).pledgeForAReward(caller, address(token), shippingFee, reward); logs = vm.getRecordedLogs(); - bytes memory data = decodeEventFromLogs( - logs, - "Receipt(address,bytes32,uint256,uint256,uint256,bytes32[])", - allOrNothingAddress + (bytes32[] memory topics, bytes memory data) = decodeTopicsAndData( + logs, "Receipt(address,address,bytes32,uint256,uint256,uint256,bytes32[])", allOrNothingAddress ); - (, , tokenId, rewards) = abi.decode( - data, - (uint256, uint256, uint256, bytes32[]) - ); + // Indexed params: backer (topics[1]), pledgeToken (topics[2]) + // Data params: reward, pledgeAmount, shippingFee, tokenId, rewards + (,,, tokenId, rewards) = abi.decode(data, (bytes32, uint256, uint256, uint256, bytes32[])); vm.stopPrank(); } /** - * @notice Simulates pledging without selecting a reward - * @dev Creates a pledge without reward selection and captures the receipt event - * @param caller The address making the pledge - * @param warpTime The block timestamp to warp to - * @param allOrNothingAddress The treasury contract address - * @param pledgeAmount The amount to pledge - * @return logs The transaction logs - * @return tokenId The NFT token ID representing the pledge + * @notice Implements pledgeWithoutAReward helper function. */ function pledgeWithoutAReward( address caller, - uint256 warpTime, + address token, address allOrNothingAddress, - uint256 pledgeAmount + uint256 pledgeAmount, + uint256 launchTime ) internal returns (Vm.Log[] memory logs, uint256 tokenId) { vm.startPrank(caller); - vm.warp(warpTime); vm.recordLogs(); testToken.approve(allOrNothingAddress, pledgeAmount); + vm.warp(launchTime); - AllOrNothing(allOrNothingAddress).pledgeWithoutAReward( - caller, - pledgeAmount - ); + AllOrNothing(allOrNothingAddress).pledgeWithoutAReward(caller, address(token), pledgeAmount); logs = vm.getRecordedLogs(); // Decode receipt event if available - bytes memory data = decodeEventFromLogs( - logs, - "Receipt(address,bytes32,uint256,uint256,uint256,bytes32[])", - allOrNothingAddress + (bytes32[] memory topics, bytes memory data) = decodeTopicsAndData( + logs, "Receipt(address,address,bytes32,uint256,uint256,uint256,bytes32[])", allOrNothingAddress ); - (, , tokenId, ) = abi.decode( - data, - (uint256, uint256, uint256, bytes32[]) - ); + // Indexed params: backer (topics[1]), pledgeToken (topics[2]) + // Data params: reward, pledgeAmount, shippingFee, tokenId, rewards + (,,, tokenId,) = abi.decode(data, (bytes32, uint256, uint256, uint256, bytes32[])); vm.stopPrank(); } /** - * @notice Simulates claiming a refund for a failed campaign - * @dev Claims refund for a pledge token and captures the refund event - * @param caller The address claiming the refund - * @param warpTime The block timestamp to warp to - * @param allOrNothingAddress The treasury contract address - * @param tokenId The pledge token ID to refund - * @return logs The transaction logs - * @return refundedTokenId The token ID that was refunded - * @return refundAmount The amount refunded - * @return claimer The address that claimed the refund + * @notice Implements claimRefund helper function. */ - function claimRefund( - address caller, - uint256 warpTime, - address allOrNothingAddress, - uint256 tokenId - ) + function claimRefund(address caller, address allOrNothingAddress, uint256 tokenId, uint256 warpTime) internal - returns ( - Vm.Log[] memory logs, - uint256 refundedTokenId, - uint256 refundAmount, - address claimer - ) + returns (Vm.Log[] memory logs, uint256 refundedTokenId, uint256 refundAmount, address claimer) { - vm.startPrank(caller); vm.warp(warpTime); + vm.startPrank(caller); + + // Approve treasury to burn NFT + CampaignInfo(campaignAddress).approve(allOrNothingAddress, tokenId); + vm.recordLogs(); AllOrNothing(allOrNothingAddress).claimRefund(tokenId); logs = vm.getRecordedLogs(); - bytes memory data = decodeEventFromLogs( - logs, - "RefundClaimed(uint256,uint256,address)", - allOrNothingAddress - ); + bytes memory data = decodeEventFromLogs(logs, "RefundClaimed(uint256,uint256,address)", allOrNothingAddress); - (refundedTokenId, refundAmount, claimer) = abi.decode( - data, - (uint256, uint256, address) - ); + (refundedTokenId, refundAmount, claimer) = abi.decode(data, (uint256, uint256, address)); vm.stopPrank(); } /** - * @notice Simulates fee disbursement for a successful campaign - * @dev Disburses protocol and platform fees and captures the disbursement event - * @param allOrNothingAddress The treasury contract address - * @param warpTime The block timestamp to warp to - * @return logs The transaction logs - * @return protocolShare The amount allocated to protocol fees - * @return platformShare The amount allocated to platform fees + * @notice Implements disburseFees helper function. */ - function disburseFees( - address allOrNothingAddress, - uint256 warpTime - ) + function disburseFees(address allOrNothingAddress, uint256 warpTime) internal - returns ( - Vm.Log[] memory logs, - uint256 protocolShare, - uint256 platformShare - ) + returns (Vm.Log[] memory logs, uint256 protocolShare, uint256 platformShare) { vm.warp(warpTime); vm.recordLogs(); @@ -369,28 +249,20 @@ abstract contract AllOrNothing_Integration_Shared_Test is logs = vm.getRecordedLogs(); - bytes memory data = decodeEventFromLogs( - logs, - "FeesDisbursed(uint256,uint256)", - allOrNothingAddress - ); + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "FeesDisbursed(address,uint256,uint256)", allOrNothingAddress); + // topics[1] is the indexed token (protocolShare, platformShare) = abi.decode(data, (uint256, uint256)); } /** - * @notice Simulates withdrawal of funds from a successful campaign - * @dev Withdraws remaining funds to campaign creator and captures the withdrawal event - * @param allOrNothingAddress The treasury contract address - * @param warpTime The block timestamp to warp to - * @return logs The transaction logs - * @return to The address that received the withdrawal - * @return amount The amount withdrawn + * @notice Implements withdraw helper function. */ - function withdraw( - address allOrNothingAddress, - uint256 warpTime - ) internal returns (Vm.Log[] memory logs, address to, uint256 amount) { + function withdraw(address allOrNothingAddress, uint256 warpTime) + internal + returns (Vm.Log[] memory logs, address to, uint256 amount) + { vm.warp(warpTime); // Start recording logs and simulate the withdrawal process vm.recordLogs(); @@ -402,103 +274,13 @@ abstract contract AllOrNothing_Integration_Shared_Test is logs = vm.getRecordedLogs(); // Decode the data from the logs - bytes memory data = decodeEventFromLogs( - logs, - "WithdrawalSuccessful(address,uint256)", - allOrNothingAddress - ); + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "WithdrawalSuccessful(address,address,uint256)", allOrNothingAddress); + // topics[1] is the indexed token // Decode the amount and the address of the receiver (to, amount) = abi.decode(data, (address, uint256)); return (logs, to, amount); } - - /** - * @notice Removes a reward from a treasury contract - * @dev Helper function to remove a reward from an AllOrNothing treasury - * @param caller The address that will call the removeReward function - * @param treasury The treasury contract address - * @param rewardName The name of the reward to remove - * @return logs The transaction logs - */ - function removeReward( - address caller, - address treasury, - bytes32 rewardName - ) internal returns (Vm.Log[] memory logs) { - vm.startPrank(caller); - vm.recordLogs(); - - AllOrNothing(treasury).removeReward(rewardName); - - logs = vm.getRecordedLogs(); - vm.stopPrank(); - } - - /** - * @notice Pauses a treasury contract - * @dev Helper function to pause an AllOrNothing treasury - * @param caller The address that will call the pauseTreasury function - * @param treasury The treasury contract address - * @param reason The reason for pausing - * @return logs The transaction logs - */ - function pauseTreasury( - address caller, - address treasury, - bytes32 reason - ) internal returns (Vm.Log[] memory logs) { - vm.startPrank(caller); - vm.recordLogs(); - - AllOrNothing(treasury).pauseTreasury(reason); - - logs = vm.getRecordedLogs(); - vm.stopPrank(); - } - - /** - * @notice Unpauses a treasury contract - * @dev Helper function to unpause an AllOrNothing treasury - * @param caller The address that will call the unpauseTreasury function - * @param treasury The treasury contract address - * @param reason The reason for unpausing - * @return logs The transaction logs - */ - function unpauseTreasury( - address caller, - address treasury, - bytes32 reason - ) internal returns (Vm.Log[] memory logs) { - vm.startPrank(caller); - vm.recordLogs(); - - AllOrNothing(treasury).unpauseTreasury(reason); - - logs = vm.getRecordedLogs(); - vm.stopPrank(); - } - - /** - * @notice Cancels a treasury contract - * @dev Helper function to cancel an AllOrNothing treasury - * @param caller The address that will call the cancelTreasury function - * @param treasury The treasury contract address - * @param reason The reason for cancellation - * @return logs The transaction logs - */ - function cancelTreasury( - address caller, - address treasury, - bytes32 reason - ) internal returns (Vm.Log[] memory logs) { - vm.startPrank(caller); - vm.recordLogs(); - - AllOrNothing(treasury).cancelTreasury(reason); - - logs = vm.getRecordedLogs(); - vm.stopPrank(); - } } diff --git a/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol b/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol index fef07b0d..6037e124 100644 --- a/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol +++ b/test/foundry/integration/AllOrNothing/AllOrNothingFunction.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import "./AllOrNothing.t.sol"; import "forge-std/console.sol"; @@ -9,30 +9,11 @@ import {Defaults} from "../../utils/Defaults.sol"; import {Constants} from "../../utils/Constants.sol"; import {Users} from "../../utils/Types.sol"; import {IReward} from "src/interfaces/IReward.sol"; +import {TestToken} from "../../../mocks/TestToken.sol"; -/** - * @title AllOrNothing Function Integration Test Contract - * @notice Comprehensive integration tests for AllOrNothing treasury contract functionality - * @dev Inherits from AllOrNothing_Integration_Shared_Test to access common setup and utilities. - * Tests cover the full lifecycle of campaign operations including reward setup, pledging, - * refund claims, fee disbursement, and fund withdrawal scenarios. - */ -contract AllOrNothingFunction_Integration_Shared_Test is - AllOrNothing_Integration_Shared_Test -{ - /** - * @notice Tests the addRewards functionality - * @dev Verifies that rewards can be properly added to the treasury contract and that - * all reward properties are stored correctly including values, tiers, items, and quantities. - * Tests multiple rewards with different configurations to ensure proper storage and retrieval. - */ +contract AllOrNothingFunction_Integration_Shared_Test is AllOrNothing_Integration_Shared_Test { function test_addRewards() external { - addRewards( - users.creator1Address, - address(allOrNothing), - REWARD_NAMES, - REWARDS - ); + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); // Verify all rewards were added correctly // First reward @@ -62,579 +43,333 @@ contract AllOrNothingFunction_Integration_Shared_Test is assertEq(REWARDS[2].itemId.length, resultReward3.itemId.length); } - /** - * @notice Tests the removeReward functionality - * @dev Verifies that rewards can be properly removed from the treasury contract and that - * the reward is no longer accessible after removal. Ensures the RewardRemoved event - * is emitted correctly and attempts to access removed rewards result in reverts. - */ - function test_removeReward() external { - addRewards( - users.creator1Address, - address(allOrNothing), - REWARD_NAMES, - REWARDS - ); - - // Verify reward exists before removal - Reward memory existingReward = allOrNothing.getReward(REWARD_NAMES[0]); - assertEq(existingReward.rewardValue, REWARDS[0].rewardValue); + function test_pledgeForAReward() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); - // Remove the reward using helper function - Vm.Log[] memory logs = removeReward( - users.creator1Address, + (Vm.Log[] memory logs, uint256 tokenId, bytes32[] memory rewards) = pledgeForAReward( + users.backer1Address, + address(testToken), address(allOrNothing), - REWARD_NAMES[0] + PLEDGE_AMOUNT, + SHIPPING_FEE, + LAUNCH_TIME, + REWARD_NAME_1_HASH ); - // For indexed parameters, we need to check topics - (bytes32[] memory topics, ) = decodeTopicsAndData( - logs, - "RewardRemoved(bytes32)", - address(allOrNothing) - ); - assertEq(topics[1], REWARD_NAMES[0], "Removed reward name should match"); + uint256 backerBalance = testToken.balanceOf(users.backer1Address); + uint256 treasuryBalance = testToken.balanceOf(address(allOrNothing)); + uint256 backerNftBalance = CampaignInfo(campaignAddress).balanceOf(users.backer1Address); + address nftOwnerAddress = CampaignInfo(campaignAddress).ownerOf(tokenId); - // Verify reward no longer exists (should revert) - vm.expectRevert(); - allOrNothing.getReward(REWARD_NAMES[0]); + assertEq(users.backer1Address, nftOwnerAddress); + assertEq(PLEDGE_AMOUNT + SHIPPING_FEE, treasuryBalance); + assertEq(1, backerNftBalance); } - /** - * @notice Tests the getReward functionality - * @dev Verifies that reward details can be properly retrieved from the treasury contract. - * Tests retrieval of all reward properties including values, tier flags, item arrays, - * and validates that non-existent rewards cause appropriate reverts. - */ - function test_getReward() external { - addRewards( - users.creator1Address, + function test_claimRefund() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); + + (, uint256 rewardTokenId,) = pledgeForAReward( + users.backer1Address, + address(testToken), address(allOrNothing), - REWARD_NAMES, - REWARDS + PLEDGE_AMOUNT, + SHIPPING_FEE, + LAUNCH_TIME, + REWARD_NAME_1_HASH ); - // Test getting each reward - for (uint i = 0; i < REWARD_NAMES.length; i++) { - Reward memory retrievedReward = allOrNothing.getReward(REWARD_NAMES[i]); - - assertEq(retrievedReward.rewardValue, REWARDS[i].rewardValue, "Reward value mismatch"); - assertEq(retrievedReward.isRewardTier, REWARDS[i].isRewardTier, "Reward tier flag mismatch"); - assertEq(retrievedReward.itemId.length, REWARDS[i].itemId.length, "Item ID array length mismatch"); - assertEq(retrievedReward.itemValue.length, REWARDS[i].itemValue.length, "Item value array length mismatch"); - assertEq(retrievedReward.itemQuantity.length, REWARDS[i].itemQuantity.length, "Item quantity array length mismatch"); - - // Check array contents - for (uint j = 0; j < retrievedReward.itemId.length; j++) { - assertEq(retrievedReward.itemId[j], REWARDS[i].itemId[j], "Item ID mismatch"); - assertEq(retrievedReward.itemValue[j], REWARDS[i].itemValue[j], "Item value mismatch"); - assertEq(retrievedReward.itemQuantity[j], REWARDS[i].itemQuantity[j], "Item quantity mismatch"); - } - } - - // Test getting non-existent reward (should revert) - vm.expectRevert(); - allOrNothing.getReward(keccak256("NonExistentReward")); - } - - /** - * @notice Tests the getRaisedAmount functionality - * @dev Verifies that the total raised amount is correctly tracked and returned. - * Tests progression from zero to multiple pledges to ensure accurate accumulation. - * Note that raised amount only tracks pledge amounts, not shipping fees. - */ - function test_getRaisedAmount() external { - addRewards( - users.creator1Address, - address(allOrNothing), - REWARD_NAMES, - REWARDS + (, uint256 tokenId) = pledgeWithoutAReward( + users.backer1Address, address(testToken), address(allOrNothing), PLEDGE_AMOUNT, LAUNCH_TIME ); - // Initially should be zero - uint256 initialRaised = allOrNothing.getRaisedAmount(); - assertEq(initialRaised, 0, "Initial raised amount should be zero"); + (Vm.Log[] memory refundLogs, uint256 refundedTokenId, uint256 refundAmount, address claimer) = + claimRefund(users.backer1Address, address(allOrNothing), tokenId, LAUNCH_TIME + 1 days); + + assertEq(refundedTokenId, tokenId); + assertEq(refundAmount, PLEDGE_AMOUNT); + assertEq(claimer, users.backer1Address); + } + + function test_disburseFees() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); - // Make a pledge and check raised amount pledgeForAReward( users.backer1Address, - LAUNCH_TIME, + address(testToken), address(allOrNothing), PLEDGE_AMOUNT, SHIPPING_FEE, + LAUNCH_TIME, REWARD_NAME_1_HASH ); + pledgeWithoutAReward(users.backer2Address, address(testToken), address(allOrNothing), GOAL_AMOUNT, LAUNCH_TIME); - uint256 raisedAfterFirstPledge = allOrNothing.getRaisedAmount(); - assertEq(raisedAfterFirstPledge, PLEDGE_AMOUNT, "Raised amount should equal first pledge amount"); + uint256 totalPledged = GOAL_AMOUNT + PLEDGE_AMOUNT; - // Make another pledge and check raised amount - pledgeWithoutAReward( - users.backer2Address, - LAUNCH_TIME, - address(allOrNothing), - GOAL_AMOUNT - ); + (Vm.Log[] memory logs, uint256 protocolShare, uint256 platformShare) = + disburseFees(address(allOrNothing), DEADLINE + 1 days); - uint256 finalRaised = allOrNothing.getRaisedAmount(); - assertEq(finalRaised, PLEDGE_AMOUNT + GOAL_AMOUNT, "Raised amount should equal sum of all pledges"); + uint256 expectedProtocolShare = (totalPledged * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedPlatformShare = (totalPledged * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + + assertEq(protocolShare, expectedProtocolShare, "Incorrect protocol fee"); + assertEq(platformShare, expectedPlatformShare, "Incorrect platform fee"); } - /** - * @notice Tests the pauseTreasury functionality - * @dev Verifies that the treasury can be paused by platform admin and that the paused - * state is correctly set. Validates that the Paused event is emitted from the - * correct contract when the pause operation is executed. - */ - function test_pauseTreasury() external { - bytes32 pauseReason = keccak256("Test pause"); - - assertFalse(allOrNothing.paused(), "Treasury should not be paused initially"); - - Vm.Log[] memory logs = pauseTreasury( - users.platform1AdminAddress, + function test_withdraw() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); + + pledgeForAReward( + users.backer1Address, + address(testToken), address(allOrNothing), - pauseReason + PLEDGE_AMOUNT, + SHIPPING_FEE, + LAUNCH_TIME, + REWARD_NAME_1_HASH ); + pledgeWithoutAReward(users.backer2Address, address(testToken), address(allOrNothing), GOAL_AMOUNT, LAUNCH_TIME); - assertTrue(allOrNothing.paused(), "Treasury should be paused"); - - // Use LogDecoder to find and verify the Paused event - Vm.Log memory pausedLog = findLogByTopic( - logs, - keccak256("Paused(address,bytes32)") - ); - - assertEq(pausedLog.emitter, address(allOrNothing), "Event should be emitted by allOrNothing contract"); - } + uint256 totalPledged = GOAL_AMOUNT + PLEDGE_AMOUNT; + disburseFees(address(allOrNothing), DEADLINE + 1 days); - /** - * @notice Tests the unpauseTreasury functionality - * @dev Verifies that the treasury can be unpaused by platform admin after being paused. - * Ensures proper state transition from paused to unpaused and validates that the - * Unpaused event is correctly emitted from the treasury contract. - */ - function test_unpauseTreasury() external { - bytes32 pauseReason = keccak256("Test pause"); - bytes32 unpauseReason = keccak256("Test unpause"); - - pauseTreasury(users.platform1AdminAddress, address(allOrNothing), pauseReason); - assertTrue(allOrNothing.paused(), "Treasury should be paused"); - - Vm.Log[] memory logs = unpauseTreasury( - users.platform1AdminAddress, - address(allOrNothing), - unpauseReason - ); + (Vm.Log[] memory logs, address to, uint256 amount) = withdraw(address(allOrNothing), DEADLINE); - assertFalse(allOrNothing.paused(), "Treasury should not be paused"); + uint256 protocolShare = (totalPledged * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 platformShare = (totalPledged * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedAmount = totalPledged + SHIPPING_FEE - protocolShare - platformShare; - // Use LogDecoder to find and verify the Unpaused event - Vm.Log memory unpausedLog = findLogByTopic( - logs, - keccak256("Unpaused(address,bytes32)") - ); - - assertEq(unpausedLog.emitter, address(allOrNothing), "Event should be emitted by allOrNothing contract"); + assertEq(to, users.creator1Address, "Incorrect address receiving the funds"); + assertEq(amount, expectedAmount, "Incorrect withdrawal amount"); } - /** - * @notice Tests the cancelTreasury functionality by platform admin - * @dev Verifies that the treasury can be cancelled by platform admin and that the cancelled - * state is permanently set. Validates that the Cancelled event is emitted correctly - * and that cancellation is an irreversible operation. - */ - function test_cancelTreasury() external { - bytes32 cancelReason = keccak256("Test cancellation"); - - assertFalse(allOrNothing.cancelled(), "Treasury should not be cancelled initially"); - - Vm.Log[] memory logs = cancelTreasury( - users.platform1AdminAddress, - address(allOrNothing), - cancelReason - ); - - assertTrue(allOrNothing.cancelled(), "Treasury should be cancelled"); + /*////////////////////////////////////////////////////////////// + MULTI-TOKEN FUNCTIONALITY TESTS + //////////////////////////////////////////////////////////////*/ - // Use LogDecoder to find and verify the Cancelled event - Vm.Log memory cancelledLog = findLogByTopic( - logs, - keccak256("Cancelled(address,bytes32)") - ); - - assertEq(cancelledLog.emitter, address(allOrNothing), "Event should be emitted by allOrNothing contract"); - } + function test_pledgeWithMultipleTokens() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); - /** - * @notice Tests cancelTreasury functionality by campaign owner - * @dev Verifies that the campaign owner can also cancel the treasury, demonstrating - * the dual authorization model where both platform admin and campaign owner - * have cancellation privileges. Validates proper event emission and state change. - */ - function test_cancelTreasuryByCampaignOwner() external { - bytes32 cancelReason = keccak256("Owner cancellation"); - - assertFalse(allOrNothing.cancelled(), "Treasury should not be cancelled initially"); - - // Cancel the treasury as campaign owner using helper function - Vm.Log[] memory logs = cancelTreasury( - users.creator1Address, - address(allOrNothing), - cancelReason - ); + // Pledge with USDC (6 decimals) + uint256 usdcPledgeAmount = getTokenAmount(address(usdcToken), PLEDGE_AMOUNT); + uint256 usdcShippingFee = getTokenAmount(address(usdcToken), SHIPPING_FEE); - // Verify treasury is cancelled - assertTrue(allOrNothing.cancelled(), "Treasury should be cancelled by owner"); + vm.startPrank(users.backer1Address); + usdcToken.approve(address(allOrNothing), usdcPledgeAmount + usdcShippingFee); + vm.warp(LAUNCH_TIME); - // Use LogDecoder to find and verify the Cancelled event - Vm.Log memory cancelledLog = findLogByTopic( - logs, - keccak256("Cancelled(address,bytes32)") - ); - - assertEq(cancelledLog.emitter, address(allOrNothing), "Event should be emitted by allOrNothing contract"); - } + bytes32[] memory reward1 = new bytes32[](1); + reward1[0] = REWARD_NAME_1_HASH; + allOrNothing.pledgeForAReward(users.backer1Address, address(usdcToken), usdcShippingFee, reward1); + vm.stopPrank(); - /** - * @notice Tests the name functionality - * @dev Verifies that the contract name is correctly returned and matches the value - * that was set during contract initialization. Tests the ERC721 metadata extension. - */ - function test_name() external { - string memory contractName = allOrNothing.name(); - assertEq(contractName, NAME, "Contract name should match initialized name"); - } + // Pledge with cUSD (18 decimals) - no conversion needed + vm.startPrank(users.backer2Address); + cUSDToken.approve(address(allOrNothing), PLEDGE_AMOUNT); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT); + vm.stopPrank(); - /** - * @notice Tests the symbol functionality - * @dev Verifies that the contract symbol is correctly returned and matches the value - * that was set during contract initialization. Tests the ERC721 metadata extension. - */ - function test_symbol() external { - string memory contractSymbol = allOrNothing.symbol(); - assertEq(contractSymbol, SYMBOL, "Contract symbol should match initialized symbol"); - } + // Verify balances + assertEq(usdcToken.balanceOf(address(allOrNothing)), usdcPledgeAmount + usdcShippingFee); + assertEq(cUSDToken.balanceOf(address(allOrNothing)), PLEDGE_AMOUNT); - /** - * @notice Tests the getPlatformHash functionality - * @dev Verifies that the platform hash is correctly returned and matches the value - * that was set during contract initialization. This hash identifies which platform - * the treasury belongs to. - */ - function test_getPlatformHash() external { - bytes32 platformHash = allOrNothing.getPlatformHash(); - assertEq(platformHash, PLATFORM_1_HASH, "Platform hash should match initialized value"); + // Verify normalized raised amount + uint256 totalRaised = allOrNothing.getRaisedAmount(); + assertEq(totalRaised, PLEDGE_AMOUNT * 2, "Total raised should be sum of normalized amounts"); } - /** - * @notice Tests the getPlatformFeePercent functionality - * @dev Verifies that the platform fee percentage is correctly returned and matches - * the value that was set during contract initialization. This percentage determines - * the platform's share of successful campaign funds. - */ - function test_getPlatformFeePercent() external { - uint256 platformFeePercent = allOrNothing.getPlatformFeePercent(); - assertEq(platformFeePercent, PLATFORM_FEE_PERCENT, "Platform fee percent should match initialized value"); - } + function test_getRaisedAmountNormalizesCorrectly() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); - /** - * @notice Tests the pledgeForAReward functionality - * @dev Verifies that users can pledge for specific rewards, including proper token transfers, - * NFT minting, and balance updates. Confirms that the backer receives an NFT representing - * their pledge and that funds (pledge amount + shipping fee) are correctly transferred - * to the treasury. Tests the complete reward-based pledging workflow. - */ - function test_pledgeForAReward() external { - addRewards( - users.creator1Address, - address(allOrNothing), - REWARD_NAMES, - REWARDS - ); + // Pledge same base amount in different tokens + uint256 baseAmount = 1000e18; - ( - Vm.Log[] memory logs, - uint256 tokenId, - bytes32[] memory rewards - ) = pledgeForAReward( - users.backer1Address, - LAUNCH_TIME, - address(allOrNothing), - PLEDGE_AMOUNT, - SHIPPING_FEE, - REWARD_NAME_1_HASH - ); + // USDC pledge (6 decimals) + uint256 usdcAmount = baseAmount / 1e12; + vm.startPrank(users.backer1Address); + usdcToken.approve(address(allOrNothing), usdcAmount); + vm.warp(LAUNCH_TIME); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + vm.stopPrank(); - uint256 treasuryBalance = testToken.balanceOf(address(allOrNothing)); - uint256 backerNftBalance = allOrNothing.balanceOf(users.backer1Address); - address nftOwnerAddress = allOrNothing.ownerOf(tokenId); + uint256 raisedAfterUSDC = allOrNothing.getRaisedAmount(); + assertEq(raisedAfterUSDC, baseAmount, "USDC amount should be normalized to 18 decimals"); - // Verify Receipt event was emitted with correct data - Vm.Log memory receiptLog = findLogByTopic( - logs, - keccak256("Receipt(address,bytes32,uint256,uint256,uint256,bytes32[])") - ); - assertEq(receiptLog.emitter, address(allOrNothing), "Receipt event should be emitted by allOrNothing contract"); + // cUSD pledge (18 decimals) + vm.startPrank(users.backer2Address); + cUSDToken.approve(address(allOrNothing), baseAmount); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), baseAmount); + vm.stopPrank(); - // Verify state changes - assertEq(users.backer1Address, nftOwnerAddress, "Backer should own the NFT"); - assertEq(PLEDGE_AMOUNT + SHIPPING_FEE, treasuryBalance, "Treasury should contain pledge amount + shipping fee"); - assertEq(1, backerNftBalance, "Backer should have exactly 1 NFT"); - assertEq(rewards[0], REWARD_NAME_1_HASH, "Reward name should match"); + uint256 raisedAfterCUSD = allOrNothing.getRaisedAmount(); + assertEq(raisedAfterCUSD, baseAmount * 2, "Total should be sum of normalized amounts"); } - /** - * @notice Tests the pledgeWithoutAReward functionality - * @dev Verifies that users can make pledges without selecting rewards, including proper - * token transfers, NFT minting, and balance updates. Confirms that the backer receives - * an NFT representing their pledge and that only the pledge amount is transferred - * (no shipping fees since no rewards are selected). Tests the basic pledging workflow. - */ - function test_pledgeWithoutAReward() external { - // Get initial balances - uint256 initialBackerBalance = testToken.balanceOf(users.backer1Address); - uint256 initialTreasuryBalance = testToken.balanceOf(address(allOrNothing)); - uint256 initialBackerNftBalance = allOrNothing.balanceOf(users.backer1Address); - - // Make a pledge without reward - (, uint256 tokenId) = pledgeWithoutAReward( - users.backer1Address, - LAUNCH_TIME, - address(allOrNothing), - PLEDGE_AMOUNT - ); + function test_disburseFeesWithMultipleTokens() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); + + // Pledge with USDC + uint256 usdcAmount = getTokenAmount(address(usdcToken), PLEDGE_AMOUNT); + vm.startPrank(users.backer1Address); + usdcToken.approve(address(allOrNothing), usdcAmount); + vm.warp(LAUNCH_TIME); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + vm.stopPrank(); + + // Pledge with cUSD to meet goal + vm.startPrank(users.backer2Address); + cUSDToken.approve(address(allOrNothing), GOAL_AMOUNT); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), GOAL_AMOUNT); + vm.stopPrank(); + + uint256 protocolBalanceUSDCBefore = usdcToken.balanceOf(users.protocolAdminAddress); + uint256 platformBalanceUSDCBefore = usdcToken.balanceOf(users.platform1AdminAddress); + uint256 protocolBalanceCUSDBefore = cUSDToken.balanceOf(users.protocolAdminAddress); + uint256 platformBalanceCUSDBefore = cUSDToken.balanceOf(users.platform1AdminAddress); + + // Disburse fees + vm.warp(DEADLINE + 1 days); + allOrNothing.disburseFees(); - // Get final balances - uint256 finalBackerBalance = testToken.balanceOf(users.backer1Address); - uint256 finalTreasuryBalance = testToken.balanceOf(address(allOrNothing)); - uint256 finalBackerNftBalance = allOrNothing.balanceOf(users.backer1Address); - address nftOwnerAddress = allOrNothing.ownerOf(tokenId); + // Verify USDC fees + uint256 expectedUSDCProtocolFee = (usdcAmount * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedUSDCPlatformFee = (usdcAmount * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; - // Verify token transfers assertEq( - initialBackerBalance - finalBackerBalance, - PLEDGE_AMOUNT, - "Incorrect amount deducted from backer" + usdcToken.balanceOf(users.protocolAdminAddress) - protocolBalanceUSDCBefore, + expectedUSDCProtocolFee, + "Incorrect USDC protocol fee" ); assertEq( - finalTreasuryBalance - initialTreasuryBalance, - PLEDGE_AMOUNT, - "Incorrect amount transferred to treasury" + usdcToken.balanceOf(users.platform1AdminAddress) - platformBalanceUSDCBefore, + expectedUSDCPlatformFee, + "Incorrect USDC platform fee" ); - - // Verify NFT minting + + // Verify cUSD fees + uint256 expectedCUSDProtocolFee = (GOAL_AMOUNT * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedCUSDPlatformFee = (GOAL_AMOUNT * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + assertEq( - finalBackerNftBalance - initialBackerNftBalance, - 1, - "Backer should receive exactly one NFT" + cUSDToken.balanceOf(users.protocolAdminAddress) - protocolBalanceCUSDBefore, + expectedCUSDProtocolFee, + "Incorrect cUSD protocol fee" ); assertEq( - nftOwnerAddress, - users.backer1Address, - "Backer should own the minted NFT" + cUSDToken.balanceOf(users.platform1AdminAddress) - platformBalanceCUSDBefore, + expectedCUSDPlatformFee, + "Incorrect cUSD platform fee" ); + } - // Verify treasury balance matches expected amount (no shipping fees) - assertEq( - finalTreasuryBalance, - PLEDGE_AMOUNT, - "Treasury should only contain the pledge amount" - ); + function test_withdrawWithMultipleTokens() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); + + // Pledge with multiple tokens + uint256 usdcAmount = getTokenAmount(address(usdcToken), PLEDGE_AMOUNT); + uint256 usdtAmount = getTokenAmount(address(usdtToken), PLEDGE_AMOUNT); + + vm.startPrank(users.backer1Address); + usdcToken.approve(address(allOrNothing), usdcAmount); + vm.warp(LAUNCH_TIME); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + vm.stopPrank(); + + vm.startPrank(users.backer2Address); + usdtToken.approve(address(allOrNothing), usdtAmount); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(usdtToken), usdtAmount); + vm.stopPrank(); + + // Need cUSD pledge to meet goal + vm.startPrank(users.backer1Address); + cUSDToken.approve(address(allOrNothing), GOAL_AMOUNT); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(cUSDToken), GOAL_AMOUNT); + vm.stopPrank(); + + // Disburse fees and withdraw + vm.warp(DEADLINE + 1 days); + allOrNothing.disburseFees(); + + uint256 creatorUSDCBefore = usdcToken.balanceOf(users.creator1Address); + uint256 creatorUSDTBefore = usdtToken.balanceOf(users.creator1Address); + uint256 creatorCUSDBefore = cUSDToken.balanceOf(users.creator1Address); + + allOrNothing.withdraw(); + + // Verify all tokens were withdrawn + assertTrue(usdcToken.balanceOf(users.creator1Address) > creatorUSDCBefore, "Creator should receive USDC"); + assertTrue(usdtToken.balanceOf(users.creator1Address) > creatorUSDTBefore, "Creator should receive USDT"); + assertTrue(cUSDToken.balanceOf(users.creator1Address) > creatorCUSDBefore, "Creator should receive cUSD"); + + // Verify treasury is empty + assertEq(usdcToken.balanceOf(address(allOrNothing)), 0, "USDC should be fully withdrawn"); + assertEq(usdtToken.balanceOf(address(allOrNothing)), 0, "USDT should be fully withdrawn"); + assertEq(cUSDToken.balanceOf(address(allOrNothing)), 0, "cUSD should be fully withdrawn"); } - /** - * @notice Tests the claimRefund functionality for both reward and non-reward pledges - * @dev Verifies that backers can claim refunds when campaigns fail to meet their goals. - * Tests both reward pledges (with shipping fees) and non-reward pledges, ensuring - * proper refund amounts and that the correct addresses receive refunds for both types. - * Validates that refunds include shipping fees for reward pledges and that NFTs are - * burned upon successful refund claims. - */ - function test_claimRefund() external { - addRewards( - users.creator1Address, - address(allOrNothing), - REWARD_NAMES, - REWARDS - ); + function test_refundWithCorrectToken() external { + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); - // Create a pledge with reward - (, uint256 rewardTokenId, ) = pledgeForAReward( - users.backer1Address, - LAUNCH_TIME, - address(allOrNothing), - PLEDGE_AMOUNT, - SHIPPING_FEE, - REWARD_NAME_1_HASH - ); + // Backer1 pledges with USDC + uint256 usdcAmount = getTokenAmount(address(usdcToken), PLEDGE_AMOUNT); + vm.startPrank(users.backer1Address); + usdcToken.approve(address(allOrNothing), usdcAmount); + vm.warp(LAUNCH_TIME); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(usdcToken), usdcAmount); + uint256 usdcTokenId = 1; // First pledge + vm.stopPrank(); - // Create a pledge without reward - (, uint256 nonRewardTokenId) = pledgeWithoutAReward( - users.backer1Address, - LAUNCH_TIME, - address(allOrNothing), - PLEDGE_AMOUNT - ); + // Backer2 pledges with cUSD + vm.startPrank(users.backer2Address); + cUSDToken.approve(address(allOrNothing), PLEDGE_AMOUNT); + allOrNothing.pledgeWithoutAReward(users.backer2Address, address(cUSDToken), PLEDGE_AMOUNT); + uint256 cUSDTokenId = 2; // Second pledge + vm.stopPrank(); - // Test refund for pledge without reward - ( - , - uint256 refundedNonRewardTokenId, - uint256 nonRewardRefundAmount, - address nonRewardClaimer - ) = claimRefund( - users.backer1Address, - LAUNCH_TIME + 1, - address(allOrNothing), - nonRewardTokenId - ); - - // Verify non-reward refund - assertEq(refundedNonRewardTokenId, nonRewardTokenId, "Incorrect non-reward token ID refunded"); - assertEq(nonRewardRefundAmount, PLEDGE_AMOUNT, "Incorrect non-reward refund amount"); - assertEq(nonRewardClaimer, users.backer1Address, "Incorrect non-reward claimer address"); - - // Test refund for pledge with reward - ( - , - uint256 refundedRewardTokenId, - uint256 rewardRefundAmount, - address rewardClaimer - ) = claimRefund( - users.backer1Address, - LAUNCH_TIME + 1, - address(allOrNothing), - rewardTokenId - ); - - // Verify reward refund (should include pledge amount + shipping fee) - assertEq(refundedRewardTokenId, rewardTokenId, "Incorrect reward token ID refunded"); - assertEq(rewardRefundAmount, PLEDGE_AMOUNT + SHIPPING_FEE, "Incorrect reward refund amount"); - assertEq(rewardClaimer, users.backer1Address, "Incorrect reward claimer address"); - } + uint256 backer1USDCBefore = usdcToken.balanceOf(users.backer1Address); + uint256 backer2CUSDBefore = cUSDToken.balanceOf(users.backer2Address); - /** - * @notice Tests the disburseFees functionality - * @dev Verifies that protocol and platform fees are correctly calculated and distributed - * when a campaign succeeds. Tests the fee calculation logic and ensures proper - * allocation between protocol and platform shares. Only executes after campaign - * deadline and when success conditions are met. - */ - function test_disburseFees() external { - addRewards( - users.creator1Address, - address(allOrNothing), - REWARD_NAMES, - REWARDS - ); + // Claim refunds + vm.warp(LAUNCH_TIME + 1 days); - pledgeForAReward( - users.backer1Address, - LAUNCH_TIME, - address(allOrNothing), - PLEDGE_AMOUNT, - SHIPPING_FEE, - REWARD_NAME_1_HASH - ); - pledgeWithoutAReward( - users.backer2Address, - LAUNCH_TIME, - address(allOrNothing), - GOAL_AMOUNT - ); + // Approve treasury to burn NFTs + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(allOrNothing), usdcTokenId); - uint256 totalPledged = GOAL_AMOUNT + PLEDGE_AMOUNT; + vm.prank(users.backer1Address); + allOrNothing.claimRefund(usdcTokenId); - ( - Vm.Log[] memory logs, - uint256 protocolShare, - uint256 platformShare - ) = disburseFees(address(allOrNothing), DEADLINE + 1); - - uint256 expectedProtocolShare = (totalPledged * PROTOCOL_FEE_PERCENT) / - PERCENT_DIVIDER; - uint256 expectedPlatformShare = (totalPledged * PLATFORM_FEE_PERCENT) / - PERCENT_DIVIDER; - - // Verify FeesDisbursed event was emitted - Vm.Log memory feesLog = findLogByTopic( - logs, - keccak256("FeesDisbursed(uint256,uint256)") - ); - assertEq(feesLog.emitter, address(allOrNothing), "FeesDisbursed event should be emitted by allOrNothing contract"); + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(allOrNothing), cUSDTokenId); - assertEq( - protocolShare, - expectedProtocolShare, - "Incorrect protocol fee" - ); - assertEq( - platformShare, - expectedPlatformShare, - "Incorrect platform fee" - ); - } + vm.prank(users.backer2Address); + allOrNothing.claimRefund(cUSDTokenId); - /** - * @notice Tests the withdraw functionality - * @dev Verifies that campaign creators can withdraw remaining funds after successful - * campaigns and fee disbursement. Tests proper calculation of withdrawal amounts - * after deducting protocol and platform fees, and confirms funds go to the correct - * recipient (campaign owner). Includes shipping fees in the final withdrawal amount. - */ - function test_withdraw() external { - addRewards( - users.creator1Address, - address(allOrNothing), - REWARD_NAMES, - REWARDS - ); + // Verify refunds in correct tokens + assertEq(usdcToken.balanceOf(users.backer1Address) - backer1USDCBefore, usdcAmount, "Should refund in USDC"); + assertEq(cUSDToken.balanceOf(users.backer2Address) - backer2CUSDBefore, PLEDGE_AMOUNT, "Should refund in cUSD"); - pledgeForAReward( - users.backer1Address, - LAUNCH_TIME, - address(allOrNothing), - PLEDGE_AMOUNT, - SHIPPING_FEE, - REWARD_NAME_1_HASH - ); - pledgeWithoutAReward( - users.backer2Address, - LAUNCH_TIME, - address(allOrNothing), - GOAL_AMOUNT - ); + // Verify no cross-token refunds + assertEq(cUSDToken.balanceOf(users.backer1Address), TOKEN_MINT_AMOUNT, "Should not receive cUSD"); + assertEq(usdcToken.balanceOf(users.backer2Address), TOKEN_MINT_AMOUNT / 1e12, "Should not receive USDC"); + } - uint256 totalPledged = GOAL_AMOUNT + PLEDGE_AMOUNT; - disburseFees(address(allOrNothing), DEADLINE + 1); + function test_revertWhenPledgingWithUnacceptedToken() external { + // Create a token not in the accepted list + TestToken unacceptedToken = new TestToken("Unaccepted", "UNA", 18); + unacceptedToken.mint(users.backer1Address, PLEDGE_AMOUNT); - (Vm.Log[] memory logs, address to, uint256 amount) = withdraw( - address(allOrNothing), - DEADLINE - ); + addRewards(users.creator1Address, address(allOrNothing), REWARD_NAMES, REWARDS); - uint256 protocolShare = (totalPledged * PROTOCOL_FEE_PERCENT) / - PERCENT_DIVIDER; - uint256 platformShare = (totalPledged * PLATFORM_FEE_PERCENT) / - PERCENT_DIVIDER; - uint256 expectedAmount = totalPledged + - SHIPPING_FEE - - protocolShare - - platformShare; - - // Verify WithdrawalSuccessful event was emitted - Vm.Log memory withdrawalLog = findLogByTopic( - logs, - keccak256("WithdrawalSuccessful(address,uint256)") - ); - assertEq(withdrawalLog.emitter, address(allOrNothing), "WithdrawalSuccessful event should be emitted by allOrNothing contract"); + vm.startPrank(users.backer1Address); + unacceptedToken.approve(address(allOrNothing), PLEDGE_AMOUNT); + vm.warp(LAUNCH_TIME); - assertEq( - to, - users.creator1Address, - "Incorrect address receiving the funds" + vm.expectRevert( + abi.encodeWithSelector(AllOrNothing.AllOrNothingTokenNotAccepted.selector, address(unacceptedToken)) ); - assertEq(amount, expectedAmount, "Incorrect withdrawal amount"); + allOrNothing.pledgeWithoutAReward(users.backer1Address, address(unacceptedToken), PLEDGE_AMOUNT); + vm.stopPrank(); } -} \ No newline at end of file +} diff --git a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol new file mode 100644 index 00000000..02bdcd9b --- /dev/null +++ b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaised.t.sol @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {IReward} from "src/interfaces/IReward.sol"; +import {ICampaignData} from "src/interfaces/ICampaignData.sol"; +import {LogDecoder} from "../../utils/LogDecoder.sol"; +import {Base_Test} from "../../Base.t.sol"; + +/// @notice Common testing logic needed by all KeepWhatsRaised integration tests. +abstract contract KeepWhatsRaised_Integration_Shared_Test is IReward, LogDecoder, Base_Test { + address campaignAddress; + address treasuryAddress; + KeepWhatsRaised internal keepWhatsRaised; + + uint256 pledgeForARewardTokenId; + + /// @dev Initial dependent functions setup included for KeepWhatsRaised Integration Tests. + function setUp() public virtual override { + super.setUp(); + console.log("setUp: enlistPlatform"); + + // Enlist Platform + enlistPlatform(PLATFORM_2_HASH); + console.log("enlisted platform"); + + registerTreasuryImplementation(PLATFORM_2_HASH); + console.log("registered treasury"); + + approveTreasuryImplementation(PLATFORM_2_HASH); + console.log("approved treasury"); + + // Create Campaign + createCampaign(PLATFORM_2_HASH); + console.log("created campaign"); + + // Deploy Treasury Contract + deploy(PLATFORM_2_HASH); + console.log("deployed treasury"); + + // Create FeeValues struct + KeepWhatsRaised.FeeValues memory feeValues = KeepWhatsRaised.FeeValues({ + flatFeeValue: uint256(FLAT_FEE_VALUE), + cumulativeFlatFeeValue: uint256(CUMULATIVE_FLAT_FEE_VALUE), + grossPercentageFeeValues: new uint256[](2) + }); + feeValues.grossPercentageFeeValues[0] = uint256(PLATFORM_FEE_VALUE); + feeValues.grossPercentageFeeValues[1] = uint256(VAKI_COMMISSION_VALUE); + + // Configure Treasury with fee values + configureTreasury(users.platform2AdminAddress, treasuryAddress, CONFIG, CAMPAIGN_DATA, FEE_KEYS, feeValues); + console.log("configured treasury"); + } + + /** + * @notice Implements enlistPlatform helper function. + * @param platformHash The platform bytes. + */ + function enlistPlatform(bytes32 platformHash) internal { + vm.startPrank(users.protocolAdminAddress); + globalParams.enlistPlatform(platformHash, users.platform2AdminAddress, PLATFORM_FEE_PERCENT, address(0)); + vm.stopPrank(); + } + + /** + * @notice Adds platform data keys. + * @param platformHash The platform bytes. + */ + function addPlatformData(bytes32 platformHash) internal { + vm.startPrank(users.platform2AdminAddress); + + // Add platform data keys (flat fees only, percentage fees are in GROSS_PERCENTAGE_FEE_KEYS) + globalParams.addPlatformData(platformHash, FLAT_FEE_KEY); + globalParams.addPlatformData(platformHash, CUMULATIVE_FLAT_FEE_KEY); + + // Add gross percentage fee keys (includes PLATFORM_FEE_KEY and VAKI_COMMISSION_KEY) + for (uint256 i = 0; i < GROSS_PERCENTAGE_FEE_KEYS.length; i++) { + globalParams.addPlatformData(platformHash, GROSS_PERCENTAGE_FEE_KEYS[i]); + } + + vm.stopPrank(); + } + + function registerTreasuryImplementation(bytes32 platformHash) internal { + vm.startPrank(users.platform2AdminAddress); + treasuryFactory.registerTreasuryImplementation(platformHash, 1, address(keepWhatsRaisedImplementation)); + vm.stopPrank(); + } + + function approveTreasuryImplementation(bytes32 platformHash) internal { + vm.startPrank(users.protocolAdminAddress); + treasuryFactory.approveTreasuryImplementation(platformHash, 1); + vm.stopPrank(); + } + + /** + * @notice Implements createCampaign helper function. It creates new campaign info contract + * @param platformHash The platform bytes. + */ + function createCampaign(bytes32 platformHash) internal { + bytes32 identifierHash = keccak256(abi.encodePacked(platformHash)); + bytes32[] memory selectedPlatformHash = new bytes32[](1); + selectedPlatformHash[0] = platformHash; + + // Pass empty arrays since fees are now configured via configureTreasury + 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" + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + vm.stopPrank(); + + (bytes32[] memory topics,) = decodeTopicsAndData( + entries, "CampaignInfoFactoryCampaignCreated(bytes32,address)", address(campaignInfoFactory) + ); + + require(topics.length == 3, "Unexpected topic length for event"); + + campaignAddress = address(uint160(uint256(topics[2]))); + } + + /** + * @notice Implements deploy helper function. It deploys treasury contract. + */ + function deploy(bytes32 platformHash) internal { + vm.startPrank(users.platform2AdminAddress); + vm.recordLogs(); + + // Deploy the treasury contract + treasuryFactory.deploy(platformHash, campaignAddress, 1); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + vm.stopPrank(); + + // Decode the TreasuryDeployed event + (bytes32[] memory topics, bytes memory data) = decodeTopicsAndData( + entries, "TreasuryFactoryTreasuryDeployed(bytes32,uint256,address,address)", address(treasuryFactory) + ); + + require(topics.length >= 3, "Expected indexed params missing"); + + treasuryAddress = abi.decode(data, (address)); + + keepWhatsRaised = KeepWhatsRaised(treasuryAddress); + } + + /** + * @notice Implements configureTreasury helper function. + */ + function configureTreasury( + address caller, + address treasury, + KeepWhatsRaised.Config memory _config, + ICampaignData.CampaignData memory campaignData, + KeepWhatsRaised.FeeKeys memory _feeKeys, + KeepWhatsRaised.FeeValues memory _feeValues + ) internal { + vm.startPrank(caller); + KeepWhatsRaised(treasury).configureTreasury(_config, campaignData, _feeKeys, _feeValues); + vm.stopPrank(); + } + + /** + * @notice Helper function to create FeeValues struct + */ + function createFeeValues() internal pure returns (KeepWhatsRaised.FeeValues memory) { + KeepWhatsRaised.FeeValues memory feeValues; + feeValues.flatFeeValue = uint256(FLAT_FEE_VALUE); + feeValues.cumulativeFlatFeeValue = uint256(CUMULATIVE_FLAT_FEE_VALUE); + feeValues.grossPercentageFeeValues = new uint256[](2); + feeValues.grossPercentageFeeValues[0] = uint256(PLATFORM_FEE_VALUE); + feeValues.grossPercentageFeeValues[1] = uint256(VAKI_COMMISSION_VALUE); + return feeValues; + } + + /** + * @notice Approves withdrawal for the treasury. + */ + function approveWithdrawal(address caller, address treasury) internal { + vm.startPrank(caller); + KeepWhatsRaised(treasury).approveWithdrawal(); + vm.stopPrank(); + } + + /** + * @notice Updates the deadline of the campaign. + */ + function updateDeadline(address caller, address treasury, uint256 newDeadline) internal { + vm.startPrank(caller); + KeepWhatsRaised(treasury).updateDeadline(newDeadline); + vm.stopPrank(); + } + + /** + * @notice Updates the goal amount of the campaign. + */ + function updateGoalAmount(address caller, address treasury, uint256 newGoalAmount) internal { + vm.startPrank(caller); + KeepWhatsRaised(treasury).updateGoalAmount(newGoalAmount); + vm.stopPrank(); + } + + /** + * @notice Adds rewards to the campaign. + */ + function addRewards(address caller, address treasury, bytes32[] memory rewardNames, Reward[] memory rewards) + internal + { + vm.startPrank(caller); + KeepWhatsRaised(treasury).addRewards(rewardNames, rewards); + vm.stopPrank(); + } + + /** + * @notice Removes a reward from the campaign. + */ + function removeReward(address caller, address treasury, bytes32 rewardName) internal { + vm.startPrank(caller); + KeepWhatsRaised(treasury).removeReward(rewardName); + vm.stopPrank(); + } + + /** + * @notice Sets payment gateway fee for a pledge. + */ + function setPaymentGatewayFee(address caller, address treasury, bytes32 pledgeId, uint256 fee) internal { + vm.startPrank(caller); + KeepWhatsRaised(treasury).setPaymentGatewayFee(pledgeId, fee); + vm.stopPrank(); + } + + /** + * @notice Implements setFeeAndPledge helper function. + */ + function setFeeAndPledge( + address caller, + address treasury, + bytes32 pledgeId, + address backer, + uint256 pledgeAmount, + uint256 tip, + uint256 fee, + bytes32[] memory reward, + bool isPledgeForAReward + ) internal returns (Vm.Log[] memory logs, uint256 tokenId, bytes32[] memory rewards) { + vm.startPrank(caller); + vm.recordLogs(); + + // Approve tokens from admin (caller) since admin will be the token source + if (isPledgeForAReward) { + // Calculate total pledge amount from rewards + uint256 totalPledgeAmount = 0; + for (uint256 i = 0; i < reward.length; i++) { + totalPledgeAmount += KeepWhatsRaised(treasury).getReward(reward[i]).rewardValue; + } + testToken.approve(treasury, totalPledgeAmount + tip); + } else { + testToken.approve(treasury, pledgeAmount + tip); + } + + KeepWhatsRaised(treasury).setFeeAndPledge( + pledgeId, backer, address(testToken), pledgeAmount, tip, fee, reward, isPledgeForAReward + ); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "Receipt(address,address,bytes32,uint256,uint256,uint256,bytes32[])", treasury); + + // Indexed params: backer (topics[1]), pledgeToken (topics[2]) + // Data params: reward, pledgeAmount, tip, tokenId, rewards + (,,, tokenId, rewards) = abi.decode(data, (bytes32, uint256, uint256, uint256, bytes32[])); + + vm.stopPrank(); + } + + /** + * @notice Implements pledgeForAReward helper function with tip. + */ + function pledgeForAReward( + address caller, + address token, + address keepWhatsRaisedAddress, + bytes32 pledgeId, + uint256 pledgeAmount, + uint256 tip, + uint256 launchTime, + bytes32 rewardName + ) internal returns (Vm.Log[] memory logs, uint256 tokenId, bytes32[] memory rewards) { + vm.startPrank(caller); + vm.recordLogs(); + + testToken.approve(keepWhatsRaisedAddress, pledgeAmount + tip); + vm.warp(launchTime); + + bytes32[] memory reward = new bytes32[](1); + reward[0] = rewardName; + + KeepWhatsRaised(keepWhatsRaisedAddress).pledgeForAReward(pledgeId, caller, token, tip, reward); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = decodeTopicsAndData( + logs, "Receipt(address,address,bytes32,uint256,uint256,uint256,bytes32[])", keepWhatsRaisedAddress + ); + + // Indexed params: backer (topics[1]), pledgeToken (topics[2]) + // Data params: reward, pledgeAmount, tip, tokenId, rewards + (,,, tokenId, rewards) = abi.decode(data, (bytes32, uint256, uint256, uint256, bytes32[])); + + vm.stopPrank(); + } + + /** + * @notice Implements pledgeWithoutAReward helper function with tip. + */ + function pledgeWithoutAReward( + address caller, + address token, + address keepWhatsRaisedAddress, + bytes32 pledgeId, + uint256 pledgeAmount, + uint256 tip, + uint256 launchTime + ) internal returns (Vm.Log[] memory logs, uint256 tokenId) { + vm.startPrank(caller); + vm.recordLogs(); + + testToken.approve(keepWhatsRaisedAddress, pledgeAmount + tip); + vm.warp(launchTime); + + KeepWhatsRaised(keepWhatsRaisedAddress).pledgeWithoutAReward(pledgeId, caller, token, pledgeAmount, tip); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = decodeTopicsAndData( + logs, "Receipt(address,address,bytes32,uint256,uint256,uint256,bytes32[])", keepWhatsRaisedAddress + ); + + // Indexed params: backer (topics[1]), pledgeToken (topics[2]) + // Data params: reward, pledgeAmount, tip, tokenId, rewards + (,,, tokenId,) = abi.decode(data, (bytes32, uint256, uint256, uint256, bytes32[])); + vm.stopPrank(); + } + + /** + * @notice Implements withdraw helper function with amount parameter. + */ + function withdraw(address caller, address keepWhatsRaisedAddress, uint256 amount, uint256 warpTime) + internal + returns (Vm.Log[] memory logs, address to, uint256 withdrawalAmount, uint256 fee) + { + vm.warp(warpTime); + vm.startPrank(caller); + vm.recordLogs(); + + KeepWhatsRaised(keepWhatsRaisedAddress).withdraw(address(testToken), amount); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "WithdrawalWithFeeSuccessful(address,uint256,uint256)", keepWhatsRaisedAddress); + + to = address(uint160(uint256(topics[1]))); + + (withdrawalAmount, fee) = abi.decode(data, (uint256, uint256)); + + vm.stopPrank(); + } + + /** + * @notice Implements claimRefund helper function. + */ + function claimRefund(address caller, address keepWhatsRaisedAddress, uint256 tokenId) + internal + returns (Vm.Log[] memory logs, uint256 refundedTokenId, uint256 refundAmount, address claimer) + { + vm.startPrank(caller); + + // Approve treasury to burn NFT + CampaignInfo(campaignAddress).approve(keepWhatsRaisedAddress, tokenId); + + vm.recordLogs(); + + KeepWhatsRaised(keepWhatsRaisedAddress).claimRefund(tokenId); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "RefundClaimed(uint256,uint256,address)", keepWhatsRaisedAddress); + + refundedTokenId = uint256(topics[1]); + claimer = address(uint160(uint256(topics[2]))); + + refundAmount = abi.decode(data, (uint256)); + + vm.stopPrank(); + } + + /** + * @notice Implements claimTip helper function. + */ + function claimTip(address caller, address keepWhatsRaisedAddress, uint256 warpTime) + internal + returns (Vm.Log[] memory logs, uint256 amount, address claimer) + { + vm.warp(warpTime); + vm.startPrank(caller); + vm.recordLogs(); + + KeepWhatsRaised(keepWhatsRaisedAddress).claimTip(); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "TipClaimed(uint256,address)", keepWhatsRaisedAddress); + + claimer = address(uint160(uint256(topics[1]))); + amount = abi.decode(data, (uint256)); + + vm.stopPrank(); + } + + /** + * @notice Implements claimFund helper function. + */ + function claimFund(address caller, address keepWhatsRaisedAddress, uint256 warpTime) + internal + returns (Vm.Log[] memory logs, uint256 amount, address claimer) + { + vm.warp(warpTime); + vm.startPrank(caller); + vm.recordLogs(); + + KeepWhatsRaised(keepWhatsRaisedAddress).claimFund(); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "FundClaimed(uint256,address)", keepWhatsRaisedAddress); + + claimer = address(uint160(uint256(topics[1]))); + + amount = abi.decode(data, (uint256)); + + vm.stopPrank(); + } + + /** + * @notice Implements disburseFees helper function. + */ + function disburseFees(address keepWhatsRaisedAddress, uint256 warpTime) + internal + returns (Vm.Log[] memory logs, uint256 protocolShare, uint256 platformShare) + { + vm.warp(warpTime); + vm.recordLogs(); + + KeepWhatsRaised(keepWhatsRaisedAddress).disburseFees(); + + logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "FeesDisbursed(address,uint256,uint256)", keepWhatsRaisedAddress); + + // topics[1] is the indexed token + (protocolShare, platformShare) = abi.decode(data, (uint256, uint256)); + } + + /** + * @notice Helper to cancel treasury. + */ + function cancelTreasury(address caller, address treasury, bytes32 message) internal { + vm.startPrank(caller); + KeepWhatsRaised(treasury).cancelTreasury(message); + vm.stopPrank(); + } +} diff --git a/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaisedFunction.t.sol b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaisedFunction.t.sol new file mode 100644 index 00000000..35cd7b7f --- /dev/null +++ b/test/foundry/integration/KeepWhatsRaised/KeepWhatsRaisedFunction.t.sol @@ -0,0 +1,674 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "./KeepWhatsRaised.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 {IReward} from "src/interfaces/IReward.sol"; +import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; + +contract KeepWhatsRaisedFunction_Integration_Shared_Test is KeepWhatsRaised_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.platform2AdminAddress, 1_000_000e18); + } + + function test_addRewards() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // First reward + Reward memory resultReward1 = keepWhatsRaised.getReward(REWARD_NAMES[0]); + assertEq(REWARDS[0].rewardValue, resultReward1.rewardValue); + assertEq(REWARDS[0].isRewardTier, resultReward1.isRewardTier); + assertEq(REWARDS[0].itemId[0], resultReward1.itemId[0]); + assertEq(REWARDS[0].itemValue[0], resultReward1.itemValue[0]); + assertEq(REWARDS[0].itemQuantity[0], resultReward1.itemQuantity[0]); + + // Second reward + Reward memory resultReward2 = keepWhatsRaised.getReward(REWARD_NAMES[1]); + assertEq(REWARDS[1].rewardValue, resultReward2.rewardValue); + assertEq(REWARDS[1].isRewardTier, resultReward2.isRewardTier); + assertEq(REWARDS[1].itemId.length, resultReward2.itemId.length); + assertEq(REWARDS[1].itemId[0], resultReward2.itemId[0]); + assertEq(REWARDS[1].itemId[1], resultReward2.itemId[1]); + assertEq(REWARDS[1].itemValue[0], resultReward2.itemValue[0]); + assertEq(REWARDS[1].itemValue[1], resultReward2.itemValue[1]); + assertEq(REWARDS[1].itemQuantity[0], resultReward2.itemQuantity[0]); + assertEq(REWARDS[1].itemQuantity[1], resultReward2.itemQuantity[1]); + + // Third reward + Reward memory resultReward3 = keepWhatsRaised.getReward(REWARD_NAMES[2]); + assertEq(REWARDS[2].rewardValue, resultReward3.rewardValue); + assertEq(REWARDS[2].isRewardTier, resultReward3.isRewardTier); + assertEq(REWARDS[2].itemId.length, resultReward3.itemId.length); + } + + function test_setPaymentGatewayFee() external { + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + + uint256 fee = keepWhatsRaised.getPaymentGatewayFee(TEST_PLEDGE_ID_1); + assertEq(fee, PAYMENT_GATEWAY_FEE); + } + + function test_pledgeForARewardWithGatewayFee() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Set gateway fee first + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + + (Vm.Log[] memory logs, uint256 tokenId, bytes32[] memory rewards) = pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + uint256 backerBalance = testToken.balanceOf(users.backer1Address); + uint256 treasuryBalance = testToken.balanceOf(address(keepWhatsRaised)); + uint256 backerNftBalance = CampaignInfo(campaignAddress).balanceOf(users.backer1Address); + address nftOwnerAddress = CampaignInfo(campaignAddress).ownerOf(tokenId); + + assertEq(users.backer1Address, nftOwnerAddress); + assertEq(PLEDGE_AMOUNT + TIP_AMOUNT, treasuryBalance); + assertEq(1, backerNftBalance); + assertEq(keepWhatsRaised.getRaisedAmount(), PLEDGE_AMOUNT); + + // Account for protocol fee being deducted during pledge + assertTrue(keepWhatsRaised.getAvailableRaisedAmount() < PLEDGE_AMOUNT); + } + + function test_pledgeWithoutARewardWithGatewayFee() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Set gateway fee first + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + + (, uint256 tokenId) = pledgeWithoutAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME + ); + + uint256 treasuryBalance = testToken.balanceOf(address(keepWhatsRaised)); + uint256 backerNftBalance = CampaignInfo(campaignAddress).balanceOf(users.backer1Address); + address nftOwnerAddress = CampaignInfo(campaignAddress).ownerOf(tokenId); + + assertEq(users.backer1Address, nftOwnerAddress); + assertEq(PLEDGE_AMOUNT + TIP_AMOUNT, treasuryBalance); + assertEq(1, backerNftBalance); + assertEq(keepWhatsRaised.getRaisedAmount(), PLEDGE_AMOUNT); + + // Account for protocol fee being deducted during pledge + assertTrue(keepWhatsRaised.getAvailableRaisedAmount() < PLEDGE_AMOUNT); + } + + function test_setFeeAndPledgeForReward() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + vm.warp(LAUNCH_TIME); + + bytes32[] memory reward = new bytes32[](1); + reward[0] = REWARD_NAME_1_HASH; + + (Vm.Log[] memory logs, uint256 tokenId, bytes32[] memory rewards) = setFeeAndPledge( + users.platform2AdminAddress, + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + users.backer1Address, + 0, // pledgeAmount is ignored for reward pledges + TIP_AMOUNT, + PAYMENT_GATEWAY_FEE, + reward, + true + ); + + // Verify fee was set + assertEq(keepWhatsRaised.getPaymentGatewayFee(TEST_PLEDGE_ID_1), PAYMENT_GATEWAY_FEE); + + // Verify pledge was made - tokens come from admin not backer + address nftOwnerAddress = CampaignInfo(campaignAddress).ownerOf(tokenId); + assertEq(users.backer1Address, nftOwnerAddress); + } + + function test_setFeeAndPledgeWithoutReward() external { + vm.warp(LAUNCH_TIME); + + bytes32[] memory emptyReward = new bytes32[](0); + + (Vm.Log[] memory logs, uint256 tokenId, bytes32[] memory rewards) = setFeeAndPledge( + users.platform2AdminAddress, + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + users.backer1Address, + PLEDGE_AMOUNT, + TIP_AMOUNT, + PAYMENT_GATEWAY_FEE, + emptyReward, + false + ); + + // Verify fee was set + assertEq(keepWhatsRaised.getPaymentGatewayFee(TEST_PLEDGE_ID_1), PAYMENT_GATEWAY_FEE); + + // Verify pledge was made - tokens come from admin not backer + address nftOwnerAddress = CampaignInfo(campaignAddress).ownerOf(tokenId); + assertEq(users.backer1Address, nftOwnerAddress); + } + + function test_withdrawWithColombianCreatorTax() external { + // Configure with Colombian creator + KeepWhatsRaised.FeeValues memory feeValues = createFeeValues(); + configureTreasury( + users.platform2AdminAddress, address(keepWhatsRaised), CONFIG_COLOMBIAN, CAMPAIGN_DATA, FEE_KEYS, feeValues + ); + + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make pledges with gateway fees + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_2, PAYMENT_GATEWAY_FEE + ); + pledgeWithoutAReward( + users.backer2Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_2, + GOAL_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME + ); + + approveWithdrawal(users.platform2AdminAddress, address(keepWhatsRaised)); + + uint256 totalPledged = GOAL_AMOUNT + PLEDGE_AMOUNT; + address actualOwner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(actualOwner); + + (Vm.Log[] memory logs, address to, uint256 withdrawalAmount, uint256 fee) = + withdraw(users.platform2AdminAddress, address(keepWhatsRaised), 0, DEADLINE + 1 days); + + uint256 ownerBalanceAfter = testToken.balanceOf(actualOwner); + + assertEq(to, actualOwner, "Incorrect address receiving the funds"); + assertTrue(withdrawalAmount < totalPledged, "Withdrawal should be less than total pledged due to fees"); + assertEq(ownerBalanceAfter - ownerBalanceBefore, withdrawalAmount, "Incorrect balance change"); + assertEq(keepWhatsRaised.getAvailableRaisedAmount(), 0, "Available amount should be zero"); + } + + function test_refundWithPaymentFees() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make pledge with gateway fee + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + (, uint256 tokenId,) = pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + uint256 backerBalanceBefore = testToken.balanceOf(users.backer1Address); + + vm.warp(DEADLINE + 1 days); + (Vm.Log[] memory refundLogs, uint256 refundedTokenId, uint256 refundAmount, address claimer) = + claimRefund(users.backer1Address, address(keepWhatsRaised), tokenId); + + uint256 backerBalanceAfter = testToken.balanceOf(users.backer1Address); + + assertEq(refundedTokenId, tokenId); + + // Account for all fees including protocol fee + uint256 platformFee = (PLEDGE_AMOUNT * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 vakiCommission = (PLEDGE_AMOUNT * uint256(VAKI_COMMISSION_VALUE)) / PERCENT_DIVIDER; + uint256 protocolFee = (PLEDGE_AMOUNT * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedRefund = PLEDGE_AMOUNT - PAYMENT_GATEWAY_FEE - platformFee - vakiCommission - protocolFee; + + assertEq(refundAmount, expectedRefund); + assertEq(claimer, users.backer1Address); + assertEq(backerBalanceAfter - backerBalanceBefore, refundAmount); + } + + function test_disburseFees() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make pledges with gateway fees + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_2, PAYMENT_GATEWAY_FEE + ); + pledgeWithoutAReward( + users.backer2Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_2, + PLEDGE_AMOUNT, + 0, + LAUNCH_TIME + ); + + // Approve and withdraw (as platform admin) + approveWithdrawal(users.platform2AdminAddress, address(keepWhatsRaised)); + + vm.warp(DEADLINE - 1 days); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), PLEDGE_AMOUNT); + + uint256 protocolAdminBalanceBefore = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform2AdminAddress); + + (Vm.Log[] memory logs, uint256 protocolShare, uint256 platformShare) = + disburseFees(address(keepWhatsRaised), block.timestamp); + + uint256 protocolAdminBalanceAfter = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformAdminBalanceAfter = testToken.balanceOf(users.platform2AdminAddress); + + assertEq( + protocolAdminBalanceAfter - protocolAdminBalanceBefore, protocolShare, "Incorrect protocol fee disbursed" + ); + assertEq( + platformAdminBalanceAfter - platformAdminBalanceBefore, platformShare, "Incorrect platform fee disbursed" + ); + assertTrue(protocolShare > 0, "Protocol share should be greater than zero"); + assertTrue(platformShare > 0, "Platform share should be greater than zero"); + } + + function test_updateDeadlineByPlatformAdmin() external { + vm.warp(LAUNCH_TIME + 1 days); + + uint256 originalDeadline = keepWhatsRaised.getDeadline(); + uint256 newDeadline = originalDeadline + 14 days; + + updateDeadline(users.platform2AdminAddress, address(keepWhatsRaised), newDeadline); + + assertEq(keepWhatsRaised.getDeadline(), newDeadline); + } + + function test_updateDeadlineByCampaignOwner() external { + vm.warp(LAUNCH_TIME + 1 days); + + uint256 originalDeadline = keepWhatsRaised.getDeadline(); + uint256 newDeadline = originalDeadline + 14 days; + + address campaignOwner = CampaignInfo(campaignAddress).owner(); + updateDeadline(campaignOwner, address(keepWhatsRaised), newDeadline); + + assertEq(keepWhatsRaised.getDeadline(), newDeadline); + } + + function test_updateGoalAmountByPlatformAdmin() external { + vm.warp(LAUNCH_TIME + 1 days); + + uint256 originalGoal = keepWhatsRaised.getGoalAmount(); + uint256 newGoal = originalGoal * 3; + + updateGoalAmount(users.platform2AdminAddress, address(keepWhatsRaised), newGoal); + + assertEq(keepWhatsRaised.getGoalAmount(), newGoal); + } + + function test_updateGoalAmountByCampaignOwner() external { + vm.warp(LAUNCH_TIME + 1 days); + + uint256 originalGoal = keepWhatsRaised.getGoalAmount(); + uint256 newGoal = originalGoal * 3; + + address campaignOwner = CampaignInfo(campaignAddress).owner(); + updateGoalAmount(campaignOwner, address(keepWhatsRaised), newGoal); + + assertEq(keepWhatsRaised.getGoalAmount(), newGoal); + } + + function test_approveWithdrawal() external { + assertFalse(keepWhatsRaised.getWithdrawalApprovalStatus()); + + approveWithdrawal(users.platform2AdminAddress, address(keepWhatsRaised)); + + assertTrue(keepWhatsRaised.getWithdrawalApprovalStatus()); + } + + function test_withdraw() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make pledges + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_2, PAYMENT_GATEWAY_FEE + ); + pledgeWithoutAReward( + users.backer2Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_2, + GOAL_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME + ); + + approveWithdrawal(users.platform2AdminAddress, address(keepWhatsRaised)); + + uint256 totalPledged = GOAL_AMOUNT + PLEDGE_AMOUNT; + address actualOwner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(actualOwner); + + (Vm.Log[] memory logs, address to, uint256 withdrawalAmount, uint256 fee) = + withdraw(users.platform2AdminAddress, address(keepWhatsRaised), 0, DEADLINE + 1 days); + + uint256 ownerBalanceAfter = testToken.balanceOf(actualOwner); + + assertEq(to, actualOwner, "Incorrect address receiving the funds"); + assertTrue(withdrawalAmount < totalPledged, "Should have fees deducted"); + assertEq(ownerBalanceAfter - ownerBalanceBefore, withdrawalAmount, "Incorrect balance change"); + assertEq(keepWhatsRaised.getAvailableRaisedAmount(), 0, "Available amount should be zero"); + } + + function test_withdrawPartial() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeWithoutAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + 0, + LAUNCH_TIME + ); + + // Approve withdrawal + approveWithdrawal(users.platform2AdminAddress, address(keepWhatsRaised)); + + uint256 availableBefore = keepWhatsRaised.getAvailableRaisedAmount(); + + // Calculate safe withdrawal amount that accounts for cumulative fee + // For small withdrawals, cumulative fee (200e18) is applied + // So we need available >= withdrawalAmount + cumulativeFee + uint256 partialAmount = 300e18; // Small amount to ensure we have enough for fees + + (Vm.Log[] memory logs, address to, uint256 withdrawalAmount, uint256 fee) = + withdraw(users.platform2AdminAddress, address(keepWhatsRaised), partialAmount, DEADLINE - 1 days); + + uint256 availableAfter = keepWhatsRaised.getAvailableRaisedAmount(); + + // For partial withdrawals, the full amount requested is transferred + assertEq(withdrawalAmount, partialAmount, "Incorrect partial withdrawal"); + // Available amount is reduced by withdrawal amount plus fees + assertEq( + availableBefore - availableAfter, partialAmount + fee, "Available should be reduced by withdrawal plus fee" + ); + } + + function test_claimTip() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make pledges with tips + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_2, PAYMENT_GATEWAY_FEE + ); + pledgeWithoutAReward( + users.backer2Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_2, + GOAL_AMOUNT, + TIP_AMOUNT * 2, + LAUNCH_TIME + ); + + uint256 totalTips = TIP_AMOUNT + (TIP_AMOUNT * 2); + uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform2AdminAddress); + + // Claim tips after deadline + (Vm.Log[] memory logs, uint256 amount, address claimer) = + claimTip(users.platform2AdminAddress, address(keepWhatsRaised), DEADLINE + 1 days); + + uint256 platformAdminBalanceAfter = testToken.balanceOf(users.platform2AdminAddress); + + assertEq(amount, totalTips, "Incorrect tip amount"); + assertEq(claimer, users.platform2AdminAddress, "Incorrect claimer"); + assertEq(platformAdminBalanceAfter - platformAdminBalanceBefore, totalTips); + } + + function test_claimFund() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make a pledge + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform2AdminAddress); + uint256 expectedAmount = keepWhatsRaised.getAvailableRaisedAmount(); + + // Claim fund after withdrawal delay has passed + (Vm.Log[] memory logs, uint256 amount, address claimer) = + claimFund(users.platform2AdminAddress, address(keepWhatsRaised), DEADLINE + WITHDRAWAL_DELAY + 1 days); + + uint256 platformAdminBalanceAfter = testToken.balanceOf(users.platform2AdminAddress); + + assertEq(amount, expectedAmount, "Incorrect fund amount"); + assertEq(claimer, users.platform2AdminAddress, "Incorrect claimer"); + assertEq(platformAdminBalanceAfter - platformAdminBalanceBefore, expectedAmount); + assertEq(keepWhatsRaised.getAvailableRaisedAmount(), 0); + } + + function test_removeReward() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Verify reward exists before removal + Reward memory rewardBefore = keepWhatsRaised.getReward(REWARD_NAMES[1]); + assertEq(rewardBefore.rewardValue, REWARDS[1].rewardValue); + + // Remove the reward + removeReward(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES[1]); + + // Verify reward is removed + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + keepWhatsRaised.getReward(REWARD_NAMES[1]); + } + + function test_cancelTreasuryByPlatformAdmin() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make a pledge + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + bytes32 cancellationMessage = keccak256(abi.encodePacked("Platform cancellation")); + + // Cancel by platform admin + cancelTreasury(users.platform2AdminAddress, address(keepWhatsRaised), cancellationMessage); + + // Verify campaign is cancelled + vm.startPrank(users.backer2Address); + testToken.approve(address(keepWhatsRaised), PLEDGE_AMOUNT); + vm.expectRevert(); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID_2, users.backer2Address, address(testToken), PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + } + + function test_cancelTreasuryByCampaignOwner() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make a pledge + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + bytes32 cancellationMessage = keccak256(abi.encodePacked("Owner cancellation")); + address campaignOwner = CampaignInfo(campaignAddress).owner(); + + // Cancel by campaign owner + cancelTreasury(campaignOwner, address(keepWhatsRaised), cancellationMessage); + + // Verify campaign is cancelled + vm.startPrank(users.backer2Address); + testToken.approve(address(keepWhatsRaised), PLEDGE_AMOUNT); + vm.expectRevert(); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID_2, users.backer2Address, address(testToken), PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + } + + function test_refundAfterCancellation() external { + addRewards(users.creator1Address, address(keepWhatsRaised), REWARD_NAMES, REWARDS); + + // Make pledge with gateway fee + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID_1, PAYMENT_GATEWAY_FEE + ); + (, uint256 tokenId,) = pledgeForAReward( + users.backer1Address, + address(testToken), + address(keepWhatsRaised), + TEST_PLEDGE_ID_1, + PLEDGE_AMOUNT, + TIP_AMOUNT, + LAUNCH_TIME, + REWARD_NAME_1_HASH + ); + + // Cancel campaign + cancelTreasury(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cancelled")); + + uint256 backerBalanceBefore = testToken.balanceOf(users.backer1Address); + + // Try to claim refund immediately after cancellation + vm.warp(block.timestamp + 1); + (Vm.Log[] memory refundLogs, uint256 refundedTokenId, uint256 refundAmount, address claimer) = + claimRefund(users.backer1Address, address(keepWhatsRaised), tokenId); + + uint256 backerBalanceAfter = testToken.balanceOf(users.backer1Address); + + assertEq(refundedTokenId, tokenId); + + // Account for all fees including protocol fee + uint256 platformFee = (PLEDGE_AMOUNT * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 vakiCommission = (PLEDGE_AMOUNT * uint256(VAKI_COMMISSION_VALUE)) / PERCENT_DIVIDER; + uint256 protocolFee = (PLEDGE_AMOUNT * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedRefund = PLEDGE_AMOUNT - PAYMENT_GATEWAY_FEE - platformFee - vakiCommission - protocolFee; + + assertEq(refundAmount, expectedRefund, "Refund amount should be pledge minus fees"); + assertEq(claimer, users.backer1Address); + assertEq(backerBalanceAfter - backerBalanceBefore, refundAmount); + } +} diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol new file mode 100644 index 00000000..71004aac --- /dev/null +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasury.t.sol @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import {Base_Test} from "../../Base.t.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import {PaymentTreasury} from "src/treasuries/PaymentTreasury.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; +import {LogDecoder} from "../../utils/LogDecoder.sol"; +import {TestToken} from "../../../mocks/TestToken.sol"; + +/// @notice Common testing logic needed by all PaymentTreasury integration tests. +abstract contract PaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Test { + address campaignAddress; + address treasuryAddress; + PaymentTreasury internal paymentTreasury; + + // 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"); + + /// @dev Initial dependent functions setup included for PaymentTreasury 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"); + + // Create Campaign + createCampaign(PLATFORM_1_HASH); + console.log("created campaign"); + + // Deploy Treasury Contract + deploy(PLATFORM_1_HASH); + console.log("deployed treasury"); + } + + /** + * @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 { + PaymentTreasury implementation = new PaymentTreasury(); + vm.startPrank(users.platform1AdminAddress); + treasuryFactory.registerTreasuryImplementation(platformHash, 2, address(implementation)); + vm.stopPrank(); + } + + function approveTreasuryImplementation(bytes32 platformHash) internal { + vm.startPrank(users.protocolAdminAddress); + treasuryFactory.approveTreasuryImplementation(platformHash, 2); + vm.stopPrank(); + } + + /** + * @notice Implements createCampaign helper function. It creates new campaign info contract + * @param platformHash The platform bytes. + */ + function createCampaign(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" + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + vm.stopPrank(); + + (bytes32[] memory topics,) = decodeTopicsAndData( + entries, "CampaignInfoFactoryCampaignCreated(bytes32,address)", address(campaignInfoFactory) + ); + + require(topics.length == 3, "Unexpected topic length for event"); + + campaignAddress = address(uint160(uint256(topics[2]))); + } + + /** + * @notice Implements deploy helper function. It deploys treasury contract. + */ + function deploy(bytes32 platformHash) internal { + vm.startPrank(users.platform1AdminAddress); + vm.recordLogs(); + + // Deploy the treasury contract with implementation ID 2 for PaymentTreasury + treasuryFactory.deploy(platformHash, campaignAddress, 2); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + vm.stopPrank(); + + (bytes32[] memory topics, bytes memory data) = decodeTopicsAndData( + entries, "TreasuryFactoryTreasuryDeployed(bytes32,uint256,address,address)", address(treasuryFactory) + ); + + require(topics.length >= 3, "Expected indexed params missing"); + + treasuryAddress = abi.decode(data, (address)); + paymentTreasury = PaymentTreasury(treasuryAddress); + } + + /** + * @notice Creates a payment + */ + function createPayment( + address caller, + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + address paymentToken, + uint256 amount, + uint256 expiration, + ICampaignPaymentTreasury.LineItem[] memory lineItems, + ICampaignPaymentTreasury.ExternalFees[] memory externalFees + ) internal { + vm.prank(caller); + paymentTreasury.createPayment( + paymentId, buyerId, itemId, paymentToken, amount, expiration, lineItems, externalFees + ); + } + + /** + * @notice Processes a crypto payment + */ + function processCryptoPayment( + address caller, + bytes32 paymentId, + bytes32 itemId, + address buyerAddress, + address paymentToken, + uint256 amount, + ICampaignPaymentTreasury.LineItem[] memory lineItems, + ICampaignPaymentTreasury.ExternalFees[] memory externalFees + ) internal { + vm.prank(caller); + paymentTreasury.processCryptoPayment( + paymentId, itemId, buyerAddress, paymentToken, amount, lineItems, externalFees + ); + } + + /** + * @notice Cancels a payment + */ + function cancelPayment(address caller, bytes32 paymentId) internal { + vm.prank(caller); + paymentTreasury.cancelPayment(paymentId); + } + + /** + * @notice Confirms a payment + */ + function confirmPayment(address caller, bytes32 paymentId) internal { + vm.prank(caller); + paymentTreasury.confirmPayment(paymentId, address(0)); + } + + /** + * @notice Confirms multiple payments in batch + */ + function confirmPaymentBatch(address caller, bytes32[] memory paymentIds) internal { + vm.prank(caller); + paymentTreasury.confirmPaymentBatch(paymentIds, _createZeroAddressArray(paymentIds.length)); + } + + /** + * @notice Claims a refund + */ + function claimRefund(address caller, bytes32 paymentId, address refundAddress) + internal + returns (uint256 refundAmount) + { + vm.startPrank(caller); + vm.recordLogs(); + + paymentTreasury.claimRefund(paymentId, refundAddress); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "RefundClaimed(bytes32,uint256,address)", treasuryAddress); + + refundAmount = abi.decode(data, (uint256)); + + vm.stopPrank(); + } + + /** + * @notice Claims a refund (buyer-initiated) + */ + function claimRefund(address caller, bytes32 paymentId, uint256 tokenId) internal returns (uint256 refundAmount) { + vm.startPrank(caller); + + // Approve treasury to burn NFT + CampaignInfo(campaignAddress).approve(treasuryAddress, tokenId); + + vm.recordLogs(); + + paymentTreasury.claimRefund(paymentId); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "RefundClaimed(bytes32,uint256,address)", treasuryAddress); + + refundAmount = abi.decode(data, (uint256)); + + vm.stopPrank(); + } + + /** + * @notice Disburses fees + */ + function disburseFees(address treasury) internal returns (uint256 protocolShare, uint256 platformShare) { + vm.recordLogs(); + + PaymentTreasury(treasury).disburseFees(); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "FeesDisbursed(address,uint256,uint256)", treasury); + + // topics[1] is the indexed token + // Data contains protocolShare and platformShare + (protocolShare, platformShare) = abi.decode(data, (uint256, uint256)); + } + + /** + * @notice Withdraws funds + */ + function withdraw(address treasury) internal returns (address to, uint256 amount, uint256 fee) { + vm.recordLogs(); + + // Use campaign owner as the authorized caller for withdraw + address campaignOwner = CampaignInfo(campaignAddress).owner(); + vm.prank(campaignOwner); + PaymentTreasury(treasury).withdraw(); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + (bytes32[] memory topics, bytes memory data) = + decodeTopicsAndData(logs, "WithdrawalWithFeeSuccessful(address,address,uint256,uint256)", treasury); + + // topics[0] is the event signature hash + // topics[1] is the indexed token + // topics[2] is the indexed to address + to = address(uint160(uint256(topics[2]))); + (amount, fee) = abi.decode(data, (uint256, uint256)); + } + + /** + * @notice Pauses the treasury + */ + function pauseTreasury(address caller, address treasury, bytes32 message) internal { + vm.prank(caller); + PaymentTreasury(treasury).pauseTreasury(message); + } + + /** + * @notice Unpauses the treasury + */ + function unpauseTreasury(address caller, address treasury, bytes32 message) internal { + vm.prank(caller); + PaymentTreasury(treasury).unpauseTreasury(message); + } + + /** + * @notice Cancels the treasury + */ + function cancelTreasury(address caller, address treasury, bytes32 message) internal { + vm.prank(caller); + PaymentTreasury(treasury).cancelTreasury(message); + } + + /** + * @notice Helper to create and fund a payment from buyer + */ + 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 token specified + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + ICampaignPaymentTreasury.ExternalFees[] memory emptyExternalFees = + new ICampaignPaymentTreasury.ExternalFees[](0); + createPayment( + users.platform1AdminAddress, + paymentId, + buyerId, + itemId, + address(testToken), + amount, + expiration, + emptyLineItems, + emptyExternalFees + ); + + // Transfer tokens from buyer to treasury + vm.prank(buyerAddress); + testToken.transfer(treasuryAddress, amount); + } + + /** + * @notice Helper 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); + processCryptoPayment( + buyerAddress, + paymentId, + itemId, + buyerAddress, + address(testToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /** + * @notice Helper to create multiple test payments + */ + function _createTestPayments() internal { + _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); + } + + /** + * @notice Helper to create and fund a payment from buyer with specific token + */ + function _createAndFundPaymentWithToken( + bytes32 paymentId, + bytes32 buyerId, + bytes32 itemId, + uint256 amount, + address buyerAddress, + address token + ) internal { + // Fund buyer + deal(token, buyerAddress, amount); + + // Buyer approves treasury + vm.prank(buyerAddress); + TestToken(token).approve(treasuryAddress, amount); + + // Create payment with token specified + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + ICampaignPaymentTreasury.ExternalFees[] memory emptyExternalFees = + new ICampaignPaymentTreasury.ExternalFees[](0); + createPayment( + users.platform1AdminAddress, + paymentId, + buyerId, + itemId, + token, + amount, + expiration, + emptyLineItems, + emptyExternalFees + ); + + // Transfer tokens from buyer to treasury + vm.prank(buyerAddress); + TestToken(token).transfer(treasuryAddress, amount); + } + + /** + * @notice Helper to create and process a crypto payment with specific token + */ + function _createAndProcessCryptoPaymentWithToken( + bytes32 paymentId, + bytes32 itemId, + uint256 amount, + address buyerAddress, + address token + ) internal { + // Fund buyer + deal(token, buyerAddress, amount); + + // Buyer approves treasury + vm.prank(buyerAddress); + TestToken(token).approve(treasuryAddress, amount); + + // Process crypto payment + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + buyerAddress, + paymentId, + itemId, + buyerAddress, + token, + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } +} diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasuryBatchLimitTest.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasuryBatchLimitTest.t.sol new file mode 100644 index 00000000..f1d349b8 --- /dev/null +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasuryBatchLimitTest.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import "./PaymentTreasury.t.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import "forge-std/Test.sol"; +import {LogDecoder} from "../../utils/LogDecoder.sol"; +import {PaymentTreasury} from "src/treasuries/PaymentTreasury.sol"; +import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; + +contract PaymentTreasuryBatchLimit_Test is PaymentTreasury_Integration_Shared_Test { + uint256 constant CELO_BLOCK_GAS_LIMIT = 30_000_000; + + function setUp() public override { + super.setUp(); + console.log("=== CELO BATCH LIMIT TEST ==="); + console.log("Block Gas Limit: 30,000,000"); + console.log("Block gas target: 6,000,000"); + console.log(""); + } + + /** + * @notice Creates payments for testing + */ + function _createBatchPayments(uint256 count) internal returns (bytes32[] memory paymentIds) { + paymentIds = new bytes32[](count); + uint256 paymentAmount = 10e18; + uint256 expiration = block.timestamp + 7 days; + + vm.startPrank(users.platform1AdminAddress); + for (uint256 i = 0; i < count; i++) { + bytes32 paymentId = keccak256(abi.encodePacked("payment", i)); + bytes32 buyerId = keccak256(abi.encodePacked("buyer", i)); + bytes32 itemId = keccak256(abi.encodePacked("item", i)); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + ICampaignPaymentTreasury.ExternalFees[] memory emptyExternalFees = + new ICampaignPaymentTreasury.ExternalFees[](0); + paymentTreasury.createPayment( + paymentId, + buyerId, + itemId, + address(testToken), + paymentAmount, + expiration, + emptyLineItems, + emptyExternalFees + ); + + paymentIds[i] = paymentId; + } + vm.stopPrank(); + + // Fund the treasury + deal(address(testToken), treasuryAddress, paymentAmount * count); + + return paymentIds; + } + + /** + * @notice Test to find batch limits + */ + function test_FindBatchLimits() public { + uint256[] memory batchSizes = new uint256[](5); + batchSizes[0] = 100; + batchSizes[1] = 200; + batchSizes[2] = 300; + batchSizes[3] = 400; + batchSizes[4] = 500; + + console.log("TESTING BATCH SIZES FROM 100 TO 500"); + console.log("===================================="); + + for (uint256 i = 0; i < batchSizes.length; i++) { + uint256 batchSize = batchSizes[i]; + + bytes32[] memory paymentIds = _createBatchPayments(batchSize); + + vm.prank(users.platform1AdminAddress); + uint256 gasStart = gasleft(); + + try paymentTreasury.confirmPaymentBatch(paymentIds, _createZeroAddressArray(paymentIds.length)) { + uint256 gasUsed = gasStart - gasleft(); + uint256 percentOfBlock = (gasUsed * 100) / CELO_BLOCK_GAS_LIMIT; + + console.log(string(abi.encodePacked("Batch Size: ", vm.toString(batchSize)))); + console.log(string(abi.encodePacked("Gas Used: ", vm.toString(gasUsed)))); + console.log(string(abi.encodePacked("Percent of Block: ", vm.toString(percentOfBlock), "%"))); + + // Check safety thresholds + if (percentOfBlock <= 30) { + console.log("Status: SAFE"); + } else { + console.log("Status: RISKY"); + } + console.log("----------------------------"); + } catch { + console.log(string(abi.encodePacked("Batch Size: ", vm.toString(batchSize)))); + console.log("FAILED - Exceeds gas limit or reverted"); + console.log("----------------------------"); + break; + } + + setUp(); + } + } +} diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasuryFunction.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasuryFunction.t.sol new file mode 100644 index 00000000..7a72f3af --- /dev/null +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasuryFunction.t.sol @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "./PaymentTreasury.t.sol"; +import "forge-std/console.sol"; +import "forge-std/Vm.sol"; +import "forge-std/Test.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {PaymentTreasury} from "src/treasuries/PaymentTreasury.sol"; +import {TestToken} from "../../../mocks/TestToken.sol"; + +/** + * @title PaymentTreasuryFunction_Integration_Test + * @notice This contract contains integration tests for the happy-path functionality + * of the PaymentTreasury contract. Each test focuses on a single core function. + */ +contract PaymentTreasuryFunction_Integration_Test is PaymentTreasury_Integration_Shared_Test { + /** + * @notice Tests the successful confirmation of a single payment. + */ + function test_confirmPayment() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + assertEq(testToken.balanceOf(treasuryAddress), PAYMENT_AMOUNT_1); + + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); // Removed token parameter + + assertEq(paymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1, "Raised amount should match the payment amount"); + assertEq( + paymentTreasury.getAvailableRaisedAmount(), + PAYMENT_AMOUNT_1, + "Available raised amount should match the payment amount" + ); + } + + /** + * @notice Tests the successful confirmation of multiple payments in a batch. + */ + function test_confirmPaymentBatch() public { + _createTestPayments(); // Creates PAYMENT_ID_1 and PAYMENT_ID_2 + uint256 totalAmount = PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2; + assertEq(testToken.balanceOf(treasuryAddress), totalAmount); + + bytes32[] memory paymentIds = new bytes32[](2); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + confirmPaymentBatch(users.platform1AdminAddress, paymentIds); // Removed token array + + assertEq( + paymentTreasury.getRaisedAmount(), totalAmount, "Raised amount should match the total of batched payments" + ); + assertEq( + paymentTreasury.getAvailableRaisedAmount(), + totalAmount, + "Available raised amount should match the total of batched payments" + ); + } + + /** + * @notice Tests that a confirmed payment can be successfully refunded. + */ + function test_claimRefund() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); + uint256 backerBalanceBefore = testToken.balanceOf(users.backer1Address); + + uint256 refundAmount = claimRefund(users.platform1AdminAddress, PAYMENT_ID_1, users.backer1Address); + + // Verify the refund amount is correct and all balances are updated as expected. + assertEq(refundAmount, PAYMENT_AMOUNT_1, "Refunded amount is incorrect"); + assertEq( + testToken.balanceOf(users.backer1Address), + backerBalanceBefore + PAYMENT_AMOUNT_1, + "Backer did not receive the correct refund amount" + ); + assertEq(paymentTreasury.getRaisedAmount(), 0, "Raised amount should be zero after refund"); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0, "Available amount should be zero after refund"); + assertEq(testToken.balanceOf(treasuryAddress), 0, "Treasury token balance should be zero after refund"); + } + + /** + * @notice Tests the processing of a crypto payment. + */ + function test_processCryptoPayment() public { + uint256 amount = 1500e18; + deal(address(testToken), users.backer1Address, amount); + + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, amount); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + users.backer1Address, + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + assertEq(paymentTreasury.getRaisedAmount(), amount, "Raised amount should match crypto payment"); + assertEq(paymentTreasury.getAvailableRaisedAmount(), amount, "Available amount should match crypto payment"); + assertEq(testToken.balanceOf(treasuryAddress), amount, "Treasury should hold the tokens"); + } + + /** + * @notice Tests buyer-initiated refund for crypto payment. + */ + function test_claimRefundBuyerInitiated() public { + uint256 amount = 1500e18; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, amount, users.backer1Address); + + uint256 buyerBalanceBefore = testToken.balanceOf(users.backer1Address); + uint256 refundAmount = claimRefund(users.backer1Address, PAYMENT_ID_1, 1); // tokenId 1 + + assertEq(refundAmount, amount, "Refund amount should match payment"); + assertEq(testToken.balanceOf(users.backer1Address), buyerBalanceBefore + amount, "Buyer should receive refund"); + assertEq(paymentTreasury.getRaisedAmount(), 0, "Raised amount should be zero after refund"); + } + + /** + * @notice Tests the final withdrawal of funds by the campaign owner after fees have been calculated. + */ + function test_withdraw() public { + _createTestPayments(); + uint256 totalAmount = PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2; + bytes32[] memory paymentIds = new bytes32[](2); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + confirmPaymentBatch(users.platform1AdminAddress, paymentIds); + + address campaignOwner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(campaignOwner); + + (address recipient, uint256 withdrawnAmount, uint256 fee) = withdraw(treasuryAddress); + uint256 ownerBalanceAfter = testToken.balanceOf(campaignOwner); + + // Check that the correct amount is withdrawn to the campaign owner's address. + uint256 expectedProtocolFee = (totalAmount * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedPlatformFee = (totalAmount * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedTotalFee = expectedProtocolFee + expectedPlatformFee; + uint256 expectedWithdrawalAmount = totalAmount - expectedTotalFee; + + assertEq(recipient, campaignOwner, "Funds withdrawn to incorrect address"); + assertEq(withdrawnAmount, expectedWithdrawalAmount, "Incorrect amount withdrawn"); + assertEq(fee, expectedTotalFee, "Incorrect fee amount"); + assertEq( + ownerBalanceAfter - ownerBalanceBefore, + expectedWithdrawalAmount, + "Campaign owner did not receive correct withdrawn amount" + ); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0, "Available amount should be zero after withdrawal"); + } + + /** + * @notice Tests the correct disbursement of fees to the protocol and platform after withdrawal. + */ + function test_disburseFeesAfterWithdraw() public { + _createTestPayments(); + uint256 totalAmount = PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2; + bytes32[] memory paymentIds = new bytes32[](2); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + confirmPaymentBatch(users.platform1AdminAddress, paymentIds); + + // Withdraw first to calculate fees + withdraw(treasuryAddress); + + uint256 protocolAdminBalanceBefore = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); + + (uint256 protocolShare, uint256 platformShare) = disburseFees(treasuryAddress); + + // Verify fees are calculated and transferred correctly. + uint256 expectedProtocolShare = (totalAmount * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedPlatformShare = (totalAmount * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + + assertEq(protocolShare, expectedProtocolShare, "Incorrect protocol fee disbursed"); + assertEq(platformShare, expectedPlatformShare, "Incorrect platform fee disbursed"); + + assertEq( + testToken.balanceOf(users.protocolAdminAddress), + protocolAdminBalanceBefore + expectedProtocolShare, + "Protocol admin did not receive correct fee amount" + ); + assertEq( + testToken.balanceOf(users.platform1AdminAddress), + platformAdminBalanceBefore + expectedPlatformShare, + "Platform admin did not receive correct fee amount" + ); + + assertEq(testToken.balanceOf(treasuryAddress), 0, "Treasury should have zero balance after disbursing fees"); + } + + /*////////////////////////////////////////////////////////////// + MULTI-TOKEN FUNCTIONALITY TESTS + //////////////////////////////////////////////////////////////*/ + + function test_confirmPaymentWithMultipleTokens() public { + // Create payments with different tokens + uint256 usdtAmount = getTokenAmount(address(usdtToken), PAYMENT_AMOUNT_1); + uint256 usdcAmount = getTokenAmount(address(usdcToken), PAYMENT_AMOUNT_2); + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, usdcAmount, users.backer2Address, address(usdcToken) + ); + + // Confirm without specifying token (already set during creation) + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_2); + + // Verify normalized raised amount + uint256 expectedNormalized = PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2; + assertEq(paymentTreasury.getRaisedAmount(), expectedNormalized, "Raised amount should be normalized sum"); + } + + function test_getRaisedAmountNormalizesCorrectly() public { + // Create payments with same base amount in different tokens + uint256 baseAmount = 1000e18; + uint256 usdtAmount = baseAmount / 1e12; // 6 decimals + uint256 cUSDAmount = baseAmount; // 18 decimals + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); + + uint256 raisedAfterUSDT = paymentTreasury.getRaisedAmount(); + assertEq(raisedAfterUSDT, baseAmount, "USDT should normalize to base amount"); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_2); + + uint256 raisedAfterCUSD = paymentTreasury.getRaisedAmount(); + assertEq(raisedAfterCUSD, baseAmount * 2, "Total should be sum of normalized amounts"); + } + + function test_batchConfirmWithMultipleTokens() public { + // Create payments with different tokens + uint256 usdtAmount = getTokenAmount(address(usdtToken), 500e18); + uint256 usdcAmount = getTokenAmount(address(usdcToken), 700e18); + uint256 cUSDAmount = 900e18; + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, usdcAmount, users.backer2Address, address(usdcToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_3, BUYER_ID_1, ITEM_ID_1, cUSDAmount, users.backer1Address, address(cUSDToken) + ); + + bytes32[] memory paymentIds = new bytes32[](3); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + paymentIds[2] = PAYMENT_ID_3; + + // Batch confirm without token array (tokens already set during creation) + confirmPaymentBatch(users.platform1AdminAddress, paymentIds); + + uint256 expectedTotal = 500e18 + 700e18 + 900e18; + assertEq(paymentTreasury.getRaisedAmount(), expectedTotal, "Should sum all normalized amounts"); + } + + function test_processCryptoPaymentWithMultipleTokens() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), 800e18); + uint256 cUSDAmount = 1200e18; + + // Process USDT payment + _createAndProcessCryptoPaymentWithToken( + PAYMENT_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + // Process cUSD payment + _createAndProcessCryptoPaymentWithToken( + PAYMENT_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + uint256 expectedTotal = 800e18 + 1200e18; + assertEq(paymentTreasury.getRaisedAmount(), expectedTotal, "Should track both crypto payments"); + assertEq(usdtToken.balanceOf(treasuryAddress), usdtAmount, "Should hold USDT"); + assertEq(cUSDToken.balanceOf(treasuryAddress), cUSDAmount, "Should hold cUSD"); + } + + function test_refundReturnsCorrectToken() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), PAYMENT_AMOUNT_1); + uint256 cUSDAmount = PAYMENT_AMOUNT_2; + + // Create and confirm USDT payment + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); // No token parameter + + // Create and confirm cUSD payment + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_2); // No token parameter + + uint256 backer1USDTBefore = usdtToken.balanceOf(users.backer1Address); + uint256 backer2CUSDBefore = cUSDToken.balanceOf(users.backer2Address); + + // Claim refunds + uint256 refund1 = claimRefund(users.platform1AdminAddress, PAYMENT_ID_1, users.backer1Address); + uint256 refund2 = claimRefund(users.platform1AdminAddress, PAYMENT_ID_2, users.backer2Address); + + // Verify correct tokens refunded + assertEq(refund1, usdtAmount, "Should refund USDT amount"); + assertEq(refund2, cUSDAmount, "Should refund cUSD amount"); + assertEq(usdtToken.balanceOf(users.backer1Address) - backer1USDTBefore, usdtAmount, "Should receive USDT"); + assertEq(cUSDToken.balanceOf(users.backer2Address) - backer2CUSDBefore, cUSDAmount, "Should receive cUSD"); + + // Verify no cross-token contamination + assertEq(cUSDToken.balanceOf(users.backer1Address), TOKEN_MINT_AMOUNT, "Backer1 shouldn't have cUSD changes"); + assertEq( + usdtToken.balanceOf(users.backer2Address), TOKEN_MINT_AMOUNT / 1e12, "Backer2 shouldn't have USDT changes" + ); + } + + function test_cryptoPaymentRefundWithMultipleTokens() public { + uint256 usdcAmount = getTokenAmount(address(usdcToken), 1500e18); + uint256 cUSDAmount = 2000e18; + + // Process crypto payments + _createAndProcessCryptoPaymentWithToken( + PAYMENT_ID_1, ITEM_ID_1, usdcAmount, users.backer1Address, address(usdcToken) + ); + + _createAndProcessCryptoPaymentWithToken( + PAYMENT_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + uint256 backer1USDCBefore = usdcToken.balanceOf(users.backer1Address); + uint256 backer2CUSDBefore = cUSDToken.balanceOf(users.backer2Address); + + // Buyers claim their own refunds + uint256 refund1 = claimRefund(users.backer1Address, PAYMENT_ID_1, 1); // tokenId 1 + uint256 refund2 = claimRefund(users.backer2Address, PAYMENT_ID_2, 2); // tokenId 2 + + assertEq(refund1, usdcAmount, "Should refund USDC amount"); + assertEq(refund2, cUSDAmount, "Should refund cUSD amount"); + assertEq(usdcToken.balanceOf(users.backer1Address) - backer1USDCBefore, usdcAmount); + assertEq(cUSDToken.balanceOf(users.backer2Address) - backer2CUSDBefore, cUSDAmount); + } + + function test_withdrawWithMultipleTokens() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), 1000e18); + uint256 usdcAmount = getTokenAmount(address(usdcToken), 1500e18); + uint256 cUSDAmount = 2000e18; + + // Create and confirm payments with different tokens + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, usdcAmount, users.backer2Address, address(usdcToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_2); + + _createAndFundPaymentWithToken( + PAYMENT_ID_3, BUYER_ID_1, ITEM_ID_1, cUSDAmount, users.backer1Address, address(cUSDToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_3); + + address campaignOwner = CampaignInfo(campaignAddress).owner(); + uint256 ownerUSDTBefore = usdtToken.balanceOf(campaignOwner); + uint256 ownerUSDCBefore = usdcToken.balanceOf(campaignOwner); + uint256 ownerCUSDBefore = cUSDToken.balanceOf(campaignOwner); + + // Withdraw all tokens + withdraw(treasuryAddress); + + // Verify owner received all tokens (minus fees) + assertTrue(usdtToken.balanceOf(campaignOwner) > ownerUSDTBefore, "Should receive USDT"); + assertTrue(usdcToken.balanceOf(campaignOwner) > ownerUSDCBefore, "Should receive USDC"); + assertTrue(cUSDToken.balanceOf(campaignOwner) > ownerCUSDBefore, "Should receive cUSD"); + + // Verify available amount is zero + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0, "Should have zero available after withdrawal"); + } + + function test_disburseFeesWithMultipleTokens() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), PAYMENT_AMOUNT_1); + uint256 cUSDAmount = PAYMENT_AMOUNT_2; + + // Create and confirm payments + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_2); + + // Withdraw to calculate fees + withdraw(treasuryAddress); + + uint256 protocolUSDTBefore = usdtToken.balanceOf(users.protocolAdminAddress); + uint256 protocolCUSDBefore = cUSDToken.balanceOf(users.protocolAdminAddress); + uint256 platformUSDTBefore = usdtToken.balanceOf(users.platform1AdminAddress); + uint256 platformCUSDBefore = cUSDToken.balanceOf(users.platform1AdminAddress); + + // Disburse fees + disburseFees(treasuryAddress); + + // Verify fees distributed for both tokens + uint256 expectedUSDTProtocolFee = (usdtAmount * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedUSDTPlatformFee = (usdtAmount * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedCUSDProtocolFee = (cUSDAmount * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedCUSDPlatformFee = (cUSDAmount * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + + assertEq( + usdtToken.balanceOf(users.protocolAdminAddress) - protocolUSDTBefore, + expectedUSDTProtocolFee, + "USDT protocol fee incorrect" + ); + assertEq( + cUSDToken.balanceOf(users.protocolAdminAddress) - protocolCUSDBefore, + expectedCUSDProtocolFee, + "cUSD protocol fee incorrect" + ); + assertEq( + usdtToken.balanceOf(users.platform1AdminAddress) - platformUSDTBefore, + expectedUSDTPlatformFee, + "USDT platform fee incorrect" + ); + assertEq( + cUSDToken.balanceOf(users.platform1AdminAddress) - platformCUSDBefore, + expectedCUSDPlatformFee, + "cUSD platform fee incorrect" + ); + + // Treasury should be empty + assertEq(usdtToken.balanceOf(treasuryAddress), 0, "USDT should be fully disbursed"); + assertEq(cUSDToken.balanceOf(treasuryAddress), 0, "cUSD should be fully disbursed"); + } + + function test_mixedPaymentTypesWithMultipleTokens() public { + // Regular payment with USDT + uint256 usdtAmount = getTokenAmount(address(usdtToken), 1000e18); + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); + + // Crypto payment with USDC + uint256 usdcAmount = getTokenAmount(address(usdcToken), 1500e18); + _createAndProcessCryptoPaymentWithToken( + PAYMENT_ID_2, ITEM_ID_2, usdcAmount, users.backer2Address, address(usdcToken) + ); + + // Regular payment with cUSD + uint256 cUSDAmount = 2000e18; + _createAndFundPaymentWithToken( + PAYMENT_ID_3, BUYER_ID_1, ITEM_ID_1, cUSDAmount, users.backer1Address, address(cUSDToken) + ); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_3); + + // Verify all contribute to raised amount + uint256 expectedTotal = 1000e18 + 1500e18 + 2000e18; + assertEq(paymentTreasury.getRaisedAmount(), expectedTotal, "Should sum all payment types"); + + // Withdraw and disburse + withdraw(treasuryAddress); + disburseFees(treasuryAddress); + + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function test_revertWhenCreatingWithUnacceptedToken() public { + // Create a token not in the accepted list + TestToken rejectedToken = new TestToken("Rejected", "REJ", 18); + uint256 amount = 1000e18; + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + + // Try to create payment with unaccepted token + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(rejectedToken), + amount, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_revertWhenTokenNotAccepted() public { + // Create a token not in the accepted list + TestToken rejectedToken = new TestToken("Rejected", "REJ", 18); + uint256 amount = 1000e18; + rejectedToken.mint(users.backer1Address, amount); + + vm.prank(users.backer1Address); + rejectedToken.approve(treasuryAddress, amount); + + // Try to process crypto payment with unaccepted token + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + users.backer1Address, + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(rejectedToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_balanceTrackingAcrossMultipleTokens() public { + // Create multiple payments with different tokens + uint256 usdtAmount1 = getTokenAmount(address(usdtToken), 500e18); + uint256 usdtAmount2 = getTokenAmount(address(usdtToken), 300e18); + uint256 cUSDAmount = 1000e18; + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount1, users.backer1Address, address(usdtToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, usdtAmount2, users.backer2Address, address(usdtToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_3, BUYER_ID_1, ITEM_ID_1, cUSDAmount, users.backer1Address, address(cUSDToken) + ); + + // Confirm all payments + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_1); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_2); + confirmPayment(users.platform1AdminAddress, PAYMENT_ID_3); + + // Verify raised amounts + uint256 expectedTotal = 500e18 + 300e18 + 1000e18; + assertEq(paymentTreasury.getRaisedAmount(), expectedTotal, "Should track total correctly"); + + // Refund one USDT payment + claimRefund(users.platform1AdminAddress, PAYMENT_ID_1, users.backer1Address); + + uint256 afterRefund = 300e18 + 1000e18; + assertEq(paymentTreasury.getRaisedAmount(), afterRefund, "Should update after refund"); + + // Verify token balances + assertEq(usdtToken.balanceOf(treasuryAddress), usdtAmount2, "Should only have remaining USDT"); + assertEq(cUSDToken.balanceOf(treasuryAddress), cUSDAmount, "cUSD should be unchanged"); + } + + /** + * @notice Tests that cancelled treasuries are excluded from getTotalRaisedAmount. + */ + function test_getTotalRaisedAmountExcludesCancelledTreasuries() public { + // Setup: Create a campaign with 2 platforms + bytes32 identifierHash = keccak256(abi.encodePacked("multi-platform-campaign")); + bytes32[] memory selectedPlatforms = new bytes32[](2); + selectedPlatforms[0] = PLATFORM_1_HASH; + selectedPlatforms[1] = PLATFORM_2_HASH; + + // Enlist second platform + vm.startPrank(users.protocolAdminAddress); + globalParams.enlistPlatform(PLATFORM_2_HASH, users.platform2AdminAddress, PLATFORM_FEE_PERCENT, address(0)); + vm.stopPrank(); + + // Register and approve treasury for platform 2 + PaymentTreasury platform2Implementation = new PaymentTreasury(); + vm.startPrank(users.platform2AdminAddress); + treasuryFactory.registerTreasuryImplementation(PLATFORM_2_HASH, 2, address(platform2Implementation)); + vm.stopPrank(); + + vm.startPrank(users.protocolAdminAddress); + treasuryFactory.approveTreasuryImplementation(PLATFORM_2_HASH, 2); + vm.stopPrank(); + + bytes32[] memory platformDataKey = new bytes32[](0); + bytes32[] memory platformDataValue = new bytes32[](0); + + // Create multi-platform campaign + vm.startPrank(users.creator1Address); + vm.recordLogs(); + campaignInfoFactory.createCampaign( + users.creator1Address, + identifierHash, + selectedPlatforms, + platformDataKey, + platformDataValue, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + Vm.Log[] memory entries = vm.getRecordedLogs(); + vm.stopPrank(); + + (bytes32[] memory topics,) = decodeTopicsAndData( + entries, "CampaignInfoFactoryCampaignCreated(bytes32,address)", address(campaignInfoFactory) + ); + address multiPlatformCampaign = address(uint160(uint256(topics[2]))); + CampaignInfo campaignInfo = CampaignInfo(multiPlatformCampaign); + + // Deploy treasury for platform 1 + vm.startPrank(users.platform1AdminAddress); + vm.recordLogs(); + treasuryFactory.deploy(PLATFORM_1_HASH, multiPlatformCampaign, 2); + Vm.Log[] memory entries1 = vm.getRecordedLogs(); + vm.stopPrank(); + + (bytes32[] memory topics1, bytes memory data1) = decodeTopicsAndData( + entries1, "TreasuryFactoryTreasuryDeployed(bytes32,uint256,address,address)", address(treasuryFactory) + ); + address treasury1 = abi.decode(data1, (address)); + + // Deploy treasury for platform 2 + vm.startPrank(users.platform2AdminAddress); + vm.recordLogs(); + treasuryFactory.deploy(PLATFORM_2_HASH, multiPlatformCampaign, 2); + Vm.Log[] memory entries2 = vm.getRecordedLogs(); + vm.stopPrank(); + + (bytes32[] memory topics2, bytes memory data2) = decodeTopicsAndData( + entries2, "TreasuryFactoryTreasuryDeployed(bytes32,uint256,address,address)", address(treasuryFactory) + ); + address treasury2 = abi.decode(data2, (address)); + + PaymentTreasury paymentTreasury1 = PaymentTreasury(treasury1); + PaymentTreasury paymentTreasury2 = PaymentTreasury(treasury2); + + // Add payments to both treasuries + uint256 amount1 = 1000e18; + uint256 amount2 = 2000e18; + + // Treasury 1: Create, fund, and confirm payment + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury1.createPayment( + keccak256("payment-p1"), + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + amount1, + block.timestamp + PAYMENT_EXPIRATION, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Fund backer and transfer to treasury + deal(address(testToken), users.backer1Address, amount1); + vm.prank(users.backer1Address); + testToken.transfer(treasury1, amount1); + + vm.prank(users.platform1AdminAddress); + paymentTreasury1.confirmPayment(keccak256("payment-p1"), address(0)); + + // Treasury 2: Create, fund, and confirm payment + vm.prank(users.platform2AdminAddress); + paymentTreasury2.createPayment( + keccak256("payment-p2"), + BUYER_ID_2, + ITEM_ID_2, + address(testToken), + amount2, + block.timestamp + PAYMENT_EXPIRATION, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Fund backer and transfer to treasury + deal(address(testToken), users.backer2Address, amount2); + vm.prank(users.backer2Address); + testToken.transfer(treasury2, amount2); + + vm.prank(users.platform2AdminAddress); + paymentTreasury2.confirmPayment(keccak256("payment-p2"), address(0)); + + // Verify both treasuries have raised amounts + assertEq(paymentTreasury1.getRaisedAmount(), amount1, "Treasury 1 should have raised amount"); + assertEq(paymentTreasury2.getRaisedAmount(), amount2, "Treasury 2 should have raised amount"); + + // Verify total includes both treasuries + uint256 totalBefore = campaignInfo.getTotalRaisedAmount(); + assertEq(totalBefore, amount1 + amount2, "Total should include both treasuries"); + + // Cancel treasury 1 + vm.prank(users.platform1AdminAddress); + paymentTreasury1.cancelTreasury(keccak256("test-cancellation")); + + // Verify treasury 1 is cancelled + assertTrue(paymentTreasury1.cancelled(), "Treasury 1 should be cancelled"); + assertFalse(paymentTreasury2.cancelled(), "Treasury 2 should not be cancelled"); + + // Verify total now excludes cancelled treasury + uint256 totalAfter = campaignInfo.getTotalRaisedAmount(); + assertEq(totalAfter, amount2, "Total should only include non-cancelled treasury"); + } +} diff --git a/test/foundry/integration/PaymentTreasury/PaymentTreasuryLineItems.t.sol b/test/foundry/integration/PaymentTreasury/PaymentTreasuryLineItems.t.sol new file mode 100644 index 00000000..dc7dfcb7 --- /dev/null +++ b/test/foundry/integration/PaymentTreasury/PaymentTreasuryLineItems.t.sol @@ -0,0 +1,647 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "./PaymentTreasury.t.sol"; +import {ICampaignPaymentTreasury} from "src/interfaces/ICampaignPaymentTreasury.sol"; + +/// @notice Tests for PaymentTreasury with line items and expiration +contract PaymentTreasuryLineItems_Test is PaymentTreasury_Integration_Shared_Test { + // Line item type IDs + bytes32 internal constant SHIPPING_FEE_TYPE_ID = keccak256("shipping_fee"); + bytes32 internal constant TIP_TYPE_ID = keccak256("tip"); + bytes32 internal constant INTEREST_TYPE_ID = keccak256("interest"); + bytes32 internal constant REFUNDABLE_FEE_WITH_PROTOCOL_TYPE_ID = keccak256("refundable_fee_with_protocol"); + + function setUp() public override { + super.setUp(); + + // Register line item types + vm.prank(users.platform1AdminAddress); + globalParams.setPlatformLineItemType( + PLATFORM_1_HASH, + SHIPPING_FEE_TYPE_ID, + "shipping_fee", + true, // countsTowardGoal + false, // applyProtocolFee + true, // canRefund + false // instantTransfer + ); + + vm.prank(users.platform1AdminAddress); + globalParams.setPlatformLineItemType( + PLATFORM_1_HASH, + TIP_TYPE_ID, + "tip", + false, // countsTowardGoal + false, // applyProtocolFee + false, // canRefund + true // instantTransfer + ); + + vm.prank(users.platform1AdminAddress); + globalParams.setPlatformLineItemType( + PLATFORM_1_HASH, + INTEREST_TYPE_ID, + "interest", + false, // countsTowardGoal + true, // applyProtocolFee + false, // canRefund + false // instantTransfer + ); + + vm.prank(users.platform1AdminAddress); + globalParams.setPlatformLineItemType( + PLATFORM_1_HASH, + REFUNDABLE_FEE_WITH_PROTOCOL_TYPE_ID, + "refundable_fee_with_protocol", + false, // countsTowardGoal + true, // applyProtocolFee + true, // canRefund + false // instantTransfer + ); + } + + /*////////////////////////////////////////////////////////////// + LINE ITEMS - CREATE PAYMENT + //////////////////////////////////////////////////////////////*/ + + function test_createPaymentWithShippingFee() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + uint256 shippingFeeAmount = 50e18; + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: shippingFeeAmount}); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Shipping fee counts toward goal, so it should be tracked in pending payments + assertEq(paymentTreasury.getRaisedAmount(), 0); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function test_createPaymentWithTip() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + uint256 tipAmount = 25e18; + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: tipAmount}); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Tip doesn't count toward goal, so it should be tracked separately + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function test_createPaymentWithInterest() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + uint256 interestAmount = 100e18; + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: INTEREST_TYPE_ID, amount: interestAmount}); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Interest doesn't count toward goal but applies protocol fee + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function test_createPaymentWithMultipleLineItems() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + uint256 shippingFeeAmount = 50e18; + uint256 tipAmount = 25e18; + uint256 interestAmount = 100e18; + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](3); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: shippingFeeAmount}); + lineItems[1] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: tipAmount}); + lineItems[2] = ICampaignPaymentTreasury.LineItem({typeId: INTEREST_TYPE_ID, amount: interestAmount}); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function test_createPaymentRevertWhenLineItemTypeDoesNotExist() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + bytes32 nonExistentTypeId = keccak256("non_existent"); + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: nonExistentTypeId, amount: 50e18}); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_createPaymentRevertWhenLineItemHasZeroTypeId() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: bytes32(0), amount: 50e18}); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_createPaymentRevertWhenLineItemHasZeroAmount() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: 0}); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /*////////////////////////////////////////////////////////////// + LINE ITEMS - PROCESS CRYPTO PAYMENT + //////////////////////////////////////////////////////////////*/ + + function test_processCryptoPaymentWithShippingFee() public { + uint256 shippingFeeAmount = 50e18; + uint256 totalAmount = PAYMENT_AMOUNT_1 + shippingFeeAmount; + + deal(address(testToken), users.backer1Address, totalAmount); + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, totalAmount); + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: shippingFeeAmount}); + + vm.prank(users.backer1Address); + paymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payment should be confirmed immediately for crypto payments + assertEq(paymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1 + shippingFeeAmount); + assertEq(testToken.balanceOf(treasuryAddress), totalAmount); + } + + function test_processCryptoPaymentWithTip() public { + uint256 tipAmount = 25e18; + uint256 totalAmount = PAYMENT_AMOUNT_1 + tipAmount; + + deal(address(testToken), users.backer1Address, totalAmount); + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, totalAmount); + + uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: tipAmount}); + + vm.prank(users.backer1Address); + paymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Tip doesn't count toward goal, but payment amount does + assertEq(paymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + // Tip is instantly transferred to platform admin, so treasury only holds payment amount + assertEq(testToken.balanceOf(treasuryAddress), PAYMENT_AMOUNT_1); + // Platform admin received the tip + assertEq(testToken.balanceOf(users.platform1AdminAddress), platformAdminBalanceBefore + tipAmount); + } + + function test_processCryptoPaymentWithMultipleLineItems() public { + uint256 shippingFeeAmount = 50e18; + uint256 tipAmount = 25e18; + uint256 interestAmount = 100e18; + uint256 totalAmount = PAYMENT_AMOUNT_1 + shippingFeeAmount + tipAmount + interestAmount; + + deal(address(testToken), users.backer1Address, totalAmount); + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, totalAmount); + + uint256 platformAdminBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); + uint256 tipNetAmount = tipAmount; // No protocol fee on tip + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](3); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: shippingFeeAmount}); + lineItems[1] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: tipAmount}); + lineItems[2] = ICampaignPaymentTreasury.LineItem({typeId: INTEREST_TYPE_ID, amount: interestAmount}); + + vm.prank(users.backer1Address); + paymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Only payment amount + shipping fee count toward goal + assertEq(paymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1 + shippingFeeAmount); + // Treasury holds: payment amount + shipping fee + interest (full amount, protocol fee tracked separately) + // Tip is instantly transferred to platform admin + assertEq(testToken.balanceOf(treasuryAddress), PAYMENT_AMOUNT_1 + shippingFeeAmount + interestAmount); + // Platform admin received the tip instantly + assertEq(testToken.balanceOf(users.platform1AdminAddress), platformAdminBalanceBefore + tipNetAmount); + } + + /*////////////////////////////////////////////////////////////// + EXPIRATION TESTS + //////////////////////////////////////////////////////////////*/ + + function test_createPaymentWithValidExpiration() public { + uint256 expiration = block.timestamp + 1 days; + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function test_createPaymentRevertWhenExpirationInPast() public { + uint256 expiration = block.timestamp - 1; + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_createPaymentRevertWhenExpirationIsCurrentTime() public { + uint256 expiration = block.timestamp; + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_createPaymentWithLongExpiration() public { + uint256 expiration = block.timestamp + 365 days; + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function test_cancelPaymentRevertWhenExpired() public { + uint256 expiration = block.timestamp + 1 hours; + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Advance time past expiration + vm.warp(block.timestamp + 2 hours); + + // Should revert when trying to cancel expired payment + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelPayment(PAYMENT_ID_1); + } + + function test_confirmPaymentBeforeExpiration() public { + uint256 expiration = block.timestamp + 1 days; + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Fund the payment + deal(address(testToken), users.backer1Address, PAYMENT_AMOUNT_1); + vm.prank(users.backer1Address); + testToken.transfer(treasuryAddress, PAYMENT_AMOUNT_1); + + // Should be able to confirm before expiration + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + assertEq(paymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function test_getPaymentDataIncludesLineItemsAndExternalFees() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + uint256 shippingFeeAmount = 50e18; + uint256 tipAmount = 25e18; + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](2); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: shippingFeeAmount}); + lineItems[1] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: tipAmount}); + + ICampaignPaymentTreasury.ExternalFees[] memory externalFees = new ICampaignPaymentTreasury.ExternalFees[](2); + externalFees[0] = ICampaignPaymentTreasury.ExternalFees({ + feeType: keccak256(abi.encodePacked("gateway_fee")), + feeAmount: 15e18 + }); + externalFees[1] = ICampaignPaymentTreasury.ExternalFees({ + feeType: keccak256(abi.encodePacked("processing_fee")), + feeAmount: 5e18 + }); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + lineItems, + externalFees + ); + + ICampaignPaymentTreasury.PaymentData memory paymentData = paymentTreasury.getPaymentData(PAYMENT_ID_1); + + assertEq(paymentData.buyerId, BUYER_ID_1); + assertEq(paymentData.itemId, ITEM_ID_1); + assertEq(paymentData.amount, PAYMENT_AMOUNT_1); + assertEq(paymentData.expiration, expiration); + assertFalse(paymentData.isConfirmed); + assertFalse(paymentData.isCryptoPayment); + assertEq(paymentData.lineItemCount, lineItems.length); + assertEq(paymentData.paymentToken, address(testToken)); + + assertEq(paymentData.lineItems.length, lineItems.length); + assertEq(paymentData.lineItems[0].typeId, SHIPPING_FEE_TYPE_ID); + assertEq(paymentData.lineItems[0].amount, shippingFeeAmount); + assertEq(keccak256(bytes(paymentData.lineItems[0].label)), keccak256(bytes("shipping_fee"))); + assertTrue(paymentData.lineItems[0].countsTowardGoal); + assertFalse(paymentData.lineItems[0].applyProtocolFee); + assertTrue(paymentData.lineItems[0].canRefund); + assertFalse(paymentData.lineItems[0].instantTransfer); + + assertEq(paymentData.lineItems[1].typeId, TIP_TYPE_ID); + assertEq(paymentData.lineItems[1].amount, tipAmount); + assertEq(keccak256(bytes(paymentData.lineItems[1].label)), keccak256(bytes("tip"))); + assertFalse(paymentData.lineItems[1].countsTowardGoal); + assertFalse(paymentData.lineItems[1].applyProtocolFee); + assertFalse(paymentData.lineItems[1].canRefund); + assertTrue(paymentData.lineItems[1].instantTransfer); + + assertEq(paymentData.externalFees.length, externalFees.length); + assertEq(paymentData.externalFees[0].feeType, externalFees[0].feeType); + assertEq(paymentData.externalFees[0].feeAmount, externalFees[0].feeAmount); + assertEq(paymentData.externalFees[1].feeType, externalFees[1].feeType); + assertEq(paymentData.externalFees[1].feeAmount, externalFees[1].feeAmount); + } + + function test_processCryptoPaymentRefundableNonGoalWithProtocolFee() public { + uint256 baseAmount = PAYMENT_AMOUNT_1; + uint256 lineItemAmount = 200e18; + uint256 totalAmount = baseAmount + lineItemAmount; + + deal(address(testToken), users.backer1Address, totalAmount); + + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, totalAmount); + + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = + ICampaignPaymentTreasury.LineItem({typeId: REFUNDABLE_FEE_WITH_PROTOCOL_TYPE_ID, amount: lineItemAmount}); + + bytes32 paymentId = keccak256("refundableFeePayment"); + + vm.prank(users.backer1Address); + paymentTreasury.processCryptoPayment( + paymentId, + ITEM_ID_1, + users.backer1Address, + address(testToken), + baseAmount, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + uint256 buyerBalanceAfterPayment = testToken.balanceOf(users.backer1Address); + + uint256 expectedFee = (lineItemAmount * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedNetLineItem = lineItemAmount - expectedFee; + uint256 expectedRefund = baseAmount + expectedNetLineItem; + + vm.prank(users.backer1Address); + // Approve treasury to burn NFT + CampaignInfo(campaignAddress).approve(address(paymentTreasury), 1); + paymentTreasury.claimRefund(paymentId); + + assertEq(testToken.balanceOf(users.backer1Address), buyerBalanceAfterPayment + expectedRefund); + assertEq(testToken.balanceOf(treasuryAddress), expectedFee); + } + + /*////////////////////////////////////////////////////////////// + LINE ITEMS - BATCH OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function test_createPaymentBatchWithLineItems() public { + 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 lineItemsArray = new ICampaignPaymentTreasury.LineItem[][](2); + + // First payment with shipping fee + lineItemsArray[0] = new ICampaignPaymentTreasury.LineItem[](1); + lineItemsArray[0][0] = ICampaignPaymentTreasury.LineItem({typeId: SHIPPING_FEE_TYPE_ID, amount: 50e18}); + + // Second payment with tip + lineItemsArray[1] = new ICampaignPaymentTreasury.LineItem[](1); + lineItemsArray[1][0] = ICampaignPaymentTreasury.LineItem({typeId: TIP_TYPE_ID, amount: 25e18}); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](2); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + externalFeesArray[1] = new ICampaignPaymentTreasury.ExternalFees[](0); + paymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, lineItemsArray, externalFeesArray + ); + + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function test_createPaymentBatchRevertWhenLineItemsArrayLengthMismatch() public { + 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; + + // Wrong length - only 1 item instead of 2 + ICampaignPaymentTreasury.LineItem[][] memory lineItemsArray = new ICampaignPaymentTreasury.LineItem[][](1); + lineItemsArray[0] = new ICampaignPaymentTreasury.LineItem[](0); + // Also wrong length for externalFeesArray to match the test intent + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](1); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, lineItemsArray, externalFeesArray + ); + } +} diff --git a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasury.t.sol b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasury.t.sol new file mode 100644 index 00000000..b25a5adf --- /dev/null +++ b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasury.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaymentTreasury.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 TimeConstrainedPaymentTreasury integration tests. +abstract contract TimeConstrainedPaymentTreasury_Integration_Shared_Test is LogDecoder, Base_Test { + address campaignAddress; + address treasuryAddress; + TimeConstrainedPaymentTreasury internal timeConstrainedPaymentTreasury; + + // 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; + + /// @dev Initial dependent functions setup included for TimeConstrainedPaymentTreasury 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 { + TimeConstrainedPaymentTreasury implementation = new TimeConstrainedPaymentTreasury(); + vm.startPrank(users.platform1AdminAddress); + treasuryFactory.registerTreasuryImplementation(platformHash, 3, address(implementation)); + vm.stopPrank(); + } + + function approveTreasuryImplementation(bytes32 platformHash) internal { + vm.startPrank(users.protocolAdminAddress); + treasuryFactory.approveTreasuryImplementation(platformHash, 3); + 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(); + + // 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, + 3 // TimeConstrainedPaymentTreasury type + ); + + timeConstrainedPaymentTreasury = TimeConstrainedPaymentTreasury(treasuryAddress); + vm.stopPrank(); + } + + /** + * @notice Helper function to advance time to within the allowed range + */ + 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 + buffer time + */ + function advanceToAfterDeadline() 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); + } +} diff --git a/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol new file mode 100644 index 00000000..f832b036 --- /dev/null +++ b/test/foundry/integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "./TimeConstrainedPaymentTreasury.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 {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaymentTreasury.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {TestToken} from "../../../mocks/TestToken.sol"; + +contract TimeConstrainedPaymentTreasuryFunction_Integration_Shared_Test is + TimeConstrainedPaymentTreasury_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); + } + + function test_createPayment() external { + advanceToWithinRange(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.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(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + assertEq(timeConstrainedPaymentTreasury.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); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](2); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + externalFeesArray[1] = new ICampaignPaymentTreasury.ExternalFees[](0); + timeConstrainedPaymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, emptyLineItemsArray, externalFeesArray + ); + + // Payments created successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_processCryptoPayment() external { + advanceToWithinRange(); + + // Approve tokens for the treasury + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payment processed successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + assertEq(timeConstrainedPaymentTreasury.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); + timeConstrainedPaymentTreasury.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); + timeConstrainedPaymentTreasury.cancelPayment(PAYMENT_ID_1); + + // Payment cancelled successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_confirmPayment() external { + advanceToWithinRange(); + + // Use a unique payment ID for this test + bytes32 uniquePaymentId = keccak256("confirmPaymentTest"); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + uniquePaymentId, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payment created and confirmed successfully by processCryptoPayment + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + assertEq(timeConstrainedPaymentTreasury.getAvailableRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function test_confirmPaymentBatch() external { + advanceToWithinRange(); + + // Use unique payment IDs for this test + bytes32 uniquePaymentId1 = keccak256("confirmPaymentBatchTest1"); + bytes32 uniquePaymentId2 = keccak256("confirmPaymentBatchTest2"); + + // Use processCryptoPayment for both payments which creates and confirms them + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + uniquePaymentId1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + vm.prank(users.backer2Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_2); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems2 = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + uniquePaymentId2, + ITEM_ID_2, + users.backer2Address, + address(testToken), + PAYMENT_AMOUNT_2, + emptyLineItems2, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payments created and confirmed successfully by processCryptoPayment + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2); + assertEq(timeConstrainedPaymentTreasury.getAvailableRaisedAmount(), PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2); + } + + function test_claimRefund() external { + // First create payment within the allowed time range + advanceToWithinRange(); + + // Use a unique payment ID for this test + bytes32 uniquePaymentId = keccak256("claimRefundTest"); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + uniquePaymentId, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch to be able to claim refund + advanceToAfterLaunch(); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(timeConstrainedPaymentTreasury), 1); // tokenId 1 + + // Then claim refund (use the overload without refundAddress since processCryptoPayment uses buyerAddress) + vm.prank(users.backer1Address); + timeConstrainedPaymentTreasury.claimRefund(uniquePaymentId); + + // Refund claimed successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + assertEq(timeConstrainedPaymentTreasury.getAvailableRaisedAmount(), 0); + } + + function test_disburseFees() external { + // First create payment within the allowed time range + advanceToWithinRange(); + + // Use a unique payment ID for this test + bytes32 uniquePaymentId = keccak256("disburseFeesTest"); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + uniquePaymentId, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch to be able to disburse fees + advanceToAfterLaunch(); + + // Then disburse fees + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.disburseFees(); + + // Fees disbursed successfully (no revert) + } + + function test_withdraw() external { + // First create payment within the allowed time range + advanceToWithinRange(); + + // Use a unique payment ID for this test + bytes32 uniquePaymentId = keccak256("withdrawTest"); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + uniquePaymentId, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch to be able to withdraw + advanceToAfterLaunch(); + + // Then withdraw + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.withdraw(); + + // Withdrawal successful (no revert) + } + + function test_timeConstraints_createPaymentBeforeLaunch() external { + advanceToBeforeLaunch(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_timeConstraints_createPaymentAfterDeadline() external { + advanceToAfterDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_timeConstraints_claimRefundBeforeLaunch() external { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + } + + function test_timeConstraints_disburseFeesBeforeLaunch() external { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.disburseFees(); + } + + function test_timeConstraints_withdrawBeforeLaunch() external { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.withdraw(); + } + + function test_bufferTimeRetrieval() external { + // Test that buffer time is correctly retrieved from GlobalParams + // We can't access _getBufferTime() directly, so we test it indirectly + // by checking that operations work within the buffer time window + // Use a time that should be within the allowed range + vm.warp(campaignDeadline - 1); // Use deadline - 1 to be within range + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at deadline - 1 + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_operationsAtDeadlinePlusBuffer() external { + // Test operations at the exact deadline + buffer time + vm.warp(campaignDeadline - 1); // Use deadline - 1 to be within range + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at deadline - 1 + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_operationsAfterDeadlinePlusBuffer() external { + // Test operations after deadline + buffer time + advanceToAfterDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function test_operationsAtExactLaunchTime() external { + // Test operations at the exact launch time + vm.warp(campaignLaunchTime); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at the exact launch time + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_operationsAtExactDeadline() external { + // Test operations at the exact deadline + vm.warp(campaignDeadline); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at the exact deadline + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function test_multipleTimeConstraintChecks() external { + // Test that multiple operations respect time constraints + advanceToWithinRange(); + + // Use a unique payment ID for this test + bytes32 uniquePaymentId = keccak256("multipleTimeConstraintChecksTest"); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.processCryptoPayment( + uniquePaymentId, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch time + advanceToAfterLaunch(); + + // Withdraw (should work after launch time) + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.withdraw(); + + // All operations should succeed + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } +} diff --git a/test/foundry/unit/CampaignInfo.t.sol b/test/foundry/unit/CampaignInfo.t.sol new file mode 100644 index 00000000..84241fe3 --- /dev/null +++ b/test/foundry/unit/CampaignInfo.t.sol @@ -0,0 +1,924 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import "forge-std/Test.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; +import {GlobalParams} from "src/GlobalParams.sol"; +import {TreasuryFactory} from "src/TreasuryFactory.sol"; +import {AllOrNothing} from "src/treasuries/AllOrNothing.sol"; +import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {ICampaignData} from "src/interfaces/ICampaignData.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {TestToken} from "../../mocks/TestToken.sol"; +import {Defaults} from "../Base.t.sol"; +import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; + +contract CampaignInfo_UnitTest is Test, Defaults { + CampaignInfo internal campaignInfo; + CampaignInfoFactory internal campaignInfoFactory; + GlobalParams internal globalParams; + TreasuryFactory internal treasuryFactory; + AllOrNothing internal allOrNothingImpl; + TestToken internal testToken; + + address internal admin = address(0xA11CE); + address internal campaignOwner = address(0xB22DE); + address internal newOwner = address(0xC33FF); + bytes32 internal platformHash1 = keccak256("platform1"); + bytes32 internal platformHash2 = keccak256("platform2"); + bytes32 internal platformDataKey1 = keccak256("key1"); + bytes32 internal platformDataKey2 = keccak256("key2"); + bytes32 internal platformDataValue1 = keccak256("value1"); + bytes32 internal platformDataValue2 = keccak256("value2"); + + function setUp() public { + testToken = new TestToken(tokenName, tokenSymbol, 18); + + // Setup currencies and tokens + bytes32[] memory currencies = new bytes32[](1); + currencies[0] = bytes32("USD"); + + address[][] memory tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](1); + tokensPerCurrency[0][0] = address(testToken); + + // Deploy GlobalParams with proxy + GlobalParams globalParamsImpl = new GlobalParams(); + bytes memory globalParamsInitData = abi.encodeWithSelector( + GlobalParams.initialize.selector, admin, PROTOCOL_FEE_PERCENT, currencies, tokensPerCurrency + ); + ERC1967Proxy globalParamsProxy = new ERC1967Proxy(address(globalParamsImpl), globalParamsInitData); + globalParams = GlobalParams(address(globalParamsProxy)); + + // Setup platforms in GlobalParams + vm.startPrank(admin); + globalParams.enlistPlatform(platformHash1, admin, 1000, address(0)); // 10% fee + globalParams.enlistPlatform(platformHash2, admin, 2000, address(0)); // 20% fee + + // Add platform data keys + globalParams.addPlatformData(platformHash1, platformDataKey1); + globalParams.addPlatformData(platformHash2, platformDataKey2); + vm.stopPrank(); + + // Deploy TreasuryFactory + TreasuryFactory treasuryFactoryImpl = new TreasuryFactory(); + bytes memory treasuryFactoryInitData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(address(globalParams))); + ERC1967Proxy treasuryFactoryProxy = new ERC1967Proxy(address(treasuryFactoryImpl), treasuryFactoryInitData); + treasuryFactory = TreasuryFactory(address(treasuryFactoryProxy)); + + // Deploy AllOrNothing implementation for testing + allOrNothingImpl = new AllOrNothing(); + + // Register and approve treasury implementation (after platform is enlisted) + vm.startPrank(admin); + treasuryFactory.registerTreasuryImplementation(platformHash1, 1, address(allOrNothingImpl)); + treasuryFactory.approveTreasuryImplementation(platformHash1, 1); + vm.stopPrank(); + + // Deploy CampaignInfoFactory + CampaignInfoFactory campaignInfoFactoryImpl = new CampaignInfoFactory(); + bytes memory campaignInfoFactoryInitData = abi.encodeWithSelector( + CampaignInfoFactory.initialize.selector, + admin, + IGlobalParams(address(globalParams)), + address(new CampaignInfo()), + address(treasuryFactory) + ); + ERC1967Proxy campaignInfoFactoryProxy = + new ERC1967Proxy(address(campaignInfoFactoryImpl), campaignInfoFactoryInitData); + campaignInfoFactory = CampaignInfoFactory(address(campaignInfoFactoryProxy)); + + // Create a campaign using the factory + ICampaignData.CampaignData memory campaignData = ICampaignData.CampaignData({ + launchTime: block.timestamp + 1 days, + deadline: block.timestamp + 30 days, + goalAmount: 1000 * 10 ** 18, + currency: bytes32("USD") + }); + + bytes32[] memory selectedPlatformHashes = new bytes32[](0); // No platforms selected initially + bytes32[] memory platformDataKeys = new bytes32[](0); + bytes32[] memory platformDataValues = new bytes32[](0); + bytes32 identifierHash = keccak256("test-campaign"); + + vm.startPrank(admin); + campaignInfoFactory.createCampaign( + campaignOwner, + identifierHash, + selectedPlatformHashes, + platformDataKeys, + platformDataValues, + campaignData, + "Test Campaign NFT", + "TCNFT", + "ipfs://QmTest123", + "ipfs://QmContractTest123" + ); + vm.stopPrank(); + + campaignInfo = CampaignInfo(campaignInfoFactory.identifierToCampaignInfo(identifierHash)); + } + + // ============ View Functions Tests ============ + + function test_Owner() public { + assertEq(campaignInfo.owner(), campaignOwner); + } + + function test_IsLocked_InitiallyFalse() public { + assertFalse(campaignInfo.isLocked()); + } + + function test_GetLaunchTime() public { + uint256 launchTime = campaignInfo.getLaunchTime(); + assertTrue(launchTime > 0); + } + + function test_GetDeadline() public { + uint256 deadline = campaignInfo.getDeadline(); + assertTrue(deadline > campaignInfo.getLaunchTime()); + } + + function test_GetGoalAmount() public { + uint256 goalAmount = campaignInfo.getGoalAmount(); + assertEq(goalAmount, 1000 * 10 ** 18); + } + + function test_GetCampaignCurrency() public { + bytes32 currency = campaignInfo.getCampaignCurrency(); + assertEq(currency, bytes32("USD")); + } + + function test_GetAcceptedTokens() public { + address[] memory tokens = campaignInfo.getAcceptedTokens(); + assertEq(tokens.length, 1); + assertEq(tokens[0], address(testToken)); + } + + function test_IsTokenAccepted() public { + assertTrue(campaignInfo.isTokenAccepted(address(testToken))); + assertFalse(campaignInfo.isTokenAccepted(address(0x1234))); + } + + function test_GetPlatformFeePercent() public { + // Initially no platforms selected, should return 0 + assertEq(campaignInfo.getPlatformFeePercent(platformHash1), 0); + } + + function test_GetPlatformData_NotSet() public { + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.getPlatformData(platformDataKey1); + } + + function test_CheckIfPlatformSelected_InitiallyFalse() public { + assertFalse(campaignInfo.checkIfPlatformSelected(platformHash1)); + } + + function test_CheckIfPlatformApproved_InitiallyFalse() public { + assertFalse(campaignInfo.checkIfPlatformApproved(platformHash1)); + } + + function test_GetApprovedPlatformHashes_InitiallyEmpty() public { + bytes32[] memory approvedPlatforms = campaignInfo.getApprovedPlatformHashes(); + assertEq(approvedPlatforms.length, 0); + } + + function test_Paused_InitiallyFalse() public { + assertFalse(campaignInfo.paused()); + } + + function test_Cancelled_InitiallyFalse() public { + assertFalse(campaignInfo.cancelled()); + } + + // ============ UpdateSelectedPlatform Tests ============ + + function test_UpdateSelectedPlatform_SelectPlatform_Success() public { + vm.startPrank(campaignOwner); + + bytes32[] memory dataKeys = new bytes32[](2); + dataKeys[0] = platformDataKey1; + dataKeys[1] = platformDataKey2; + + bytes32[] memory dataValues = new bytes32[](2); + dataValues[0] = platformDataValue1; + dataValues[1] = platformDataValue2; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + + // Verify platform is selected + assertTrue(campaignInfo.checkIfPlatformSelected(platformHash1)); + + // Verify platform data is stored + assertEq(campaignInfo.getPlatformData(platformDataKey1), platformDataValue1); + assertEq(campaignInfo.getPlatformData(platformDataKey2), platformDataValue2); + + // Verify platform fee is set + assertEq(campaignInfo.getPlatformFeePercent(platformHash1), 1000); + + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_InvalidPlatform_Reverts() public { + vm.startPrank(campaignOwner); + + bytes32 invalidPlatformHash = keccak256("invalid"); + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + vm.expectRevert( + abi.encodeWithSelector(CampaignInfo.CampaignInfoInvalidPlatformUpdate.selector, invalidPlatformHash, true) + ); + campaignInfo.updateSelectedPlatform(invalidPlatformHash, true, dataKeys, dataValues); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_AlreadySelected_Reverts() public { + vm.startPrank(campaignOwner); + + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + // Select platform first time + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + + // Try to select again - should revert + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_DataKeyValueLengthMismatch_Reverts() public { + vm.startPrank(campaignOwner); + + bytes32[] memory dataKeys = new bytes32[](2); + dataKeys[0] = platformDataKey1; + dataKeys[1] = platformDataKey2; + + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_InvalidDataKey_Reverts() public { + vm.startPrank(campaignOwner); + + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = keccak256("invalid_key"); + + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_ZeroDataValue_Reverts() public { + vm.startPrank(campaignOwner); + + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = bytes32(0); + + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_Unauthorized_Reverts() public { + address unauthorizedUser = address(0xC33FF); + + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + vm.startPrank(unauthorizedUser); + vm.expectRevert(); + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + } + + // ============ Update Functions Tests ============ + + function test_UpdateLaunchTime_Success() public { + vm.startPrank(campaignOwner); + + uint256 newLaunchTime = block.timestamp + 1 days; + + vm.expectEmit(true, false, false, true); + emit CampaignInfo.CampaignInfoLaunchTimeUpdated(newLaunchTime); + + campaignInfo.updateLaunchTime(newLaunchTime); + + assertEq(campaignInfo.getLaunchTime(), newLaunchTime); + vm.stopPrank(); + } + + function test_UpdateLaunchTime_InvalidTime_Reverts() public { + vm.startPrank(campaignOwner); + + // Launch time in the past + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateLaunchTime(block.timestamp - 1); + + vm.stopPrank(); + } + + function test_UpdateLaunchTime_ViolatesMinimumDuration_Reverts() public { + // Set minimum campaign duration to 1 day + vm.startPrank(admin); + globalParams.addToRegistry( + DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION, + bytes32(uint256(1 days)) + ); + vm.stopPrank(); + + vm.startPrank(campaignOwner); + + uint256 currentDeadline = campaignInfo.getDeadline(); + + // Try to update launch time to a value that would make duration less than minimum (1 day) + // If deadline is at currentLaunchTime + 29 days, and we move launch time forward, + // the duration would be less than 1 day, which violates the minimum + uint256 newLaunchTime = currentDeadline - 12 hours; // Only 12 hours duration, less than 1 day minimum + + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateLaunchTime(newLaunchTime); + + vm.stopPrank(); + } + + function test_UpdateLaunchTime_MeetsMinimumDuration_Success() public { + // Set minimum campaign duration to 1 day + vm.startPrank(admin); + globalParams.addToRegistry( + DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION, + bytes32(uint256(1 days)) + ); + vm.stopPrank(); + + vm.startPrank(campaignOwner); + + uint256 currentDeadline = campaignInfo.getDeadline(); + + // Update launch time to a value that still meets the minimum duration requirement + // Leave at least 1 day between new launch time and deadline + uint256 newLaunchTime = currentDeadline - 2 days; // 2 days duration, meets 1 day minimum + + vm.expectEmit(true, false, false, true); + emit CampaignInfo.CampaignInfoLaunchTimeUpdated(newLaunchTime); + + campaignInfo.updateLaunchTime(newLaunchTime); + + assertEq(campaignInfo.getLaunchTime(), newLaunchTime); + vm.stopPrank(); + } + + function test_UpdateLaunchTime_MoveCloserToNow_Success() public { + // Set minimum campaign duration to 1 day + vm.startPrank(admin); + globalParams.addToRegistry( + DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION, + bytes32(uint256(1 days)) + ); + vm.stopPrank(); + + vm.startPrank(campaignOwner); + + uint256 originalLaunchTime = campaignInfo.getLaunchTime(); + uint256 currentDeadline = campaignInfo.getDeadline(); + + // Scenario: Initially set launchTime far in future, but now want to move it closer to now + // This should be allowed as long as: + // 1. New launchTime >= block.timestamp (not in the past) + // 2. Deadline still meets minimum duration requirement + + // First, move launch time further away to simulate initial far-future setup + uint256 farFutureLaunchTime = block.timestamp + 20 days; + if (currentDeadline >= farFutureLaunchTime + 1 days) { + campaignInfo.updateLaunchTime(farFutureLaunchTime); + originalLaunchTime = farFutureLaunchTime; + } + + // Now move launch time closer to now (but still in future and meeting minimum duration) + uint256 newLaunchTime = block.timestamp + 2 days; // Move to 2 days from now + + // Ensure deadline still meets minimum duration + require(currentDeadline >= newLaunchTime + 1 days, "Test setup: deadline must meet minimum duration"); + + vm.expectEmit(true, false, false, true); + emit CampaignInfo.CampaignInfoLaunchTimeUpdated(newLaunchTime); + + campaignInfo.updateLaunchTime(newLaunchTime); + + assertEq(campaignInfo.getLaunchTime(), newLaunchTime); + assertLt(newLaunchTime, originalLaunchTime, "Launch time should be moved closer to now"); + vm.stopPrank(); + } + + function test_UpdateDeadline_Success() public { + vm.startPrank(campaignOwner); + + uint256 newDeadline = campaignInfo.getLaunchTime() + 60 days; + + vm.expectEmit(true, false, false, true); + emit CampaignInfo.CampaignInfoDeadlineUpdated(newDeadline); + + campaignInfo.updateDeadline(newDeadline); + + assertEq(campaignInfo.getDeadline(), newDeadline); + vm.stopPrank(); + } + + function test_UpdateGoalAmount_Success() public { + vm.startPrank(campaignOwner); + + uint256 newGoalAmount = 2000 * 10 ** 18; + + vm.expectEmit(true, false, false, true); + emit CampaignInfo.CampaignInfoGoalAmountUpdated(newGoalAmount); + + campaignInfo.updateGoalAmount(newGoalAmount); + + assertEq(campaignInfo.getGoalAmount(), newGoalAmount); + vm.stopPrank(); + } + + function test_UpdateGoalAmount_ZeroAmount_Reverts() public { + vm.startPrank(campaignOwner); + + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateGoalAmount(0); + + vm.stopPrank(); + } + + // ============ Transfer Ownership Tests ============ + + function test_TransferOwnership_Success() public { + vm.startPrank(campaignOwner); + + campaignInfo.transferOwnership(newOwner); + + assertEq(campaignInfo.owner(), newOwner); + vm.stopPrank(); + } + + function test_TransferOwnership_WhenPaused_Reverts() public { + // Pause the campaign + vm.startPrank(admin); + campaignInfo._pauseCampaign(keccak256("test")); + vm.stopPrank(); + + vm.startPrank(campaignOwner); + vm.expectRevert(); + campaignInfo.transferOwnership(newOwner); + vm.stopPrank(); + } + + function test_TransferOwnership_WhenCancelled_Reverts() public { + // Cancel the campaign + vm.startPrank(admin); + campaignInfo._cancelCampaign(keccak256("test")); + vm.stopPrank(); + + vm.startPrank(campaignOwner); + vm.expectRevert(); + campaignInfo.transferOwnership(newOwner); + vm.stopPrank(); + } + + // ============ Admin Functions Tests ============ + + function test_PauseCampaign_Success() public { + vm.startPrank(admin); + + bytes32 message = keccak256("test pause"); + campaignInfo._pauseCampaign(message); + + assertTrue(campaignInfo.paused()); + vm.stopPrank(); + } + + function test_UnpauseCampaign_Success() public { + // First pause + vm.startPrank(admin); + campaignInfo._pauseCampaign(keccak256("test pause")); + vm.stopPrank(); + + // Then unpause + vm.startPrank(admin); + bytes32 message = keccak256("test unpause"); + campaignInfo._unpauseCampaign(message); + + assertFalse(campaignInfo.paused()); + vm.stopPrank(); + } + + function test_CancelCampaign_ByAdmin_Success() public { + vm.startPrank(admin); + + bytes32 message = keccak256("test cancel"); + campaignInfo._cancelCampaign(message); + + assertTrue(campaignInfo.cancelled()); + vm.stopPrank(); + } + + function test_CancelCampaign_ByOwner_Success() public { + vm.startPrank(campaignOwner); + + bytes32 message = keccak256("test cancel"); + campaignInfo._cancelCampaign(message); + + assertTrue(campaignInfo.cancelled()); + vm.stopPrank(); + } + + function test_CancelCampaign_Unauthorized_Reverts() public { + address unauthorizedUser = address(0xD44F); + + vm.startPrank(unauthorizedUser); + vm.expectRevert(CampaignInfo.CampaignInfoUnauthorized.selector); + campaignInfo._cancelCampaign(keccak256("test cancel")); + vm.stopPrank(); + } + + // ============ Locked Functionality Tests ============ + + function test_UpdateSelectedPlatform_SelectPlatform_WhenNotLocked_Success() public { + // Test that platform selection works when not locked + vm.startPrank(campaignOwner); + + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + + // Verify platform is selected + assertTrue(campaignInfo.checkIfPlatformSelected(platformHash1)); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_DeselectPlatform_WhenNotLocked_Success() public { + // First select a platform + vm.startPrank(campaignOwner); + + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + + // Now deselect it + campaignInfo.updateSelectedPlatform(platformHash1, false, new bytes32[](0), new bytes32[](0)); + + // Verify platform is not selected + assertFalse(campaignInfo.checkIfPlatformSelected(platformHash1)); + + // Verify platform fee is reset to 0 + assertEq(campaignInfo.getPlatformFeePercent(platformHash1), 0); + + vm.stopPrank(); + } + + function test_UpdateLaunchTime_WhenNotLocked_Success() public { + // Test that launch time update works when not locked + vm.startPrank(campaignOwner); + + uint256 newLaunchTime = block.timestamp + 1 days; + + campaignInfo.updateLaunchTime(newLaunchTime); + + assertEq(campaignInfo.getLaunchTime(), newLaunchTime); + vm.stopPrank(); + } + + function test_UpdateDeadline_WhenNotLocked_Success() public { + // Test that deadline update works when not locked + vm.startPrank(campaignOwner); + + uint256 newDeadline = campaignInfo.getLaunchTime() + 60 days; + + campaignInfo.updateDeadline(newDeadline); + + assertEq(campaignInfo.getDeadline(), newDeadline); + vm.stopPrank(); + } + + function test_UpdateGoalAmount_WhenNotLocked_Success() public { + // Test that goal amount update works when not locked + vm.startPrank(campaignOwner); + + uint256 newGoalAmount = 2000 * 10 ** 18; + + campaignInfo.updateGoalAmount(newGoalAmount); + + assertEq(campaignInfo.getGoalAmount(), newGoalAmount); + vm.stopPrank(); + } + + // ============ Locked Campaign Tests ============ + + function test_LockMechanism_AfterTreasuryDeployment() public { + // First select a platform + vm.startPrank(campaignOwner); + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + + // Verify campaign is not locked initially + assertFalse(campaignInfo.isLocked()); + + // Deploy a treasury using the treasury factory - this will call _setPlatformInfo + vm.startPrank(admin); + address treasury = treasuryFactory.deploy( + platformHash1, + address(campaignInfo), + 1 // implementationId + ); + vm.stopPrank(); + + // Verify campaign is now locked + assertTrue(campaignInfo.isLocked()); + assertTrue(campaignInfo.checkIfPlatformApproved(platformHash1)); + } + + function test_UpdateLaunchTime_WhenLocked_Reverts() public { + // Lock the campaign first + _lockCampaign(); + + vm.startPrank(campaignOwner); + uint256 newLaunchTime = block.timestamp + 1 days; + + vm.expectRevert(CampaignInfo.CampaignInfoIsLocked.selector); + campaignInfo.updateLaunchTime(newLaunchTime); + vm.stopPrank(); + } + + function test_UpdateDeadline_WhenLocked_Reverts() public { + // Lock the campaign first + _lockCampaign(); + + vm.startPrank(campaignOwner); + uint256 newDeadline = campaignInfo.getLaunchTime() + 60 days; + + vm.expectRevert(CampaignInfo.CampaignInfoIsLocked.selector); + campaignInfo.updateDeadline(newDeadline); + vm.stopPrank(); + } + + function test_UpdateGoalAmount_WhenLocked_Reverts() public { + // Lock the campaign first + _lockCampaign(); + + vm.startPrank(campaignOwner); + uint256 newGoalAmount = 2000 * 10 ** 18; + + vm.expectRevert(CampaignInfo.CampaignInfoIsLocked.selector); + campaignInfo.updateGoalAmount(newGoalAmount); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_DeselectPlatform_WhenLocked_Reverts() public { + // First select a platform and lock the campaign + vm.startPrank(campaignOwner); + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + + // Lock the campaign by deploying treasury + vm.startPrank(admin); + treasuryFactory.deploy( + platformHash1, + address(campaignInfo), + 1 // implementationId + ); + vm.stopPrank(); + + // Now try to deselect the platform - should revert with already approved error + vm.startPrank(campaignOwner); + vm.expectRevert( + abi.encodeWithSelector(CampaignInfo.CampaignInfoPlatformAlreadyApproved.selector, platformHash1) + ); + campaignInfo.updateSelectedPlatform(platformHash1, false, new bytes32[](0), new bytes32[](0)); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_SelectNewPlatform_WhenLocked_Success() public { + // Lock the campaign first + _lockCampaign(); + + // Selecting a new platform should still work when locked + vm.startPrank(campaignOwner); + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey2; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue2; + + campaignInfo.updateSelectedPlatform(platformHash2, true, dataKeys, dataValues); + + // Verify platform is selected + assertTrue(campaignInfo.checkIfPlatformSelected(platformHash2)); + assertEq(campaignInfo.getPlatformFeePercent(platformHash2), 2000); // 20% fee + vm.stopPrank(); + } + + function test_TransferOwnership_WhenLocked_Success() public { + // Lock the campaign first + _lockCampaign(); + + // Transfer ownership should still work when locked + vm.startPrank(campaignOwner); + campaignInfo.transferOwnership(newOwner); + + assertEq(campaignInfo.owner(), newOwner); + vm.stopPrank(); + } + + function test_PauseCampaign_WhenLocked_Success() public { + // Lock the campaign first + _lockCampaign(); + + // Pausing should still work when locked + vm.startPrank(admin); + bytes32 message = keccak256("test pause"); + campaignInfo._pauseCampaign(message); + + assertTrue(campaignInfo.paused()); + vm.stopPrank(); + } + + function test_UnpauseCampaign_WhenLocked_Success() public { + // Lock the campaign first + _lockCampaign(); + + // First pause + vm.startPrank(admin); + campaignInfo._pauseCampaign(keccak256("test pause")); + vm.stopPrank(); + + // Then unpause - should still work when locked + vm.startPrank(admin); + bytes32 message = keccak256("test unpause"); + campaignInfo._unpauseCampaign(message); + + assertFalse(campaignInfo.paused()); + vm.stopPrank(); + } + + function test_CancelCampaign_ByAdmin_WhenLocked_Success() public { + // Lock the campaign first + _lockCampaign(); + + // Cancelling should still work when locked + vm.startPrank(admin); + bytes32 message = keccak256("test cancel"); + campaignInfo._cancelCampaign(message); + + assertTrue(campaignInfo.cancelled()); + vm.stopPrank(); + } + + function test_CancelCampaign_ByOwner_WhenLocked_Success() public { + // Lock the campaign first + _lockCampaign(); + + // Cancelling should still work when locked + vm.startPrank(campaignOwner); + bytes32 message = keccak256("test cancel"); + campaignInfo._cancelCampaign(message); + + assertTrue(campaignInfo.cancelled()); + vm.stopPrank(); + } + + function test_ViewFunctions_WhenLocked_StillWork() public { + // Lock the campaign first + _lockCampaign(); + + // All view functions should still work when locked + assertTrue(campaignInfo.isLocked()); + assertEq(campaignInfo.owner(), campaignOwner); + assertTrue(campaignInfo.getLaunchTime() > 0); + assertTrue(campaignInfo.getDeadline() > campaignInfo.getLaunchTime()); + assertEq(campaignInfo.getGoalAmount(), 1000 * 10 ** 18); + assertEq(campaignInfo.getCampaignCurrency(), bytes32("USD")); + assertFalse(campaignInfo.paused()); + assertFalse(campaignInfo.cancelled()); + } + + function test_PlatformOperations_WhenLocked_StillWork() public { + // Lock the campaign first + _lockCampaign(); + + // Platform-related view functions should still work + assertFalse(campaignInfo.checkIfPlatformSelected(platformHash2)); + assertFalse(campaignInfo.checkIfPlatformApproved(platformHash2)); + + bytes32[] memory approvedPlatforms = campaignInfo.getApprovedPlatformHashes(); + assertEq(approvedPlatforms.length, 1); + assertEq(approvedPlatforms[0], platformHash1); + } + + function test_UpdateSelectedPlatform_AlreadyApproved_WhenLocked_Reverts() public { + // First select and approve a platform (this locks the campaign) + vm.startPrank(campaignOwner); + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + + // Approve the platform (this locks the campaign) + vm.startPrank(admin); + treasuryFactory.deploy( + platformHash1, + address(campaignInfo), + 1 // implementationId + ); + vm.stopPrank(); + + // Now try to deselect the already approved platform - should revert with already approved error + vm.startPrank(campaignOwner); + vm.expectRevert( + abi.encodeWithSelector(CampaignInfo.CampaignInfoPlatformAlreadyApproved.selector, platformHash1) + ); + campaignInfo.updateSelectedPlatform(platformHash1, false, new bytes32[](0), new bytes32[](0)); + vm.stopPrank(); + } + + function test_UpdateSelectedPlatform_SelectApprovedPlatform_WhenLocked_Reverts() public { + // First select and approve a platform (this locks the campaign) + vm.startPrank(campaignOwner); + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + + // Approve the platform (this locks the campaign) + vm.startPrank(address(treasuryFactory)); + campaignInfo._setPlatformInfo(platformHash1, address(0x1234)); + vm.stopPrank(); + + // Now try to select the already approved platform again - should revert + vm.startPrank(campaignOwner); + vm.expectRevert(CampaignInfo.CampaignInfoInvalidInput.selector); + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + } + + // Helper function to lock the campaign + function _lockCampaign() internal { + // First select a platform + vm.startPrank(campaignOwner); + bytes32[] memory dataKeys = new bytes32[](1); + dataKeys[0] = platformDataKey1; + bytes32[] memory dataValues = new bytes32[](1); + dataValues[0] = platformDataValue1; + + campaignInfo.updateSelectedPlatform(platformHash1, true, dataKeys, dataValues); + vm.stopPrank(); + + // Then deploy a treasury (this locks the campaign) + vm.startPrank(admin); + treasuryFactory.deploy( + platformHash1, + address(campaignInfo), + 1 // implementationId + ); + vm.stopPrank(); + } +} diff --git a/test/foundry/unit/CampaignInfoFactory.t.sol b/test/foundry/unit/CampaignInfoFactory.t.sol index 803a83bb..234379e4 100644 --- a/test/foundry/unit/CampaignInfoFactory.t.sol +++ b/test/foundry/unit/CampaignInfoFactory.t.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import "forge-std/Test.sol"; import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; import {GlobalParams} from "src/GlobalParams.sol"; import {TreasuryFactory} from "src/TreasuryFactory.sol"; +import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {TestToken} from "../../mocks/TestToken.sol"; import {Defaults} from "../Base.t.sol"; import {ICampaignData} from "src/interfaces/ICampaignData.sol"; import {CampaignInfo} from "src/CampaignInfo.sol"; +import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; contract CampaignInfoFactory_UnitTest is Test, Defaults { CampaignInfoFactory internal factory; @@ -20,38 +23,61 @@ contract CampaignInfoFactory_UnitTest is Test, Defaults { address internal admin = address(0xA11CE); function setUp() public { - testToken = new TestToken(tokenName, tokenSymbol); - globalParams = new GlobalParams( - admin, - address(testToken), - PROTOCOL_FEE_PERCENT + testToken = new TestToken(tokenName, tokenSymbol, 18); + + // Setup currencies and tokens for multi-token support + bytes32[] memory currencies = new bytes32[](1); + currencies[0] = bytes32("USD"); + + address[][] memory tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](1); + tokensPerCurrency[0][0] = address(testToken); + + // Deploy GlobalParams with proxy + GlobalParams globalParamsImpl = new GlobalParams(); + bytes memory globalParamsInitData = abi.encodeWithSelector( + GlobalParams.initialize.selector, admin, PROTOCOL_FEE_PERCENT, currencies, tokensPerCurrency ); - campaignInfoImplementation = new CampaignInfo(address(this)); - treasuryFactory = new TreasuryFactory(globalParams); - factory = new CampaignInfoFactory( - globalParams, - address(campaignInfoImplementation) + ERC1967Proxy globalParamsProxy = new ERC1967Proxy(address(globalParamsImpl), globalParamsInitData); + globalParams = GlobalParams(address(globalParamsProxy)); + + // Deploy CampaignInfo implementation + campaignInfoImplementation = new CampaignInfo(); + + // Deploy TreasuryFactory with proxy + TreasuryFactory treasuryFactoryImpl = new TreasuryFactory(); + bytes memory treasuryFactoryInitData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(address(globalParams))); + ERC1967Proxy treasuryFactoryProxy = new ERC1967Proxy(address(treasuryFactoryImpl), treasuryFactoryInitData); + treasuryFactory = TreasuryFactory(address(treasuryFactoryProxy)); + + // Deploy CampaignInfoFactory with proxy + CampaignInfoFactory factoryImpl = new CampaignInfoFactory(); + bytes memory factoryInitData = abi.encodeWithSelector( + CampaignInfoFactory.initialize.selector, + address(this), + IGlobalParams(address(globalParams)), + address(campaignInfoImplementation), + address(treasuryFactory) ); + ERC1967Proxy factoryProxy = new ERC1967Proxy(address(factoryImpl), factoryInitData); + factory = CampaignInfoFactory(address(factoryProxy)); + vm.startPrank(admin); globalParams.enlistPlatform( PLATFORM_1_HASH, admin, - PLATFORM_FEE_PERCENT + PLATFORM_FEE_PERCENT, + address(0) // Platform adapter - can be set later with setPlatformAdapter ); - vm.stopPrank(); - } - function testInitializeSetsTreasuryAndGlobalParams() public { - // vm.startPrank(address(this)); // this is owner - - factory._initialize(address(treasuryFactory), address(globalParams)); - // Success assumed if no revert - // vm.stopPrank(); + // Set time constraints in dataRegistry + globalParams.addToRegistry(DataRegistryKeys.CAMPAIGN_LAUNCH_BUFFER, bytes32(uint256(1 hours))); + globalParams.addToRegistry(DataRegistryKeys.MINIMUM_CAMPAIGN_DURATION, bytes32(uint256(1 days))); + vm.stopPrank(); } function testCreateCampaignDeploysSuccessfully() public { - factory._initialize(address(treasuryFactory), address(globalParams)); - bytes32[] memory platforms = new bytes32[](1); platforms[0] = PLATFORM_1_HASH; @@ -69,7 +95,11 @@ contract CampaignInfoFactory_UnitTest is Test, Defaults { platforms, keys, values, - CAMPAIGN_DATA + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" ); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -79,9 +109,7 @@ contract CampaignInfoFactory_UnitTest is Test, Defaults { assertGt(logs.length, 0, "Expected at least one log"); // Decode expected event - bytes32 eventSig = keccak256( - "CampaignInfoFactoryCampaignCreated(bytes32,address)" - ); + bytes32 eventSig = keccak256("CampaignInfoFactoryCampaignCreated(bytes32,address)"); bool found = false; address campaignAddr; @@ -98,15 +126,128 @@ contract CampaignInfoFactory_UnitTest is Test, Defaults { assertTrue(campaignAddr != address(0), "Invalid campaign address"); // Check that campaign was stored in mapping - address storedCampaign = factory.identifierToCampaignInfo( - CAMPAIGN_1_IDENTIFIER_HASH - ); + address storedCampaign = factory.identifierToCampaignInfo(CAMPAIGN_1_IDENTIFIER_HASH); assertEq(storedCampaign, campaignAddr, "Stored campaign doesn't match"); // Check that it's valid - assertTrue( - factory.isValidCampaignInfo(campaignAddr), - "Campaign not marked valid" + assertTrue(factory.isValidCampaignInfo(campaignAddr), "Campaign not marked valid"); + } + + function testUpgrade() public { + // Deploy new implementation + CampaignInfoFactory newImplementation = new CampaignInfoFactory(); + + // Upgrade as owner (address(this)) + factory.upgradeToAndCall(address(newImplementation), ""); + + // Factory should still work after upgrade + bytes32[] memory platforms = new bytes32[](1); + platforms[0] = PLATFORM_1_HASH; + + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + + address creator = address(0xBEEF); + + vm.prank(admin); + factory.createCampaign( + creator, + CAMPAIGN_1_IDENTIFIER_HASH, + platforms, + keys, + values, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + } + + function testUpgradeUnauthorizedReverts() public { + // Deploy new implementation + CampaignInfoFactory newImplementation = new CampaignInfoFactory(); + + // Try to upgrade as non-owner (should revert) + vm.prank(admin); + vm.expectRevert(); + factory.upgradeToAndCall(address(newImplementation), ""); + } + + function testCannotInitializeTwice() public { + // Try to initialize again (should revert) + vm.expectRevert(); + factory.initialize( + address(this), + IGlobalParams(address(globalParams)), + address(campaignInfoImplementation), + address(treasuryFactory) + ); + } + + function testCreateCampaignRevertsWithInsufficientLaunchBuffer() public { + bytes32[] memory platforms = new bytes32[](1); + platforms[0] = PLATFORM_1_HASH; + + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + + address creator = address(0xBEEF); + + // Create campaign data with launch time less than required buffer (1 hour) + ICampaignData.CampaignData memory campaignData = ICampaignData.CampaignData({ + launchTime: block.timestamp + 30 minutes, // Only 30 minutes buffer + deadline: block.timestamp + 30 minutes + 7 days, + goalAmount: GOAL_AMOUNT, + currency: bytes32("USD") + }); + + vm.prank(admin); + vm.expectRevert(CampaignInfoFactory.CampaignInfoFactoryInvalidInput.selector); + factory.createCampaign( + creator, + CAMPAIGN_1_IDENTIFIER_HASH, + platforms, + keys, + values, + campaignData, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + } + + function testCreateCampaignRevertsWithInsufficientDuration() public { + bytes32[] memory platforms = new bytes32[](1); + platforms[0] = PLATFORM_1_HASH; + + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + + address creator = address(0xBEEF); + + // Create campaign data with duration less than minimum (1 day) + ICampaignData.CampaignData memory campaignData = ICampaignData.CampaignData({ + launchTime: block.timestamp + 2 hours, // Good buffer + deadline: block.timestamp + 2 hours + 12 hours, // Only 12 hours duration + goalAmount: GOAL_AMOUNT, + currency: bytes32("USD") + }); + + vm.prank(admin); + vm.expectRevert(CampaignInfoFactory.CampaignInfoFactoryInvalidInput.selector); + factory.createCampaign( + creator, + CAMPAIGN_1_IDENTIFIER_HASH, + platforms, + keys, + values, + campaignData, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" ); } } diff --git a/test/foundry/unit/GlobalParams.t.sol b/test/foundry/unit/GlobalParams.t.sol index 05c82ffa..4fece932 100644 --- a/test/foundry/unit/GlobalParams.t.sol +++ b/test/foundry/unit/GlobalParams.t.sol @@ -1,27 +1,73 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import "forge-std/Test.sol"; import {GlobalParams} from "src/GlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {Defaults} from "../Base.t.sol"; import {TestToken} from "../../mocks/TestToken.sol"; -contract GlobalParams_UnitTest is Test, Defaults{ +contract GlobalParams_UnitTest is Test, Defaults { GlobalParams internal globalParams; - TestToken internal token; + GlobalParams internal implementation; + TestToken internal token1; + TestToken internal token2; + TestToken internal token3; address internal admin = address(0xA11CE); uint256 internal protocolFee = 300; // 3% + bytes32 internal constant USD = bytes32("USD"); + bytes32 internal constant EUR = bytes32("EUR"); + bytes32 internal constant BRL = bytes32("BRL"); + function setUp() public { - token = new TestToken(tokenName, tokenSymbol); - globalParams = new GlobalParams(admin, address(token), protocolFee); + token1 = new TestToken("Token1", "TK1", 18); + token2 = new TestToken("Token2", "TK2", 18); + token3 = new TestToken("Token3", "TK3", 18); + + // Setup initial currencies and tokens + bytes32[] memory currencies = new bytes32[](2); + currencies[0] = USD; + currencies[1] = EUR; + + address[][] memory tokensPerCurrency = new address[][](2); + tokensPerCurrency[0] = new address[](2); + tokensPerCurrency[0][0] = address(token1); + tokensPerCurrency[0][1] = address(token2); + + tokensPerCurrency[1] = new address[](1); + tokensPerCurrency[1][0] = address(token3); + + // Deploy implementation + implementation = new GlobalParams(); + + // Prepare initialization data + bytes memory initData = + abi.encodeWithSelector(GlobalParams.initialize.selector, admin, protocolFee, currencies, tokensPerCurrency); + + // Deploy proxy + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); + globalParams = GlobalParams(address(proxy)); } function testInitialValues() public { assertEq(globalParams.getProtocolAdminAddress(), admin); - assertEq(globalParams.getTokenAddress(), address(token)); assertEq(globalParams.getProtocolFeePercent(), protocolFee); + + // Test USD tokens + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 2); + assertEq(usdTokens[0], address(token1)); + assertEq(usdTokens[1], address(token2)); + + // Test EUR tokens + address[] memory eurTokens = globalParams.getTokensForCurrency(EUR); + assertEq(eurTokens.length, 1); + assertEq(eurTokens[0], address(token3)); + + // Token validation is done by checking if token is in the returned array + // This is handled by the getTokensForCurrency function above } function testSetProtocolAdmin() public { @@ -31,24 +77,334 @@ contract GlobalParams_UnitTest is Test, Defaults{ assertEq(globalParams.getProtocolAdminAddress(), newAdmin); } - function testSetAcceptedToken() public { - address newToken = address(0xDEAD); - vm.prank(admin); - globalParams.updateTokenAddress(newToken); - assertEq(globalParams.getTokenAddress(), newToken); - } - function testSetProtocolFeePercent() public { vm.prank(admin); globalParams.updateProtocolFeePercent(500); // 5% assertEq(globalParams.getProtocolFeePercent(), 500); } + function testAddTokenToCurrency() public { + TestToken newToken = new TestToken("NewToken", "NEW", 18); + + vm.prank(admin); + globalParams.addTokenToCurrency(USD, address(newToken)); + + // Verify token was added + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 3); + assertEq(usdTokens[2], address(newToken)); + } + + function testAddTokenToNewCurrency() public { + TestToken newToken = new TestToken("BRLToken", "BRL", 18); + + vm.prank(admin); + globalParams.addTokenToCurrency(BRL, address(newToken)); + + // Verify token was added to new currency + address[] memory brlTokens = globalParams.getTokensForCurrency(BRL); + assertEq(brlTokens.length, 1); + assertEq(brlTokens[0], address(newToken)); + } + + function testAddTokenRevertWhenNotOwner() public { + TestToken newToken = new TestToken("NewToken", "NEW", 18); + + vm.expectRevert(); + globalParams.addTokenToCurrency(USD, address(newToken)); + } + + function testAddTokenToMultipleCurrencies() public { + // A token can be assigned to multiple currencies + vm.prank(admin); + globalParams.addTokenToCurrency(EUR, address(token1)); + + // Verify token is now in both USD and EUR + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + address[] memory eurTokens = globalParams.getTokensForCurrency(EUR); + + assertEq(usdTokens.length, 2); + assertEq(eurTokens.length, 2); + assertEq(eurTokens[1], address(token1)); + } + + function testRemoveTokenFromCurrency() public { + vm.prank(admin); + globalParams.removeTokenFromCurrency(USD, address(token1)); + + // Verify token was removed + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 1); + assertEq(usdTokens[0], address(token2)); + } + + function testRemoveTokenRevertWhenNotOwner() public { + vm.expectRevert(); + globalParams.removeTokenFromCurrency(USD, address(token1)); + } + + function testRemoveTokenThatDoesNotExist() public { + // Removing a non-existent token + TestToken nonExistentToken = new TestToken("NonExistent", "NE", 18); + + vm.expectRevert(); + vm.prank(admin); + globalParams.removeTokenFromCurrency(USD, address(nonExistentToken)); + } + + function testUpdatePlatformClaimDelay() public { + bytes32 platformHash = keccak256("claimDelayPlatform"); + address platformAdmin = address(0xB0B); + + vm.prank(admin); + globalParams.enlistPlatform(platformHash, platformAdmin, 500, address(0)); + + uint256 claimDelay = 5 days; + vm.prank(platformAdmin); + globalParams.updatePlatformClaimDelay(platformHash, claimDelay); + + assertEq(globalParams.getPlatformClaimDelay(platformHash), claimDelay); + } + + function testUpdatePlatformClaimDelayRevertsForNonAdmin() public { + bytes32 platformHash = keccak256("claimDelayPlatformRevert"); + address platformAdmin = address(0xC0DE); + + vm.prank(admin); + globalParams.enlistPlatform(platformHash, platformAdmin, 600, address(0)); + + vm.expectRevert(abi.encodeWithSelector(GlobalParams.GlobalParamsUnauthorized.selector)); + globalParams.updatePlatformClaimDelay(platformHash, 3 days); + } + + function testSetPlatformLineItemTypeAllowsRefundableWithProtocolFee() public { + bytes32 platformHash = keccak256("lineItemPlatform"); + address platformAdmin = address(0xCAFE); + bytes32 typeId = keccak256("refundable_fee_with_protocol"); + + vm.prank(admin); + globalParams.enlistPlatform(platformHash, platformAdmin, 500, address(0)); + + vm.prank(platformAdmin); + globalParams.setPlatformLineItemType( + platformHash, + typeId, + "refundable_fee_with_protocol", + false, // countsTowardGoal + true, // applyProtocolFee + true, // canRefund + false // instantTransfer + ); + + (bool exists,, bool countsTowardGoal, bool applyProtocolFee, bool canRefund, bool instantTransfer) = + globalParams.getPlatformLineItemType(platformHash, typeId); + + assertTrue(exists); + assertFalse(countsTowardGoal); + assertTrue(applyProtocolFee); + assertTrue(canRefund); + assertFalse(instantTransfer); + } + + function testSetPlatformLineItemTypeRevertsWhenGoalAppliesProtocolFee() public { + bytes32 platformHash = keccak256("goalPlatform"); + address platformAdmin = address(0xDEAD); + bytes32 typeId = keccak256("goal_type"); + + vm.prank(admin); + globalParams.enlistPlatform(platformHash, platformAdmin, 400, address(0)); + + vm.expectRevert(GlobalParams.GlobalParamsInvalidInput.selector); + vm.prank(platformAdmin); + globalParams.setPlatformLineItemType( + platformHash, + typeId, + "goal_type", + true, // countsTowardGoal + true, // applyProtocolFee (should revert) + true, // canRefund + false // instantTransfer + ); + } + + function testSetPlatformLineItemTypeRevertsWhenGoalCannotRefund() public { + bytes32 platformHash = keccak256("goalRefundPlatform"); + address platformAdmin = address(0xFEED); + bytes32 typeId = keccak256("goal_no_refund"); + + vm.prank(admin); + globalParams.enlistPlatform(platformHash, platformAdmin, 450, address(0)); + + vm.expectRevert(GlobalParams.GlobalParamsInvalidInput.selector); + vm.prank(platformAdmin); + globalParams.setPlatformLineItemType( + platformHash, + typeId, + "goal_no_refund", + true, // countsTowardGoal + false, // applyProtocolFee + false, // canRefund (should revert) + false // instantTransfer + ); + } + + function testSetPlatformLineItemTypeRevertsWhenInstantTransferRefundable() public { + bytes32 platformHash = keccak256("instantPlatform"); + address platformAdmin = address(0xABCD); + bytes32 typeId = keccak256("instant_refundable"); + + vm.prank(admin); + globalParams.enlistPlatform(platformHash, platformAdmin, 300, address(0)); + + vm.expectRevert(GlobalParams.GlobalParamsInvalidInput.selector); + vm.prank(platformAdmin); + globalParams.setPlatformLineItemType( + platformHash, + typeId, + "instant_refundable", + false, // countsTowardGoal (non-goal) + false, // applyProtocolFee + true, // canRefund (should revert with instantTransfer) + true // instantTransfer + ); + } + + function testGetTokensForCurrency() public { + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 2); + + address[] memory eurTokens = globalParams.getTokensForCurrency(EUR); + assertEq(eurTokens.length, 1); + + // Non-existent currency returns empty array + address[] memory nonExistentTokens = globalParams.getTokensForCurrency(BRL); + assertEq(nonExistentTokens.length, 0); + } + function testUnauthorizedSettersRevert() public { vm.expectRevert(); globalParams.updateProtocolFeePercent(1000); vm.expectRevert(); - globalParams.updateTokenAddress(address(0xBEEF)); + globalParams.updateProtocolAdminAddress(address(0xBEEF)); + } + + function testInitializerWithEmptyArrays() public { + bytes32[] memory currencies = new bytes32[](0); + address[][] memory tokensPerCurrency = new address[][](0); + + GlobalParams emptyImpl = new GlobalParams(); + bytes memory initData = + abi.encodeWithSelector(GlobalParams.initialize.selector, admin, protocolFee, currencies, tokensPerCurrency); + + ERC1967Proxy emptyProxy = new ERC1967Proxy(address(emptyImpl), initData); + GlobalParams emptyGlobalParams = GlobalParams(address(emptyProxy)); + + address[] memory tokens = emptyGlobalParams.getTokensForCurrency(USD); + assertEq(tokens.length, 0); + } + + function testInitializerRevertOnMismatchedArrays() public { + bytes32[] memory currencies = new bytes32[](2); + currencies[0] = USD; + currencies[1] = EUR; + + address[][] memory tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](1); + tokensPerCurrency[0][0] = address(token1); + + GlobalParams mismatchImpl = new GlobalParams(); + bytes memory initData = + abi.encodeWithSelector(GlobalParams.initialize.selector, admin, protocolFee, currencies, tokensPerCurrency); + + vm.expectRevert(); + new ERC1967Proxy(address(mismatchImpl), initData); + } + + function testMultipleTokensPerCurrency() public { + // USD should have 2 tokens + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 2); + + // Add a third token to USD + TestToken token4 = new TestToken("Token4", "TK4", 18); + vm.prank(admin); + globalParams.addTokenToCurrency(USD, address(token4)); + + // Verify USD now has 3 tokens + usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 3); + assertEq(usdTokens[0], address(token1)); + assertEq(usdTokens[1], address(token2)); + assertEq(usdTokens[2], address(token4)); + } + + function testRemoveMiddleToken() public { + // Remove token1 (first token) from USD + vm.prank(admin); + globalParams.removeTokenFromCurrency(USD, address(token1)); + + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 1); + assertEq(usdTokens[0], address(token2)); + } + + function testAddRemoveMultipleTokens() public { + TestToken token4 = new TestToken("Token4", "TK4", 18); + TestToken token5 = new TestToken("Token5", "TK5", 18); + + // Add two new tokens + vm.startPrank(admin); + globalParams.addTokenToCurrency(USD, address(token4)); + globalParams.addTokenToCurrency(USD, address(token5)); + vm.stopPrank(); + + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 4); + + // Remove original tokens + vm.startPrank(admin); + globalParams.removeTokenFromCurrency(USD, address(token1)); + globalParams.removeTokenFromCurrency(USD, address(token2)); + vm.stopPrank(); + + usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 2); + assertEq(usdTokens[0], address(token5)); // token5 moved to index 0 + assertEq(usdTokens[1], address(token4)); // token4 moved to index 1 + } + + function testUpgrade() public { + // Deploy new implementation + GlobalParams newImplementation = new GlobalParams(); + + // Upgrade as admin + vm.prank(admin); + globalParams.upgradeToAndCall(address(newImplementation), ""); + + // Verify state is preserved after upgrade + assertEq(globalParams.getProtocolAdminAddress(), admin); + assertEq(globalParams.getProtocolFeePercent(), protocolFee); + + address[] memory usdTokens = globalParams.getTokensForCurrency(USD); + assertEq(usdTokens.length, 2); + } + + function testUpgradeUnauthorizedReverts() public { + // Deploy new implementation + GlobalParams newImplementation = new GlobalParams(); + + // Try to upgrade as non-admin (should revert) + vm.expectRevert(); + globalParams.upgradeToAndCall(address(newImplementation), ""); + } + + function testCannotInitializeTwice() public { + bytes32[] memory currencies = new bytes32[](0); + address[][] memory tokensPerCurrency = new address[][](0); + + // Try to initialize again (should revert) + vm.expectRevert(); + globalParams.initialize(admin, protocolFee, currencies, tokensPerCurrency); } } diff --git a/test/foundry/unit/KeepWhatsRaised.t.sol b/test/foundry/unit/KeepWhatsRaised.t.sol new file mode 100644 index 00000000..d31a64d2 --- /dev/null +++ b/test/foundry/unit/KeepWhatsRaised.t.sol @@ -0,0 +1,1871 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "../integration/KeepWhatsRaised/KeepWhatsRaised.t.sol"; +import "forge-std/Test.sol"; +import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; +import {GlobalParams} from "src/GlobalParams.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; +import {TreasuryFactory} from "src/TreasuryFactory.sol"; +import {TestToken} from "../../mocks/TestToken.sol"; +import {Defaults} from "../Base.t.sol"; +import {IReward} from "src/interfaces/IReward.sol"; +import {ICampaignData} from "src/interfaces/ICampaignData.sol"; +import {TestToken} from "../../mocks/TestToken.sol"; + +contract KeepWhatsRaised_UnitTest is Test, KeepWhatsRaised_Integration_Shared_Test { + // Test constants + uint256 internal constant TEST_PLEDGE_AMOUNT = 1000e18; + uint256 internal constant TEST_TIP_AMOUNT = 50e18; + bytes32 internal constant TEST_REWARD_NAME = keccak256("testReward"); + bytes32 internal constant TEST_PLEDGE_ID = keccak256("testPledgeId"); + + function setUp() public virtual override { + super.setUp(); + deal(address(testToken), users.backer1Address, 100_000e18); + deal(address(testToken), users.backer2Address, 100_000e18); + + // Label addresses + vm.label(users.protocolAdminAddress, "ProtocolAdmin"); + vm.label(users.platform2AdminAddress, "PlatformAdmin"); + vm.label(users.contractOwner, "CampaignOwner"); + vm.label(users.backer1Address, "Backer1"); + vm.label(users.backer2Address, "Backer2"); + vm.label(address(keepWhatsRaised), "KeepWhatsRaised"); + vm.label(address(globalParams), "GlobalParams"); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + function testInitialize() public { + bytes32 newIdentifierHash = keccak256(abi.encodePacked("newCampaign")); + bytes32[] memory selectedPlatformHash = new bytes32[](1); + selectedPlatformHash[0] = PLATFORM_2_HASH; + + // Pass empty arrays since platform data is not used by the new treasury + bytes32[] memory platformDataKey = new bytes32[](0); + bytes32[] memory platformDataValue = new bytes32[](0); + + vm.prank(users.creator1Address); + campaignInfoFactory.createCampaign( + users.creator1Address, + newIdentifierHash, + selectedPlatformHash, + platformDataKey, // Empty array + platformDataValue, // Empty array + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + + address newCampaignAddress = campaignInfoFactory.identifierToCampaignInfo(newIdentifierHash); + + // Deploy + vm.prank(users.platform2AdminAddress); + address newTreasury = treasuryFactory.deploy(PLATFORM_2_HASH, newCampaignAddress, 1); + + KeepWhatsRaised newContract = KeepWhatsRaised(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"); + } + + /*////////////////////////////////////////////////////////////// + TREASURY CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + function testConfigureTreasury() public { + ICampaignData.CampaignData memory newCampaignData = ICampaignData.CampaignData({ + launchTime: block.timestamp + 1 days, + deadline: block.timestamp + 31 days, + goalAmount: 5000, + currency: bytes32("USD") + }); + + KeepWhatsRaised.FeeValues memory feeValues = _createFeeValues(); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.configureTreasury(CONFIG, newCampaignData, FEE_KEYS, feeValues); + + assertEq(keepWhatsRaised.getLaunchTime(), newCampaignData.launchTime); + assertEq(keepWhatsRaised.getDeadline(), newCampaignData.deadline); + assertEq(keepWhatsRaised.getGoalAmount(), newCampaignData.goalAmount); + + // Verify fee values are stored + assertEq(keepWhatsRaised.getFeeValue(FLAT_FEE_KEY), uint256(FLAT_FEE_VALUE)); + assertEq(keepWhatsRaised.getFeeValue(CUMULATIVE_FLAT_FEE_KEY), uint256(CUMULATIVE_FLAT_FEE_VALUE)); + assertEq(keepWhatsRaised.getFeeValue(PLATFORM_FEE_KEY), uint256(PLATFORM_FEE_VALUE)); + assertEq(keepWhatsRaised.getFeeValue(VAKI_COMMISSION_KEY), uint256(VAKI_COMMISSION_VALUE)); + } + + function testConfigureTreasuryWithColombianCreator() public { + ICampaignData.CampaignData memory newCampaignData = ICampaignData.CampaignData({ + launchTime: block.timestamp + 1 days, + deadline: block.timestamp + 31 days, + goalAmount: 5000, + currency: bytes32("USD") + }); + + KeepWhatsRaised.FeeValues memory feeValues = _createFeeValues(); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.configureTreasury(CONFIG_COLOMBIAN, newCampaignData, FEE_KEYS, feeValues); + + // Test that Colombian creator tax is not applied in pledges + _setupReward(); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(keepWhatsRaised.getLaunchTime()); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + vm.stopPrank(); + + // Available amount should not include Colombian tax deduction at pledge time + uint256 availableAmount = keepWhatsRaised.getAvailableRaisedAmount(); + uint256 expectedWithoutColombianTax = TEST_PLEDGE_AMOUNT + - (TEST_PLEDGE_AMOUNT * PLATFORM_FEE_PERCENT / PERCENT_DIVIDER) + - (TEST_PLEDGE_AMOUNT * 6 * 100 / PERCENT_DIVIDER) + - (TEST_PLEDGE_AMOUNT * PROTOCOL_FEE_PERCENT / PERCENT_DIVIDER); + assertEq(availableAmount, expectedWithoutColombianTax, "Colombian tax should not be applied at pledge time"); + } + + function testConfigureTreasuryRevertWhenNotPlatformAdmin() public { + KeepWhatsRaised.FeeValues memory feeValues = _createFeeValues(); + + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.configureTreasury(CONFIG, CAMPAIGN_DATA, FEE_KEYS, feeValues); + } + + function testConfigureTreasuryRevertWhenInvalidCampaignData() public { + // Invalid launch time (in the past) + ICampaignData.CampaignData memory invalidCampaignData = ICampaignData.CampaignData({ + launchTime: block.timestamp - 1, + deadline: block.timestamp + 31 days, + goalAmount: 5000, + currency: bytes32("USD") + }); + + KeepWhatsRaised.FeeValues memory feeValues = _createFeeValues(); + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.configureTreasury(CONFIG, invalidCampaignData, FEE_KEYS, feeValues); + } + + function testConfigureTreasuryRevertWhenMismatchedFeeArrays() public { + // Create mismatched fee arrays + KeepWhatsRaised.FeeKeys memory mismatchedKeys = FEE_KEYS; + KeepWhatsRaised.FeeValues memory mismatchedValues = _createFeeValues(); + mismatchedValues.grossPercentageFeeValues = new uint256[](1); // Wrong length + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.configureTreasury(CONFIG, CAMPAIGN_DATA, mismatchedKeys, mismatchedValues); + } + + /*////////////////////////////////////////////////////////////// + PAYMENT GATEWAY FEES + //////////////////////////////////////////////////////////////*/ + + function testSetPaymentGatewayFee() public { + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.setPaymentGatewayFee(TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + assertEq(keepWhatsRaised.getPaymentGatewayFee(TEST_PLEDGE_ID), PAYMENT_GATEWAY_FEE); + } + + function testSetPaymentGatewayFeeRevertWhenNotPlatformAdmin() public { + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.setPaymentGatewayFee(TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + } + + function testSetPaymentGatewayFeeRevertWhenPaused() public { + _pauseTreasury(); + + vm.expectRevert(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.setPaymentGatewayFee(TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + } + + /*////////////////////////////////////////////////////////////// + WITHDRAWAL APPROVAL + //////////////////////////////////////////////////////////////*/ + + function testApproveWithdrawal() public { + assertFalse(keepWhatsRaised.getWithdrawalApprovalStatus()); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + assertTrue(keepWhatsRaised.getWithdrawalApprovalStatus()); + } + + function testApproveWithdrawalRevertWhenAlreadyApproved() public { + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedAlreadyEnabled.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + } + + function testApproveWithdrawalRevertWhenNotPlatformAdmin() public { + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.approveWithdrawal(); + } + + /*////////////////////////////////////////////////////////////// + DEADLINE AND GOAL UPDATES + //////////////////////////////////////////////////////////////*/ + + function testUpdateDeadlineByPlatformAdmin() public { + uint256 newDeadline = DEADLINE + 10 days; + + vm.warp(LAUNCH_TIME + 1 days); // Within config lock period + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.updateDeadline(newDeadline); + + assertEq(keepWhatsRaised.getDeadline(), newDeadline); + } + + function testUpdateDeadlineByCampaignOwner() public { + uint256 newDeadline = DEADLINE + 10 days; + address campaignOwner = CampaignInfo(campaignAddress).owner(); + + vm.warp(LAUNCH_TIME + 1 days); + vm.prank(campaignOwner); + keepWhatsRaised.updateDeadline(newDeadline); + + assertEq(keepWhatsRaised.getDeadline(), newDeadline); + } + + function testUpdateDeadlineRevertWhenNotAuthorized() public { + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedUnAuthorized.selector); + vm.prank(users.backer1Address); + keepWhatsRaised.updateDeadline(DEADLINE + 10 days); + } + + function testUpdateDeadlineRevertWhenPastConfigLock() public { + // Warp to past config lock period + vm.warp(DEADLINE - CONFIG_LOCK_PERIOD + 1); + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedConfigLocked.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.updateDeadline(DEADLINE + 10 days); + } + + function testUpdateDeadlineRevertWhenDeadlineBeforeLaunchTime() public { + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.updateDeadline(LAUNCH_TIME - 1); + } + + function testUpdateDeadlineRevertWhenDeadlineBeforeCurrentTime() public { + vm.warp(LAUNCH_TIME + 5 days); + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.updateDeadline(LAUNCH_TIME + 4 days); + } + + function testUpdateDeadlineRevertWhenPaused() public { + _pauseTreasury(); + + // Try to update deadline + vm.warp(LAUNCH_TIME + 1 days); + vm.expectRevert(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.updateDeadline(DEADLINE + 10 days); + } + + function testUpdateGoalAmountByPlatformAdmin() public { + uint256 newGoal = GOAL_AMOUNT * 2; + + vm.warp(LAUNCH_TIME + 1 days); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.updateGoalAmount(newGoal); + + assertEq(keepWhatsRaised.getGoalAmount(), newGoal); + } + + function testUpdateGoalAmountByCampaignOwner() public { + uint256 newGoal = GOAL_AMOUNT * 2; + address campaignOwner = CampaignInfo(campaignAddress).owner(); + + vm.warp(LAUNCH_TIME + 1 days); + vm.prank(campaignOwner); + keepWhatsRaised.updateGoalAmount(newGoal); + + assertEq(keepWhatsRaised.getGoalAmount(), newGoal); + } + + function testUpdateGoalAmountRevertWhenNotAuthorized() public { + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedUnAuthorized.selector); + vm.prank(users.backer1Address); + keepWhatsRaised.updateGoalAmount(GOAL_AMOUNT * 2); + } + + function testUpdateGoalAmountRevertWhenZero() public { + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.updateGoalAmount(0); + } + + /*////////////////////////////////////////////////////////////// + REWARDS MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function testAddRewards() public { + bytes32[] memory rewardNames = new bytes32[](1); + rewardNames[0] = TEST_REWARD_NAME; + + Reward[] memory rewards = new Reward[](1); + rewards[0] = _createTestReward(TEST_PLEDGE_AMOUNT, true); + + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + + Reward memory retrievedReward = keepWhatsRaised.getReward(TEST_REWARD_NAME); + assertEq(retrievedReward.rewardValue, TEST_PLEDGE_AMOUNT); + assertTrue(retrievedReward.isRewardTier); + } + + function testAddRewardsRevertWhenMismatchedArrays() public { + bytes32[] memory rewardNames = new bytes32[](2); + Reward[] memory rewards = new Reward[](1); + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + } + + function testAddRewardsRevertWhenDuplicateReward() public { + bytes32[] memory rewardNames = new bytes32[](1); + rewardNames[0] = TEST_REWARD_NAME; + + Reward[] memory rewards = new Reward[](1); + rewards[0] = _createTestReward(TEST_PLEDGE_AMOUNT, true); + + // Add first time + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + + // Try to add again + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedRewardExists.selector); + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + } + + function testRemoveReward() public { + // First add a reward + bytes32[] memory rewardNames = new bytes32[](1); + rewardNames[0] = TEST_REWARD_NAME; + + Reward[] memory rewards = new Reward[](1); + rewards[0] = _createTestReward(TEST_PLEDGE_AMOUNT, true); + + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + + // Remove reward + vm.prank(users.creator1Address); + keepWhatsRaised.removeReward(TEST_REWARD_NAME); + + // Verify removal + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + keepWhatsRaised.getReward(TEST_REWARD_NAME); + } + + function testRemoveRewardRevertWhenRewardDoesNotExist() public { + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + vm.prank(users.creator1Address); + keepWhatsRaised.removeReward(TEST_REWARD_NAME); + } + + function testAddRewardsRevertWhenPaused() public { + _pauseTreasury(); + + // Try to add rewards + bytes32[] memory rewardNames = new bytes32[](1); + rewardNames[0] = TEST_REWARD_NAME; + + Reward[] memory rewards = new Reward[](1); + rewards[0] = _createTestReward(TEST_PLEDGE_AMOUNT, true); + + vm.expectRevert(); + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + } + + function testRemoveRewardRevertWhenPaused() public { + // First add a reward + bytes32[] memory rewardNames = new bytes32[](1); + rewardNames[0] = TEST_REWARD_NAME; + + Reward[] memory rewards = new Reward[](1); + rewards[0] = _createTestReward(TEST_PLEDGE_AMOUNT, true); + + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + + _pauseTreasury(); + + // Try to remove reward - should revert + vm.expectRevert(); + vm.prank(users.creator1Address); + keepWhatsRaised.removeReward(TEST_REWARD_NAME); + } + + /*////////////////////////////////////////////////////////////// + PLEDGING + //////////////////////////////////////////////////////////////*/ + + function testPledgeForAReward() public { + // Add reward first + _setupReward(); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Pledge + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + + keepWhatsRaised.pledgeForAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + ); + vm.stopPrank(); + + // Verify + assertEq(testToken.balanceOf(users.backer1Address), balanceBefore - TEST_PLEDGE_AMOUNT - TEST_TIP_AMOUNT); + assertEq(keepWhatsRaised.getRaisedAmount(), TEST_PLEDGE_AMOUNT); + assertTrue(keepWhatsRaised.getAvailableRaisedAmount() < TEST_PLEDGE_AMOUNT); // Less due to fees + assertEq(CampaignInfo(campaignAddress).balanceOf(users.backer1Address), 1); + } + + function testPledgeForARewardRevertWhenDuplicatePledgeId() public { + _setupReward(); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT * 2); + + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + + // First pledge + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + + // Try to pledge with same ID + bytes32 internalPledgeId = keccak256(abi.encodePacked(TEST_PLEDGE_ID, users.backer1Address)); + vm.expectRevert( + abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, internalPledgeId) + ); + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + vm.stopPrank(); + } + + function testPledgeForARewardRevertWhenNotRewardTier() public { + // Add non-reward tier + bytes32[] memory rewardNames = new bytes32[](1); + rewardNames[0] = TEST_REWARD_NAME; + + Reward[] memory rewards = new Reward[](1); + rewards[0] = _createTestReward(TEST_PLEDGE_AMOUNT, false); + + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + + // Try to pledge + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + keepWhatsRaised.pledgeForAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), 0, rewardSelection); + vm.stopPrank(); + } + + function testPledgeWithoutAReward() public { + uint256 pledgeAmount = 500e18; + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + // Pledge + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), pledgeAmount + TEST_TIP_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), pledgeAmount, TEST_TIP_AMOUNT + ); + vm.stopPrank(); + + // Verify + assertEq(testToken.balanceOf(users.backer1Address), balanceBefore - pledgeAmount - TEST_TIP_AMOUNT); + assertEq(keepWhatsRaised.getRaisedAmount(), pledgeAmount); + assertTrue(keepWhatsRaised.getAvailableRaisedAmount() < pledgeAmount); // Less due to fees + assertEq(CampaignInfo(campaignAddress).balanceOf(users.backer1Address), 1); + } + + function testPledgeWithoutARewardRevertWhenDuplicatePledgeId() public { + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT * 2); + + // First pledge + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + + // Try to pledge with same ID - internal pledge ID includes caller + bytes32 internalPledgeId = keccak256(abi.encodePacked(TEST_PLEDGE_ID, users.backer1Address)); + vm.expectRevert( + abi.encodeWithSelector(KeepWhatsRaised.KeepWhatsRaisedPledgeAlreadyProcessed.selector, internalPledgeId) + ); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + } + + function testPledgeRevertWhenOutsideCampaignPeriod() public { + // Before launch + vm.warp(LAUNCH_TIME - 1); + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + + // After deadline + vm.warp(DEADLINE + 1); + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("newPledge"), users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + } + + function testPledgeForARewardRevertWhenPaused() public { + // Add reward first + _setupReward(); + + _pauseTreasury(); + + // Try to pledge + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + + vm.expectRevert(); + keepWhatsRaised.pledgeForAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + ); + vm.stopPrank(); + } + + function testSetFeeAndPledge() public { + _setupReward(); + + vm.warp(LAUNCH_TIME); + + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + + // Fund admin with tokens since they will be the token source + deal(address(testToken), users.platform2AdminAddress, TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + + vm.startPrank(users.platform2AdminAddress); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + + keepWhatsRaised.setFeeAndPledge( + TEST_PLEDGE_ID, + users.backer1Address, + address(testToken), + 0, // ignored for reward pledges + TEST_TIP_AMOUNT, + PAYMENT_GATEWAY_FEE, + rewardSelection, + true + ); + vm.stopPrank(); + + // Verify fee was set + assertEq(keepWhatsRaised.getPaymentGatewayFee(TEST_PLEDGE_ID), PAYMENT_GATEWAY_FEE); + + // Verify pledge was made + assertEq(keepWhatsRaised.getRaisedAmount(), TEST_PLEDGE_AMOUNT); + assertEq(CampaignInfo(campaignAddress).balanceOf(users.backer1Address), 1); + } + + /*////////////////////////////////////////////////////////////// + WITHDRAWALS + //////////////////////////////////////////////////////////////*/ + + function testWithdrawFullAmountAfterDeadline() public { + // Setup pledges + _setupPledges(); + + // Approve withdrawal + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + address owner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(owner); + + // Withdraw after deadline (as platform admin) + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + + uint256 ownerBalanceAfter = testToken.balanceOf(owner); + + // Verify (accounting for fees) + assertTrue(ownerBalanceAfter > ownerBalanceBefore); + assertEq(keepWhatsRaised.getAvailableRaisedAmount(), 0); + } + + function testWithdrawPartialAmountBeforeDeadline() public { + // Setup pledges + _setupPledges(); + + // Approve withdrawal + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + uint256 partialAmount = 500e18; + uint256 availableBefore = keepWhatsRaised.getAvailableRaisedAmount(); + + // Withdraw partial amount before deadline (as platform admin) + vm.warp(LAUNCH_TIME + 1 days); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), partialAmount); + + uint256 availableAfter = keepWhatsRaised.getAvailableRaisedAmount(); + + // Verify - available is reduced by withdrawal plus fees + assertTrue(availableAfter < availableBefore - partialAmount); + } + + function testWithdrawRevertWhenNotApproved() public { + _setupPledges(); + + vm.warp(DEADLINE + 1); + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedDisabled.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + } + + function testWithdrawRevertWhenAmountExceedsAvailable() public { + _setupPledges(); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + uint256 available = keepWhatsRaised.getAvailableRaisedAmount(); + + vm.warp(LAUNCH_TIME + 1 days); + vm.expectRevert(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), available + 1e18); + } + + function testWithdrawRevertWhenAlreadyWithdrawn() public { + _setupPledges(); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + // First withdrawal + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + + // Second withdrawal attempt + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedAlreadyWithdrawn.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + } + + function testWithdrawRevertWhenPaused() public { + // Setup pledges and approve withdrawal first + _setupPledges(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + _pauseTreasury(); + + // Try to withdraw + vm.warp(DEADLINE + 1); + vm.expectRevert(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + } + + function testWithdrawWithMinimumFeeExemption() public { + // Calculate pledge amount needed to have available amount above exemption after fees + // We need the available amount after all pledge fees to be > MINIMUM_WITHDRAWAL_FOR_FEE_EXEMPTION + // Total fees during pledge: platform (10%) + vaki (6%) + protocol (20%) = 36% + // So available = pledge * 0.64 + // We need: pledge * 0.64 > 50,000e18 + // Therefore: pledge > 78,125e18 + uint256 largePledge = 80_000e18; + + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), largePledge); + keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), largePledge, 0); + vm.stopPrank(); + + uint256 availableAfterPledge = keepWhatsRaised.getAvailableRaisedAmount(); + + // Verify available amount is above exemption threshold + assertTrue( + availableAfterPledge > MINIMUM_WITHDRAWAL_FOR_FEE_EXEMPTION, + "Available amount should be above exemption threshold" + ); + + // Approve and withdraw + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + address owner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(owner); + + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + + uint256 ownerBalanceAfter = testToken.balanceOf(owner); + uint256 received = ownerBalanceAfter - ownerBalanceBefore; + + // For final withdrawal above exemption threshold, no flat fee is applied + // The owner should receive the full available amount + assertEq(received, availableAfterPledge, "Should receive full available amount without flat fee"); + } + + function testWithdrawWithColombianCreatorTax() public { + // Configure with Colombian creator + KeepWhatsRaised.FeeValues memory feeValues = _createFeeValues(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.configureTreasury(CONFIG_COLOMBIAN, CAMPAIGN_DATA, FEE_KEYS, feeValues); + + // Make a pledge + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + + // Approve withdrawal + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + address owner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(owner); + uint256 availableBeforeWithdraw = keepWhatsRaised.getAvailableRaisedAmount(); + + // Withdraw after deadline + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + + uint256 ownerBalanceAfter = testToken.balanceOf(owner); + uint256 received = ownerBalanceAfter - ownerBalanceBefore; + + // Calculate expected amount after Colombian tax + uint256 flatFee = uint256(FLAT_FEE_VALUE); + uint256 amountAfterFlatFee = availableBeforeWithdraw - flatFee; + + // Colombian tax: (availableBeforeWithdraw * 0.004) / 1.004 + uint256 colombianTax = (availableBeforeWithdraw * 40) / 10040; + uint256 expectedAmount = amountAfterFlatFee - colombianTax; + + assertApproxEqAbs(received, expectedAmount, 10, "Should receive amount minus flat fee and Colombian tax"); + } + + /*////////////////////////////////////////////////////////////// + REFUNDS + //////////////////////////////////////////////////////////////*/ + + function testClaimRefundAfterDeadline() public { + // Make pledge + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + uint256 tokenId = 1; // First token ID after pledge + vm.stopPrank(); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Claim refund within refund window + vm.warp(DEADLINE + 1 days); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(keepWhatsRaised), tokenId); + + vm.prank(users.backer1Address); + keepWhatsRaised.claimRefund(tokenId); + + // Calculate expected refund (pledge minus all fees including protocol) + uint256 platformFee = (TEST_PLEDGE_AMOUNT * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 vakiCommission = (TEST_PLEDGE_AMOUNT * uint256(VAKI_COMMISSION_VALUE)) / PERCENT_DIVIDER; + uint256 protocolFee = (TEST_PLEDGE_AMOUNT * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedRefund = TEST_PLEDGE_AMOUNT - PAYMENT_GATEWAY_FEE - platformFee - vakiCommission - protocolFee; + + // Verify refund amount is pledge minus fees + assertEq(testToken.balanceOf(users.backer1Address), balanceBefore + expectedRefund); + vm.expectRevert(); + campaignInfo.ownerOf(tokenId); // Token should be burned + } + + function testClaimRefundRevertWhenOutsideRefundWindow() public { + // Make pledge + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + uint256 tokenId = 1; // First token ID after pledge + vm.stopPrank(); + + // Try to claim after refund window + vm.warp(DEADLINE + REFUND_DELAY + 1); + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.claimRefund(tokenId); + } + + function testClaimRefundAfterCancellation() public { + // Make pledge + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + uint256 tokenId = 1; // First token ID after pledge + vm.stopPrank(); + + // Cancel campaign + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.cancelTreasury(keccak256("cancelled")); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Claim refund + vm.warp(block.timestamp + 1); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(keepWhatsRaised), tokenId); + + vm.prank(users.backer1Address); + keepWhatsRaised.claimRefund(tokenId); + + // Calculate expected refund (pledge minus all fees) + uint256 platformFee = (TEST_PLEDGE_AMOUNT * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 vakiCommission = (TEST_PLEDGE_AMOUNT * uint256(VAKI_COMMISSION_VALUE)) / PERCENT_DIVIDER; + uint256 protocolFee = (TEST_PLEDGE_AMOUNT * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedRefund = TEST_PLEDGE_AMOUNT - PAYMENT_GATEWAY_FEE - platformFee - vakiCommission - protocolFee; + + // Verify refund amount is pledge minus fees + assertEq(testToken.balanceOf(users.backer1Address), balanceBefore + expectedRefund); + } + + function testClaimRefundRevertWhenPaused() public { + // Make pledge first + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + uint256 tokenId = 0; + vm.stopPrank(); + + _pauseTreasury(); + + // Try to claim refund + vm.warp(DEADLINE + 1 days); + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.claimRefund(tokenId); + } + + function testClaimRefundRevertWhenInsufficientFunds() public { + // Make pledge + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + uint256 tokenId = 0; + vm.stopPrank(); + + // Withdraw all funds + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + + // Try to claim refund + vm.warp(DEADLINE + 1 days); + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.claimRefund(tokenId); + } + + /*////////////////////////////////////////////////////////////// + TIPS AND FUNDS CLAIMING + //////////////////////////////////////////////////////////////*/ + + function testClaimTipAfterDeadline() public { + // Setup pledges with tips + _setupPledges(); + + uint256 platformBalanceBefore = testToken.balanceOf(users.platform2AdminAddress); + uint256 totalTips = TEST_TIP_AMOUNT * 2; + + // Claim tips after deadline + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimTip(); + + // Verify + assertEq(testToken.balanceOf(users.platform2AdminAddress), platformBalanceBefore + totalTips); + } + + function testClaimTipRevertWhenBeforeDeadline() public { + _setupPledges(); + + vm.warp(DEADLINE - 1); + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedNotClaimableAdmin.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimTip(); + } + + function testClaimTipRevertWhenAlreadyClaimed() public { + _setupPledges(); + + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimTip(); + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedAlreadyClaimed.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimTip(); + } + + function testClaimTipRevertWhenPaused() public { + // Setup pledges with tips + _setupPledges(); + _pauseTreasury(); + + vm.warp(DEADLINE + 1); + vm.expectRevert(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimTip(); + } + + function testClaimFundAfterWithdrawalDelay() public { + // Setup pledges + _setupPledges(); + + uint256 platformBalanceBefore = testToken.balanceOf(users.platform2AdminAddress); + uint256 availableFunds = keepWhatsRaised.getAvailableRaisedAmount(); + + // Claim funds after withdrawal delay + vm.warp(DEADLINE + WITHDRAWAL_DELAY + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimFund(); + + // Verify + assertEq(testToken.balanceOf(users.platform2AdminAddress), platformBalanceBefore + availableFunds); + assertEq(keepWhatsRaised.getAvailableRaisedAmount(), 0); + } + + function testClaimFundAfterCancellation() public { + // Setup pledges + _setupPledges(); + + uint256 platformBalanceBefore = testToken.balanceOf(users.platform2AdminAddress); + uint256 availableFunds = keepWhatsRaised.getAvailableRaisedAmount(); + + // Cancel treasury + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.cancelTreasury(keccak256("cancelled")); + + // Claim funds after refund delay from cancellation + vm.warp(block.timestamp + REFUND_DELAY + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimFund(); + + // Verify + assertEq(testToken.balanceOf(users.platform2AdminAddress), platformBalanceBefore + availableFunds); + assertEq(keepWhatsRaised.getAvailableRaisedAmount(), 0); + } + + function testClaimFundRevertWhenBeforeWithdrawalDelay() public { + _setupPledges(); + + vm.warp(DEADLINE + WITHDRAWAL_DELAY - 1); + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedNotClaimableAdmin.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimFund(); + } + + function testClaimFundRevertWhenAlreadyClaimed() public { + _setupPledges(); + + vm.warp(DEADLINE + WITHDRAWAL_DELAY + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimFund(); + + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedAlreadyClaimed.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimFund(); + } + + function testClaimFundRevertWhenPaused() public { + // Setup pledges + _setupPledges(); + _pauseTreasury(); + + vm.warp(DEADLINE + WITHDRAWAL_DELAY + 1); + vm.expectRevert(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimFund(); + } + + /*////////////////////////////////////////////////////////////// + FEE DISBURSEMENT + //////////////////////////////////////////////////////////////*/ + + function testDisburseFees() public { + // Setup pledges - protocol fees are collected during pledge + _setupPledges(); + + // Approve and withdraw to generate withdrawal fees + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + + uint256 protocolBalanceBefore = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformBalanceBefore = testToken.balanceOf(users.platform2AdminAddress); + + // Disburse fees immediately + keepWhatsRaised.disburseFees(); + + // Verify fees were distributed + assertTrue(testToken.balanceOf(users.protocolAdminAddress) > protocolBalanceBefore); + assertTrue(testToken.balanceOf(users.platform2AdminAddress) > platformBalanceBefore); + } + + function testDisburseFeesRevertWhenPaused() public { + // Setup pledges and withdraw to generate fees + _setupPledges(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + + _pauseTreasury(); + + // Try to disburse fees - should revert + vm.expectRevert(); + keepWhatsRaised.disburseFees(); + } + + /*////////////////////////////////////////////////////////////// + CANCEL TREASURY + //////////////////////////////////////////////////////////////*/ + + function testCancelTreasuryByPlatformAdmin() public { + bytes32 message = keccak256("Platform cancellation"); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.cancelTreasury(message); + + // Verify campaign is cancelled + vm.warp(LAUNCH_TIME); + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + } + + function testCancelTreasuryByCampaignOwner() public { + bytes32 message = keccak256("Owner cancellation"); + address campaignOwner = CampaignInfo(campaignAddress).owner(); + + vm.prank(campaignOwner); + keepWhatsRaised.cancelTreasury(message); + + // Verify campaign is cancelled + vm.warp(LAUNCH_TIME); + vm.expectRevert(); + vm.prank(users.backer1Address); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + } + + function testCancelTreasuryRevertWhenUnauthorized() public { + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedUnAuthorized.selector); + vm.prank(users.backer1Address); + keepWhatsRaised.cancelTreasury(keccak256("unauthorized")); + } + + /*////////////////////////////////////////////////////////////// + EDGE CASES + //////////////////////////////////////////////////////////////*/ + + function testMultiplePartialWithdrawals() public { + _setupPledges(); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + uint256 available = keepWhatsRaised.getAvailableRaisedAmount(); + + // First withdrawal: small amount that will incur cumulative fee + // Need to ensure available >= withdrawal + cumulativeFee + uint256 firstWithdrawal = 200e18; // Reduced to ensure enough for fee + + // First withdrawal + vm.warp(LAUNCH_TIME + 1 days); + uint256 availableBefore1 = keepWhatsRaised.getAvailableRaisedAmount(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), firstWithdrawal); + uint256 availableAfter1 = keepWhatsRaised.getAvailableRaisedAmount(); + + // Verify first withdrawal reduced available amount by withdrawal + fees + uint256 expectedReduction1 = firstWithdrawal + uint256(CUMULATIVE_FLAT_FEE_VALUE); + assertApproxEqAbs( + availableBefore1 - availableAfter1, + expectedReduction1, + 10, + "First withdrawal should reduce by amount plus cumulative fee" + ); + + // Second withdrawal + // Calculate safe amount based on remaining balance + uint256 secondWithdrawal = 150e18; // Reduced to ensure enough for fee + + // Only do second withdrawal if we have enough funds + if (availableAfter1 >= secondWithdrawal + uint256(CUMULATIVE_FLAT_FEE_VALUE)) { + vm.warp(LAUNCH_TIME + 2 days); + uint256 availableBefore2 = keepWhatsRaised.getAvailableRaisedAmount(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), secondWithdrawal); + uint256 availableAfter2 = keepWhatsRaised.getAvailableRaisedAmount(); + + // Verify second withdrawal reduced available amount by withdrawal + fees + uint256 expectedReduction2 = secondWithdrawal + uint256(CUMULATIVE_FLAT_FEE_VALUE); + assertApproxEqAbs( + availableBefore2 - availableAfter2, + expectedReduction2, + 10, + "Second withdrawal should reduce by amount plus cumulative fee" + ); + } + + // Verify remaining amount + assertTrue(keepWhatsRaised.getAvailableRaisedAmount() > 0, "Should still have funds available"); + } + + function testWithdrawalRevertWhenFeesExceedAmount() public { + // Make a small pledge + uint256 smallPledge = 300e18; // Small enough that fees might exceed available + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), smallPledge); + keepWhatsRaised.pledgeWithoutAReward(TEST_PLEDGE_ID, users.backer1Address, address(testToken), smallPledge, 0); + vm.stopPrank(); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + // Try to withdraw partial amount that would cause available < withdrawal + fees + vm.warp(LAUNCH_TIME + 1 days); + uint256 available = keepWhatsRaised.getAvailableRaisedAmount(); + + // Try to withdraw an amount that with fees would exceed available + uint256 withdrawAmount = available - 50e18; // Leave less than cumulative fee + vm.expectRevert(); + keepWhatsRaised.withdraw(address(testToken), withdrawAmount); + } + + function testZeroTipPledge() public { + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + + assertEq(keepWhatsRaised.getRaisedAmount(), TEST_PLEDGE_AMOUNT); + } + + function testFeeCalculationWithoutColombianTax() public { + // Make a pledge (non-Colombian) + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), TEST_PLEDGE_ID, PAYMENT_GATEWAY_FEE); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + TEST_PLEDGE_ID, users.backer1Address, address(testToken), TEST_PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + + uint256 available = keepWhatsRaised.getAvailableRaisedAmount(); + uint256 platformFee = (TEST_PLEDGE_AMOUNT * uint256(PLATFORM_FEE_VALUE)) / PERCENT_DIVIDER; + uint256 vakiCommission = (TEST_PLEDGE_AMOUNT * uint256(VAKI_COMMISSION_VALUE)) / PERCENT_DIVIDER; + uint256 protocolFee = (TEST_PLEDGE_AMOUNT * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 totalFees = platformFee + vakiCommission + PAYMENT_GATEWAY_FEE + protocolFee; + + uint256 expectedAvailable = TEST_PLEDGE_AMOUNT - totalFees; + + assertEq(available, expectedAvailable); + } + + function testGetRewardRevertWhenNotExists() public { + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedInvalidInput.selector); + keepWhatsRaised.getReward(keccak256("nonexistent")); + } + + function testWithdrawRevertWhenZeroAfterDeadline() public { + // No pledges made + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + vm.warp(DEADLINE + 1); + vm.expectRevert(KeepWhatsRaised.KeepWhatsRaisedAlreadyWithdrawn.selector); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), 0); + } + + /*////////////////////////////////////////////////////////////// + COMPREHENSIVE FEE TESTS + //////////////////////////////////////////////////////////////*/ + + function testComplexFeeScenario() public { + // Testing multiple pledges with different fee structures + + // Configure Colombian creator for complex fee testing + KeepWhatsRaised.FeeValues memory feeValues = _createFeeValues(); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.configureTreasury(CONFIG_COLOMBIAN, CAMPAIGN_DATA, FEE_KEYS, feeValues); + + // Add rewards + _setupReward(); + + // Pledge 1: With reward and tip + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), keccak256("pledge1"), PAYMENT_GATEWAY_FEE + ); + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + keepWhatsRaised.pledgeForAReward( + keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + ); + vm.stopPrank(); + + // Pledge 2: Without reward, different gateway fee + uint256 differentGatewayFee = 20e18; + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), keccak256("pledge2"), differentGatewayFee + ); + vm.startPrank(users.backer2Address); + testToken.approve(address(keepWhatsRaised), 2000e18); + keepWhatsRaised.pledgeWithoutAReward(keccak256("pledge2"), users.backer2Address, address(testToken), 2000e18, 0); + vm.stopPrank(); + + // Verify total raised and available amounts + uint256 totalRaised = keepWhatsRaised.getRaisedAmount(); + uint256 totalAvailable = keepWhatsRaised.getAvailableRaisedAmount(); + + assertEq(totalRaised, TEST_PLEDGE_AMOUNT + 2000e18); + assertTrue(totalAvailable < totalRaised); // No colombian tax yet + + // Test partial withdrawal with Colombian tax applied + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + uint256 partialWithdrawAmount = 1000e18; + address owner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(owner); + + vm.warp(LAUNCH_TIME + 1 days); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), partialWithdrawAmount); + + uint256 ownerBalanceAfter = testToken.balanceOf(owner); + uint256 netReceived = ownerBalanceAfter - ownerBalanceBefore; + + // Verify withdrawal amount equals requested (fees deducted from available) + assertEq(netReceived, partialWithdrawAmount); + } + + function testWithdrawalFeeStructure() public { + // Testing different withdrawal scenarios and their fee implications + + // Small withdrawal (below exemption) before deadline + uint256 smallAmount = 1000e18; + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("small"), 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), smallAmount); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("small"), users.backer1Address, address(testToken), smallAmount, 0 + ); + vm.stopPrank(); + + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + address owner = CampaignInfo(campaignAddress).owner(); + uint256 balanceBefore = testToken.balanceOf(owner); + + // Calculate available after pledge fees + uint256 availableBeforeWithdraw = keepWhatsRaised.getAvailableRaisedAmount(); + + // Withdraw before deadline - should apply cumulative fee + vm.warp(LAUNCH_TIME + 1 days); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(testToken), availableBeforeWithdraw - uint256(CUMULATIVE_FLAT_FEE_VALUE) - 10); // Leave small buffer + + uint256 received = testToken.balanceOf(owner) - balanceBefore; + + assertTrue(received > 0, "Should receive something"); + } + + /*////////////////////////////////////////////////////////////// + FEE VALUE TESTS + //////////////////////////////////////////////////////////////*/ + + function testGetFeeValue() public { + // Test retrieval of stored fee values + assertEq(keepWhatsRaised.getFeeValue(FLAT_FEE_KEY), uint256(FLAT_FEE_VALUE)); + assertEq(keepWhatsRaised.getFeeValue(CUMULATIVE_FLAT_FEE_KEY), uint256(CUMULATIVE_FLAT_FEE_VALUE)); + assertEq(keepWhatsRaised.getFeeValue(PLATFORM_FEE_KEY), uint256(PLATFORM_FEE_VALUE)); + assertEq(keepWhatsRaised.getFeeValue(VAKI_COMMISSION_KEY), uint256(VAKI_COMMISSION_VALUE)); + } + + function testGetFeeValueForNonExistentKey() public { + // Should return 0 for non-existent keys + bytes32 nonExistentKey = keccak256("nonExistentFee"); + assertEq(keepWhatsRaised.getFeeValue(nonExistentKey), 0); + } + + /*////////////////////////////////////////////////////////////// + HELPER FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _createTestReward(uint256 value, bool isRewardTier) internal pure returns (Reward memory) { + bytes32[] memory itemIds = new bytes32[](1); + uint256[] memory itemValues = new uint256[](1); + uint256[] memory itemQuantities = new uint256[](1); + + itemIds[0] = keccak256("testItem"); + itemValues[0] = value; + itemQuantities[0] = 1; + + return Reward({ + rewardValue: value, + isRewardTier: isRewardTier, + itemId: itemIds, + itemValue: itemValues, + itemQuantity: itemQuantities + }); + } + + function _setupReward() internal { + bytes32[] memory rewardNames = new bytes32[](1); + rewardNames[0] = TEST_REWARD_NAME; + + Reward[] memory rewards = new Reward[](1); + rewards[0] = _createTestReward(TEST_PLEDGE_AMOUNT, true); + + vm.prank(users.creator1Address); + keepWhatsRaised.addRewards(rewardNames, rewards); + } + + function _setupPledges() internal { + _setupReward(); + + // Set gateway fees for pledges + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), keccak256("pledge1"), PAYMENT_GATEWAY_FEE + ); + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), keccak256("pledge2"), PAYMENT_GATEWAY_FEE + ); + + // Make pledges from two backers + vm.warp(LAUNCH_TIME); + + // Backer 1 pledge with reward + vm.startPrank(users.backer1Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + bytes32[] memory rewardSelection = new bytes32[](1); + rewardSelection[0] = TEST_REWARD_NAME; + keepWhatsRaised.pledgeForAReward( + keccak256("pledge1"), users.backer1Address, address(testToken), TEST_TIP_AMOUNT, rewardSelection + ); + vm.stopPrank(); + + // Backer 2 pledge without reward + vm.startPrank(users.backer2Address); + testToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + TEST_TIP_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("pledge2"), users.backer2Address, address(testToken), TEST_PLEDGE_AMOUNT, TEST_TIP_AMOUNT + ); + vm.stopPrank(); + } + + function _pauseTreasury() internal { + // Pause treasury + bytes32 message = keccak256("Pause"); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.pauseTreasury(message); + } + + function _createFeeValues() internal pure returns (KeepWhatsRaised.FeeValues memory) { + KeepWhatsRaised.FeeValues memory feeValues; + feeValues.flatFeeValue = uint256(FLAT_FEE_VALUE); + feeValues.cumulativeFlatFeeValue = uint256(CUMULATIVE_FLAT_FEE_VALUE); + feeValues.grossPercentageFeeValues = new uint256[](2); + feeValues.grossPercentageFeeValues[0] = uint256(PLATFORM_FEE_VALUE); + feeValues.grossPercentageFeeValues[1] = uint256(VAKI_COMMISSION_VALUE); + return feeValues; + } + + /*////////////////////////////////////////////////////////////// + MULTI-TOKEN SPECIFIC TESTS + //////////////////////////////////////////////////////////////*/ + + function test_pledgeWithMultipleTokenTypes() public { + _setupReward(); + + // Pledge with USDC + uint256 usdcAmount = getTokenAmount(address(usdcToken), TEST_PLEDGE_AMOUNT); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdc_pledge"), 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + usdcToken.approve(address(keepWhatsRaised), usdcAmount); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0 + ); + vm.stopPrank(); + + // Pledge with cUSD + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd_pledge"), 0); + + vm.startPrank(users.backer2Address); + cUSDToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + + // Verify raised amount is normalized + uint256 totalRaised = keepWhatsRaised.getRaisedAmount(); + assertEq(totalRaised, TEST_PLEDGE_AMOUNT * 2, "Should normalize to same value"); + } + + function test_withdrawMultipleTokensCorrectly() public { + _setupReward(); + + // Use larger amounts to ensure enough remains after fees + uint256 largeAmount = 100_000e18; // 100k base amount + uint256 usdcAmount = getTokenAmount(address(usdcToken), largeAmount); + uint256 cUSDAmount = largeAmount; + + // Pledge with USDC + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdc"), 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + deal(address(usdcToken), users.backer1Address, usdcAmount); // Ensure enough tokens + usdcToken.approve(address(keepWhatsRaised), usdcAmount); + keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0); + vm.stopPrank(); + + // Pledge with cUSD + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd"), 0); + + vm.startPrank(users.backer2Address); + deal(address(cUSDToken), users.backer2Address, cUSDAmount); // Ensure enough tokens + cUSDToken.approve(address(keepWhatsRaised), cUSDAmount); + keepWhatsRaised.pledgeWithoutAReward(keccak256("cusd"), users.backer2Address, address(cUSDToken), cUSDAmount, 0); + vm.stopPrank(); + + // Approve withdrawal + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + address owner = CampaignInfo(campaignAddress).owner(); + uint256 ownerUSDCBefore = usdcToken.balanceOf(owner); + uint256 ownerCUSDBefore = cUSDToken.balanceOf(owner); + + // Withdraw USDC + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(usdcToken), 0); + + // Withdraw cUSD + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(cUSDToken), 0); + + // Verify withdrawals + assertTrue(usdcToken.balanceOf(owner) > ownerUSDCBefore, "Should receive USDC"); + assertTrue(cUSDToken.balanceOf(owner) > ownerCUSDBefore, "Should receive cUSD"); + } + + function test_disburseFeesForMultipleTokens() public { + _setupReward(); + + // Make pledges with different tokens + uint256 usdcAmount = getTokenAmount(address(usdcToken), PLEDGE_AMOUNT); + uint256 usdtAmount = getTokenAmount(address(usdtToken), PLEDGE_AMOUNT); + + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdc"), 0); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdt"), 0); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd"), 0); + + vm.warp(LAUNCH_TIME); + + // USDC pledge + vm.startPrank(users.backer1Address); + usdcToken.approve(address(keepWhatsRaised), usdcAmount); + keepWhatsRaised.pledgeWithoutAReward(keccak256("usdc"), users.backer1Address, address(usdcToken), usdcAmount, 0); + vm.stopPrank(); + + // USDT pledge + vm.startPrank(users.backer2Address); + usdtToken.approve(address(keepWhatsRaised), usdtAmount); + keepWhatsRaised.pledgeWithoutAReward(keccak256("usdt"), users.backer2Address, address(usdtToken), usdtAmount, 0); + vm.stopPrank(); + + // cUSD pledge + vm.startPrank(users.backer1Address); + cUSDToken.approve(address(keepWhatsRaised), PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("cusd"), users.backer1Address, address(cUSDToken), PLEDGE_AMOUNT, 0 + ); + vm.stopPrank(); + + // Approve and make partial withdrawal to generate fees + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.approveWithdrawal(); + + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.withdraw(address(cUSDToken), 0); + + // Track balances before disbursement + uint256 protocolUSDCBefore = usdcToken.balanceOf(users.protocolAdminAddress); + uint256 protocolUSDTBefore = usdtToken.balanceOf(users.protocolAdminAddress); + uint256 protocolCUSDBefore = cUSDToken.balanceOf(users.protocolAdminAddress); + + uint256 platformUSDCBefore = usdcToken.balanceOf(users.platform2AdminAddress); + uint256 platformUSDTBefore = usdtToken.balanceOf(users.platform2AdminAddress); + uint256 platformCUSDBefore = cUSDToken.balanceOf(users.platform2AdminAddress); + + // Disburse fees + keepWhatsRaised.disburseFees(); + + // Verify fees were distributed for all tokens + assertTrue( + usdcToken.balanceOf(users.protocolAdminAddress) > protocolUSDCBefore, "Should receive USDC protocol fees" + ); + assertTrue( + usdtToken.balanceOf(users.protocolAdminAddress) > protocolUSDTBefore, "Should receive USDT protocol fees" + ); + assertTrue( + cUSDToken.balanceOf(users.protocolAdminAddress) > protocolCUSDBefore, "Should receive cUSD protocol fees" + ); + + assertTrue( + usdcToken.balanceOf(users.platform2AdminAddress) > platformUSDCBefore, "Should receive USDC platform fees" + ); + assertTrue( + usdtToken.balanceOf(users.platform2AdminAddress) > platformUSDTBefore, "Should receive USDT platform fees" + ); + assertTrue( + cUSDToken.balanceOf(users.platform2AdminAddress) > platformCUSDBefore, "Should receive cUSD platform fees" + ); + } + + function test_refundReturnsCorrectToken() public { + _setupReward(); + + // Backer1 pledges with USDC + uint256 usdcAmount = getTokenAmount(address(usdcToken), TEST_PLEDGE_AMOUNT); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdc_pledge"), 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + usdcToken.approve(address(keepWhatsRaised), usdcAmount); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("usdc_pledge"), users.backer1Address, address(usdcToken), usdcAmount, 0 + ); + uint256 usdcTokenId = 1; // First pledge + vm.stopPrank(); + + // Backer2 pledges with cUSD + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd_pledge"), 0); + + vm.startPrank(users.backer2Address); + cUSDToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("cusd_pledge"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, 0 + ); + uint256 cUSDTokenId = 2; // Second pledge + vm.stopPrank(); + + uint256 backer1USDCBefore = usdcToken.balanceOf(users.backer1Address); + uint256 backer2CUSDBefore = cUSDToken.balanceOf(users.backer2Address); + + // Claim refunds after deadline + vm.warp(DEADLINE + 1); + + // Approve treasury to burn NFTs + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(keepWhatsRaised), usdcTokenId); + + vm.prank(users.backer1Address); + keepWhatsRaised.claimRefund(usdcTokenId); + + vm.prank(users.backer2Address); + CampaignInfo(campaignAddress).approve(address(keepWhatsRaised), cUSDTokenId); + + vm.prank(users.backer2Address); + keepWhatsRaised.claimRefund(cUSDTokenId); + + // Verify correct tokens were refunded (should get something back even after fees) + assertTrue(usdcToken.balanceOf(users.backer1Address) > backer1USDCBefore, "Should refund USDC"); + assertTrue(cUSDToken.balanceOf(users.backer2Address) > backer2CUSDBefore, "Should refund cUSD"); + } + + function test_claimTipWithMultipleTokens() public { + _setupReward(); + + uint256 tipAmountUSDC = getTokenAmount(address(usdcToken), TIP_AMOUNT); + uint256 tipAmountCUSD = TIP_AMOUNT; + + // Pledge with USDC + tip + uint256 usdcPledge = getTokenAmount(address(usdcToken), TEST_PLEDGE_AMOUNT); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdc"), 0); + + vm.warp(LAUNCH_TIME); + vm.startPrank(users.backer1Address); + usdcToken.approve(address(keepWhatsRaised), usdcPledge + tipAmountUSDC); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("usdc"), users.backer1Address, address(usdcToken), usdcPledge, tipAmountUSDC + ); + vm.stopPrank(); + + // Pledge with cUSD + tip + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("cusd"), 0); + + vm.startPrank(users.backer2Address); + cUSDToken.approve(address(keepWhatsRaised), TEST_PLEDGE_AMOUNT + tipAmountCUSD); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("cusd"), users.backer2Address, address(cUSDToken), TEST_PLEDGE_AMOUNT, tipAmountCUSD + ); + vm.stopPrank(); + + uint256 platformUSDCBefore = usdcToken.balanceOf(users.platform2AdminAddress); + uint256 platformCUSDBefore = cUSDToken.balanceOf(users.platform2AdminAddress); + + // Claim tips + vm.warp(DEADLINE + 1); + vm.prank(users.platform2AdminAddress); + keepWhatsRaised.claimTip(); + + // Verify tips in both tokens + assertEq( + usdcToken.balanceOf(users.platform2AdminAddress) - platformUSDCBefore, + tipAmountUSDC, + "Should receive USDC tips" + ); + assertEq( + cUSDToken.balanceOf(users.platform2AdminAddress) - platformCUSDBefore, + tipAmountCUSD, + "Should receive cUSD tips" + ); + } + + function test_mixedTokenPledgesWithDecimalNormalization() public { + _setupReward(); + + // Make three pledges with same normalized value but different decimals + uint256 baseAmount = 1000e18; + uint256 usdcAmount = baseAmount / 1e12; // 6 decimals + uint256 usdtAmount = baseAmount / 1e12; // 6 decimals + uint256 cUSDAmount = baseAmount; // 18 decimals + + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("p1"), 0); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("p2"), 0); + setPaymentGatewayFee(users.platform2AdminAddress, address(keepWhatsRaised), keccak256("p3"), 0); + + vm.warp(LAUNCH_TIME); + + // USDC pledge + vm.startPrank(users.backer1Address); + usdcToken.approve(address(keepWhatsRaised), usdcAmount); + keepWhatsRaised.pledgeWithoutAReward(keccak256("p1"), users.backer1Address, address(usdcToken), usdcAmount, 0); + vm.stopPrank(); + + uint256 raisedAfterUSDC = keepWhatsRaised.getRaisedAmount(); + assertEq(raisedAfterUSDC, baseAmount, "USDC should normalize to base amount"); + + // USDT pledge + vm.startPrank(users.backer2Address); + usdtToken.approve(address(keepWhatsRaised), usdtAmount); + keepWhatsRaised.pledgeWithoutAReward(keccak256("p2"), users.backer2Address, address(usdtToken), usdtAmount, 0); + vm.stopPrank(); + + uint256 raisedAfterUSDT = keepWhatsRaised.getRaisedAmount(); + assertEq(raisedAfterUSDT, baseAmount * 2, "USDT should normalize to base amount"); + + // cUSD pledge + vm.startPrank(users.backer1Address); + cUSDToken.approve(address(keepWhatsRaised), cUSDAmount); + keepWhatsRaised.pledgeWithoutAReward(keccak256("p3"), users.backer1Address, address(cUSDToken), cUSDAmount, 0); + vm.stopPrank(); + + uint256 finalRaised = keepWhatsRaised.getRaisedAmount(); + assertEq(finalRaised, baseAmount * 3, "All pledges should contribute equally after normalization"); + } + + function testPaymentGatewayFeeWithDifferentDecimalTokens() public { + // Test that payment gateway fee is properly denormalized for different decimal tokens + uint256 baseAmount = 1000e18; // 1000 tokens in 18 decimals + uint256 usdtAmount = baseAmount / 1e12; // 1000 USDT (6 decimals) + uint256 usdcAmount = baseAmount / 1e12; // 1000 USDC (6 decimals) + + // Set payment gateway fee (stored in 18 decimals) + uint256 gatewayFee18Decimals = 40e18; // 40 tokens in 18 decimals + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdt_pledge"), gatewayFee18Decimals + ); + setPaymentGatewayFee( + users.platform2AdminAddress, address(keepWhatsRaised), keccak256("usdc_pledge"), gatewayFee18Decimals + ); + + vm.warp(LAUNCH_TIME); + + // USDT pledge + vm.startPrank(users.backer1Address); + usdtToken.approve(address(keepWhatsRaised), usdtAmount); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("usdt_pledge"), users.backer1Address, address(usdtToken), usdtAmount, 0 + ); + vm.stopPrank(); + + // USDC pledge + vm.startPrank(users.backer2Address); + usdcToken.approve(address(keepWhatsRaised), usdcAmount); + keepWhatsRaised.pledgeWithoutAReward( + keccak256("usdc_pledge"), users.backer2Address, address(usdcToken), usdcAmount, 0 + ); + vm.stopPrank(); + + // Verify that both pledges contribute equally to raised amount (normalized) + uint256 raisedAmount = keepWhatsRaised.getRaisedAmount(); + assertEq( + raisedAmount, baseAmount * 2, "Both 6-decimal token pledges should normalize to same 18-decimal amount" + ); + + // Verify that the payment gateway fees were properly denormalized + // For 6-decimal tokens, 40e18 should become 40e6 + uint256 expectedGatewayFee6Decimals = 40e6; + + // Check that fees were calculated correctly by checking available amount + uint256 availableAmount = keepWhatsRaised.getAvailableRaisedAmount(); + + // Calculate expected available amount after fees + uint256 platformFee = (baseAmount * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 vakiCommission = (baseAmount * uint256(VAKI_COMMISSION_VALUE)) / PERCENT_DIVIDER; + uint256 protocolFee = (baseAmount * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 gatewayFeeNormalized = expectedGatewayFee6Decimals * 1e12; // Convert 6-decimal fee to 18-decimal for comparison + + uint256 expectedAvailable = + (baseAmount * 2) - (platformFee * 2) - (vakiCommission * 2) - (protocolFee * 2) - (gatewayFeeNormalized * 2); + + assertEq( + availableAmount, expectedAvailable, "Available amount should account for properly denormalized gateway fees" + ); + } +} diff --git a/test/foundry/unit/PaymentTreasury.t.sol b/test/foundry/unit/PaymentTreasury.t.sol new file mode 100644 index 00000000..a07907c2 --- /dev/null +++ b/test/foundry/unit/PaymentTreasury.t.sol @@ -0,0 +1,1826 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "../integration/PaymentTreasury/PaymentTreasury.t.sol"; +import "forge-std/Test.sol"; +import {PaymentTreasury} from "src/treasuries/PaymentTreasury.sol"; +import {BasePaymentTreasury} from "src/utils/BasePaymentTreasury.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {TestToken} from "../../mocks/TestToken.sol"; + +contract PaymentTreasury_UnitTest is Test, PaymentTreasury_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_000e18); + deal(address(testToken), users.backer2Address, 10_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(paymentTreasury), "PaymentTreasury"); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + function testInitialize() public { + // Create a new campaign for this test + bytes32 newIdentifierHash = keccak256(abi.encodePacked("newPaymentCampaign")); + 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, 2); + PaymentTreasury newContract = PaymentTreasury(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); + } + + /*////////////////////////////////////////////////////////////// + PAYMENT CREATION + //////////////////////////////////////////////////////////////*/ + + function testCreatePayment() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), // Added token parameter + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Payment created but not confirmed + assertEq(paymentTreasury.getRaisedAmount(), 0); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function testCreatePaymentRevertWhenNotPlatformAdmin() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.expectRevert(); + vm.prank(users.backer1Address); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenZeroBuyerId() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + bytes32(0), + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenZeroAmount() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + 0, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenExpired() public { + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + block.timestamp - 1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenZeroPaymentId() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + bytes32(0), + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenZeroItemId() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + bytes32(0), + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenZeroTokenAddress() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(0), // Zero token address + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenTokenNotAccepted() public { + // Create unaccepted token + TestToken unacceptedToken = new TestToken("Unaccepted", "UNACC", 18); + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(unacceptedToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenPaymentExists() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.startPrank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems2 = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_2, + ITEM_ID_2, + address(testToken), + PAYMENT_AMOUNT_2, + expiration, + emptyLineItems2, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + vm.stopPrank(); + } + + function testCreatePaymentRevertWhenPaused() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + // Pause the treasury + vm.prank(users.platform1AdminAddress); + paymentTreasury.pauseTreasury(keccak256("Pause")); + + // createPayment checks both treasury and campaign pause + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenCampaignPaused() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + // Pause the campaign + CampaignInfo actualCampaignInfo = CampaignInfo(campaignAddress); + vm.prank(users.protocolAdminAddress); + actualCampaignInfo._pauseCampaign(keccak256("Pause")); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /*////////////////////////////////////////////////////////////// + CRYPTO PAYMENT PROCESSING + //////////////////////////////////////////////////////////////*/ + + function testProcessCryptoPayment() public { + uint256 amount = 1500e18; + deal(address(testToken), users.backer1Address, amount); + + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, amount); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + users.backer1Address, + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + assertEq(paymentTreasury.getRaisedAmount(), amount); + assertEq(paymentTreasury.getAvailableRaisedAmount(), amount); + assertEq(testToken.balanceOf(treasuryAddress), amount); + } + + function testProcessCryptoPaymentStoresExternalFees() public { + uint256 amount = 1000e18; + deal(address(testToken), users.backer1Address, amount); + + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, amount); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + ICampaignPaymentTreasury.ExternalFees[] memory externalFees = new ICampaignPaymentTreasury.ExternalFees[](2); + externalFees[0] = ICampaignPaymentTreasury.ExternalFees({feeType: keccak256("feeType1"), feeAmount: 10}); + externalFees[1] = ICampaignPaymentTreasury.ExternalFees({feeType: keccak256("feeType2"), feeAmount: 25}); + + bytes32 cryptoPaymentId = keccak256("cryptoPaymentWithFees"); + processCryptoPayment( + users.backer1Address, + cryptoPaymentId, + ITEM_ID_1, + users.backer1Address, + address(testToken), + amount, + emptyLineItems, + externalFees + ); + + ICampaignPaymentTreasury.PaymentData memory paymentData = paymentTreasury.getPaymentData(cryptoPaymentId); + assertEq(paymentData.amount, amount); + assertTrue(paymentData.isConfirmed); + assertEq(paymentData.externalFees.length, 2); + assertEq(paymentData.externalFees[0].feeType, keccak256("feeType1")); + assertEq(paymentData.externalFees[0].feeAmount, 10); + assertEq(paymentData.externalFees[1].feeType, keccak256("feeType2")); + assertEq(paymentData.externalFees[1].feeAmount, 25); + } + + function testProcessCryptoPaymentRevertWhenZeroBuyerAddress() public { + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + users.platform1AdminAddress, + PAYMENT_ID_1, + ITEM_ID_1, + address(0), + address(testToken), + 1000e18, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testProcessCryptoPaymentRevertWhenZeroAmount() public { + vm.expectRevert(); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + users.platform1AdminAddress, + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + 0, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testProcessCryptoPaymentRevertWhenPaymentExists() public { + uint256 amount = 1500e18; + deal(address(testToken), users.backer1Address, amount * 2); + + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, amount * 2); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + users.backer1Address, + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + vm.expectRevert(); + processCryptoPayment( + users.backer1Address, + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + amount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testClaimExpiredFundsRevertsBeforeWindow() public { + uint256 claimDelay = 7 days; + vm.prank(users.platform1AdminAddress); + globalParams.updatePlatformClaimDelay(PLATFORM_1_HASH, claimDelay); + + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + + uint256 claimableAt = CampaignInfo(campaignAddress).getDeadline() + claimDelay; + vm.warp(claimableAt - 1); + + vm.prank(users.platform1AdminAddress); + vm.expectRevert( + abi.encodeWithSelector(BasePaymentTreasury.PaymentTreasuryClaimWindowNotReached.selector, claimableAt) + ); + paymentTreasury.claimExpiredFunds(); + } + + function testClaimExpiredFundsTransfersAllBalances() public { + uint256 claimDelay = 7 days; + vm.prank(users.platform1AdminAddress); + globalParams.updatePlatformClaimDelay(PLATFORM_1_HASH, claimDelay); + + bytes32 refundableTypeId = keccak256("refundable_non_goal_type"); + vm.prank(users.platform1AdminAddress); + globalParams.setPlatformLineItemType( + PLATFORM_1_HASH, refundableTypeId, "Refundable Non Goal", false, false, true, false + ); + + uint256 lineItemAmount = 250e18; + ICampaignPaymentTreasury.LineItem[] memory lineItems = new ICampaignPaymentTreasury.LineItem[](1); + lineItems[0] = ICampaignPaymentTreasury.LineItem({typeId: refundableTypeId, amount: lineItemAmount}); + + uint256 totalAmount = PAYMENT_AMOUNT_1 + lineItemAmount; + deal(address(testToken), users.backer1Address, totalAmount); + + vm.prank(users.backer1Address); + testToken.approve(treasuryAddress, totalAmount); + + vm.prank(users.backer1Address); + paymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + lineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + uint256 claimableAt = CampaignInfo(campaignAddress).getDeadline() + claimDelay; + vm.warp(claimableAt + 1); + + uint256 platformBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); + uint256 protocolBalanceBefore = testToken.balanceOf(users.protocolAdminAddress); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimExpiredFunds(); + + assertEq( + testToken.balanceOf(users.platform1AdminAddress), + platformBalanceBefore + totalAmount, + "Platform admin should receive all remaining funds" + ); + assertEq( + testToken.balanceOf(users.protocolAdminAddress), + protocolBalanceBefore, + "Protocol admin should not receive funds when no protocol fees accrued" + ); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0, "Available raised amount should be zero"); + assertEq(testToken.balanceOf(treasuryAddress), 0, "Treasury token balance should be zero"); + } + + function testClaimExpiredFundsRevertsWhenNoFunds() public { + uint256 claimDelay = 1 days; + vm.prank(users.platform1AdminAddress); + globalParams.updatePlatformClaimDelay(PLATFORM_1_HASH, claimDelay); + + uint256 claimableAt = CampaignInfo(campaignAddress).getDeadline() + claimDelay; + vm.warp(claimableAt + 1); + + vm.prank(users.platform1AdminAddress); + vm.expectRevert(abi.encodeWithSelector(BasePaymentTreasury.PaymentTreasuryNoFundsToClaim.selector)); + paymentTreasury.claimExpiredFunds(); + } + + /*////////////////////////////////////////////////////////////// + PAYMENT CANCELLATION + //////////////////////////////////////////////////////////////*/ + + function testCancelPayment() public { + // Create payment first + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Cancel it + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelPayment(PAYMENT_ID_1); + // Payment should be deleted + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function testCancelPaymentRevertWhenNotExists() public { + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelPayment(PAYMENT_ID_1); + } + + function testCancelPaymentRevertWhenAlreadyConfirmed() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelPayment(PAYMENT_ID_1); + } + + function testCancelPaymentRevertWhenExpired() public { + uint256 expiration = block.timestamp + 1 hours; + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Warp past expiration + vm.warp(expiration + 1); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelPayment(PAYMENT_ID_1); + } + + function testCancelPaymentRevertWhenCryptoPayment() public { + uint256 amount = 1500e18; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, amount, users.backer1Address); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelPayment(PAYMENT_ID_1); + } + + /*////////////////////////////////////////////////////////////// + PAYMENT CONFIRMATION + //////////////////////////////////////////////////////////////*/ + + function testConfirmPayment() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + assertEq(paymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + assertEq(paymentTreasury.getAvailableRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function testConfirmPaymentBatch() public { + _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); + _createAndFundPayment(PAYMENT_ID_3, BUYER_ID_1, ITEM_ID_1, 500e18, users.backer1Address); + + bytes32[] memory paymentIds = new bytes32[](3); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + paymentIds[2] = PAYMENT_ID_3; + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPaymentBatch(paymentIds, _createZeroAddressArray(paymentIds.length)); // Removed token array + + uint256 totalAmount = PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2 + 500e18; + assertEq(paymentTreasury.getRaisedAmount(), totalAmount); + assertEq(paymentTreasury.getAvailableRaisedAmount(), totalAmount); + } + + function testConfirmPaymentRevertWhenNotExists() public { + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + } + + function testConfirmPaymentRevertWhenAlreadyConfirmed() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + vm.expectRevert(); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + vm.stopPrank(); + } + + function testConfirmPaymentRevertWhenCryptoPayment() public { + uint256 amount = 1500e18; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, amount, users.backer1Address); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + } + + /*////////////////////////////////////////////////////////////// + REFUNDS + //////////////////////////////////////////////////////////////*/ + + function testClaimRefund() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + + uint256 balanceAfter = testToken.balanceOf(users.backer1Address); + + assertEq(balanceAfter - balanceBefore, PAYMENT_AMOUNT_1); + assertEq(paymentTreasury.getRaisedAmount(), 0); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function testClaimRefundBuyerInitiated() public { + uint256 amount = 1500e18; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, amount, users.backer1Address); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(paymentTreasury), 1); + + vm.prank(users.backer1Address); + paymentTreasury.claimRefund(PAYMENT_ID_1); + + uint256 balanceAfter = testToken.balanceOf(users.backer1Address); + + assertEq(balanceAfter - balanceBefore, amount); + assertEq(paymentTreasury.getRaisedAmount(), 0); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function testClaimRefundByPlatformAdminForCryptoPayment() public { + uint256 amount = 1500e18; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, amount, users.backer1Address); + + uint256 balanceBefore = testToken.balanceOf(users.backer1Address); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(paymentTreasury), 1); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1); + + uint256 balanceAfter = testToken.balanceOf(users.backer1Address); + + assertEq(balanceAfter - balanceBefore, amount); + assertEq(paymentTreasury.getRaisedAmount(), 0); + } + + function testClaimRefundRevertWhenNotConfirmed() public { + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + } + + function testClaimRefundRevertWhenNotExists() public { + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + } + + function testClaimRefundRevertWhenPaused() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + // Pause treasury + vm.prank(users.platform1AdminAddress); + paymentTreasury.pauseTreasury(keccak256("Pause")); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + } + + function testClaimRefundRevertWhenUnauthorizedForCryptoPayment() public { + uint256 amount = 1500e18; + _createAndProcessCryptoPayment(PAYMENT_ID_1, ITEM_ID_1, amount, users.backer1Address); + + vm.expectRevert(); + vm.prank(users.backer2Address); // Different buyer + paymentTreasury.claimRefund(PAYMENT_ID_1); + } + + /*////////////////////////////////////////////////////////////// + FEE DISBURSEMENT + //////////////////////////////////////////////////////////////*/ + + function testDisburseFees() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + // Withdraw first to calculate fees + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + + uint256 protocolBalanceBefore = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformBalanceBefore = testToken.balanceOf(users.platform1AdminAddress); + + paymentTreasury.disburseFees(); + + uint256 protocolBalanceAfter = testToken.balanceOf(users.protocolAdminAddress); + uint256 platformBalanceAfter = testToken.balanceOf(users.platform1AdminAddress); + assertTrue(protocolBalanceAfter > protocolBalanceBefore); + assertTrue(platformBalanceAfter > platformBalanceBefore); + } + + function testDisburseFeesMultipleTimes() public { + _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); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + // First withdrawal and disbursement + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + paymentTreasury.disburseFees(); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); // Removed token parameter + + // Second withdrawal and disbursement + vm.prank(owner); + paymentTreasury.withdraw(); + paymentTreasury.disburseFees(); + } + + function testDisburseFeesRevertWhenPaused() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + + // Pause treasury + vm.prank(users.platform1AdminAddress); + paymentTreasury.pauseTreasury(keccak256("Pause")); + + vm.expectRevert(); + paymentTreasury.disburseFees(); + } + + /*////////////////////////////////////////////////////////////// + WITHDRAWALS + //////////////////////////////////////////////////////////////*/ + + function testWithdraw() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + address owner = CampaignInfo(campaignAddress).owner(); + uint256 ownerBalanceBefore = testToken.balanceOf(owner); + + vm.prank(owner); + paymentTreasury.withdraw(); + + uint256 ownerBalanceAfter = testToken.balanceOf(owner); + + uint256 expectedProtocolFee = (PAYMENT_AMOUNT_1 * PROTOCOL_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedPlatformFee = (PAYMENT_AMOUNT_1 * PLATFORM_FEE_PERCENT) / PERCENT_DIVIDER; + uint256 expectedWithdrawal = PAYMENT_AMOUNT_1 - expectedProtocolFee - expectedPlatformFee; + + assertEq(ownerBalanceAfter - ownerBalanceBefore, expectedWithdrawal); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function testWithdrawRevertWhenAlreadyWithdrawn() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + paymentTreasury.disburseFees(); + + vm.expectRevert(); + vm.prank(owner); + paymentTreasury.withdraw(); + } + + function testWithdrawRevertWhenPaused() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + // Pause treasury + vm.prank(users.platform1AdminAddress); + paymentTreasury.pauseTreasury(keccak256("Pause")); + + address owner = CampaignInfo(campaignAddress).owner(); + vm.expectRevert(); + vm.prank(owner); + paymentTreasury.withdraw(); + } + + /*////////////////////////////////////////////////////////////// + PAUSE AND CANCEL + //////////////////////////////////////////////////////////////*/ + + function testPauseTreasury() public { + // First create and confirm a payment to test functions that require it + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + // Pause the treasury + vm.prank(users.platform1AdminAddress); + paymentTreasury.pauseTreasury(keccak256("Pause")); + // Functions that check treasury pause status should revert + // claimRefund uses whenNotPaused + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + + // disburseFees uses whenNotPaused + vm.expectRevert(); + paymentTreasury.disburseFees(); + + // createPayment checks treasury pause as well + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_2, + BUYER_ID_1, + ITEM_ID_2, + address(testToken), + PAYMENT_AMOUNT_2, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testUnpauseTreasury() public { + vm.prank(users.platform1AdminAddress); + paymentTreasury.pauseTreasury(keccak256("Pause")); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.unpauseTreasury(keccak256("Unpause")); + + // Should be able to create payment + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCancelTreasuryByPlatformAdmin() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelTreasury(keccak256("Cancel")); + + // disburseFees() should succeed even when cancelled (fixes vulnerability) + paymentTreasury.disburseFees(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_2, + BUYER_ID_1, + ITEM_ID_2, + address(testToken), + PAYMENT_AMOUNT_2, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCancelTreasuryByCampaignOwner() public { + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.cancelTreasury(keccak256("Cancel")); + + // disburseFees() should succeed even when cancelled (fixes vulnerability) + paymentTreasury.disburseFees(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_2, + BUYER_ID_1, + ITEM_ID_2, + address(testToken), + PAYMENT_AMOUNT_2, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCancelTreasuryRevertWhenUnauthorized() public { + vm.expectRevert(); + vm.prank(users.backer1Address); + paymentTreasury.cancelTreasury(keccak256("Cancel")); + } + + /*////////////////////////////////////////////////////////////// + EDGE CASES + //////////////////////////////////////////////////////////////*/ + + function testMultipleRefundsAfterBatchConfirm() public { + // Create multiple 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); + _createAndFundPayment(PAYMENT_ID_3, BUYER_ID_1, ITEM_ID_1, 500e18, users.backer1Address); + + // Confirm all in batch + bytes32[] memory paymentIds = new bytes32[](3); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + paymentIds[2] = PAYMENT_ID_3; + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPaymentBatch(paymentIds, _createZeroAddressArray(paymentIds.length)); // Removed token array + + uint256 totalAmount = PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2 + 500e18; + assertEq(paymentTreasury.getRaisedAmount(), totalAmount); + + // Refund payments one by one + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + assertEq(paymentTreasury.getRaisedAmount(), totalAmount - PAYMENT_AMOUNT_1); + + paymentTreasury.claimRefund(PAYMENT_ID_2, users.backer2Address); + assertEq(paymentTreasury.getRaisedAmount(), totalAmount - PAYMENT_AMOUNT_1 - PAYMENT_AMOUNT_2); + + paymentTreasury.claimRefund(PAYMENT_ID_3, users.backer1Address); + assertEq(paymentTreasury.getRaisedAmount(), 0); + vm.stopPrank(); + } + + function testZeroBalanceAfterAllRefunds() public { + _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); + + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); // Removed token parameter + + // Refund all payments + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + paymentTreasury.claimRefund(PAYMENT_ID_2, users.backer2Address); + vm.stopPrank(); + + assertEq(paymentTreasury.getRaisedAmount(), 0); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + + // Withdraw should revert because balance is 0 + address owner = CampaignInfo(campaignAddress).owner(); + vm.expectRevert(); + vm.prank(owner); + paymentTreasury.withdraw(); + } + + function testPaymentExpirationScenarios() public { + uint256 shortExpiration = block.timestamp + 1 hours; + uint256 longExpiration = block.timestamp + 7 days; + + // Create payments with different expirations + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + shortExpiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + paymentTreasury.createPayment( + PAYMENT_ID_2, + BUYER_ID_2, + ITEM_ID_2, + address(testToken), + PAYMENT_AMOUNT_2, + longExpiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + vm.stopPrank(); + + // Fund both payments + vm.prank(users.backer1Address); + testToken.transfer(treasuryAddress, PAYMENT_AMOUNT_1); + + vm.prank(users.backer2Address); + testToken.transfer(treasuryAddress, PAYMENT_AMOUNT_2); + // Confirm first payment before expiration + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + // Warp past first expiration but before second + vm.warp(shortExpiration + 1); + // Cannot cancel or confirm expired payment + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.cancelPayment(PAYMENT_ID_1); + // Can still confirm non-expired payment + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); // Removed token parameter + + assertEq(paymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2); + } + + function testMixedPaymentTypes() public { + // Create both regular and crypto payments + _createAndFundPayment(PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, PAYMENT_AMOUNT_1, users.backer1Address); + _createAndProcessCryptoPayment(PAYMENT_ID_2, ITEM_ID_2, PAYMENT_AMOUNT_2, users.backer2Address); + + // Confirm regular payment + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + // Both should contribute to raised amount + uint256 totalAmount = PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2; + assertEq(paymentTreasury.getRaisedAmount(), totalAmount); + assertEq(paymentTreasury.getAvailableRaisedAmount(), totalAmount); + + // Withdraw and disburse fees + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + paymentTreasury.disburseFees(); + + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function testCannotCreatePhantomBalances() public { + // Create payment for 1000 tokens with USDC specified + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), // Token specified during creation + 1000e18, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Try to confirm without any tokens - should revert + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + // Send the tokens + deal(address(testToken), users.backer1Address, 1000e18); + vm.prank(users.backer1Address); + testToken.transfer(treasuryAddress, 1000e18); + + // Now confirmation works + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + assertEq(paymentTreasury.getRaisedAmount(), 1000e18); + } + + function testCannotConfirmMoreThanBalance() public { + // Create two payments of 500 each, both with testToken + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.startPrank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + 500e18, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + paymentTreasury.createPayment( + PAYMENT_ID_2, + BUYER_ID_2, + ITEM_ID_2, + address(testToken), + 500e18, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + vm.stopPrank(); + + // Send only 500 tokens total + deal(address(testToken), users.backer1Address, 500e18); + vm.prank(users.backer1Address); + testToken.transfer(treasuryAddress, 500e18); + + // Can confirm one payment + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); // Removed token parameter + + // Cannot confirm second payment - total would exceed balance + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); // Removed token parameter + + assertEq(paymentTreasury.getRaisedAmount(), 500e18); + } + + function testBatchConfirmRespectsBalance() public { + // Create two payments + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.startPrank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + ICampaignPaymentTreasury.ExternalFees[] memory emptyExternalFees = + new ICampaignPaymentTreasury.ExternalFees[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + 500e18, + expiration, + emptyLineItems, + emptyExternalFees + ); + paymentTreasury.createPayment( + PAYMENT_ID_2, + BUYER_ID_2, + ITEM_ID_2, + address(testToken), + 500e18, + expiration, + emptyLineItems, + emptyExternalFees + ); + vm.stopPrank(); + + // Send only 500 tokens + deal(address(testToken), users.backer1Address, 500e18); + vm.prank(users.backer1Address); + testToken.transfer(treasuryAddress, 500e18); + + // Try to confirm both + bytes32[] memory paymentIds = new bytes32[](2); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPaymentBatch(paymentIds, _createZeroAddressArray(paymentIds.length)); // Removed token array + } + + /*////////////////////////////////////////////////////////////// + MULTI-TOKEN SPECIFIC UNIT TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfirmPaymentWithDifferentTokens() public { + // Create payments expecting different tokens + uint256 usdtAmount = getTokenAmount(address(usdtToken), 500e18); + uint256 cUSDAmount = 700e18; + + // Create USDT payment - token specified during creation + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + // Create cUSD payment - token specified during creation + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + // Confirm without specifying token + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); + vm.stopPrank(); + + uint256 expectedTotal = 500e18 + 700e18; + assertEq(paymentTreasury.getRaisedAmount(), expectedTotal); + } + + function testProcessCryptoPaymentWithDifferentTokens() public { + uint256 usdcAmount = getTokenAmount(address(usdcToken), 800e18); + uint256 cUSDAmount = 1200e18; + + // USDC payment + deal(address(usdcToken), users.backer1Address, usdcAmount); + vm.prank(users.backer1Address); + usdcToken.approve(treasuryAddress, usdcAmount); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + processCryptoPayment( + users.backer1Address, + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(usdcToken), + usdcAmount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // cUSD payment + deal(address(cUSDToken), users.backer2Address, cUSDAmount); + vm.prank(users.backer2Address); + cUSDToken.approve(treasuryAddress, cUSDAmount); + processCryptoPayment( + users.backer2Address, + PAYMENT_ID_2, + ITEM_ID_2, + users.backer2Address, + address(cUSDToken), + cUSDAmount, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + uint256 expectedTotal = 800e18 + 1200e18; + assertEq(paymentTreasury.getRaisedAmount(), expectedTotal); + assertEq(usdcToken.balanceOf(treasuryAddress), usdcAmount); + assertEq(cUSDToken.balanceOf(treasuryAddress), cUSDAmount); + } + + function testBatchConfirmWithMixedTokens() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), 500e18); + uint256 usdcAmount = getTokenAmount(address(usdcToken), 600e18); + uint256 cUSDAmount = 700e18; + + // Create payments with tokens specified + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, usdcAmount, users.backer2Address, address(usdcToken) + ); + _createAndFundPaymentWithToken( + PAYMENT_ID_3, BUYER_ID_3, ITEM_ID_1, cUSDAmount, users.backer1Address, address(cUSDToken) + ); + + // Batch confirm without token array + bytes32[] memory paymentIds = new bytes32[](3); + paymentIds[0] = PAYMENT_ID_1; + paymentIds[1] = PAYMENT_ID_2; + paymentIds[2] = PAYMENT_ID_3; + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPaymentBatch(paymentIds, _createZeroAddressArray(paymentIds.length)); + + uint256 expectedTotal = 500e18 + 600e18 + 700e18; + assertEq(paymentTreasury.getRaisedAmount(), expectedTotal); + } + + function testRefundReturnsCorrectTokenType() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), PAYMENT_AMOUNT_1); + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + uint256 usdtBefore = usdtToken.balanceOf(users.backer1Address); + uint256 cUSDBefore = cUSDToken.balanceOf(users.backer1Address); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + + // Should receive USDT, not cUSD + assertEq(usdtToken.balanceOf(users.backer1Address) - usdtBefore, usdtAmount, "Should receive USDT"); + assertEq(cUSDToken.balanceOf(users.backer1Address), cUSDBefore, "cUSD should be unchanged"); + } + + function testWithdrawDistributesAllTokens() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), 1000e18); + uint256 cUSDAmount = 1500e18; + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); + vm.stopPrank(); + + address owner = CampaignInfo(campaignAddress).owner(); + uint256 ownerUSDTBefore = usdtToken.balanceOf(owner); + uint256 ownerCUSDBefore = cUSDToken.balanceOf(owner); + + vm.prank(owner); + paymentTreasury.withdraw(); + + // Should receive both tokens + assertTrue(usdtToken.balanceOf(owner) > ownerUSDTBefore, "Should receive USDT"); + assertTrue(cUSDToken.balanceOf(owner) > ownerCUSDBefore, "Should receive cUSD"); + } + + function testDisburseFeesDistributesAllTokens() public { + uint256 usdcAmount = getTokenAmount(address(usdcToken), PAYMENT_AMOUNT_1); + uint256 cUSDAmount = PAYMENT_AMOUNT_2; + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdcAmount, users.backer1Address, address(usdcToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); + vm.stopPrank(); + + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + + uint256 protocolUSDCBefore = usdcToken.balanceOf(users.protocolAdminAddress); + uint256 protocolCUSDBefore = cUSDToken.balanceOf(users.protocolAdminAddress); + uint256 platformUSDCBefore = usdcToken.balanceOf(users.platform1AdminAddress); + uint256 platformCUSDBefore = cUSDToken.balanceOf(users.platform1AdminAddress); + + paymentTreasury.disburseFees(); + + // All token types should have fees disbursed + assertTrue( + usdcToken.balanceOf(users.protocolAdminAddress) > protocolUSDCBefore, "Should disburse USDC to protocol" + ); + assertTrue( + cUSDToken.balanceOf(users.protocolAdminAddress) > protocolCUSDBefore, "Should disburse cUSD to protocol" + ); + assertTrue( + usdcToken.balanceOf(users.platform1AdminAddress) > platformUSDCBefore, "Should disburse USDC to platform" + ); + assertTrue( + cUSDToken.balanceOf(users.platform1AdminAddress) > platformCUSDBefore, "Should disburse cUSD to platform" + ); + } + + function testDecimalNormalizationAccuracy() public { + // Test that 1000 USDT (6 decimals) = 1000 cUSD (18 decimals) after normalization + uint256 baseAmount = 1000e18; + uint256 usdtAmount = baseAmount / 1e12; // 1000 USDT (1000000000) + uint256 cUSDAmount = baseAmount; // 1000 cUSD (1000000000000000000000) + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + uint256 raisedAfterUSDT = paymentTreasury.getRaisedAmount(); + assertEq(raisedAfterUSDT, baseAmount, "1000 USDT should equal 1000e18 normalized"); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); + + uint256 totalRaised = paymentTreasury.getRaisedAmount(); + assertEq(totalRaised, baseAmount * 2, "Both should contribute equally"); + } + + function testCannotConfirmWithInsufficientBalancePerToken() public { + uint256 usdtAmount = getTokenAmount(address(usdtToken), 1000e18); + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + + // Create two payments expecting USDT + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + paymentTreasury.createPayment( + PAYMENT_ID_2, + BUYER_ID_2, + ITEM_ID_2, + address(usdtToken), // Token specified + usdtAmount, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Only funded first payment, second has no tokens + + // Can confirm first + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + // Cannot confirm second - insufficient USDT balance + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); + } + + function testMixedTokenRefundsAfterPartialWithdraw() public { + // This tests the edge case where some tokens are withdrawn but others have pending refunds + uint256 usdtAmount = getTokenAmount(address(usdtToken), 1000e18); + uint256 cUSDAmount = 1500e18; + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + _createAndFundPaymentWithToken( + PAYMENT_ID_2, BUYER_ID_2, ITEM_ID_2, cUSDAmount, users.backer2Address, address(cUSDToken) + ); + + vm.startPrank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + paymentTreasury.confirmPayment(PAYMENT_ID_2, address(0)); + vm.stopPrank(); + + // Withdraw (takes fees) + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + + // Try to refund - should fail because funds were withdrawn + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + paymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + } + + function testZeroBalanceTokensHandledGracefully() public { + // Create payment with USDT only + uint256 usdtAmount = getTokenAmount(address(usdtToken), PAYMENT_AMOUNT_1); + + _createAndFundPaymentWithToken( + PAYMENT_ID_1, BUYER_ID_1, ITEM_ID_1, usdtAmount, users.backer1Address, address(usdtToken) + ); + + vm.prank(users.platform1AdminAddress); + paymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + + // Withdraw should handle zero-balance tokens (USDC, cUSD) gracefully + address owner = CampaignInfo(campaignAddress).owner(); + vm.prank(owner); + paymentTreasury.withdraw(); + + // Disburse should also handle it + paymentTreasury.disburseFees(); + + // Verify only USDT was processed + assertEq(usdcToken.balanceOf(treasuryAddress), 0, "USDC should remain zero"); + assertEq(cUSDToken.balanceOf(treasuryAddress), 0, "cUSD should remain zero"); + } + + /*////////////////////////////////////////////////////////////// + PAYMENT BATCH CREATION + //////////////////////////////////////////////////////////////*/ + + function testCreatePaymentBatch() public { + bytes32[] memory paymentIds = new bytes32[](3); + bytes32[] memory buyerIds = new bytes32[](3); + bytes32[] memory itemIds = new bytes32[](3); + uint256[] memory amounts = new uint256[](3); + uint256[] memory expirations = new uint256[](3); + + // Set up payment data + paymentIds[0] = keccak256("batchPayment1"); + paymentIds[1] = keccak256("batchPayment2"); + paymentIds[2] = keccak256("batchPayment3"); + + buyerIds[0] = BUYER_ID_1; + buyerIds[1] = BUYER_ID_2; + buyerIds[2] = BUYER_ID_3; + + itemIds[0] = ITEM_ID_1; + itemIds[1] = ITEM_ID_2; + itemIds[2] = ITEM_ID_1; // Reuse existing item ID + + amounts[0] = 100 * 10 ** 18; + amounts[1] = 200 * 10 ** 18; + amounts[2] = 300 * 10 ** 18; + + expirations[0] = block.timestamp + 1 days; + expirations[1] = block.timestamp + 2 days; + expirations[2] = block.timestamp + 3 days; + + // Execute batch creation + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = new ICampaignPaymentTreasury.LineItem[][](3); + emptyLineItemsArray[0] = new ICampaignPaymentTreasury.LineItem[](0); + emptyLineItemsArray[1] = new ICampaignPaymentTreasury.LineItem[](0); + emptyLineItemsArray[2] = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.ExternalFees[][] memory emptyExternalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](3); + for (uint256 i = 0; i < 3; i++) { + emptyExternalFeesArray[i] = new ICampaignPaymentTreasury.ExternalFees[](0); + } + paymentTreasury.createPaymentBatch( + paymentIds, + buyerIds, + itemIds, + _createPaymentTokensArray(3, address(testToken)), + amounts, + expirations, + emptyLineItemsArray, + emptyExternalFeesArray + ); + + // Verify that payments were created by checking raised amount is still 0 (not confirmed yet) + assertEq(paymentTreasury.getRaisedAmount(), 0); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + } + + function testCreatePaymentBatchRevertWhenArrayLengthMismatch() public { + bytes32[] memory paymentIds = new bytes32[](2); + bytes32[] memory buyerIds = new bytes32[](3); // Different length + bytes32[] memory itemIds = new bytes32[](2); + uint256[] memory amounts = new uint256[](2); + uint256[] memory expirations = new uint256[](2); + + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = + new ICampaignPaymentTreasury.LineItem[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyLineItemsArray[i] = new ICampaignPaymentTreasury.LineItem[](0); + } + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.ExternalFees[][] memory emptyExternalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyExternalFeesArray[i] = new ICampaignPaymentTreasury.ExternalFees[](0); + } + vm.expectRevert(); + paymentTreasury.createPaymentBatch( + paymentIds, + buyerIds, + itemIds, + _createPaymentTokensArray(paymentIds.length, address(testToken)), + amounts, + expirations, + emptyLineItemsArray, + emptyExternalFeesArray + ); + } + + function testCreatePaymentBatchRevertWhenEmptyArray() public { + bytes32[] memory paymentIds = new bytes32[](0); + bytes32[] memory buyerIds = new bytes32[](0); + bytes32[] memory itemIds = new bytes32[](0); + uint256[] memory amounts = new uint256[](0); + uint256[] memory expirations = new uint256[](0); + + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = + new ICampaignPaymentTreasury.LineItem[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyLineItemsArray[i] = new ICampaignPaymentTreasury.LineItem[](0); + } + ICampaignPaymentTreasury.ExternalFees[][] memory emptyExternalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyExternalFeesArray[i] = new ICampaignPaymentTreasury.ExternalFees[](0); + } + vm.prank(users.platform1AdminAddress); + vm.expectRevert(); + paymentTreasury.createPaymentBatch( + paymentIds, + buyerIds, + itemIds, + _createPaymentTokensArray(paymentIds.length, address(testToken)), + amounts, + expirations, + emptyLineItemsArray, + emptyExternalFeesArray + ); + } + + function testCreatePaymentBatchRevertWhenPaymentAlreadyExists() public { + // First create a single payment + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + paymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Now try to create a batch with the same payment ID + bytes32[] memory paymentIds = new bytes32[](1); + bytes32[] memory buyerIds = new bytes32[](1); + bytes32[] memory itemIds = new bytes32[](1); + uint256[] memory amounts = new uint256[](1); + uint256[] memory expirations = new uint256[](1); + + paymentIds[0] = PAYMENT_ID_1; // Same ID as above + buyerIds[0] = BUYER_ID_2; + itemIds[0] = ITEM_ID_2; + amounts[0] = PAYMENT_AMOUNT_2; + expirations[0] = block.timestamp + 2 days; + + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = + new ICampaignPaymentTreasury.LineItem[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyLineItemsArray[i] = new ICampaignPaymentTreasury.LineItem[](0); + } + ICampaignPaymentTreasury.ExternalFees[][] memory emptyExternalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyExternalFeesArray[i] = new ICampaignPaymentTreasury.ExternalFees[](0); + } + vm.prank(users.platform1AdminAddress); + vm.expectRevert(); + paymentTreasury.createPaymentBatch( + paymentIds, + buyerIds, + itemIds, + _createPaymentTokensArray(paymentIds.length, address(testToken)), + amounts, + expirations, + emptyLineItemsArray, + emptyExternalFeesArray + ); + } + + function testCreatePaymentBatchRevertWhenNotPlatformAdmin() public { + bytes32[] memory paymentIds = new bytes32[](1); + bytes32[] memory buyerIds = new bytes32[](1); + bytes32[] memory itemIds = new bytes32[](1); + uint256[] memory amounts = new uint256[](1); + uint256[] memory expirations = new uint256[](1); + + paymentIds[0] = keccak256("batchPayment1"); + buyerIds[0] = BUYER_ID_1; + itemIds[0] = ITEM_ID_1; + amounts[0] = PAYMENT_AMOUNT_1; + expirations[0] = block.timestamp + 1 days; + + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = + new ICampaignPaymentTreasury.LineItem[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyLineItemsArray[i] = new ICampaignPaymentTreasury.LineItem[](0); + } + ICampaignPaymentTreasury.ExternalFees[][] memory emptyExternalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](paymentIds.length); + for (uint256 i = 0; i < paymentIds.length; i++) { + emptyExternalFeesArray[i] = new ICampaignPaymentTreasury.ExternalFees[](0); + } + vm.prank(users.creator1Address); // Not platform admin + vm.expectRevert(); + paymentTreasury.createPaymentBatch( + paymentIds, + buyerIds, + itemIds, + _createPaymentTokensArray(paymentIds.length, address(testToken)), + amounts, + expirations, + emptyLineItemsArray, + emptyExternalFeesArray + ); + } + + function testCreatePaymentBatchWithMultipleTokens() public { + // Create payments with different tokens + bytes32[] memory paymentIds = new bytes32[](3); + bytes32[] memory buyerIds = new bytes32[](3); + bytes32[] memory itemIds = new bytes32[](3); + address[] memory paymentTokens = new address[](3); + uint256[] memory amounts = new uint256[](3); + uint256[] memory expirations = new uint256[](3); + + paymentIds[0] = keccak256("payment1"); + paymentIds[1] = keccak256("payment2"); + paymentIds[2] = keccak256("payment3"); + + buyerIds[0] = BUYER_ID_1; + buyerIds[1] = BUYER_ID_2; + buyerIds[2] = BUYER_ID_3; + + itemIds[0] = ITEM_ID_1; + itemIds[1] = ITEM_ID_2; + itemIds[2] = ITEM_ID_1; + + // Use different tokens for each payment + paymentTokens[0] = address(testToken); // cUSD + paymentTokens[1] = address(testToken); // cUSD (same token for simplicity in test) + paymentTokens[2] = address(testToken); // cUSD (same token for simplicity in test) + + amounts[0] = 100 * 10 ** 18; + amounts[1] = 200 * 10 ** 18; + amounts[2] = 300 * 10 ** 18; + + expirations[0] = block.timestamp + 1 days; + expirations[1] = block.timestamp + 2 days; + expirations[2] = block.timestamp + 3 days; + + // Execute batch creation with multiple tokens + ICampaignPaymentTreasury.LineItem[][] memory emptyLineItemsArray = new ICampaignPaymentTreasury.LineItem[][](3); + emptyLineItemsArray[0] = new ICampaignPaymentTreasury.LineItem[](0); + emptyLineItemsArray[1] = new ICampaignPaymentTreasury.LineItem[](0); + emptyLineItemsArray[2] = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](3); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + externalFeesArray[1] = new ICampaignPaymentTreasury.ExternalFees[](0); + externalFeesArray[2] = new ICampaignPaymentTreasury.ExternalFees[](0); + paymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, emptyLineItemsArray, externalFeesArray + ); + + // Verify that payments were created + assertEq(paymentTreasury.getRaisedAmount(), 0); + assertEq(paymentTreasury.getAvailableRaisedAmount(), 0); + + // Verify that the batch operation completed successfully + // (The fact that no revert occurred means all payments were created successfully) + assertTrue(true); // This test passes if no revert occurred during batch creation + } +} diff --git a/test/foundry/unit/PledgeNFT.t.sol b/test/foundry/unit/PledgeNFT.t.sol new file mode 100644 index 00000000..a79122d7 --- /dev/null +++ b/test/foundry/unit/PledgeNFT.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import "../Base.t.sol"; + +contract PledgeNFT_Test is Base_Test { + CampaignInfo public campaign; + KeepWhatsRaised public treasury; + + bytes32 public constant PLATFORM_HASH = keccak256("PLATFORM_1"); + bytes32 public constant TREASURY_ROLE = keccak256("TREASURY_ROLE"); + + function setUp() public override { + super.setUp(); + + // Enlist platform + vm.prank(users.protocolAdminAddress); + globalParams.enlistPlatform(PLATFORM_HASH, users.platform1AdminAddress, PLATFORM_FEE_PERCENT, address(0)); + + // Register treasury implementation + vm.startPrank(users.platform1AdminAddress); + treasuryFactory.registerTreasuryImplementation(PLATFORM_HASH, 1, address(keepWhatsRaisedImplementation)); + vm.stopPrank(); + + vm.prank(users.protocolAdminAddress); + treasuryFactory.approveTreasuryImplementation(PLATFORM_HASH, 1); + + // Create a campaign + bytes32 identifierHash = keccak256("TEST_CAMPAIGN"); + bytes32[] memory selectedPlatforms = new bytes32[](1); + selectedPlatforms[0] = PLATFORM_HASH; + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + + vm.prank(users.creator1Address); + campaignInfoFactory.createCampaign( + users.creator1Address, + identifierHash, + selectedPlatforms, + keys, + values, + CAMPAIGN_DATA, + "Test Campaign NFT", + "TCNFT", + "ipfs://QmTestImage", + "ipfs://QmTestContract" + ); + + address campaignAddress = campaignInfoFactory.identifierToCampaignInfo(identifierHash); + campaign = CampaignInfo(campaignAddress); + + // Deploy treasury + vm.prank(users.platform1AdminAddress); + address treasuryAddress = treasuryFactory.deploy(PLATFORM_HASH, campaignAddress, 1); + treasury = KeepWhatsRaised(treasuryAddress); + } + + function test_OnlyTreasuryCanMintNFT() public { + // Try to mint without TREASURY_ROLE - should revert + vm.expectRevert(); + vm.prank(users.backer1Address); + campaign.mintNFTForPledge(users.backer1Address, bytes32(0), address(testToken), 100e18, 0, 0); + } + + function test_TreasuryCanMintNFT() public { + // Treasury has TREASURY_ROLE, should be able to mint + vm.prank(address(treasury)); + uint256 tokenId = campaign.mintNFTForPledge(users.backer1Address, bytes32(0), address(testToken), 100e18, 0, 0); + + // Verify NFT was minted + assertEq(tokenId, 1, "First token ID should be 1"); + assertEq(campaign.balanceOf(users.backer1Address), 1, "Backer should have 1 NFT"); + assertEq(campaign.ownerOf(tokenId), users.backer1Address, "Backer should own the NFT"); + } + + function test_TokenIdIncrementsAndNeverReuses() public { + // Mint first NFT + vm.prank(address(treasury)); + uint256 tokenId1 = campaign.mintNFTForPledge(users.backer1Address, bytes32(0), address(testToken), 100e18, 0, 0); + assertEq(tokenId1, 1, "First token ID should be 1"); + + // Mint second NFT + vm.prank(address(treasury)); + uint256 tokenId2 = campaign.mintNFTForPledge(users.backer1Address, bytes32(0), address(testToken), 100e18, 0, 0); + assertEq(tokenId2, 2, "Second token ID should be 2"); + + // Burn first NFT + vm.prank(users.backer1Address); + campaign.burn(tokenId1); + + // Mint third NFT - should be 3, NOT reusing 1 + vm.prank(address(treasury)); + uint256 tokenId3 = campaign.mintNFTForPledge(users.backer1Address, bytes32(0), address(testToken), 100e18, 0, 0); + assertEq(tokenId3, 3, "Third token ID should be 3, not reusing burned ID 1"); + + // Verify balances + assertEq(campaign.balanceOf(users.backer1Address), 2, "Backer should have 2 NFTs after burn"); + } + + function test_BurnRemovesNFT() public { + // Mint NFT + vm.prank(address(treasury)); + uint256 tokenId = campaign.mintNFTForPledge(users.backer1Address, bytes32(0), address(testToken), 100e18, 0, 0); + + assertEq(campaign.balanceOf(users.backer1Address), 1, "Backer should have 1 NFT"); + + // Burn NFT + vm.prank(users.backer1Address); + campaign.burn(tokenId); + + // Verify NFT was burned + assertEq(campaign.balanceOf(users.backer1Address), 0, "Backer should have 0 NFTs after burn"); + + // Trying to query owner of burned token should revert + vm.expectRevert(); + campaign.ownerOf(tokenId); + } +} diff --git a/test/foundry/unit/TestToken.t.sol b/test/foundry/unit/TestToken.t.sol index 0ba3c4ed..4181e0c5 100644 --- a/test/foundry/unit/TestToken.t.sol +++ b/test/foundry/unit/TestToken.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import "forge-std/Test.sol"; import {TestToken} from "../../mocks/TestToken.sol"; @@ -12,7 +12,7 @@ contract TestToken_UnitTest is Test, Defaults { uint256 internal mintAmount = 1_000 * 1e18; function setUp() public { - token = new TestToken(tokenName, tokenSymbol); + token = new TestToken(tokenName, tokenSymbol, 18); } function testMintIncreasesBalance() public { diff --git a/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol b/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol new file mode 100644 index 00000000..845d2446 --- /dev/null +++ b/test/foundry/unit/TimeConstrainedPaymentTreasury.t.sol @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "../integration/TimeConstrainedPaymentTreasury/TimeConstrainedPaymentTreasuryFunction.t.sol"; +import "forge-std/Test.sol"; +import {TimeConstrainedPaymentTreasury} from "src/treasuries/TimeConstrainedPaymentTreasury.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {TestToken} from "../../mocks/TestToken.sol"; +import {DataRegistryKeys} from "src/constants/DataRegistryKeys.sol"; + +contract TimeConstrainedPaymentTreasury_UnitTest is + Test, + TimeConstrainedPaymentTreasuryFunction_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_000e18); + deal(address(testToken), users.backer2Address, 10_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(timeConstrainedPaymentTreasury), "TimeConstrainedPaymentTreasury"); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + function testInitialize() public { + // Create a new campaign for this test + bytes32 newIdentifierHash = keccak256(abi.encodePacked("newTimeConstrainedCampaign")); + 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, + 3 // TimeConstrainedPaymentTreasury type + ); + TimeConstrainedPaymentTreasury newContract = TimeConstrainedPaymentTreasury(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); + } + + /*////////////////////////////////////////////////////////////// + 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); + timeConstrainedPaymentTreasury.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(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + assertEq(timeConstrainedPaymentTreasury.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); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCreatePaymentRevertWhenAfterDeadlinePlusBuffer() public { + advanceToAfterDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.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); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.ExternalFees[][] memory externalFeesArray = + new ICampaignPaymentTreasury.ExternalFees[][](2); + externalFeesArray[0] = new ICampaignPaymentTreasury.ExternalFees[](0); + externalFeesArray[1] = new ICampaignPaymentTreasury.ExternalFees[](0); + timeConstrainedPaymentTreasury.createPaymentBatch( + paymentIds, buyerIds, itemIds, paymentTokens, amounts, expirations, emptyLineItemsArray, externalFeesArray + ); + + // Payments created successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function testCreatePaymentBatchRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + 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); + timeConstrainedPaymentTreasury.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(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payment processed successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function testProcessCryptoPaymentRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + function testCancelPaymentWithinTimeRange() public { + advanceToWithinRange(); + + // First create a payment + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.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); + timeConstrainedPaymentTreasury.cancelPayment(PAYMENT_ID_1); + + // Payment cancelled successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function testCancelPaymentRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.cancelPayment(PAYMENT_ID_1); + } + + function testConfirmPaymentWithinTimeRange() public { + advanceToWithinRange(); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payment created and confirmed successfully by processCryptoPayment + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } + + function testConfirmPaymentRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.confirmPayment(PAYMENT_ID_1, address(0)); + } + + function testConfirmPaymentBatchWithinTimeRange() public { + advanceToWithinRange(); + + // Use processCryptoPayment for both payments which creates and confirms them + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + vm.prank(users.backer2Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_2); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems2 = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_2, + ITEM_ID_2, + users.backer2Address, + address(testToken), + PAYMENT_AMOUNT_2, + emptyLineItems2, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Payments created and confirmed successfully by processCryptoPayment + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1 + PAYMENT_AMOUNT_2); + } + + function testConfirmPaymentBatchRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + bytes32[] memory paymentIds = new bytes32[](1); + paymentIds[0] = PAYMENT_ID_1; + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.confirmPaymentBatch(paymentIds, _createZeroAddressArray(paymentIds.length)); + } + + /*////////////////////////////////////////////////////////////// + POST-LAUNCH TIME TESTS + //////////////////////////////////////////////////////////////*/ + + function testClaimRefundAfterLaunchTime() public { + // First create payment within the allowed time range + advanceToWithinRange(); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch to be able to claim refund + advanceToAfterLaunch(); + + // Approve treasury to burn NFT + vm.prank(users.backer1Address); + CampaignInfo(campaignAddress).approve(address(timeConstrainedPaymentTreasury), 1); // tokenId 1 + + // Then claim refund (use the overload without refundAddress since processCryptoPayment uses buyerAddress) + vm.prank(users.backer1Address); + timeConstrainedPaymentTreasury.claimRefund(PAYMENT_ID_1); + + // Refund claimed successfully + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function testClaimRefundRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.claimRefund(PAYMENT_ID_1, users.backer1Address); + } + + function testDisburseFeesAfterLaunchTime() public { + // First create payment within the allowed time range + advanceToWithinRange(); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch time + advanceToAfterLaunch(); + + // Then disburse fees + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.disburseFees(); + + // Fees disbursed successfully (no revert) + } + + function testDisburseFeesRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.disburseFees(); + } + + function testWithdrawAfterLaunchTime() public { + // First create payment within the allowed time range + advanceToWithinRange(); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch time + advanceToAfterLaunch(); + + // Then withdraw + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.withdraw(); + + // Withdrawal successful (no revert) + } + + function testWithdrawRevertWhenBeforeLaunchTime() public { + advanceToBeforeLaunch(); + + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.withdraw(); + } + + /*////////////////////////////////////////////////////////////// + BUFFER TIME TESTS + //////////////////////////////////////////////////////////////*/ + + function testBufferTimeRetrieval() public { + // Test that buffer time is correctly retrieved from GlobalParams + // We can't access _getBufferTime() directly, so we test it indirectly + // by checking that operations work within the buffer time window + vm.warp(campaignDeadline - 1); // Use deadline - 1 to be within range + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at deadline - 1 + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function testOperationsAtDeadlinePlusBuffer() public { + // Test operations at the exact deadline + buffer time + vm.warp(campaignDeadline - 1); // Use deadline - 1 to be within range + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at deadline - 1 + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function testOperationsAfterDeadlinePlusBuffer() public { + // Test operations after deadline + buffer time + advanceToAfterDeadline(); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.expectRevert(); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + } + + /*////////////////////////////////////////////////////////////// + EDGE CASE TESTS + //////////////////////////////////////////////////////////////*/ + + function testOperationsAtExactLaunchTime() public { + // Test operations at the exact launch time + vm.warp(campaignLaunchTime); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at the exact launch time + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function testOperationsAtExactDeadline() public { + // Test operations at the exact deadline + vm.warp(campaignDeadline); + + uint256 expiration = block.timestamp + PAYMENT_EXPIRATION; + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.createPayment( + PAYMENT_ID_1, + BUYER_ID_1, + ITEM_ID_1, + address(testToken), + PAYMENT_AMOUNT_1, + expiration, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + // Should succeed at the exact deadline + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), 0); + } + + function testMultipleTimeConstraintChecks() public { + // Test that multiple operations respect time constraints + advanceToWithinRange(); + + // Use processCryptoPayment which creates and confirms payment in one step + vm.prank(users.backer1Address); + testToken.approve(address(timeConstrainedPaymentTreasury), PAYMENT_AMOUNT_1); + + vm.prank(users.platform1AdminAddress); + ICampaignPaymentTreasury.LineItem[] memory emptyLineItems = new ICampaignPaymentTreasury.LineItem[](0); + timeConstrainedPaymentTreasury.processCryptoPayment( + PAYMENT_ID_1, + ITEM_ID_1, + users.backer1Address, + address(testToken), + PAYMENT_AMOUNT_1, + emptyLineItems, + new ICampaignPaymentTreasury.ExternalFees[](0) + ); + + // Advance to after launch time + advanceToAfterLaunch(); + + // Withdraw (should work after launch time) + vm.prank(users.platform1AdminAddress); + timeConstrainedPaymentTreasury.withdraw(); + + // All operations should succeed + assertEq(timeConstrainedPaymentTreasury.getRaisedAmount(), PAYMENT_AMOUNT_1); + } +} diff --git a/test/foundry/unit/TreasuryFactory.t.sol b/test/foundry/unit/TreasuryFactory.t.sol index 3a7a6785..e1ea5ff5 100644 --- a/test/foundry/unit/TreasuryFactory.t.sol +++ b/test/foundry/unit/TreasuryFactory.t.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import "forge-std/Test.sol"; import {TreasuryFactory} from "src/TreasuryFactory.sol"; import {GlobalParams} from "src/GlobalParams.sol"; +import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {TestToken} from "../../mocks/TestToken.sol"; import {Defaults} from "../Base.t.sol"; import {AdminAccessChecker} from "src/utils/AdminAccessChecker.sol"; @@ -24,16 +26,36 @@ contract TreasuryFactory_UpdatedUnitTest is Test, Defaults { uint256 internal platformFee = 300; // 3% function setUp() public { - testToken = new TestToken(tokenName, tokenSymbol); - globalParams = new GlobalParams(protocolAdmin, address(testToken), 300); - factory = new TreasuryFactory(globalParams); + testToken = new TestToken(tokenName, tokenSymbol, 18); + + // Setup currencies and tokens for multi-token support + bytes32[] memory currencies = new bytes32[](1); + currencies[0] = bytes32("USD"); + + address[][] memory tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](1); + tokensPerCurrency[0][0] = address(testToken); + + // Deploy GlobalParams with proxy + GlobalParams globalParamsImpl = new GlobalParams(); + bytes memory globalParamsInitData = + abi.encodeWithSelector(GlobalParams.initialize.selector, protocolAdmin, 300, currencies, tokensPerCurrency); + ERC1967Proxy globalParamsProxy = new ERC1967Proxy(address(globalParamsImpl), globalParamsInitData); + globalParams = GlobalParams(address(globalParamsProxy)); + + // Deploy TreasuryFactory with proxy + TreasuryFactory factoryImpl = new TreasuryFactory(); + bytes memory factoryInitData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(address(globalParams))); + ERC1967Proxy factoryProxy = new ERC1967Proxy(address(factoryImpl), factoryInitData); + factory = TreasuryFactory(address(factoryProxy)); // Label addresses for clarity vm.label(protocolAdmin, "ProtocolAdmin"); vm.label(platformAdmin, "PlatformAdmin"); vm.label(implementation, "Implementation"); vm.startPrank(protocolAdmin); - globalParams.enlistPlatform(platformHash, platformAdmin, platformFee); + globalParams.enlistPlatform(platformHash, platformAdmin, platformFee, address(0)); vm.stopPrank(); } @@ -41,18 +63,10 @@ contract TreasuryFactory_UpdatedUnitTest is Test, Defaults { vm.startPrank(platformAdmin); vm.mockCall( address(globalParams), - abi.encodeWithSignature( - "isPlatformAdmin(bytes32,address)", - platformHash, - platformAdmin - ), + abi.encodeWithSignature("isPlatformAdmin(bytes32,address)", platformHash, platformAdmin), abi.encode(true) ); - factory.registerTreasuryImplementation( - platformHash, - implementationId, - implementation - ); + factory.registerTreasuryImplementation(platformHash, implementationId, implementation); vm.stopPrank(); } @@ -60,19 +74,11 @@ contract TreasuryFactory_UpdatedUnitTest is Test, Defaults { vm.startPrank(platformAdmin); vm.mockCall( address(globalParams), - abi.encodeWithSignature( - "isPlatformAdmin(bytes32,address)", - platformHash, - platformAdmin - ), + abi.encodeWithSignature("isPlatformAdmin(bytes32,address)", platformHash, platformAdmin), abi.encode(true) ); vm.expectRevert(TreasuryFactory.TreasuryFactoryInvalidAddress.selector); - factory.registerTreasuryImplementation( - platformHash, - implementationId, - address(0) - ); + factory.registerTreasuryImplementation(platformHash, implementationId, address(0)); vm.stopPrank(); } @@ -81,18 +87,10 @@ contract TreasuryFactory_UpdatedUnitTest is Test, Defaults { vm.startPrank(platformAdmin); vm.mockCall( address(globalParams), - abi.encodeWithSignature( - "isPlatformAdmin(bytes32,address)", - platformHash, - platformAdmin - ), + abi.encodeWithSignature("isPlatformAdmin(bytes32,address)", platformHash, platformAdmin), abi.encode(true) ); - factory.registerTreasuryImplementation( - platformHash, - implementationId, - implementation - ); + factory.registerTreasuryImplementation(platformHash, implementationId, implementation); vm.stopPrank(); // Then approve as protocol admin @@ -105,31 +103,43 @@ contract TreasuryFactory_UpdatedUnitTest is Test, Defaults { vm.startPrank(platformAdmin); vm.mockCall( address(globalParams), - abi.encodeWithSignature( - "isPlatformAdmin(bytes32,address)", - platformHash, - platformAdmin - ), + abi.encodeWithSignature("isPlatformAdmin(bytes32,address)", platformHash, platformAdmin), abi.encode(true) ); - factory.registerTreasuryImplementation( - platformHash, - implementationId, - implementation - ); + factory.registerTreasuryImplementation(platformHash, implementationId, implementation); - vm.expectRevert( - TreasuryFactory - .TreasuryFactoryImplementationNotSetOrApproved - .selector - ); - factory.deploy( - platformHash, - address(0x1234), - implementationId, - "Test", - "TST" - ); + vm.expectRevert(TreasuryFactory.TreasuryFactoryImplementationNotSetOrApproved.selector); + factory.deploy(platformHash, address(0x1234), implementationId); vm.stopPrank(); } + + function testUpgrade() public { + // Deploy new implementation + TreasuryFactory newImplementation = new TreasuryFactory(); + + // Upgrade as protocol admin + vm.prank(protocolAdmin); + factory.upgradeToAndCall(address(newImplementation), ""); + + // Factory should still work after upgrade + vm.startPrank(platformAdmin); + factory.registerTreasuryImplementation(platformHash, implementationId, implementation); + vm.stopPrank(); + } + + function testUpgradeUnauthorizedReverts() public { + // Deploy new implementation + TreasuryFactory newImplementation = new TreasuryFactory(); + + // Try to upgrade as non-protocol-admin (should revert) + vm.prank(other); + vm.expectRevert(); + factory.upgradeToAndCall(address(newImplementation), ""); + } + + function testCannotInitializeTwice() public { + // Try to initialize again (should revert) + vm.expectRevert(); + factory.initialize(IGlobalParams(address(globalParams))); + } } diff --git a/test/foundry/unit/Upgrades.t.sol b/test/foundry/unit/Upgrades.t.sol new file mode 100644 index 00000000..0dbb08e1 --- /dev/null +++ b/test/foundry/unit/Upgrades.t.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import "forge-std/Test.sol"; +import {GlobalParams} from "src/GlobalParams.sol"; +import {TreasuryFactory} from "src/TreasuryFactory.sol"; +import {CampaignInfoFactory} from "src/CampaignInfoFactory.sol"; +import {CampaignInfo} from "src/CampaignInfo.sol"; +import {IGlobalParams} from "src/interfaces/IGlobalParams.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {TestToken} from "../../mocks/TestToken.sol"; +import {Defaults} from "../Base.t.sol"; + +/** + * @title Upgrades_Test + * @notice Comprehensive upgrade tests for all UUPS upgradeable contracts + */ +contract Upgrades_Test is Test, Defaults { + GlobalParams internal globalParams; + TreasuryFactory internal treasuryFactory; + CampaignInfoFactory internal campaignFactory; + TestToken internal testToken; + + address internal admin = address(0xA11CE); + address internal platformAdmin = address(0xBEEF); + address internal attacker = address(0xDEAD); + + bytes32 internal platformHash = keccak256(abi.encodePacked("TEST_PLATFORM")); + uint256 internal protocolFee = 300; + uint256 internal platformFee = 200; + + function setUp() public { + testToken = new TestToken("Test", "TST", 18); + + bytes32[] memory currencies = new bytes32[](1); + currencies[0] = bytes32("USD"); + + address[][] memory tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](1); + tokensPerCurrency[0][0] = address(testToken); + + // Deploy GlobalParams with proxy + GlobalParams globalParamsImpl = new GlobalParams(); + bytes memory globalParamsInitData = + abi.encodeWithSelector(GlobalParams.initialize.selector, admin, protocolFee, currencies, tokensPerCurrency); + ERC1967Proxy globalParamsProxy = new ERC1967Proxy(address(globalParamsImpl), globalParamsInitData); + globalParams = GlobalParams(address(globalParamsProxy)); + + // Deploy TreasuryFactory with proxy + TreasuryFactory treasuryFactoryImpl = new TreasuryFactory(); + bytes memory treasuryFactoryInitData = + abi.encodeWithSelector(TreasuryFactory.initialize.selector, IGlobalParams(address(globalParams))); + ERC1967Proxy treasuryFactoryProxy = new ERC1967Proxy(address(treasuryFactoryImpl), treasuryFactoryInitData); + treasuryFactory = TreasuryFactory(address(treasuryFactoryProxy)); + + // Deploy CampaignInfoFactory with proxy + CampaignInfo campaignInfoImpl = new CampaignInfo(); + CampaignInfoFactory campaignFactoryImpl = new CampaignInfoFactory(); + bytes memory campaignFactoryInitData = abi.encodeWithSelector( + CampaignInfoFactory.initialize.selector, + admin, + IGlobalParams(address(globalParams)), + address(campaignInfoImpl), + address(treasuryFactory) + ); + ERC1967Proxy campaignFactoryProxy = new ERC1967Proxy(address(campaignFactoryImpl), campaignFactoryInitData); + campaignFactory = CampaignInfoFactory(address(campaignFactoryProxy)); + + // Enlist a platform + vm.prank(admin); + globalParams.enlistPlatform(platformHash, platformAdmin, platformFee, address(0)); + } + + // ============ GlobalParams Upgrade Tests ============ + + function testGlobalParamsUpgrade() public { + // Record initial state + uint256 initialFee = globalParams.getProtocolFeePercent(); + address initialAdmin = globalParams.getProtocolAdminAddress(); + + // Deploy new implementation + GlobalParams newImpl = new GlobalParams(); + + // Upgrade + vm.prank(admin); + globalParams.upgradeToAndCall(address(newImpl), ""); + + // Verify state is preserved + assertEq(globalParams.getProtocolFeePercent(), initialFee); + assertEq(globalParams.getProtocolAdminAddress(), initialAdmin); + + // Verify functionality still works + vm.prank(admin); + globalParams.updateProtocolFeePercent(500); + assertEq(globalParams.getProtocolFeePercent(), 500); + } + + function testGlobalParamsUpgradeUnauthorized() public { + GlobalParams newImpl = new GlobalParams(); + + // Try to upgrade as non-owner + vm.prank(attacker); + vm.expectRevert(); + globalParams.upgradeToAndCall(address(newImpl), ""); + } + + function testGlobalParamsStorageSlotIntegrity() public { + // Add some data + vm.prank(admin); + globalParams.addTokenToCurrency(bytes32("EUR"), address(testToken)); + + // Verify data before upgrade + address[] memory eurTokens = globalParams.getTokensForCurrency(bytes32("EUR")); + assertEq(eurTokens.length, 1); + assertEq(eurTokens[0], address(testToken)); + + // Upgrade + GlobalParams newImpl = new GlobalParams(); + vm.prank(admin); + globalParams.upgradeToAndCall(address(newImpl), ""); + + // Verify data after upgrade + eurTokens = globalParams.getTokensForCurrency(bytes32("EUR")); + assertEq(eurTokens.length, 1); + assertEq(eurTokens[0], address(testToken)); + } + + function testGlobalParamsCannotInitializeTwice() public { + bytes32[] memory currencies = new bytes32[](0); + address[][] memory tokensPerCurrency = new address[][](0); + + vm.expectRevert(); + globalParams.initialize(admin, protocolFee, currencies, tokensPerCurrency); + } + + // ============ TreasuryFactory Upgrade Tests ============ + + function testTreasuryFactoryUpgrade() public { + // Register an implementation + address mockImpl = address(0xC0DE); + vm.prank(platformAdmin); + treasuryFactory.registerTreasuryImplementation(platformHash, 1, mockImpl); + + // Deploy new implementation + TreasuryFactory newImpl = new TreasuryFactory(); + + // Upgrade as protocol admin + vm.prank(admin); + treasuryFactory.upgradeToAndCall(address(newImpl), ""); + + // Verify functionality still works after upgrade + vm.prank(platformAdmin); + treasuryFactory.registerTreasuryImplementation(platformHash, 2, address(0xBEEF)); + } + + function testTreasuryFactoryUpgradeUnauthorized() public { + TreasuryFactory newImpl = new TreasuryFactory(); + + // Try to upgrade as non-protocol-admin + vm.prank(attacker); + vm.expectRevert(); + treasuryFactory.upgradeToAndCall(address(newImpl), ""); + } + + function testTreasuryFactoryStorageSlotIntegrity() public { + // Register and approve an implementation + address mockImpl = address(0xC0DE); + vm.prank(platformAdmin); + treasuryFactory.registerTreasuryImplementation(platformHash, 1, mockImpl); + + vm.prank(admin); + treasuryFactory.approveTreasuryImplementation(platformHash, 1); + + // Upgrade + TreasuryFactory newImpl = new TreasuryFactory(); + vm.prank(admin); + treasuryFactory.upgradeToAndCall(address(newImpl), ""); + + // Verify registered implementations are preserved after upgrade + // by registering another implementation (which proves the mapping still works) + address mockImpl2 = address(0xBEEF); + vm.prank(platformAdmin); + treasuryFactory.registerTreasuryImplementation(platformHash, 2, mockImpl2); + } + + function testTreasuryFactoryCannotInitializeTwice() public { + vm.expectRevert(); + treasuryFactory.initialize(IGlobalParams(address(globalParams))); + } + + // ============ CampaignInfoFactory Upgrade Tests ============ + + function testCampaignInfoFactoryUpgrade() public { + // Create a campaign before upgrade + bytes32[] memory platforms = new bytes32[](1); + platforms[0] = platformHash; + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + + vm.prank(admin); + campaignFactory.createCampaign( + address(0xBEEF), + CAMPAIGN_1_IDENTIFIER_HASH, + platforms, + keys, + values, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + + address campaignBefore = campaignFactory.identifierToCampaignInfo(CAMPAIGN_1_IDENTIFIER_HASH); + assertTrue(campaignBefore != address(0), "Campaign not created"); + + // Deploy new implementation + CampaignInfoFactory newImpl = new CampaignInfoFactory(); + + // Upgrade as owner + vm.prank(admin); + campaignFactory.upgradeToAndCall(address(newImpl), ""); + + // Verify previous campaign is still accessible + address campaignAfter = campaignFactory.identifierToCampaignInfo(CAMPAIGN_1_IDENTIFIER_HASH); + assertEq(campaignAfter, campaignBefore, "Campaign address changed after upgrade"); + assertTrue(campaignFactory.isValidCampaignInfo(campaignAfter), "Campaign no longer valid"); + } + + function testCampaignInfoFactoryUpgradeUnauthorized() public { + CampaignInfoFactory newImpl = new CampaignInfoFactory(); + + // Try to upgrade as non-owner + vm.prank(attacker); + vm.expectRevert(); + campaignFactory.upgradeToAndCall(address(newImpl), ""); + } + + function testCampaignInfoFactoryStorageSlotIntegrity() public { + // Create multiple campaigns + bytes32[] memory platforms = new bytes32[](1); + platforms[0] = platformHash; + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + + bytes32 identifier1 = bytes32(uint256(1)); + bytes32 identifier2 = bytes32(uint256(2)); + + vm.startPrank(admin); + campaignFactory.createCampaign( + address(0xBEEF), + identifier1, + platforms, + keys, + values, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + + campaignFactory.createCampaign( + address(0xCAFE), + identifier2, + platforms, + keys, + values, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + vm.stopPrank(); + + address campaign1Before = campaignFactory.identifierToCampaignInfo(identifier1); + address campaign2Before = campaignFactory.identifierToCampaignInfo(identifier2); + + // Upgrade + CampaignInfoFactory newImpl = new CampaignInfoFactory(); + vm.prank(admin); + campaignFactory.upgradeToAndCall(address(newImpl), ""); + + // Verify all campaigns are preserved + assertEq(campaignFactory.identifierToCampaignInfo(identifier1), campaign1Before); + assertEq(campaignFactory.identifierToCampaignInfo(identifier2), campaign2Before); + assertTrue(campaignFactory.isValidCampaignInfo(campaign1Before)); + assertTrue(campaignFactory.isValidCampaignInfo(campaign2Before)); + } + + function testCampaignInfoFactoryCannotInitializeTwice() public { + CampaignInfo campaignInfoImpl = new CampaignInfo(); + + vm.expectRevert(); + campaignFactory.initialize( + admin, IGlobalParams(address(globalParams)), address(campaignInfoImpl), address(treasuryFactory) + ); + } + + // ============ Cross-Contract Upgrade Tests ============ + + function testUpgradeAllContractsIndependently() public { + // Upgrade all three contracts + GlobalParams newGlobalParamsImpl = new GlobalParams(); + TreasuryFactory newTreasuryFactoryImpl = new TreasuryFactory(); + CampaignInfoFactory newCampaignFactoryImpl = new CampaignInfoFactory(); + + vm.startPrank(admin); + globalParams.upgradeToAndCall(address(newGlobalParamsImpl), ""); + treasuryFactory.upgradeToAndCall(address(newTreasuryFactoryImpl), ""); + campaignFactory.upgradeToAndCall(address(newCampaignFactoryImpl), ""); + vm.stopPrank(); + + // Verify all contracts still function correctly + assertEq(globalParams.getProtocolAdminAddress(), admin); + + vm.prank(platformAdmin); + treasuryFactory.registerTreasuryImplementation(platformHash, 99, address(0xABCD)); + + bytes32[] memory platforms = new bytes32[](1); + platforms[0] = platformHash; + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + + vm.prank(admin); + campaignFactory.createCampaign( + address(0xBEEF), + bytes32(uint256(999)), + platforms, + keys, + values, + CAMPAIGN_DATA, + "Campaign Pledge NFT", + "PLEDGE", + "ipfs://QmExampleImageURI", + "ipfs://QmExampleContractURI" + ); + } + + function testUpgradeDoesNotAffectImplementationContract() public { + // The implementation contract itself should not be usable directly + GlobalParams standaloneImpl = new GlobalParams(); + + bytes32[] memory currencies = new bytes32[](1); + currencies[0] = bytes32("USD"); + address[][] memory tokensPerCurrency = new address[][](1); + tokensPerCurrency[0] = new address[](1); + tokensPerCurrency[0][0] = address(testToken); + + // Should revert because initializers are disabled in constructor + vm.expectRevert(); + standaloneImpl.initialize(admin, protocolFee, currencies, tokensPerCurrency); + } + + // ============ Storage Collision Tests ============ + + function testNoStorageCollisionAfterUpgrade() public { + // Add data to all storage slots + vm.startPrank(admin); + globalParams.updateProtocolFeePercent(999); + globalParams.addTokenToCurrency(bytes32("BRL"), address(0x1111)); + globalParams.enlistPlatform(bytes32("NEW_PLATFORM"), address(0x2222), 123, address(0)); + vm.stopPrank(); + + // Upgrade + GlobalParams newImpl = new GlobalParams(); + vm.prank(admin); + globalParams.upgradeToAndCall(address(newImpl), ""); + + // Verify all data is intact + assertEq(globalParams.getProtocolFeePercent(), 999); + address[] memory brlTokens = globalParams.getTokensForCurrency(bytes32("BRL")); + assertEq(brlTokens.length, 1); + assertEq(brlTokens[0], address(0x1111)); + assertTrue(globalParams.checkIfPlatformIsListed(bytes32("NEW_PLATFORM"))); + } +} diff --git a/test/foundry/utils/Constants.sol b/test/foundry/utils/Constants.sol index f25eb6f0..a1009e65 100644 --- a/test/foundry/utils/Constants.sol +++ b/test/foundry/utils/Constants.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; abstract contract Constants { uint40 internal constant OCTOBER_1_2023 = 1_696_118_400; diff --git a/test/foundry/utils/Defaults.sol b/test/foundry/utils/Defaults.sol index c77b2ea5..e590ba50 100644 --- a/test/foundry/utils/Defaults.sol +++ b/test/foundry/utils/Defaults.sol @@ -1,9 +1,10 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; import {Constants} from "./Constants.sol"; import {ICampaignData} from "src/interfaces/ICampaignData.sol"; import {AllOrNothing} from "src/treasuries/AllOrNothing.sol"; +import {KeepWhatsRaised} from "src/treasuries/KeepWhatsRaised.sol"; import {IReward} from "src/interfaces/IReward.sol"; /// @notice Contract with default values used throughout the tests. @@ -11,17 +12,15 @@ contract Defaults is Constants, ICampaignData, IReward { //Constant Variables uint256 public constant PROTOCOL_FEE_PERCENT = 20 * 100; uint256 public constant TOKEN_MINT_AMOUNT = 1_000_000e18; - uint256 public constant PLATFORM_FEE_PERCENT = 10 * 100; - bytes32 public constant PLATFORM_1_HASH = - keccak256(abi.encodePacked("KickStarter")); - bytes32 public constant REWARD_NAME_1_HASH = - keccak256(abi.encodePacked("sampleReward")); - bytes32 public constant CAMPAIGN_1_IDENTIFIER_HASH = - keccak256(abi.encodePacked("Sample Campaign")); + uint256 public constant PLATFORM_FEE_PERCENT = 10 * 100; // 10% + bytes32 public constant PLATFORM_1_HASH = keccak256(abi.encodePacked("KickStarter")); + bytes32 public constant PLATFORM_2_HASH = keccak256(abi.encodePacked("Vaki")); + bytes32 public constant REWARD_NAME_1_HASH = keccak256(abi.encodePacked("sampleReward")); + bytes32 public constant CAMPAIGN_1_IDENTIFIER_HASH = keccak256(abi.encodePacked("Sample Campaign")); string public constant NAME = "Name"; string public constant SYMBOL = "Symbol"; - uint256 public constant GOAL_AMOUNT = 100; - uint256 public constant CAMPAIGN_DURATION = 10_000 seconds; + uint256 public constant GOAL_AMOUNT = 100_000e18; // Increased to handle fees better + uint256 public constant CAMPAIGN_DURATION = 30 days; uint256 public constant PLEDGE_AMOUNT = 1_000e18; uint256 public constant PERCENT_DIVIDER = 10000; uint256 public constant SHIPPING_FEE = 10; @@ -42,19 +41,59 @@ contract Defaults is Constants, ICampaignData, IReward { bytes32[] public REWARD_NAMES; Reward[] public REWARDS; + // Fee Keys for KeepWhatsRaised + bytes32 public constant FLAT_FEE_KEY = keccak256(abi.encodePacked("flatFee")); + bytes32 public constant CUMULATIVE_FLAT_FEE_KEY = keccak256(abi.encodePacked("cumulativeFlatFee")); + bytes32 public constant PLATFORM_FEE_KEY = keccak256(abi.encodePacked("platformFee")); + bytes32 public constant VAKI_COMMISSION_KEY = keccak256(abi.encodePacked("vakiCommission")); + + // Fee Values + bytes32 public constant FLAT_FEE_VALUE = bytes32(uint256(100e18)); // 100 token flat fee + bytes32 public constant CUMULATIVE_FLAT_FEE_VALUE = bytes32(uint256(200e18)); // 200 token cumulative fee + bytes32 public constant PLATFORM_FEE_VALUE = bytes32(PLATFORM_FEE_PERCENT); // 10% + bytes32 public constant VAKI_COMMISSION_VALUE = bytes32(uint256(6 * 100)); // 6% for regular campaigns + + // Payment Gateway Fees - proportional to pledge + uint256 public constant PAYMENT_GATEWAY_FEE = 40e18; // 4% of 1000e18 + uint256 public constant PAYMENT_GATEWAY_FEE_PERCENTAGE = 4 * 100; // 4% + + // Config values for KeepWhatsRaised + uint256 public constant MINIMUM_WITHDRAWAL_FOR_FEE_EXEMPTION = 50_000e18; + uint256 public constant WITHDRAWAL_DELAY = 7 days; + uint256 public constant REFUND_DELAY = 14 days; + uint256 public constant CONFIG_LOCK_PERIOD = 2 days; + + // Additional constants + uint256 public constant TIP_AMOUNT = 10e18; + uint256 public constant WITHDRAWAL_AMOUNT = 50_000e18; + + // Test Pledge IDs + bytes32 public constant TEST_PLEDGE_ID_1 = keccak256(abi.encodePacked("pledge1")); + bytes32 public constant TEST_PLEDGE_ID_2 = keccak256(abi.encodePacked("pledge2")); + bytes32 public constant TEST_PLEDGE_ID_3 = keccak256(abi.encodePacked("pledge3")); + + KeepWhatsRaised.FeeKeys public FEE_KEYS; + KeepWhatsRaised.Config public CONFIG; + KeepWhatsRaised.Config public CONFIG_COLOMBIAN; + bytes32[] public GROSS_PERCENTAGE_FEE_KEYS; + bytes32[] public GROSS_PERCENTAGE_FEE_VALUES; + constructor() { - LAUNCH_TIME = OCTOBER_1_2023 + 300 seconds; + LAUNCH_TIME = OCTOBER_1_2023 + 2 hours; // 2 hours buffer to accommodate time constraints DEADLINE = LAUNCH_TIME + CAMPAIGN_DURATION; //Add Campaign Data CAMPAIGN_DATA = CampaignData({ launchTime: LAUNCH_TIME, deadline: DEADLINE, - goalAmount: GOAL_AMOUNT + goalAmount: GOAL_AMOUNT, + currency: bytes32("USD") }); // Initialize the reward arrays setupRewardData(); + + setupKeepWhatsRaisedData(); } // Setup the reward data that can be accessed by tests @@ -117,4 +156,40 @@ contract Defaults is Constants, ICampaignData, IReward { itemQuantity: emptyQuantities }); } + + function setupKeepWhatsRaisedData() internal { + // Setup gross percentage fee keys and values + GROSS_PERCENTAGE_FEE_KEYS = new bytes32[](2); + GROSS_PERCENTAGE_FEE_KEYS[0] = PLATFORM_FEE_KEY; + GROSS_PERCENTAGE_FEE_KEYS[1] = VAKI_COMMISSION_KEY; + + GROSS_PERCENTAGE_FEE_VALUES = new bytes32[](2); + GROSS_PERCENTAGE_FEE_VALUES[0] = PLATFORM_FEE_VALUE; + GROSS_PERCENTAGE_FEE_VALUES[1] = VAKI_COMMISSION_VALUE; + + // Setup FEE_KEYS struct + FEE_KEYS = KeepWhatsRaised.FeeKeys({ + flatFeeKey: FLAT_FEE_KEY, + cumulativeFlatFeeKey: CUMULATIVE_FLAT_FEE_KEY, + grossPercentageFeeKeys: GROSS_PERCENTAGE_FEE_KEYS + }); + + // Setup CONFIG struct for non-Colombian creator + CONFIG = KeepWhatsRaised.Config({ + minimumWithdrawalForFeeExemption: MINIMUM_WITHDRAWAL_FOR_FEE_EXEMPTION, + withdrawalDelay: WITHDRAWAL_DELAY, + refundDelay: REFUND_DELAY, + configLockPeriod: CONFIG_LOCK_PERIOD, + isColombianCreator: false + }); + + // Setup CONFIG struct for Colombian creator + CONFIG_COLOMBIAN = KeepWhatsRaised.Config({ + minimumWithdrawalForFeeExemption: MINIMUM_WITHDRAWAL_FOR_FEE_EXEMPTION, + withdrawalDelay: WITHDRAWAL_DELAY, + refundDelay: REFUND_DELAY, + configLockPeriod: CONFIG_LOCK_PERIOD, + isColombianCreator: true + }); + } } diff --git a/test/foundry/utils/LogDecoder.sol b/test/foundry/utils/LogDecoder.sol index e5255200..d6e8d791 100644 --- a/test/foundry/utils/LogDecoder.sol +++ b/test/foundry/utils/LogDecoder.sol @@ -1,13 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import "forge-std/Test.sol"; abstract contract LogDecoder is Test { - function findLogByTopic( - Vm.Log[] memory logs, - bytes32 topic0 - ) internal pure returns (Vm.Log memory) { + function findLogByTopic(Vm.Log[] memory logs, bytes32 topic0) internal pure returns (Vm.Log memory) { for (uint256 i = 0; i < logs.length; i++) { if (logs[i].topics.length > 0 && logs[i].topics[0] == topic0) { return logs[i]; @@ -16,37 +13,30 @@ abstract contract LogDecoder is Test { revert("Log with specified topic not found"); } - function decodeEventFromLogs( - Vm.Log[] memory logs, - string memory eventSignature, - address expectedEmitter - ) internal pure returns (bytes memory) { + function decodeEventFromLogs(Vm.Log[] memory logs, string memory eventSignature, address expectedEmitter) + internal + pure + returns (bytes memory) + { bytes32 expectedTopic = keccak256(bytes(eventSignature)); for (uint256 i = 0; i < logs.length; i++) { - if ( - logs[i].topics[0] == expectedTopic && - logs[i].emitter == expectedEmitter - ) { + if (logs[i].topics[0] == expectedTopic && logs[i].emitter == expectedEmitter) { return logs[i].data; } } revert("Event not found in logs"); } - function decodeTopicsAndData( - Vm.Log[] memory logs, - string memory eventSignature, - address expectedEmitter - ) internal pure returns (bytes32[] memory topics, bytes memory data) { + function decodeTopicsAndData(Vm.Log[] memory logs, string memory eventSignature, address expectedEmitter) + internal + pure + returns (bytes32[] memory topics, bytes memory data) + { bytes32 expectedTopic = keccak256(bytes(eventSignature)); for (uint256 i = 0; i < logs.length; i++) { - if ( - logs[i].topics.length > 0 && - logs[i].topics[0] == expectedTopic && - logs[i].emitter == expectedEmitter - ) { + if (logs[i].topics.length > 0 && logs[i].topics[0] == expectedTopic && logs[i].emitter == expectedEmitter) { return (logs[i].topics, logs[i].data); } } diff --git a/test/foundry/utils/Types.sol b/test/foundry/utils/Types.sol index 1331e769..67d3b4a3 100644 --- a/test/foundry/utils/Types.sol +++ b/test/foundry/utils/Types.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; struct Users { // Default owner for all contracts. diff --git a/test/mocks/TestToken.sol b/test/mocks/TestToken.sol index 19414ef1..482ad5cd 100644 --- a/test/mocks/TestToken.sol +++ b/test/mocks/TestToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {ERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import {Ownable} from "../../lib/openzeppelin-contracts/contracts/access/Ownable.sol"; @@ -9,10 +9,18 @@ import {Ownable} from "../../lib/openzeppelin-contracts/contracts/access/Ownable * @notice A test token `tUSD` which is used in the tests. */ contract TestToken is ERC20, Ownable { - constructor( - string memory _name, - string memory _symbol - ) ERC20(_name, _symbol) Ownable(msg.sender) {} + uint8 private _decimals; + + constructor(string memory _name, string memory _symbol, uint8 decimals_) + ERC20(_name, _symbol) + Ownable(msg.sender) + { + _decimals = decimals_; + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } /** * @notice Mints testToken token.