// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { NativeAssetLedgerV3 } from "./common/NativeAssetLedgerV3.sol"; import { EmergencyControlsV3 } from "./common/EmergencyControlsV3.sol"; /// @notice Recipient-created, voluntary mutual-aid campaigns with no discretionary router. contract VoluntaryMutualAidV3 is NativeAssetLedgerV3, EmergencyControlsV3 { struct Campaign { address recipient; bytes32 purposeHash; uint96 cap; uint96 contributed; uint64 deadline; bool closed; } uint256 public immutable maxCampaignCap; uint256 public immutable maxSingleContribution; uint256 public immutable maxAggregateLiability; uint256 public nextCampaignId = 1; mapping(uint256 => Campaign) private _campaigns; error BadCampaign(); error BadState(); error DeadlinePassed(); error DeadlineNotPassed(); error CapExceeded(); event CampaignCreatedV3( uint256 indexed campaignId, address indexed recipient, bytes32 purposeHash, uint256 cap, uint64 deadline ); event AidContributedV3(uint256 indexed campaignId, address indexed donor, uint256 amount); event CampaignClosedV3(uint256 indexed campaignId); constructor( address governance_, address guardian_, uint64 minimumPauseDuration_, uint256 maxCampaignCap_, uint256 maxSingleContribution_, uint256 maxAggregateLiability_ ) EmergencyControlsV3(governance_, guardian_, minimumPauseDuration_) { if ( maxSingleContribution_ == 0 || maxSingleContribution_ > type(uint96).max || maxCampaignCap_ > type(uint96).max || maxCampaignCap_ < maxSingleContribution_ || maxAggregateLiability_ < maxCampaignCap_ ) revert BadCampaign(); maxCampaignCap = maxCampaignCap_; maxSingleContribution = maxSingleContribution_; maxAggregateLiability = maxAggregateLiability_; } function createCampaign(bytes32 purposeHash, uint96 cap, uint64 deadline) external whenNotPaused returns (uint256 campaignId) { if (purposeHash == bytes32(0) || cap == 0 || cap > maxCampaignCap || deadline <= block.timestamp) { revert BadCampaign(); } campaignId = nextCampaignId++; _campaigns[campaignId] = Campaign(msg.sender, purposeHash, cap, 0, deadline, false); emit CampaignCreatedV3(campaignId, msg.sender, purposeHash, cap, deadline); } function contribute(uint256 campaignId) external payable whenNotPaused { Campaign storage campaign = _campaigns[campaignId]; if (campaign.recipient == address(0) || campaign.closed) revert BadState(); if (block.timestamp > campaign.deadline) revert DeadlinePassed(); if ( msg.value == 0 || msg.value > maxSingleContribution || uint256(campaign.contributed) + msg.value > campaign.cap || accountedLiabilities() + msg.value > maxAggregateLiability ) revert CapExceeded(); campaign.contributed += uint96(msg.value); _creditNewValue(campaign.recipient, msg.value); emit AidContributedV3(campaignId, msg.sender, msg.value); } function closeCampaign(uint256 campaignId) external { Campaign storage campaign = _campaigns[campaignId]; if (campaign.recipient == address(0) || campaign.closed) revert BadState(); if (msg.sender != campaign.recipient && block.timestamp <= campaign.deadline) revert DeadlineNotPassed(); campaign.closed = true; emit CampaignClosedV3(campaignId); } function campaignOf(uint256 campaignId) external view returns (Campaign memory) { return _campaigns[campaignId]; } }