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.