Skip to main content

Layered Architecture

The smart contract system is organized in three layers:

Separation of Concerns

Protocol Layer (PDPVerifier)

Responsibility: Neutral proof verification
  • No business logic
  • No payment handling
  • Only validates cryptographic proofs
  • Delegates to service contracts via callbacks
interface IPDPVerifier {
    function addPieces(
        uint256 dataSetId,
        PieceInfo[] calldata pieces,
        bytes calldata extraData
    ) external;
    
    function submitProof(
        uint256 dataSetId,
        bytes calldata proof
    ) external;
}

Service Layer (FWSS)

Responsibility: Business logic and payment management
  • Client authorization (EIP-712)
  • Provider whitelist management
  • Payment rail creation and management
  • Data set lifecycle
  • Implements PDPListener callbacks
interface IFilecoinWarmStorageService {
    function beforeAddPieces(
        uint256 dataSetId,
        PieceInfo[] calldata pieces,
        bytes calldata extraData
    ) external returns (bytes32);
    
    function onProofSuccess(
        uint256 dataSetId
    ) external;
    
    function onProofFailure(
        uint256 dataSetId
    ) external;
}

Payment Layer (FilecoinPay)

Responsibility: Generic payment rails
  • Deposits and withdrawals
  • Payment rail creation
  • Rate-based continuous payments
  • Operator approvals (for services)
  • Settlement with validation callbacks
interface IFilecoinPay {
    function deposit(uint256 amount, address to) external;
    function withdraw(uint256 amount) external;
    function setOperatorApproval(
        address operator,
        bool approve,
        uint256 rateAllowance,
        uint256 lockupAllowance,
        uint256 maxLockupPeriod
    ) external;
}

Callback Pattern

FWSS implements PDPListener callbacks to hook into proof events:

Contract Interaction Flow

Upload Flow

Proof Flow

State Management

FWSS State

struct DataSet {
    uint256 id;
    address client;
    address payer;
    uint256 providerId;
    uint256 railId;
    uint256 startEpoch;
    uint256 terminatedAtEpoch;
    Metadata[] metadata;
}

struct Piece {
    uint256 id;
    uint256 dataSetId;
    bytes32 pieceCid;
    uint256 size;
    Metadata[] metadata;
}

mapping(uint256 => DataSet) public dataSets;
mapping(uint256 => Piece) public pieces;
mapping(address => uint256[]) public clientDataSets;

Payment State

struct Rail {
    uint256 id;
    address client;
    address payee;
    address operator;
    uint256 rate;
    uint256 startEpoch;
    uint256 lastSettledEpoch;
    uint256 endEpoch;
}

struct OperatorApproval {
    bool isApproved;
    uint256 rateAllowance;
    uint256 lockupAllowance;
    uint256 maxLockupPeriod;
    uint256 rateUsage;
    uint256 lockupUsage;
}

mapping(uint256 => Rail) public rails;
mapping(address => mapping(address => OperatorApproval)) public operatorApprovals;

Provider Management

Endorsed vs Approved

// Endorsed: curated, high-quality providers (subset of approved)
mapping(uint256 => bool) public endorsedProviders;

// Approved: pass automated quality checks
mapping(uint256 => bool) public approvedProviders;

function addApprovedProvider(uint256 providerId) external onlyOwner {
    approvedProviders[providerId] = true;
}

function endorseProvider(uint256 providerId) external onlyOwner {
    require(approvedProviders[providerId], "Must be approved first");
    endorsedProviders[providerId] = true;
}

EIP-712 Domain Separation

Each operation has a unique EIP-712 type:
// CreateDataSet
const types = {
  DataSetInfo: [
    { name: 'client', type: 'address' },
    { name: 'payer', type: 'address' },
    { name: 'serviceProvider', type: 'address' },
    { name: 'serviceProviderId', type: 'uint256' },
    { name: 'startEpoch', type: 'uint256' },
    { name: 'rate', type: 'uint256' },
    { name: 'lockup', type: 'uint256' },
    { name: 'metadata', type: 'MetadataEntry[]' },
  ],
}

// AddPieces
const types = {
  AddPiecesInfo: [
    { name: 'dataSetId', type: 'uint256' },
    { name: 'pieces', type: 'PieceInput[]' },
  ],
}

Nonce Management

Nonces prevent replay attacks:
mapping(address => uint256) public nonces;

function incrementNonce(address account) internal {
    nonces[account]++;
}

function validateSignature(
    address signer,
    bytes32 hash,
    bytes calldata signature
) internal view {
    require(nonces[signer] == expectedNonce, "Invalid nonce");
    // ... validate signature
}

Metadata System

struct MetadataEntry {
    string key;
    string value;
}

// Validation
uint256 constant MAX_DATASET_METADATA_ENTRIES = 10;
uint256 constant MAX_PIECE_METADATA_ENTRIES = 5;
uint256 constant MAX_KEY_LENGTH = 32;
uint256 constant MAX_VALUE_LENGTH = 128;

function validateMetadata(
    MetadataEntry[] calldata metadata,
    uint256 maxEntries
) internal pure {
    require(metadata.length <= maxEntries, "Too many entries");
    
    for (uint256 i = 0; i < metadata.length; i++) {
        require(
            bytes(metadata[i].key).length <= MAX_KEY_LENGTH,
            "Key too long"
        );
        require(
            bytes(metadata[i].value).length <= MAX_VALUE_LENGTH,
            "Value too long"
        );
    }
}

Access Control

// Owner functions (FWSS)
function addApprovedProvider(uint256 providerId) external onlyOwner;
function setServicePrice(...) external onlyOwner;

// Client functions (require ownership or authorization)
function terminateDataSet(uint256 dataSetId) external {
    DataSet storage ds = dataSets[dataSetId];
    require(
        ds.payer == msg.sender || 
        isAuthorizedOperator(ds.payer, msg.sender),
        "Not authorized"
    );
    // ...
}

// Session key authorization
function isAuthorizedOperator(
    address account,
    address operator
) public view returns (bool) {
    return sessionKeyRegistry.hasPermission(
        account,
        operator,
        TERMINATE_DATASET_PERMISSION
    );
}

Contract Upgradability

Contracts are NOT upgradeable by design for security and immutability. New features require new contract deployments.

Gas Optimization

  • Packed storage slots
  • Minimal storage writes
  • Multicall support
  • Batch operations (e.g., addPieces accepts arrays)

Best Practices

EIP-712 Signing

Always use EIP-712 for off-chain authorization

Nonce Management

Track nonces to prevent replay attacks

Callback Pattern

Use callbacks for cross-contract communication

Minimal Storage

Optimize storage for gas efficiency

Next Steps

FWSS Contract

Learn about the storage service contract

Filecoin Pay

Understand payment rails

PDP Verifier

Explore proof verification

Build docs developers (and LLMs) love