Version: 0.0.4 (09/10/2025)
SPDX-License-Identifier: BSL 1.1 - Peng Protocol 2025
Solidity Version: ^0.8.2
TrustlessFund
manages timed token disbursements for ERC20 or ERC721 tokens. Users create funds with multiple grantees who can withdraw tokens (one-time or recurring) after a specified timestamp. Grantors propose and vote on grantee additions/removals or grantor changes, requiring >51% approval. A public distributeToGrantees
function enables automated disbursements. The contract ensures secure token transfers with pre/post balance checks and graceful degradation for non-critical failures, with per-grantee tracking to prevent duplicate disbursements.
- Fund: Stores fund details.
grantees
: Array of addresses allowed to receive disbursements.lockedUntil
: Timestamp when funds can be disbursed.disbursementAmount
: Amount per grantee (ERC20) or tokens (ERC721).disbursementInterval
: Interval for recurring disbursements (0 for one-time).fundType
: Enum (ERC20
,ERC721
) for token type.grantors
: Array of addresses that can propose changes.tokenContract
: Address of the token contract.tokenIds
: Array of ERC721 token IDs (empty for ERC20).active
: Fund status (false after ERC721 tokens are depleted).lastDisbursedIndex
: Tracks last grantee index fordistributeToGrantees
.disbursedGrantees
: Mapping tracking last period (recurring) or 1 (one-time) per grantee.
- Proposal: Tracks proposals for grantee or grantor changes.
fundId
: Associated fund ID.targetAddresses
: Array of addresses for grantee/grantor changes.votesFor
: Number of votes in favor.deadline
: Proposal expiration timestamp.executed
: Whether the proposal is executed.proposalType
: Enum (GranteeAddition
,GranteeRemoval
,AddGrantor
,RemoveGrantor
).voted
: Mapping tracking grantor votes.
funds
: Public mapping of fund ID toFund
struct.proposals
: Public mapping of proposal ID toProposal
struct.fundCount
: Counter for fund IDs.proposalCount
: Counter for proposal IDs.
- createOneTimeFund(address[] grantees, uint256 lockedUntil, address tokenContract, uint256 amountOrId, FundType fundType):
- Creates a one-time fund for multiple grantees, transferring tokens from the caller.
- Parameters: Grantees array, unlock timestamp, token contract, amount (ERC20) or token ID (ERC721), token type.
- Initializes
tokenIds
for ERC721, setslastDisbursedIndex
to 0. - Calls
_transferTokens
, emitsFundCreated
. - Internal Call Tree: Calls
_transferTokens
for token transfer with balance checks.
- createRecurringFund(address[] grantees, uint256 lockedUntil, uint256 disbursementAmount, uint256 disbursementInterval, address tokenContract, uint256 amountOrId, FundType fundType):
- Creates a recurring fund for multiple grantees (ERC20 only).
- Parameters: Grantees array, unlock timestamp, amount per grantee, interval (>0), token contract, initial amount, token type.
- Rejects ERC721, sets
lastDisbursedIndex
to 0. - Calls
_transferTokens
, emitsFundCreated
. - Internal Call Tree: Calls
_transferTokens
.
- disburse(uint256 fundId):
- Allows a grantee to withdraw their share if
lockedUntil
is reached. - For recurring funds, disburses
disbursementAmount * (periods - disbursedGrantees[caller])
. - For one-time funds, disburses
disbursementAmount
(ERC20) or one token ID (ERC721) if not yet disbursed. - Updates
disbursedGrantees
, calls_disburseERC20
or_disburseERC721
, emitsDisbursed
(0 for non-critical failures). - Internal Call Tree: Calls
_isGrantee
,_disburseERC20
, or_disburseERC721
.
- Allows a grantee to withdraw their share if
- distributeToGrantees(uint256 fundId, uint256 maxIterations):
- Public function to distribute tokens to grantees, up to
maxIterations
, starting fromlastDisbursedIndex
. - For ERC20, splits
disbursementAmount
(ordisbursementAmount * periods
) evenly, skips grantees with up-to-date disbursements. - For ERC721, distributes one token ID per grantee, skips previously disbursed grantees.
- Updates
disbursedGrantees
,lastDisbursedIndex
, andlockedUntil
(recurring), emitsDisbursed
per grantee (0 for non-critical failures). - Deactivates fund if ERC721 tokens are depleted.
- Internal Call Tree: None (direct token transfers).
- Public function to distribute tokens to grantees, up to
- proposeGranteeAddition(uint256 fundId, address[] newGrantees):
- Grantor proposes adding multiple grantees.
- Creates a
GranteeAddition
proposal, emitsProposalCreated
. - Internal Call Tree: Calls
_isGrantor
,_isGrantee
to check duplicates.
- proposeGranteeRemoval(uint256 fundId, address[] granteesToRemove):
- Grantor proposes removing multiple grantees (requires >0 remaining grantees).
- Creates a
GranteeRemoval
proposal, emitsProposalCreated
. - Internal Call Tree: Calls
_isGrantor
,_isGrantee
.
- proposeAddGrantor(uint256 fundId, address newGrantor):
- Grantor proposes adding a new grantor.
- Creates an
AddGrantor
proposal, emitsProposalCreated
. - Internal Call Tree: Calls
_isGrantor
to verify caller and check duplicates.
- proposeRemoveGrantor(uint256 fundId, address grantorToRemove):
- Grantor proposes removing a grantor (requires >1 grantor).
- Creates a
RemoveGrantor
proposal, emitsProposalCreated
. - Internal Call Tree: Calls
_isGrantor
to verify caller and target.
- voteOnProposal(uint256 proposalId, bool inFavor):
- Grantor votes on a proposal.
- Increments
votesFor
if in favor, calls_checkProposalOutcome
. - Emits
Voted
. - Internal Call Tree: Calls
_isGrantor
,_checkProposalOutcome
.
- granteeFunds(address grantee):
- Returns array of active fund IDs for a grantee.
- Iterates
funds
, filters by_isGrantee
andactive
. - Internal Call Tree: Calls
_isGrantee
.
- grantorFunds(address grantor):
- Returns array of active fund IDs where the caller is a grantor.
- Iterates
funds
, filters by_isGrantor
andactive
. - Internal Call Tree: Calls
_isGrantor
.
- fundProposals(uint256 fundId):
- Returns array of active proposal IDs for a fund.
- Filters
proposals
byfundId
and!executed
. - Internal Call Tree: None.
- addTokens(uint256 fundId, uint256 amountOrId):
- Deposits additional ERC20 or ERC721 tokens to an existing fund.
- Parameters: Fund ID, amount (ERC20) or token ID (ERC721).
- Requires active fund and valid token contract.
- Calls
_transferTokens
, emitsDeposited
. - Internal Call Tree: Calls
_transferTokens
.
- _transferTokens(address tokenContract, uint256 amountOrId, FundType fundType):
- Transfers ERC20 or ERC721 tokens with pre/post balance checks.
- For ERC721, appends
amountOrId
totokenIds
. - Called by
createOneTimeFund
,createRecurringFund
,addTokens
. - Emits
Deposited
.
- _disburseERC20(uint256 fundId, uint256 amount):
- Handles ERC20 token disbursement, checks balance to avoid reverts.
- Called by
disburse
,distributeToGrantees
.
- _disburseERC721(uint256 fundId):
- Transfers one ERC721 token from
tokenIds
, removes it, deactivates fund if empty. - Called by
disburse
.
- Transfers one ERC721 token from
- _isGrantee(Fund storage fund, address account):
- Checks if an address is a grantee for a fund.
- Called by
disburse
,proposeGranteeAddition
,proposeGranteeRemoval
,granteeFunds
.
- _isGrantor(Fund storage fund, address account):
- Checks if an address is a grantor for a fund.
- Called by
propose*
,voteOnProposal
,grantorFunds
.
- _checkProposalOutcome(uint256 proposalId):
- Executes proposal if >51% votes are received.
- Handles
GranteeAddition
(appends tograntees
),GranteeRemoval
(calls_removeGrantee
),AddGrantor
(appends tograntors
), orRemoveGrantor
(calls_removeGrantor
). - Emits
GranteeAdded
,GranteeRemoved
,GrantorAdded
, orGrantorRemoved
.
- _removeGrantee(Fund storage fund, address granteeToRemove):
- Removes a grantee from the
grantees
array using pop-and-swap. - Called by
_checkProposalOutcome
.
- Removes a grantee from the
- _removeGrantor(Fund storage fund, address grantorToRemove):
- Removes a grantor from the
grantors
array using pop-and-swap. - Called by
_checkProposalOutcome
.
- Removes a grantor from the
FundCreated(uint256 fundId, address firstGrantee, uint256 lockedUntil, FundType fundType)
Deposited(uint256 fundId, address tokenContract, uint256 amountOrId)
Disbursed(uint256 fundId, address grantee, uint256 amountOrId)
ProposalCreated(uint256 proposalId, uint256 fundId, address firstTarget, ProposalType proposalType)
Voted(uint256 proposalId, address grantor, bool inFavor)
GranteeAdded(uint256 fundId, address newGrantee)
GranteeRemoved(uint256 fundId, address removedGrantee)
GrantorAdded(uint256 fundId, address newGrantor)
GrantorRemoved(uint256 fundId, address removedGrantor)
- Security: Pre/post balance checks in
_transferTokens
ensure accurate token transfers. Non-critical failures (e.g., insufficient balance indisburse
ordistributeToGrantees
) emitDisbursed
with 0 amount instead of reverting. - Modularity: Helper functions (
_transferTokens
,_disburseERC20
,_disburseERC721
,_removeGrantee
,_removeGrantor
) reduce complexity and gas costs. - Voting: Proposals (
GranteeAddition
,GranteeRemoval
,AddGrantor
,RemoveGrantor
) require >51% grantor approval, evaluated in_checkProposalOutcome
. - Flexibility: Supports multiple grantees, with
distributeToGrantees
enabling public, automated disbursements. ERC721 recurring funds are blocked for consistency. - Gas Efficiency: Array resizing in
_removeGrantee
and_removeGrantor
uses pop-and-swap. View functions (granteeFunds
,grantorFunds
,fundProposals
) use single-pass array allocation.distributeToGrantees
respectsmaxIterations
for large grantee cohorts. - Disbursement Tracking:
disbursedGrantees
mapping prevents duplicate disbursements by tracking periods (recurring) or status (one-time) per grantee.lastDisbursedIndex
ensuresdistributeToGrantees
resumes correctly. - ERC721 Handling:
tokenIds
array tracks multiple ERC721 tokens, ensuring disbursements do not exceed deposits.