Skip to content

PT20 (contract)

ERC-5095 Principal Token for one (kDIEM, maturity) pair. Deployed by PT20Factory via CREATE2.

contracts/src/platforms/venice/PT20.sol

Inherits

ERC20 (OpenZeppelin)

Immutables (set at deploy)

NameTypeSource
factoryaddressConstructor arg (PT20Factory address)
underlyingaddresskDIEM
lockeraddressLocker recorded at deploy time
maturityTimestampuint64The PT20's maturity

Name / symbol set by factory: kDIEM-JUN2026 / kDIEM-JUN2026.

Deposit

solidity
function deposit(uint256 amount, address recipient)
    external onlyLocker
    returns (uint256 ptMinted);
  • block.timestamp < maturityTimestamp (PreMaturityOnly after maturity).
  • Pulls amount underlying from msg.sender (must be approved).
  • Mints amount PT20 to recipient.
  • Returns ptMinted == amount (1:1).

ERC-5095 interface

solidity
function maturity() external view returns (uint256);
function convertToUnderlying(uint256 principalAmount) external pure returns (uint256);
function convertToPrincipal(uint256 underlyingAmount) external pure returns (uint256);
function maxRedeem(address holder) external view returns (uint256);
function maxWithdraw(address holder) external view returns (uint256);
function previewRedeem(uint256 principalAmount) external pure returns (uint256);
function previewWithdraw(uint256 underlyingAmount) external pure returns (uint256);

function redeem(uint256 principalAmount, address to, address from)
    external returns (uint256 underlyingAmount);

function withdraw(uint256 underlyingAmount, address to, address from)
    external returns (uint256 principalAmount);

All ratios are 1:1. maxRedeem and maxWithdraw return balanceOf(holder) post-maturity and 0 pre-maturity.

redeem / withdraw are functionally identical — both burn amount PT20 and transfer amount underlying. They exist as separate functions for ERC-5095 spec compliance.

Redeem allowance

If from != msg.sender, an allowance is spent via OpenZeppelin's _spendAllowance(from, msg.sender, amount). Standard ERC-20 approve semantics.

Pre-maturity vs post-maturity

Phasedepositredeem / withdraw
block.timestamp < maturityTimestampOK (onlyLocker)NotMatured
block.timestamp >= maturityTimestampPreMaturityOnlyOK

Strict boundary at the exact second — no overlap.

Errors

PreMaturityOnly, NotMatured, ZeroAmount, ZeroAddress, NotLocker

Why onlyLocker on deposit

Without it, anyone holding kDIEM could create PT20 shares directly. While kDIEM is currently only mintable by the Locker (and thus shouldn't be in user hands), onlyLocker makes the invariant structural rather than relying on no kDIEM ever leaking.

Combined with Locker minting kDIEM → forceApprove → PT20.deposit atomically, this guarantees:

every PT20.totalSupply() share
    ↔ a Locker-issued lock
    ↔ a corresponding DD issuance to the same recipient
    ↔ DIEM staked in the vault

DEX integration

PT20 is standard ERC-20. Liquidity:

  • Pendle: PT20 is structurally compatible.
  • Uniswap V3: pair PT20-MMMyy with DIEM or USDC.
  • Aave/Morpho: PT20 as collateral once integrations land.

Address precomputation

Since PT20 is deployed via CREATE2 with salt = keccak256(maturity), you can inference its address before it exists:

ts
const ptAddress = getCreate2Address({
  from: pt20Factory,
  salt: keccak256(encodeAbiParameters([{type: "uint64"}], [maturity])),
  bytecodeHash: keccak256(PT_INIT_CODE),
});

(Where PT_INIT_CODE includes the constructor args factory, underlying, locker, maturity, name, symbol ABI-encoded.)

Released under the MIT License.