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)
| Name | Type | Source |
|---|---|---|
factory | address | Constructor arg (PT20Factory address) |
underlying | address | kDIEM |
locker | address | Locker recorded at deploy time |
maturityTimestamp | uint64 | The PT20's maturity |
Name / symbol set by factory: kDIEM-JUN2026 / kDIEM-JUN2026.
Deposit
function deposit(uint256 amount, address recipient)
external onlyLocker
returns (uint256 ptMinted);block.timestamp < maturityTimestamp(PreMaturityOnlyafter maturity).- Pulls
amountunderlying frommsg.sender(must be approved). - Mints
amountPT20 torecipient. - Returns
ptMinted == amount(1:1).
ERC-5095 interface
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
| Phase | deposit | redeem / withdraw |
|---|---|---|
block.timestamp < maturityTimestamp | OK (onlyLocker) | NotMatured |
block.timestamp >= maturityTimestamp | PreMaturityOnly | OK |
Strict boundary at the exact second — no overlap.
Errors
PreMaturityOnly, NotMatured, ZeroAmount, ZeroAddress, NotLockerWhy 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 vaultDEX 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:
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.)