Vault contracts
The ERC-7540 vault interface, fees, roles and events, plus the three contracts that hide its two step flow.
Why ERC-7540
The vaults implement ERC-7540, an extension of ERC-4626 that adds asynchronous deposits and redeems. ERC-4626 vaults settle deposits and withdrawals in one synchronous call, which limits their ability to:
- Execute crosschain strategies
- Integrate real-world assets (RWA) that need settlement time
- Unwind positions as part of risk management
- Support institutional compliance requirements
ERC-7540 replaces the single call with two steps:
Deposit: requestDeposit() → [wait for fulfillment] → claimDeposit()
Redeem: requestRedeem() → [wait for fulfillment] → claimRedeem()
Deployed vaults
The user facing vaults are six per-tier EIP-2535 Diamonds (ERC-7540 async) on Base Sepolia (chainId 84532).
- Denominated in tUSDC, a mintable test USDC available from a faucet.
- Each tier is its own single-vault Diamond.
- Every vault has two buckets: a principal protected Floor bucket and an Active Trading bucket.
- The addresses below are the dApp configuration set.
- The vaults hold seeded test funds. No capital has been deployed to strategies yet.
| Vault | Address | Risk | Strategy Profile |
|---|---|---|---|
| Conservative | 0x49f747BBac988Cd628a5D79AAdF865EBaDe6a34f | Low | Capital preservation |
| Moderate | 0x4B183AfF072C7DFf09bc8eB4B7f59eB3D030EEdc | Medium | Balanced growth |
| Aggressive | 0x1444Fb96961fD9b8Fc14Aa4D9A7198465b7d8CA3 | High | Maximum growth |
| Hedged | 0xDDa7655AD3c53f0F7EC5CCbc346c18baB53C5a92 | Low | Downside-protected |
| Inverse | 0xE2E4d181f2eAd5B380026Ab71F49FEBCb93f641c | High | Inverse/short exposure |
| Configurable | 0x53A2EfFaDE21055b8f81e1dE96dC5cfc29B75a83 | Medium | DAO-configurable |
Each vault has its own target APY band, listed in Core Concepts. The bands are targets, not guaranteed, and no capital is deployed on testnet.
The 8–28% range is the combined vault plus node target, available when a depositor also stakes NODR and runs a node. Node rewards are a revenue share, not a fixed APY. It is never a vault only figure. Higher APYs cited for individual underlying protocols reflect external protocol potential, not Noderr vault depositor yield.
ERC-7540 interface
Request deposit
function requestDeposit(
uint256 assets,
address controller,
address owner
) external returns (uint256 requestId);
Starts an async deposit request. Assets move from the owner to the vault.
Parameters:
assets: Amount of underlying tokens to depositcontroller: Address that can claim the deposit (usually same as owner)owner: Address that owns the assets being deposited
Returns:requestId - Unique identifier for tracking the request
Claim deposit
function claimDeposit(
uint256 assets,
address receiver,
address controller
) external returns (uint256 shares);
Claims shares after a deposit request has been fulfilled.
Parameters:
assets: Amount of assets to claim shares forreceiver: Address to receive the vault sharescontroller: Address that controls the request
Returns:shares - Number of vault shares minted
Request redeem
function requestRedeem(
uint256 shares,
address controller,
address owner
) external returns (uint256 requestId);
Starts an async redeem request. Shares move from the owner to the vault.
Claim redeem
function claimRedeem(
uint256 shares,
address receiver,
address controller
) external returns (uint256 assets);
Claims underlying assets after a redeem request has been fulfilled.
Status functions
// Check pending deposit amount
function pendingDepositRequest(uint256 requestId, address controller)
external view returns (uint256);
// Check claimable deposit amount
function claimableDepositRequest(uint256 requestId, address controller)
external view returns (uint256);
// Check pending redeem amount
function pendingRedeemRequest(uint256 requestId, address controller)
external view returns (uint256);
// Check claimable redeem amount
function claimableRedeemRequest(uint256 requestId, address controller)
external view returns (uint256);
Operator management
// Set an operator who can claim on your behalf
function setOperator(address operator, bool approved) external returns (bool);
// Check if an address is an operator for a controller
function isOperator(address controller, address operator) external view returns (bool);
Vault configuration
Values are read onchain from all six vaults:
| Parameter | Description | Value |
|---|---|---|
fulfillmentDelay | Minimum time before requests can be fulfilled | 0 (no minimum wait) |
minDeposit | Minimum deposit amount | 0 (no minimum) |
managementFee | Annual management fee | 0% (every vault) |
performanceFee | High-water-mark fee on profits, charged only from the Trading bucket | 10% to 15% on the six deployed vaults (see Fee Structure) |
hurdleRate | Per-vault soft hurdle return that must be exceeded before the performance fee applies | 4% to 8% on the six deployed vaults (see Fee Structure) |
Async flow
User Vault Keeper/Fulfiller
| | |
|-- requestDeposit() -->| |
|<-- requestId ---------| |
| | |
| [assets held in vault, request pending] |
| | |
| |<-- fulfillDeposit() ------|
| | (after delay elapsed) |
| | |
|-- claimDeposit() ---->| |
|<-- shares ------------| |
| | |
Integration examples
JavaScript/TypeScript with viem
import { parseUnits } from'viem';
// Request a depositconst requestId = await vaultContract.write.requestDeposit([
parseUnits('1000', 18), // 1000 tUSDC (underlying asset, 18 decimals)
userAddress, // controller
userAddress // owner
]);
// Check if claimable (after fulfillment)const claimable = await vaultContract.read.claimableDepositRequest([
requestId,
userAddress
]);
// Claim sharesif (claimable > 0n) {
const shares = await vaultContract.write.claimDeposit([
claimable,
userAddress, // receiver
userAddress // controller
]);
}
Solidity
import "./interfaces/IERC7540.sol";
contract VaultIntegration {
IERC7540 public vault;
// This contract holds the tUSDC, so it is the owner and controller of the request
function depositToVault(uint256 amount) external {
IERC20(vault.asset()).transferFrom(msg.sender, address(this), amount);
IERC20(vault.asset()).approve(address(vault), amount);
vault.requestDeposit(amount, address(this), address(this));
}
function claimFromVault() external {
uint256 claimable = vault.claimableDepositRequest(0, address(this));
require(claimable > 0, "Nothing to claim");
vault.claimDeposit(claimable, address(this), address(this));
}
}
Fees
Every vault charges 0% management fee. The only fee is a performance fee:
- High-water mark based. It is charged only when NAV per share rises above its prior peak. A depositor is never charged twice for the same gains.
- Taken strictly from the Trading bucket. The principal protected Floor bucket is never touched.
- Subject to a per-vault soft hurdle. It applies only on returns above the vault's hurdle rate.
| Vault | Performance Fee | Soft Hurdle |
|---|---|---|
| Conservative | 10% | 4% |
| Moderate | 10% | 6% |
| Aggressive | 15% | 8% |
| Hedged | 12% | 4% |
| Inverse | 15% | 8% |
| Configurable | 10% | 6% |
| PredictionMarkets | 25% | 15% |
PredictionMarkets is planned and not yet deployed. Its parameters are shown for reference.
Performance fees go to the protocol treasury wallet. Every vault's feeCollector() returns that wallet.
| Fee Type | Rate | Description |
|---|---|---|
| Management Fee | 0% | No annual management fee, on every vault |
| Deposit Fee | 0% | No fee to deposit |
| Withdrawal Fee | 0% | No fee to withdraw |
Example (Moderate vault), illustrative:
- User deposits: 10,000 tUSDC
- The Trading bucket gains enough to lift NAV/share to a new high-water mark. Net of the 6% soft hurdle, fee eligible profit is 1,000 tUSDC
- Performance fee (10% of the fee eligible profit): 100 tUSDC
- Net return to user: 10,900 tUSDC
Security
Access control
| Role | Permissions |
|---|---|
DEFAULT_ADMIN_ROLE | Grant/revoke roles, emergency functions |
FULFILLER_ROLE | Call fulfillDeposit/fulfillRedeem |
STRATEGY_MANAGER_ROLE | Update strategy allocations |
PAUSER_ROLE | Pause/unpause vault operations |
DEFAULT_ADMIN_ROLE and upgrade authority resolve to a MultiSigTimelock (7-day delay) driven by a 2-of-3 Gnosis Safe. There is no offchain (e.g. Snapshot) governance path. See Governance.
Emergency functions
// Pause all vault operations
function pause() external onlyRole(PAUSER_ROLE);
// Unpause vault operations
function unpause() external onlyRole(PAUSER_ROLE);
The vaults have no emergencyWithdraw function.
Audit status
The ERC-7540 vault contracts pass 24/24 internal tests. They are internally audited. An independent third-party audit is planned for Q1 2027.
Events
// Emitted when a deposit is requested
event DepositRequest(
address indexed controller,
address indexed owner,
uint256 indexed requestId,
address sender,
uint256 assets
);
// Emitted when a deposit is claimed
event DepositClaim(
address indexed controller,
address indexed receiver,
uint256 indexed requestId,
uint256 assets,
uint256 shares
);
// Emitted when a redeem is requested
event RedeemRequest(
address indexed controller,
address indexed owner,
uint256 indexed requestId,
address sender,
uint256 shares
);
// Emitted when a redeem is claimed
event RedeemClaim(
address indexed controller,
address indexed receiver,
uint256 indexed requestId,
uint256 shares,
uint256 assets
);
// Emitted when an operator is set
event OperatorSet(
address indexed controller,
address indexed operator,
bool approved
);
Vault UX contracts
Three contracts hide ERC-7540's two step flow: VaultRouter, AutoClaimKeeper and InstantLiquidityPool. Their addresses are in Contract Addresses. The fee, premium and gas figures here are testnet parameters and can change.
| Contract | Purpose | Key Benefit |
|---|---|---|
| VaultRouter | Bundled UX with auto-claim | Single-transaction experience |
| AutoClaimKeeper | Keeper auto-claim service | Hands-free claim execution |
| InstantLiquidityPool | Instant liquidity AMM | Skip the wait entirely |
VaultRouter
The main entry point for users. It handles request submission, fulfillment monitoring and claim execution, so the user acts once.
- Auto-claim: claims run automatically when a request is fulfilled
- Batch operations: deposit or redeem across several vaults in one transaction
- Gasless transactions: EIP-712 signatures
- Slippage protection: configurable minimum output
- Emergency controls: pause for security incidents
User → VaultRouter → ERC-7540 Vault
↓
AutoClaimKeeper
↓
Automatic Claim
Request deposit with auto-claim
function requestDeposit(
address vault,
uint256 assets,
uint256 minShares,
address receiver,
bool autoClaim
) external returns (uint256 requestId);
Parameters:
vault: The ERC-7540 vault addressassets: Amount of underlying tokens to depositminShares: Minimum shares to receive (slippage protection)receiver: Address to receive the vault sharesautoClaim: Enable automatic claim when fulfilled (0.1% fee)
// Deposit 1000 tUSDC with auto-claim enabledawait vaultRouter.requestDeposit(
conservativeVaultAddress,
parseUnits('1000', 18),
parseUnits('990', 18), // Accept up to 1% slippage
userAddress,
true// Enable auto-claim
);
Batch deposit
function batchDeposit(
BatchDepositParams[] calldata params,
address receiver,
bool autoClaim
) external;
struct BatchDepositParams {
address vault;
uint256 assets;
uint256 minShares;
}
Deposits to several vaults in one transaction, saving 50-70% on gas costs.
// Deposit to 3 vaults at onceawait vaultRouter.batchDeposit([
{ vault: conservativeVault, assets: parseUnits('500', 18), minShares: parseUnits('495', 18) },
{ vault: moderateVault, assets: parseUnits('300', 18), minShares: parseUnits('297', 18) },
{ vault: aggressiveVault, assets: parseUnits('200', 18), minShares: parseUnits('196', 18) }
], userAddress, true);
Gasless deposit (EIP-712)
function depositWithPermit(
address vault,
uint256 assets,
uint256 minShares,
address receiver,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 requestId);
Users sign a message offchain. Relayers submit the transaction and pay gas.
Router fees
| Service | Fee | Description |
|---|---|---|
| Standard Request | 0% | No fee for manual claim |
| Auto-Claim | 0.1% | Deducted from claimed amount |
| Gasless | 2%+ | Covers gas + relayer profit |
Router configuration
| Parameter | Default | Description |
|---|---|---|
defaultTimeout | 1 hour | Request expiration time |
autoClaimFeeBps | 10 (0.1%) | Fee for auto-claim service |
AutoClaimKeeper
Keeper nodes earn rewards by executing auto-claims for users. Rewards scale with the keeper's TrustFingerprint.
- Registration: stake NODR to become a keeper
- Batch execution: process several claims at once
- Performance tracking: success rate, gas usage, rewards
- Slashing: penalties for failed executions
Become a keeper
function registerKeeper(uint256 stake) external payable;
Requirements:
- Minimum TrustFingerprint:
MIN_TRUST_SCORE = 700on a 0 to 10,000 scale - Stake: any amount sent as
msg.value(no enforced minimum)
Execute claims
// Execute single deposit claim
function executeDepositClaim(
address user,
address vault,
address receiver
) external;
// Execute single redeem claim
function executeRedeemClaim(
address user,
address vault,
address receiver
) external;
// Batch execute multiple claims
function batchExecuteClaims(
ClaimParams[] calldata deposits,
ClaimParams[] calldata redeems
) external;
Reward calculation
Base Reward = claimValue × baseRewardBps / 10000
Trust Bonus = Base Reward × (trustScore / 100) × trustBonusBps / 10000
Total Reward = Base Reward + Trust Bonus
Example:
- Claim value: 10,000 tUSDC
- Base reward rate: 0.1%
- TrustFingerprint: 0.85
- Trust bonus rate: 0.05%
Base Reward = 10,000 × 0.001 = 10 tUSDC
Trust Bonus = 10 × 0.85 × 0.0005 = 0.00425 tUSDC
Total Reward = 10.00425 tUSDC
Keeper metrics
struct KeeperMetrics {
uint256 totalClaims; // Total claims executed
uint256 successfulClaims; // Successful executions
uint256 failedClaims; // Failed executions
uint256 totalGasUsed; // Cumulative gas usage
uint256 totalRewards; // Cumulative rewards earned
uint256 lastClaimTime; // Timestamp of last claim
}
Slashing conditions
| Condition | Penalty |
|---|---|
| Failed execution | 0.1% of stake |
| >10% failure rate | Temporary suspension |
| Malicious behavior | Full stake slash |
InstantLiquidityPool
Deposits and withdrawals that execute at once, for users who prefer speed over cost. LPs earn the fees.
- Instant deposits: skip the keeper fulfilment step
- Instant redemptions: immediate asset return
- Dynamic pricing: 2-5% fee based on utilization
- LP rewards: earn fees by providing liquidity
- Automated rebalancing: the pool maintains optimal levels
Standard Flow (until a keeper fulfils):
User → requestDeposit → [wait] → claimDeposit → Shares
Instant Flow (immediate):
User → instantDeposit → Shares (pay 2-5% premium)
Instant deposit
function instantDeposit(
address vault,
uint256 assets,
uint256 minShares,
address receiver
) external returns (uint256 shares);
// Instant deposit with 3% feeconst shares = await instantLiquidityPool.instantDeposit(
conservativeVaultAddress,
parseUnits('1000', 18),
parseUnits('950', 18), // Accept up to 5% slippage (includes fee)
userAddress
);
Instant redeem
function instantRedeem(
address vault,
uint256 shares,
uint256 minAssets,
address receiver
) external returns (uint256 assets);
Add liquidity
function addLiquidity(
address vault,
uint256 assets,
uint256 shares
) external;
LPs can provide:
- Asset liquidity: tUSDC (the vault underlying asset) for instant deposits
- Share liquidity: Vault shares for instant redemptions
Remove liquidity
function removeLiquidity(
address vault,
uint256 assets,
uint256 shares
) external;
Claim fees
function claimFees(address vault) external;
Instant fee
The fee is a linear formula of pool utilization:
feeBps = MIN_FEE_BPS + ((MAX_FEE_BPS - MIN_FEE_BPS) × utilization) / 10000
= 200 + (300 × utilization / 10000)
| Utilization | Fee (approx.) |
|---|---|
| 0% | 2.0% |
| 25% | 2.75% |
| 50% | 3.5% |
| 75% | 4.25% |
| 100% | 5.0% |
The fee scales continuously and linearly, not in discrete steps. 90% goes to LPs in proportion to stake and 10% to the protocol treasury.
LP economics
Important: Returns to instant-liquidity LPs are fee income driven entirely by instant-flow volume, fee tier, pool size, and competition among LPs. They are not a protocol yield and are separate from Noderr's per-vault target bands and the combined vault + node target of 8–28%. Node rewards are a revenue-share, not a fixed APY. The figures below are illustrative of the fee-share mechanic only and are not projected, expected, or guaranteed returns.
Illustrative mechanic (not a yield projection):
- LP provides 10,000 tUSDC liquidity
- Suppose the pool processes 50,000 tUSDC/day of instant flow at a 3% average fee
- Gross daily fees: 1,500 tUSDC (3% of 50,000)
- LP share at 90% fee distribution, holding 10% of the pool: 135 tUSDC/day
Realized returns depend on actual instant-flow volume, the prevailing utilization-based fee, the LP's share of the pool, and competition. Sustained high volume at these assumptions would be exceptional. In practice instant-flow demand is variable and may be low, so LPs should not assume the illustrative throughput above.
Choosing a flow
1. Check instant liquidity availability
2. If available and user prefers speed → Use InstantLiquidityPool
3. If user prefers cost savings → Use VaultRouter with auto-claim
4. If user wants full control → Use VaultRouter without auto-claim
TypeScript
import { parseUnits } from'viem';
// Option 1: Instant deposit (immediate, 2-5% fee)const instantShares = await instantLiquidityPool.instantDeposit(
vaultAddress,
parseUnits('1000', 18),
parseUnits('950', 18),
userAddress
);
// Option 2: Auto-claim deposit (after keeper fulfilment, 0.1% fee)await vaultRouter.requestDeposit(
vaultAddress,
parseUnits('1000', 18),
parseUnits('990', 18),
userAddress,
true// auto-claim
);
// Option 3: Manual deposit (after keeper fulfilment, 0% fee)await vaultRouter.requestDeposit(
vaultAddress,
parseUnits('1000', 18),
parseUnits('990', 18),
userAddress,
false// manual claim
);
// Later: manually claimconst shares = await vaultRouter.claimDeposit(vaultAddress, userAddress);
Solidity
import "./interfaces/IVaultRouter.sol";
import "./interfaces/IInstantLiquidityPool.sol";
contract MyProtocol {
IVaultRouter public vaultRouter;
IInstantLiquidityPool public instantPool;
function smartDeposit(
address vault,
uint256 amount,
bool preferSpeed
) external {
if (preferSpeed && instantPool.hasLiquidity(vault, amount)) {
// Instant deposit
instantPool.instantDeposit(vault, amount, 0, msg.sender);
} else {
// Auto-claim deposit
vaultRouter.requestDeposit(vault, amount, 0, msg.sender, true);
}
}
}
UX contract security
| Contract | Role | Permissions |
|---|---|---|
| VaultRouter | OPERATOR_ROLE | Execute auto-claims |
| VaultRouter | PAUSER_ROLE | Emergency pause |
| AutoClaimKeeper | KEEPER_ROLE | Execute claims |
| InstantLiquidityPool | REBALANCER_ROLE | Trigger rebalancing |
All three contracts implement:
pause()/unpause()- Halt operationsemergencyWithdraw()- Admin asset recovery- UUPS upgradeable pattern for security patches
These contracts have not had a third-party security audit. Use them on testnet only until audits are complete.
Next
- Contract Addresses
- ERC-7540 Specification
- Deposit Guide and Withdraw Guide
- Node Operators, for running keeper nodes
Last updated: December 28, 2025. Version 1.0.