Skip to main content
CDR vaults can be gated behind Story Protocol license tokens so that only license holders can decrypt the vault contents. In the current Aeneid release, this is a manual integration: you encode the CDR condition data yourself and mint Story license tokens separately.

Prerequisites

Aeneid Contracts

ContractAddress
OwnerWriteCondition0x4C9bFC96d7092b590D497A191826C3dA2277c34B
LicenseReadCondition0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3
LicenseToken0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC

How the Story License Read Pattern Works

Every CDR vault has a writeConditionAddr and readConditionAddr. For a Story license-gated vault on Aeneid:
  • writeConditionAddr usually points at OwnerWriteCondition, with writeConditionData = abi.encode(ownerAddress)
  • readConditionAddr points at LicenseReadCondition, with readConditionData = abi.encode(licenseTokenAddress, ipId)
  • accessAuxData at read time is abi.encode(uint256[] licenseTokenIds)
The CDR SDK does not register the IP Asset or mint the license token for you. Create the IP and obtain its ipId with Story tooling first, then configure the vault. If you have not registered the asset yet, start with Register IP Asset. If you need to attach or inspect license terms before minting, see Attach Terms.

Upload a License-Gated Vault

import { encodeAbiParameters } from "viem";

const globalPubKey = await client.observer.getGlobalPubKey();
const dataKey = new TextEncoder().encode("confidential IP content");

const writeCondData = encodeAbiParameters(
  [{ type: "address" }],
  [uploaderAddress],
);

const readCondData = encodeAbiParameters(
  [{ type: "address" }, { type: "address" }],
  [
    "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC",
    ipId,
  ],
);

await client.uploader.uploadCDR({
  dataKey,
  globalPubKey,
  updatable: false,
  writeConditionAddr: "0x4C9bFC96d7092b590D497A191826C3dA2277c34B",
  writeConditionData: writeCondData,
  readConditionAddr: "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3",
  readConditionData: readCondData,
  accessAuxData: "0x",
});
The same condition setup works with uploadFile() if the encrypted content lives off-chain.

Mint a License Token Before Reading

Before a user can read a Story license-gated vault, they still need to mint a license token. The current release requires three manual steps. WIP is Wrapped IP, the ERC-20 wrapped form of the native IP token. Story’s royalty / license flows use WIP, so the reader first wraps IP, then approves the RoyaltyModule to spend it.

1. Wrap IP to WIP

import { parseEther } from "viem";

const WIP_TOKEN = "0x1514000000000000000000000000000000000000";

const wrapTx = await walletClient.sendTransaction({
  to: WIP_TOKEN,
  data: "0xd0e30db0", // deposit()
  value: parseEther("1"),
});

await publicClient.waitForTransactionReceipt({ hash: wrapTx });

2. Approve the RoyaltyModule to spend WIP

import { parseEther } from "viem";

const WIP_TOKEN = "0x1514000000000000000000000000000000000000";
const ROYALTY_MODULE = "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086";

const approveTx = await walletClient.writeContract({
  address: WIP_TOKEN,
  abi: [
    {
      type: "function",
      name: "approve",
      inputs: [{ type: "address" }, { type: "uint256" }],
      outputs: [{ type: "bool" }],
      stateMutability: "nonpayable",
    },
  ],
  functionName: "approve",
  args: [ROYALTY_MODULE, parseEther("1")],
});

await publicClient.waitForTransactionReceipt({ hash: approveTx });

3. Mint the Story license token

import { StoryClient } from "@story-protocol/core-sdk";
import { http } from "viem";

const storyClient = StoryClient.newClient({
  transport: http("https://aeneid.storyrpc.io"),
  account: readerAccount,
});

const mintResult = await storyClient.license.mintLicenseTokens({
  licensorIpId: ipId,
  licenseTermsId: 2054,
  amount: 1,
  maxMintingFee: "100000000000000000",
  maxRevenueShare: 100,
});

const licenseTokenId = mintResult.licenseTokenIds[0];
licenseTermsId: 2054 is only an example. Replace it with the license terms ID actually attached to your IP Asset. You receive that ID when you register the asset or attach terms.
maxMintingFee and maxRevenueShare are user-provided protection values, not protocol defaults. Set them to the maximum fee and revenue share you are willing to accept for the specific terms you are minting.
The wrap, approve, and mint steps above are all on-chain transactions. The later accessCDR() call adds one more on-chain read request.

Read with a License Token

At read time, pass the caller’s license token ID through accessAuxData.
import { encodeAbiParameters } from "viem";

const accessAuxData = encodeAbiParameters(
  [{ type: "uint256[]" }],
  [[BigInt(licenseTokenId)]],
);

// Sends 1 read transaction, then collects partials and combines locally
const { dataKey } = await client.consumer.accessCDR({
  uuid,
  accessAuxData,
  timeoutMs: 120_000,
});

const content = new TextDecoder().decode(dataKey);
console.log(`Decrypted IP content: ${content}`);
If the caller does not hold a valid license token for the vault’s IP Asset, the read request reverts on-chain and validators will not produce partial decryptions.

Custom Condition Contracts

License gating is just one pattern. You can deploy your own condition contract for any access control logic by implementing one or both of these interfaces:
interface ICDRWriteCondition {
    function checkWriteCondition(
        uint32 uuid,
        bytes calldata accessAuxData,
        bytes calldata conditionData,
        address caller
    ) external view returns (bool);
}

interface ICDRReadCondition {
    function checkReadCondition(
        uint32 uuid,
        bytes calldata accessAuxData,
        bytes calldata conditionData,
        address caller
    ) external view returns (bool);
}
The CDR contract calls these functions before allowing a write() or read() operation. Return true to allow, false to deny. Then pass your contract’s address as readConditionAddr or writeConditionAddr when allocating a vault.