Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
524a105
add foundry skeleton
danoctavian May 13, 2026
5b591cb
add TERM redeemer spec
danoctavian May 13, 2026
cf35c87
Implement term redeemer module
danoctavian May 14, 2026
9f0e824
Refactor redeemer to OpenZeppelin primitives
danoctavian May 14, 2026
9e8d6f0
cosmetic
danoctavian May 14, 2026
114b0ca
Standardize redeemer entrypoints
danoctavian May 14, 2026
c6b015b
Refactor term redeemer lifecycle control
danoctavian May 14, 2026
dec41aa
Remove obsolete scaffold files
danoctavian May 14, 2026
cf88abc
Rename term redeemer to redeemable token
danoctavian May 14, 2026
12b7840
Simplify redemption activation checks
danoctavian May 14, 2026
4bf17c5
Move vault setup out of initializer
danoctavian May 14, 2026
d0e457e
Document paused initialization flag
danoctavian May 14, 2026
445a4fe
Use withdrawAsset in ynRWAx mock
danoctavian May 15, 2026
ba6c152
Use fee hook for post-lock accounting
danoctavian May 15, 2026
5b8e7d6
Rewrite README for current architecture
danoctavian May 15, 2026
9152924
Add README architecture diagrams
danoctavian May 15, 2026
38382f0
Add lifecycle yield accrual test
danoctavian May 15, 2026
1c212c7
Add ynRWAx fork integration test
danoctavian May 19, 2026
9f73032
Split unit and mainnet test profiles
danoctavian May 19, 2026
aed179e
Align fork tests with mainnet profile
danoctavian May 19, 2026
94173d2
Fix ynRWAx fork integration flow
danoctavian May 19, 2026
f248d99
Add redeemable token factory
danoctavian May 19, 2026
04091c0
Add test deployment scripts
danoctavian May 19, 2026
c406f0d
Restructure repo around src and script deployers
danoctavian May 19, 2026
0e5f210
Reuse mock rate provider in scripts
danoctavian May 19, 2026
d312ceb
Split script constants from addresses
danoctavian May 20, 2026
d0769f9
Update script command examples
danoctavian May 20, 2026
284f1e8
Align test vault assets with BaseVault ordering
danoctavian May 20, 2026
d1a34e1
Fix script deployment paths and wrapped USDC config
danoctavian May 20, 2026
77b12bc
Create deployment directories for scripts
danoctavian May 20, 2026
ed20b52
Add mock ynRWAx deployer
danoctavian May 20, 2026
b0471f5
add mock rwa deployment
danoctavian May 20, 2026
6f9eb39
add term redeemer deployment
danoctavian May 20, 2026
184aa8d
Add live ynRWAx rate provider
danoctavian May 29, 2026
b83c044
Add live ynRWAx script constants
danoctavian May 29, 2026
d596541
add vault with real ynRWAx
danoctavian May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

permissions: {}

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
name: Foundry project
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Show Forge version
run: forge --version

- name: Run Forge fmt
run: forge fmt --check

- name: Run Forge build
run: forge build --sizes

- name: Run Forge tests
run: forge test -vvv
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/yieldnest-vault"]
path = lib/yieldnest-vault
url = https://github.com/yieldnest/yieldnest-vault
121 changes: 120 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,120 @@
# term-redeemer
## Redeemable Token

This repo implements a term redemption system on top of YieldNest's `BaseStrategy`.

### Overview

The system has three main pieces:

- `RedeemableToken`
- a `BaseStrategy` vault whose share token is the redeemable claim token
- accepts `ynRWAx` as the deposit asset
- uses `USDC` as the default redemption asset

- `TermRedeemerController`
- controls the lifecycle after deployment
- at `lockEnd`, it finalizes the lock stage by processing accounting once, disabling new `ynRWAx` deposits, and setting the vault fee hook to `100%` performance fee
- at `redeemStart`, it activates `USDC` withdrawals

- `FeeHooks`
- starts with `0%` performance fee
- after lock, the controller sets it to `100%`
- this allows `processAccounting()` to keep running while diverting post-lock gains to the fee recipient instead of changing redeemer economics

### Architecture

```mermaid
flowchart LR
U[User]
Y[ynRWAx]
V[RedeemableToken<br/>BaseStrategy Vault]
C[TermRedeemerController]
H[FeeHooks]
P[Rate Provider]
R[USDC]
F[Fee Recipient]

U -->|depositAsset ynRWAx| V
Y -->|held by vault| V
V -->|mint wynRWAx shares| U

C -->|lock / activateRedemption| V
C -->|setPerformanceFee 100%| H
V -->|hooks callbacks| H
V -->|getRate| P

V -->|withdrawAsset USDC| U
V -->|post-lock fee shares| F
R -->|funds redemption balance| V
```

### Flow

