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
| Contract | Address |
|---|
| OwnerWriteCondition | 0x4C9bFC96d7092b590D497A191826C3dA2277c34B |
| LicenseReadCondition | 0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3 |
| LicenseToken | 0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC |
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.