|
1 | 1 | ```txt █████ |
2 | | - ░░███ |
3 | | - ███████ ████████ █████ ████ ████████ ███████ |
4 | | - ███░░███ ░░███░░███ ░░███ ░███ ░░███░░███ ░░░███░ |
| 2 | + ░░███ |
| 3 | + ███████ ████████ █████ ████ ████████ ███████ |
| 4 | +███░░███ ░░███░░███ ░░███ ░███ ░░███░░███ ░░░███░ |
5 | 5 | ░███ ░███ ░███ ░░░ ░███ ░███ ░███ ░███ ░███ |
6 | 6 | ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ███ |
7 | 7 | ░░███████ █████ ░░████████ ████ █████ ░░█████ |
|
13 | 13 |
|
14 | 14 | <p align="center" style="font-size: smaller;"> |
15 | 15 | <i>(wip)</i> |
16 | | -</p> |
| 16 | +</p> |
| 17 | + |
| 18 | +## Principal and Yield Token Vaults |
| 19 | + |
| 20 | +The vault system implements a dual-token structure separating principal and yield: |
| 21 | + |
| 22 | +### Deposit Phase |
| 23 | + |
| 24 | +When depositing into the vault, users receive two types of shares: |
| 25 | +- **Principal Shares (PT)**: Equal to the amount of assets deposited |
| 26 | +- **Yield Shares (YT)**: Equal to the expected yield amount |
| 27 | + |
| 28 | +**Example**: Depositing 1,000,000 USDC with an expected 10% return: |
| 29 | +- Receive: 1,000,000 PT + 100,000 YT |
| 30 | + |
| 31 | +During the deposit phase, withdrawals are locked (`canWithdraw = false`). |
| 32 | + |
| 33 | +### Redemption Phase |
| 34 | + |
| 35 | +Once the vault receives assets and unlocks redemption (`canWithdraw = true`), the value of each share type is determined by the total assets in the vault. |
| 36 | + |
| 37 | +#### Redemption Formula |
| 38 | + |
| 39 | +``` |
| 40 | +principalAssets = min(totalAssets, principalTokenSupply) |
| 41 | +yieldAssets = totalAssets - principalAssets |
| 42 | +
|
| 43 | +pricePerPrincipalShare = principalAssets / principalTokenSupply |
| 44 | +pricePerYieldShare = yieldAssets / yieldTokenSupply |
| 45 | +``` |
| 46 | + |
| 47 | +#### Redemption Examples |
| 48 | + |
| 49 | +Given: 1,000,000 PT and 100,000 YT outstanding |
| 50 | + |
| 51 | +| Total Assets | Principal Assets | Yield Assets | PT Price | YT Price | |
| 52 | +| ------------ | ---------------- | ------------ | -------- | -------- | |
| 53 | +| 900,000 | 900,000 | 0 | 0.9 | 0 | |
| 54 | +| 1,000,000 | 1,000,000 | 0 | 1.0 | 0 | |
| 55 | +| 1,050,000 | 1,000,000 | 50,000 | 1.0 | 0.5 | |
| 56 | +| 1,200,000 | 1,000,000 | 200,000 | 1.0 | 2.0 | |
| 57 | + |
| 58 | +**Key Properties:** |
| 59 | +- Principal holders are prioritized: they receive up to 1:1 redemption of their shares |
| 60 | +- Yield holders receive any assets beyond the principal supply |
| 61 | +- If total assets < principal supply, principal holders share the loss proportionally |
| 62 | +- If total assets > principal supply, yield holders capture all upside |
| 63 | + |
| 64 | +## Request Contract |
| 65 | + |
| 66 | +The `Request` contract is the core contract managing funding requests with dual-token (PT/YT) issuance. It combines multiple functionalities: |
| 67 | + |
| 68 | +- **OfferReceiver**: Validates and processes signed offers using EIP-712 signatures |
| 69 | +- **VaultController**: Manages PT/YT tokens with ERC4626-style redemptions |
| 70 | +- **Ownable**: Restricts admin functions to the contract owner |
| 71 | + |
| 72 | +### Request Lifecycle |
| 73 | + |
| 74 | +``` |
| 75 | +┌─────────────────────────────────────────────────────────────────────────────┐ |
| 76 | +│ REQUEST LIFECYCLE │ |
| 77 | +└─────────────────────────────────────────────────────────────────────────────┘ |
| 78 | +
|
| 79 | +1. DEPLOYMENT |
| 80 | + └─> Factory creates Request + PT Vault + YT Vault (beacon proxies) |
| 81 | + └─> Owner is set, contract initialized |
| 82 | +
|
| 83 | +2. FUNDING PHASE (canWithdraw = false) |
| 84 | + ├─> Method A: consume() - Process signed offers from prime brokers |
| 85 | + │ └─> Callback triggers → Funds pulled → PT/YT minted to maker |
| 86 | + └─> Method B: authorizeMinting() + mint() - Whitelist approach |
| 87 | + └─> Owner authorizes → Prime broker calls mint() → PT/YT minted |
| 88 | +
|
| 89 | +3. FUND UTILIZATION |
| 90 | + └─> Owner calls pullFunds() to transfer assets to borrower |
| 91 | +
|
| 92 | +4. REPAYMENT |
| 93 | + └─> Borrower transfers assets back to Request contract |
| 94 | +
|
| 95 | +5. REDEMPTION PHASE (canWithdraw = true) |
| 96 | + └─> Owner calls setRepaid() |
| 97 | + └─> PT/YT holders can redeem their tokens for underlying assets |
| 98 | +``` |
| 99 | + |
| 100 | +### Funding Methods |
| 101 | + |
| 102 | +The Request contract supports two methods for prime brokers to provide funds: |
| 103 | + |
| 104 | +#### Method 1: Signed Offer Consumption (`consume`) |
| 105 | + |
| 106 | +The owner broadcasts a signed EIP-712 offer to the contract. This method is ideal for prime brokers who implement automated fund management through smart contracts. |
| 107 | + |
| 108 | +**Flow:** |
| 109 | +1. Prime broker creates and signs an `Offer` struct (off-chain) |
| 110 | +2. Owner calls `consume(offer, signature, ptAmount)` |
| 111 | +3. Contract validates the signature and offer parameters |
| 112 | +4. Contract calls `onRequestConsumed()` callback on the maker's contract |
| 113 | +5. Contract pulls `ptAmount` of underlying asset from the owner (msg.sender) |
| 114 | +6. PT and YT tokens are minted to the maker |
| 115 | +7. The maker's offer nonce is updated |
| 116 | + |
| 117 | +```solidity |
| 118 | +// Offer struct |
| 119 | +struct Offer { |
| 120 | + address maker; // Prime broker address that receives tokens |
| 121 | + uint256 amount; // Reference principal amount |
| 122 | + uint256 expectedReturn; // Expected yield amount |
| 123 | + uint256 nonce; // Sequential nonce (must be > stored nonce) |
| 124 | + uint256 expiration; // Timestamp when offer expires |
| 125 | +} |
| 126 | +
|
| 127 | +// YT amount is calculated proportionally |
| 128 | +ytAmount = offer.expectedReturn * ptAmount / offer.amount |
| 129 | +
|
| 130 | +// The ptAmount parameter determines how many PT tokens are minted |
| 131 | +// and scales the YT amount proportionally from the offer's expectedReturn |
| 132 | +``` |
| 133 | + |
| 134 | +**Callback Interface:** |
| 135 | + |
| 136 | +Prime brokers implementing automated strategies can implement `IRequestCallback`: |
| 137 | + |
| 138 | +```solidity |
| 139 | +interface IRequestCallback { |
| 140 | + function onRequestConsumed( |
| 141 | + Offer calldata offer, |
| 142 | + bytes calldata signature, |
| 143 | + uint256 principal, // PT amount being minted |
| 144 | + uint256 yield // YT amount being minted |
| 145 | + ) external; |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +The callback is invoked **before** funds are pulled, allowing the maker to: |
| 150 | +- Withdraw from DeFi positions |
| 151 | +- Move funds from internal accounting |
| 152 | +- Set ERC20 allowances for the Request contract |
| 153 | + |
| 154 | +#### Method 2: Authorized Minting (`authorizeMinting` + `mint`) |
| 155 | + |
| 156 | +The owner whitelists specific addresses to mint PT/YT tokens. This method is simpler and suitable for prime brokers who manage funds manually or through EOAs. |
| 157 | + |
| 158 | +**Flow:** |
| 159 | +1. Owner calls `authorizeMinting(primebroker, ptAmount, ytAmount)` |
| 160 | +2. Prime broker approves the Request contract to spend their underlying asset |
| 161 | +3. Prime broker calls `mint()` |
| 162 | +4. Contract transfers `ptAmount` of underlying asset from the prime broker |
| 163 | +5. PT and YT tokens are minted to the prime broker |
| 164 | +6. Authorization is consumed (one-time use) |
| 165 | + |
| 166 | +```solidity |
| 167 | +// Owner authorizes minting |
| 168 | +request.authorizeMinting(primeBroker, 1_000_000e6, 100_000e6); |
| 169 | +
|
| 170 | +// Prime broker mints (after approving underlying asset) |
| 171 | +asset.approve(address(request), 1_000_000e6); |
| 172 | +request.mint(); // Receives 1M PT + 100k YT |
| 173 | +``` |
| 174 | + |
| 175 | +### Fund Management |
| 176 | + |
| 177 | +After offers are consumed or minting is complete: |
| 178 | + |
| 179 | +1. **Pull Funds**: Owner calls `pullFunds(receiver, amount)` to transfer collected assets to the borrower |
| 180 | +2. **Borrower Utilizes Funds**: The borrower uses the funds for their intended purpose |
| 181 | +3. **Repayment**: Borrower transfers assets back to the Request contract |
| 182 | +4. **Enable Redemptions**: Owner calls `setRepaid()` to unlock withdrawals |
| 183 | + |
| 184 | +```solidity |
| 185 | +// After funding phase |
| 186 | +request.pullFunds(borrowerAddress, totalFunded); |
| 187 | +
|
| 188 | +// After borrower repays |
| 189 | +request.setRepaid(); // Enables PT/YT holders to redeem |
| 190 | +``` |
| 191 | + |
| 192 | +The `pullFunds()` function can be called multiple times by the owner during the funding phase. The `setRepaid()` function toggles the withdrawal lock, enabling PT/YT holders to redeem their tokens for the underlying assets held by the contract. |
| 193 | + |
| 194 | +## RequestFactory |
| 195 | + |
| 196 | +The `RequestFactory` deploys Request instances using the **beacon proxy pattern** for gas-efficient and upgradeable deployments. |
| 197 | + |
| 198 | +### Architecture |
| 199 | + |
| 200 | +``` |
| 201 | +┌─────────────────────────────────────────────────────────────────┐ |
| 202 | +│ BEACON PROXY PATTERN │ |
| 203 | +└─────────────────────────────────────────────────────────────────┘ |
| 204 | +
|
| 205 | + ┌──────────────────┐ |
| 206 | + │ RequestFactory │ |
| 207 | + │ │ |
| 208 | + │ REQUEST_BEACON ──┼──> UpgradeableBeacon ──> Request Implementation |
| 209 | + │ PT_VAULT_BEACON ─┼──> UpgradeableBeacon ──> Vault(isPT=false) |
| 210 | + │ YT_VAULT_BEACON ─┼──> UpgradeableBeacon ──> Vault(isPT=true) |
| 211 | + └──────────────────┘ |
| 212 | + │ |
| 213 | + │ createRequest() |
| 214 | + ▼ |
| 215 | + ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ |
| 216 | + │ Request Proxy │ │ PT Vault Proxy │ │ YT Vault Proxy │ |
| 217 | + │ (ERC1967) │ │ (ERC1967) │ │ (ERC1967) │ |
| 218 | + └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ |
| 219 | + │ │ │ |
| 220 | + └────────────────────────┼────────────────────────┘ |
| 221 | + │ |
| 222 | + delegates to beacon |
| 223 | +``` |
| 224 | + |
| 225 | +### Deployment |
| 226 | + |
| 227 | +```solidity |
| 228 | +// Deploy factory with beacon owner |
| 229 | +RequestFactory factory = new RequestFactory(beaconOwner); |
| 230 | +
|
| 231 | +// Create a new request with PT/YT vaults |
| 232 | +(address request, address ptVault, address ytVault) = factory.createRequest( |
| 233 | + owner, // Request owner (admin) |
| 234 | + address(usdc), // Underlying asset |
| 235 | + "USDC Request", // Base name (becomes "PT-USDC Request" / "YT-USDC Request") |
| 236 | + "USDC-REQ" // Base symbol (becomes "PT-USDC-REQ" / "YT-USDC-REQ") |
| 237 | +); |
| 238 | +``` |
| 239 | + |
| 240 | +### Upgrades |
| 241 | + |
| 242 | +The beacon owner can upgrade all proxies by updating the beacon's implementation: |
| 243 | + |
| 244 | +```solidity |
| 245 | +// All existing Request proxies now use the new implementation |
| 246 | +UpgradeableBeacon(factory.REQUEST_BEACON()).upgradeTo(newImplementation); |
| 247 | +``` |
| 248 | + |
| 249 | +## Prime Broker Offers |
| 250 | + |
| 251 | +### Offer Creation and Signing |
| 252 | + |
| 253 | +Prime brokers create cryptographically signed offers to lend assets to the protocol. Each offer specifies: |
| 254 | +- **Amount**: The principal amount to lend |
| 255 | +- **Expected Return**: The absolute return expected (not a rate) |
| 256 | +- **Maker**: The address providing the funds |
| 257 | +- **Nonce**: A sequential number for offer management (must start at 1) |
| 258 | +- **Expiration**: Timestamp after which the offer is no longer valid |
| 259 | + |
| 260 | +Offers are signed using either: |
| 261 | +- **EIP-712**: Standard typed data signing for EOA accounts |
| 262 | +- **EIP-1271**: Smart contract signature validation via `isValidSignature()` |
| 263 | + |
| 264 | +### Nonce Management |
| 265 | + |
| 266 | +Nonces enable flexible offer lifecycle management: |
| 267 | + |
| 268 | +**Starting Value**: All nonces must start at 1 (nonce 0 is invalid) |
| 269 | + |
| 270 | +**Offer Cancellation**: |
| 271 | +- **Soft Cancel**: Communicate with the off-chain server to mark offers as cancelled (no on-chain transaction) |
| 272 | +- **Hard Cancel**: Set nonce on-chain to invalidate all offers at or below that nonce |
| 273 | + |
| 274 | +### Offer Validation |
| 275 | + |
| 276 | +When an offer is consumed, the protocol validates: |
| 277 | +1. **Maker validity**: Maker address is not zero |
| 278 | +2. **Amount validity**: Amount and expectedReturn are non-zero |
| 279 | +3. **Expiration**: Current timestamp < expiration |
| 280 | +4. **Nonce freshness**: Offer nonce > current stored nonce for maker |
| 281 | +5. **Signature validity**: EIP-712 or EIP-1271 signature verification |
| 282 | + |
| 283 | +Upon successful validation, the offer's nonce is stored, preventing replay attacks. |
0 commit comments