1. The vault is deployed through the factory, initialized paused, and then configured atomically with roles, provider, assets, and hooks.
2. Users deposit `ynRWAx` into `RedeemableToken` and receive `wynRWAx`.
3. When the lock period is over, `lock()` is called on the controller:
- accounting is processed once
- `ynRWAx` is marked non-depositable
- the performance fee is set to `100%`
4. After that point, later accounting gains are captured by the fee recipient, while holder redemption value remains stable.
5. When the redeem stage starts, `activateRedemption()` enables `USDC` withdrawals.
6. The vault can unwind held `ynRWAx` into `USDC`, and `wynRWAx` holders redeem against the funded `USDC` balance.

### Lifecycle

```mermaid
sequenceDiagram
participant Admin
participant Controller
participant Vault as RedeemableToken
participant Hook as FeeHooks
participant User
participant ynRWAx
participant USDC

Admin->>Vault: configure roles, provider, assets, hooks
Admin->>Vault: unpause()

User->>Vault: depositAsset(ynRWAx)
Vault->>User: mint wynRWAx

Admin->>Controller: lock() after lockEnd
Controller->>Vault: processAccounting()
Controller->>Vault: disable ynRWAx deposits
Controller->>Hook: setPerformanceFee(100%)

Admin->>Vault: processor(...) unwind ynRWAx
ynRWAx->>Vault: USDC from withdrawAsset(...)

Admin->>Controller: activateRedemption() after redeemStart
Controller->>Vault: setAssetWithdrawable(USDC, true)

User->>Vault: redeem(wynRWAx)
Vault->>User: transfer USDC
```

### Key Files

- `src/RedeemableToken.sol`
- `src/TermRedeemerController.sol`
- `script/common/RedeemableTokenDeployer.sol`
- `test/unit/TermRedeemer.t.sol`

### Commands

```sh
forge build
forge test
forge fmt
```

### Layout

- `src/`
- production contracts
- `script/deploy`
- production deployment scripts
- `script/test`
- test harness deployment and lifecycle scripts
- `deployments/`
- per-chain JSON deployment artifacts written by scripts
35 changes: 35 additions & 0 deletions TERM_REDEEMER_SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# TERM REDEEMER

The term redeemer module provides redemption functionality for MAX vault with a fixed redemption term.

It works in multiples stages.


At time T0, the module starts allowing locking of MAX vault shares for redemption. It issues a receipt token, Withdrawable ynMYVAULTx. wynMYVAULTx.

These receipt tokens are minted 1:1.

This is allowed up until time T1.

At time T1 locking ynMYVAULTx for wynMYVAULTx stops. At this point, the rate for redemption of wynMYVAULTx is locked in terms of the underyling ynMYVAULTx.asset().

This locked in rate is the redemption rate at time T2.

All this time wynMYVAULTx can move and trade freely, but going back through the system to ynMYVAULTx is not allowed.

T3 is the time at which wynMYVAULTx can be burned forthe underlying asset.

This effectively means that yield stops for wynMYVAULTx yholders at time T2.


AT t3, the module can call withrawAsset onto the MAX Vault. for the exact amount of USDC needed. then it approves the extra ynRWAx to a trusted address to pull and use as it sees fit.

At T3 the users can start burning their wynMYVAULTx for the underlying asset indefinitely.

Do a very slick, simple implementation of this.

assume all the T times are known ahead of time and setup at initialization.

Obviously it needs to be configurable in terms of the asset.


1 change: 1 addition & 0 deletions contracts
1 change: 1 addition & 0 deletions deployments/deploy/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

7 changes: 7 additions & 0 deletions deployments/test-mock-ynrwax/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"chainId": 1,
"mockYnRwa": "0xB25dfEDAe1673057bbc23C15318c1Efd0d17D70e",
"mockYnRwaFactory": "0xe05b8d05EEb88d03cEB494d24E0Be3A9EF5Cd423",
"mockYnRwaImplementation": "0xa2997E1C82137B242e1aF9E7E2D8f5ec42E12cec",
"mockYnRwaProvider": "0xE56d6cBb40c7aa6676dA00c4BDA4715f21398F72"
}
10 changes: 10 additions & 0 deletions deployments/test-term-redeemer-live-ynrwax/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"chainId": 1,
"depositToken": "0x01Ba69727E2860b37bc1a2bd56999c1aFb4C15D8",
"termController": "0x368214F6e87d29904D0d710CC10606B838386736",
"termFactory": "0x6e0d9E0c98736E7F0F95F8C69191f5c0743Fbd8A",
"termHooks": "0x4a7Bcc6E2aea3edae95598897D91fb5DA6261c75",
"termImplementation": "0x8e599a9FFC4ae558b38247DfdAbFcc50D73bce9E",
"termProvider": "0xF38613b1Ff836286E0044f230BEE0F8058aaf2BC",
"termVault": "0x87376F00bB483BFa0D4B9dEfA13a5F3754e2A280"
}
9 changes: 9 additions & 0 deletions deployments/test-term-redeemer/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"chainId": 1,
"termController": "0x02E5D54af6B5d3A5959F686Dbd17b573234D00bE",
"termFactory": "0xC691B1Da9c90E64280ef0d5Fd8AD207020d04338",
"termHooks": "0x049aEC31E49eeB2408552A04E36C22B5b36006F0",
"termImplementation": "0x8F98AD7E65Fc022be23C1D83A2058293A0043568",
"termProvider": "0xBD4391Fd6846F5a9e19bBAEd98E4B67C0ecE9a9d",
"termVault": "0x0a2Bdf3fb6c8a369a6FC603Aa3885B39618Bfd97"
}
1 change: 1 addition & 0 deletions deployments/test/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

