This guide provides a precise, deterministic, and example-driven overview of the TalentTrust Escrow system. It is intended for integrators, auditors, and operators.
The full lifecycle of a successful escrow contract follows this sequence:
Function: create_contract
Caller: Client
Pre-state: N/A
Post-state: Created
Event: created { contract_id, client, freelancer, total_amount }
Example:
escrow.create_contract(
&client_addr,
&freelancer_addr,
&Some(arbiter_addr),
&vec![&env, 500_0000000, 500_0000000], // 2 milestones
&None, // terms_hash
&Some(3600) // grace_period
);Function: deposit_funds
Caller: Client
Pre-state: Created
Post-state: Funded
Event: deposited { contract_id, amount, payer }
Example:
escrow.deposit_funds(&contract_id, &1000_0000000);Function: approve_milestone
Caller: Client
Pre-state: Funded
Post-state: Funded (Milestone marked as approved)
Event: approved { contract_id, milestone_index }
Example:
escrow.approve_milestone(&contract_id, &0);Function: release_milestone
Caller: Client / Arbiter
Pre-state: Funded (and approved)
Post-state: Funded or Completed (if last milestone)
Event: released { contract_id, milestone_index, amount }
Example:
escrow.release_milestone(&contract_id, &0);Trigger: Final release_milestone or refund_unreleased_milestones
Caller: N/A (Internal transition)
Pre-state: Funded
Post-state: Completed or Refunded
Event: completed { contract_id } or refunded { contract_id, amount }
Function: issue_reputation
Caller: Client
Pre-state: Completed
Post-state: Completed (Reputation credit consumed)
Event: rated { contract_id, freelancer, rating }
Example:
escrow.issue_reputation(&contract_id, &5);| Function | Authorized Caller(s) | Rejection Behavior |
|---|---|---|
create_contract |
Any (becomes Client) | N/A |
deposit_funds |
Client | UnauthorizedRole |
approve_milestone |
Client | UnauthorizedRole |
release_milestone |
Client, Arbiter | UnauthorizedRole |
cancel_contract |
Client, Freelancer, Arbiter | UnauthorizedRole (depends on state) |
refund_unreleased_milestones |
Arbiter | UnauthorizedRole |
finalize_contract |
Client | UnauthorizedRole |
withdraw_leftover |
Client | UnauthorizedRole |
issue_reputation |
Client | UnauthorizedRole |
Arbiter Override: The arbiter can call release_milestone or refund_unreleased_milestones to resolve disputes or unstick funds.
Events are critical for off-chain indexers to track the state of escrow contracts.
| Event Name | Payload Fields | Interpretation |
|---|---|---|
created |
contract_id, client, freelancer, total_amount |
New contract initialized in Created state. |
deposited |
contract_id, amount, payer |
Funds successfully moved into escrow. |
approved |
contract_id, milestone_index |
Work verified by client. |
released |
contract_id, milestone_index, amount |
Funds moved from escrow to freelancer. |
completed |
contract_id |
All milestones paid; reputation credit available. |
refunded |
contract_id, amount |
Funds returned to client by arbiter. |
rated |
contract_id, freelancer, rating |
Rating recorded; credit consumed. |
cancelled |
contract_id, caller, status, timestamp |
Contract terminated; remaining funds returned. |
finalized |
contract_id |
Contract closed for leftover withdrawals. |
withdrawn |
contract_id, amount, caller |
Leftover funds withdrawn by client. |
Note: All events include a ledger timestamp for ordering.
| Scenario | Behavior | Error Returned |
|---|---|---|
| Double Deposit | Allowed (increments balance) | N/A |
| Double Release | Blocked (milestone already released) | AlreadyReleased |
| Unauthorized Release | Blocked (caller is not client/arbiter) | UnauthorizedRole |
Release in Created |
Blocked (insufficient funds) | ContractNotFound (if wrong ID) |
Release in Cancelled |
Blocked (terminal state) | InvalidStatusTransition |
| Cancellation after Release | Allowed only for unreleased milestones | MilestonesAlreadyReleased (for client) |
| Over-funding | Allowed (excess can be withdrawn after finalization) | N/A |
Paths:
CreatedβCancelled: Either Client or Freelancer can trigger.FundedβCancelled:- Client (if zero milestones released)
- Freelancer (anytime, funds return to client)
- Arbiter (dispute resolution) Funds: All unreleased funds are returned to the client (accounting updated).
Trigger: refund_unreleased_milestones (Arbiter only)
Condition: Contract in Funded or Disputed state.
Effect: Specified milestones marked as refunded; funds marked as refundable to client.
Sequence: Funded β Disputed β Arbiter Decision β Release/Refund
Initiation: Either party calls dispute_contract.
Arbiter Authority: In Disputed state, the Arbiter has full authority to release or refund milestones.
| From | To | Trigger |
|---|---|---|
Created |
Funded |
deposit_funds |
Created |
Cancelled |
cancel_contract |
Funded |
Completed |
Final release_milestone |
Funded |
Disputed |
dispute_contract |
Funded |
Cancelled |
cancel_contract |
Funded |
Refunded |
Final refund_unreleased_milestones |
Disputed |
Completed |
Arbiter release_milestone |
Disputed |
Cancelled |
Arbiter cancel_contract |
// 1. Create
const contractId = await escrow.create_contract(client, freelancer, null, [100, 200]);
// 2. Deposit
await escrow.deposit_funds(contractId, 300);
// 3. Work done... Approve & Release Milestone 1
await escrow.approve_milestone(contractId, 0);
await escrow.release_milestone(contractId, 0);
// 4. Work done... Release Milestone 2 (auto-completes)
await escrow.release_milestone(contractId, 1);
// 5. Issue Reputation
await escrow.issue_reputation(contractId, 5);- Only the
protocol_fee_accountcan adjust fee rate or withdraw accrued fees. - Fee account is authenticated with
caller.require_auth(). - Fee bounds enforced at 0..=10000.
- All protocol fee operations use persisted state and safe integer arithmetic.
On each milestone release:
- Compute fee:
milestone.amount * protocol_fee_bps / 10000. - Save fee to milestone object.
- Increment
protocol_fee_accrued. - Mark milestone released and contract status completed when all milestones done.
Mainnet readiness (limits, events, risks): mainnet-readiness.md
This document summarizes the reviewer-facing architecture for contracts/escrow.
The contract persists:
- escrow lifecycle state for each contract
- participant metadata for the client and freelancer
- milestone release state
- funded and released accounting
- pending and issued reputation aggregates
- protocol governance parameters
- pause and emergency flags
Core escrow endpoints:
create_contract(client, freelancer, milestone_amounts) -> u32deposit_funds(contract_id, amount) -> boolrelease_milestone(contract_id, milestone_id) -> boolissue_reputation(contract_id, rating) -> boolget_contract(contract_id) -> EscrowContractDataget_reputation(freelancer) -> Option<ReputationRecord>get_pending_reputation_credits(freelancer) -> u32
Operational controls:
initialize(admin) -> boolpause() -> boolunpause() -> boolactivate_emergency_pause() -> boolresolve_emergency() -> boolis_paused() -> boolis_emergency() -> bool
Governance:
initialize_protocol_governance(admin, min_milestone_amount, max_milestones, min_reputation_rating, max_reputation_rating) -> boolupdate_protocol_parameters(...) -> boolpropose_governance_admin(next_admin) -> boolaccept_governance_admin() -> boolget_protocol_parameters() -> ProtocolParametersget_governance_admin() -> Option<Address>get_pending_governance_admin() -> Option<Address>
The escrow tests are grouped into dedicated modules:
To prevent out-of-gas or infinite-loop denial of service attacks, the escrow contract enforces creation limits:
- maximum milestone count is capped by
ProtocolParameters.max_milestones(defaults to 16) - total escrow amount is bounded by the immutable mainnet cap (
MAINNET_MAX_TOTAL_ESCROW_PER_CONTRACT_STROOPS)
Supported lifecycle transitions:
Created -> Acceptedafter freelancer or arbiter accepts the contract termsAccepted -> Fundedafter any positive depositFunded -> Completedafter the final unreleased milestone is released
Operational invariants:
- client and freelancer addresses are immutable after creation
- milestone amounts are immutable after creation
- each milestone can transition from
released = falsetoreleased = trueexactly once released_amountis the sum of released milestone amountsreleased_milestonesmatches the number of released milestone flagsreputation_issuedcan only becometrueafterCompleted
- Detect incident and call
activate_emergency_pause. - Investigate and remediate root cause.
- Validate mitigations in test/staging.
- Call
resolve_emergencyto restore service. - Publish incident summary for ecosystem transparency.
Each EscrowContractData record stores:
- participant addresses
- milestone vector and cached milestone count
- total escrow amount
- funded and released balances
- released milestone count
- contract status
- reputation issuance flag
- creation and update timestamps
Detailed storage-key coverage is documented in state-persistence.md.
The escrow regression suite is split by concern:
flows.rs: happy-path lifecycle and reputation aggregationlifecycle.rs: state transition persistencepersistence.rs: storage round-trip assertionssecurity.rs: failure paths and validation checksgovernance.rs: admin and parameter persistencepause_controls.rsandemergency_controls.rs: operational safety controlsperformance.rs: resource regression ceilings
Lifecycle operations now emit a standardized event shape to simplify indexing and alerting.
- Topic tuple:
("escrow", "v1", operation, contract_id) - Data tuple:
(status, amount, milestone_index, actor, timestamp)
Operation values:
createdepositapprovereleasecancel
Schema notes:
status: post-operationContractStatusamount: operation amount (or0when not applicable)milestone_index: milestone index (or0when not applicable)actor:Some(Address)when a caller identity is relevant, otherwiseNonetimestamp: ledger timestamp at emission
Backwards compatibility:
- Previous ad-hoc topics such as
("contract_cancelled", contract_id)are replaced by the v1 lifecycle schema. - Indexers should migrate to the new topic/data tuples for deterministic parsing.