11 changes: 11 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"lib/forge-std": {
"rev": "c90c02ffa470b46d92ffaab093baeb296300870b"
},
"lib/openzeppelin-contracts": {
"tag": {
"name": "v5.6.1",
"rev": "5fd1781b1454fd1ef8e722282f86f9293cacf256"
}
}
}
41 changes: 41 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[profile.default]
src = "contracts"
out = "out"
libs = ["lib"]
test = "test/unit"
remappings = [
"src/=lib/yieldnest-vault/src/",
"script/=lib/yieldnest-vault/script/",
"@openzeppelin/contracts/=lib/yieldnest-vault/lib/openzeppelin-contracts/contracts/",
"@openzeppelin/contracts-upgradeable/=lib/yieldnest-vault/lib/openzeppelin-contracts-upgradeable/contracts/",
"lib/openzeppelin-contracts/=lib/yieldnest-vault/lib/openzeppelin-contracts/",
"lib/openzeppelin-contracts-upgradeable/=lib/yieldnest-vault/lib/openzeppelin-contracts-upgradeable/"
]
via_ir = true
optimizer = true
optimizer_runs = 200
fs_permissions = [{ access = "read-write", path = "./deployments" }]

[profile.mainnet]
src = "contracts"
out = "out"
libs = ["lib"]
test = "test/integration"
remappings = [
"src/=lib/yieldnest-vault/src/",
"script/=lib/yieldnest-vault/script/",
"@openzeppelin/contracts/=lib/yieldnest-vault/lib/openzeppelin-contracts/contracts/",
"@openzeppelin/contracts-upgradeable/=lib/yieldnest-vault/lib/openzeppelin-contracts-upgradeable/contracts/",
"lib/openzeppelin-contracts/=lib/yieldnest-vault/lib/openzeppelin-contracts/",
"lib/openzeppelin-contracts-upgradeable/=lib/yieldnest-vault/lib/openzeppelin-contracts-upgradeable/"
]
eth_rpc_url = "eth_mainnet"
via_ir = true
optimizer = true
optimizer_runs = 200
fs_permissions = [{ access = "read-write", path = "./deployments" }]

[rpc_endpoints]
eth_mainnet = "${ETH_MAINNET_RPC_URL}"

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 620536
1 change: 1 addition & 0 deletions lib/yieldnest-vault
Submodule yieldnest-vault added at b71905
6 changes: 6 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
src/=lib/yieldnest-vault/src/
script/=lib/yieldnest-vault/script/
@openzeppelin/contracts/=lib/yieldnest-vault/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/yieldnest-vault/lib/openzeppelin-contracts-upgradeable/contracts/
lib/openzeppelin-contracts/=lib/yieldnest-vault/lib/openzeppelin-contracts/
lib/openzeppelin-contracts-upgradeable/=lib/yieldnest-vault/lib/openzeppelin-contracts-upgradeable/
46 changes: 46 additions & 0 deletions script/common/BaseDeploymentScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Script} from "forge-std/Script.sol";

error MissingDeployment(string namespace, string path);
error InvalidDeploymentPayload();

abstract contract BaseDeploymentScript is Script {
function _broadcaster() internal view returns (address) {
return tx.origin;
}

function _writeAddresses(string memory namespace, string[] memory keys, address[] memory values) internal {
if (keys.length != values.length) {
revert InvalidDeploymentPayload();
}

vm.createDir(_deploymentDirectory(namespace), true);

string memory objectKey = "deployment";
for (uint256 i = 0; i < keys.length; ++i) {
vm.serializeAddress(objectKey, keys[i], values[i]);
}
string memory json = vm.serializeUint(objectKey, "chainId", block.chainid);

vm.writeJson(json, _deploymentPath(namespace));
}

function _loadAddress(string memory namespace, string memory key) internal view returns (address value) {
string memory path = _deploymentPath(namespace);
if (!vm.isFile(path)) {
revert MissingDeployment(namespace, path);
}

value = vm.parseJsonAddress(vm.readFile(path), string.concat(".", key));
}

function _deploymentPath(string memory namespace) internal view returns (string memory) {
return string.concat(_deploymentDirectory(namespace), "/", vm.toString(block.chainid), ".json");
}

function _deploymentDirectory(string memory namespace) internal view returns (string memory) {
return string.concat(vm.projectRoot(), "/deployments/", namespace);
}
}
Loading
Loading