> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ripio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Access yield on wFiat with Morpho and KPK vaults

> Recipe to access yield on wARS and wBRL on Ethereum by integrating KPK's curated Morpho vaults: get wFiat, deposit, track APY and the position, withdraw and claim rewards.

This recipe shows how to offer yield on **wARS** and **wBRL** inside your product by integrating the [Morpho](https://morpho.org) vaults that [KPK](https://kpk.io) curates on Ethereum. Your users deposit wFiat, the vault lends it to over-collateralized borrowers on Morpho markets, and returns accrue in local currency stablecoins: no conversion to USD at any step.

> [!NOTE]
>
> Ripio doesn't operate, curate or guarantee these vaults. They are permissionless smart contracts on Morpho, and KPK acts as their curator. Read the [risks](#risks-to-disclose-to-your-users) before you offer them to your users.

## How it works

**1. The user deposits wFiat in the vault**

The vault is an [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) token: it takes wARS or wBRL and mints vault shares to the depositor.

**2. The curator allocates to Morpho markets**

KPK decides which [Morpho markets](https://docs.morpho.org/learn/concepts/market) the vault supplies to and how much goes to each, within the limits of its risk policy. Borrowers post collateral such as WETH, wstETH, WBTC or USDC and borrow wFiat, paying interest.

**3. Returns accrue to the shares**

Shares don't rebase: each share is worth more wFiat over time. The user gets the principal plus yield back when they redeem the shares, depending on the vault's results.

Both vaults are [Morpho Vault V2](https://docs.morpho.org/learn/concepts/vault-v2) contracts with these roles:

* **Owner and sentinel:** a 5-of-8 security council multisig. As sentinel it can veto pending timelocked changes.
* **Curator and allocator:** a 2-of-5 KPK multisig per vault, which KPK's automated agents act through. Moving funds between enabled markets is **not** timelocked, and adding allocators isn't either.
* **Timelocks:** 3 days to enable a new adapter or raise a cap, 7 days to remove an adapter, set the `sendAssetsGate` or lengthen a timelock, and 14 days to change fees.

On-chain caps are currently set to the maximum on every market, so KPK's per-market limits (for example, a maximum share of the vault in a given market) are a policy KPK enforces, not a constraint in the contract. See KPK's [Morpho vaults page](https://docs.kpk.io/vaults/vaults/morpho) for roles and governance, and read the live caps from the Morpho API (`caps` on `vaultV2ByAddress`).

## Vaults

| Vault | Underlying | Vault address (Ethereum) | Links |
| - | - | - | - |
| **KPK wARS Yield** | wARS | `0xb5ce3CA2C774b72955C25875022FdD91f7a7B938` | [Morpho](https://app.morpho.org/ethereum/vault/0xb5ce3CA2C774b72955C25875022FdD91f7a7B938/kpk-wars-yield) · [KPK docs](https://docs.kpk.io/vaults/vaults/morpho/ethereum/wars-yield) · [Dune](https://dune.com/kpk/kpk-wars-yield-vault) |
| **KPK wBRL Yield** | wBRL | `0x7873F890A3Eec30909Bdb2cF65C4FA70DB905aac` | [Morpho](https://app.morpho.org/ethereum/vault/0x7873F890A3Eec30909Bdb2cF65C4FA70DB905aac/kpk-wbrl-yield) · [KPK docs](https://docs.kpk.io/vaults/vaults/morpho/ethereum/wbrl-yield) |

Both vaults have 18 decimals, like the underlying wFiat, and currently charge **no performance or management fee**. Fee changes have a 14-day timelock.

| Vault | Markets it can allocate to (collateral / wFiat) |
| - | - |
| KPK wARS Yield | WETH, wstETH, WBTC, cbBTC, sUSDS, USDC, USDT |
| KPK wBRL Yield | wstETH, WBTC, USDC |

The curator changes allocations over time. Read the live list from the Morpho API or the vault pages above instead of hardcoding it.

## Choose your integration

> [card] Link out
>
> Send users to the vault page on the Morpho app or KPK's app. No code, but users leave your product.

> [card] Direct contract calls
>
> Call the vault's ERC-4626 functions from your app or backend. Full control; this recipe shows it with viem.

> [card] [Morpho SDK and Bundles](https://docs.morpho.org/learn/concepts/bundles)
>
> One-transaction approve and deposit with a signed permit, built with Morpho's SDK.

## Recipe

The examples below use [viem](https://viem.sh/) on Ethereum mainnet, but any EVM tooling works.

### 0. Setup, constants and ABI

```ts clients.ts theme={null}
import { createPublicClient, createWalletClient, http, type Account } from 'viem';
import { mainnet } from 'viem/chains';

// Use your own RPC endpoint: balances, liquidity and simulations come from it
const transport = http(process.env.ETHEREUM_RPC_URL);

export const publicClient = createPublicClient({ chain: mainnet, transport });

// The user's wallet (for example, from your wallet connector) or a custody
// signer such as a KMS. Never hardcode private keys.
declare const account: Account;

export const walletClient = createWalletClient({ account, chain: mainnet, transport });
```

```ts constants.ts theme={null}
// wFiat — same address on every EVM chain, 18 decimals
export const TOKENS = {
  wARS: '0x0DC4F92879B7670e5f4e4e6e3c801D229129D90D',
  wBRL: '0xD76f5Faf6888e24D9F04Bf92a0c8B921FE4390e0',
} as const;

// KPK-curated Morpho Vault V2 on Ethereum (chain id 1)
export const VAULTS = {
  wARS: '0xb5ce3CA2C774b72955C25875022FdD91f7a7B938', // KPK wARS Yield
  wBRL: '0x7873F890A3Eec30909Bdb2cF65C4FA70DB905aac', // KPK wBRL Yield
} as const;

export const DEAD = '0x000000000000000000000000000000000000dEaD';
```

```ts abi.ts theme={null}
import { parseAbi } from 'viem';

// ERC-4626 subset plus the Vault V2 reads this recipe uses
export const VAULT_ABI = parseAbi([
  'function asset() view returns (address)',
  'function totalAssets() view returns (uint256)',
  'function balanceOf(address owner) view returns (uint256)',
  'function convertToAssets(uint256 shares) view returns (uint256)',
  'function previewDeposit(uint256 assets) view returns (uint256)',
  'function previewRedeem(uint256 shares) view returns (uint256)',
  'function liquidityAdapter() view returns (address)',
  'function deposit(uint256 assets, address receiver) returns (uint256 shares)',
  'function withdraw(uint256 assets, address receiver, address owner) returns (uint256 shares)',
  'function redeem(uint256 shares, address receiver, address owner) returns (uint256 assets)',
  'event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares)',
  'event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)',
]);

export const ERC20_ABI = parseAbi([
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)',
  'function balanceOf(address owner) view returns (uint256)',
]);
```

The snippets in the next steps import from these files.

### 1. Get wFiat on Ethereum

The vaults live on Ethereum mainnet, so the user needs wARS or wBRL there:

* **Buy them:** with [Ramps](https://docs.ripio.com/ramps-api/overview/introduction.md) or [Crypto as a Service](https://docs.ripio.com/crypto-as-a-service/overview/introduction.md), deliver wFiat to the user's wallet on Ethereum.
* **Swap them:** on [Uniswap](https://app.uniswap.org/explore/tokens/ethereum/0x0DC4F92879B7670e5f4e4e6e3c801D229129D90D) or any aggregator.
* **Bridge them:** if the user holds wFiat on Base, Polygon, World Chain or any other supported chain, move it to Ethereum (chain id `1`) with the [Ripio Bridge](https://docs.ripio.com/wfiat/bridge.md). The route fee and the daily mint capacity apply as on any other route.

### 2. Show the APY and the vault size

Read the APY from the [Morpho API](https://docs.morpho.org/developers/api/morpho-vaults) instead of computing it yourself:

```bash theme={null}
curl https://api.morpho.org/graphql \
  -H 'content-type: application/json' \
  -d '{"query":"{ vaultV2ByAddress(address: \"0xb5ce3CA2C774b72955C25875022FdD91f7a7B938\", chainId: 1) { name totalAssets totalAssetsUsd liquidity netApyExcludingRewards netApy rewards { asset { symbol } supplyApr } } }"}'
```

| Field | Meaning |
| - | - |
| `netApyExcludingRewards` | Native yield from borrower interest, after vault fees. Excludes rewards. Show this as the vault's yield. |
| `netApy` | Native yield plus incentive rewards, after fees. |
| `apy` | Native yield **before** fees. Don't show it to users as their return. |
| `rewards` | Incentive campaigns and their APR, paid in tokens through [Merkl](https://merkl.xyz). |
| `liquidity` | wFiat available for immediate withdrawals: idle balance plus what the liquidity adapter can return. |
| `totalAssets` | Vault size in the underlying wFiat (18 decimals). |

> [!WARNING]
>
> Incentive campaigns are temporary and can make up almost all of `netApy`. Show `netApyExcludingRewards` as the default yield, label rewards as a temporary incentive, and never present `netApy` as guaranteed. Values are decimals: `0.05` means 5%.

### 3. Approve and deposit

The user approves the vault on the wFiat token and calls `deposit`. Vault shares are minted to `receiver`.

```ts deposit.ts theme={null}
import { parseEventLogs, parseUnits } from 'viem';
import { publicClient, walletClient } from './clients';
import { DEAD, TOKENS, VAULTS } from './constants';
import { ERC20_ABI, VAULT_ABI } from './abi';

const account = walletClient.account;
const amount = parseUnits('100000', 18); // 100,000 wARS

// 0. Safety check: the vault must hold a dead deposit, which protects new
//    depositors against share-price inflation attacks (1e9 shares for 18-decimal assets)
const deadShares = await publicClient.readContract({
  address: VAULTS.wARS,
  abi: VAULT_ABI,
  functionName: 'balanceOf',
  args: [DEAD],
});
if (deadShares < 10n ** 9n) throw new Error('Vault has no dead deposit');

// Quote shown to the user, and the minimum you accept (0.5% tolerance)
const quotedShares = await publicClient.readContract({
  address: VAULTS.wARS,
  abi: VAULT_ABI,
  functionName: 'previewDeposit',
  args: [amount],
});
const minShares = (quotedShares * 9950n) / 10000n;

// 1. Approve the vault to pull exactly this amount
const allowance = await publicClient.readContract({
  address: TOKENS.wARS,
  abi: ERC20_ABI,
  functionName: 'allowance',
  args: [account.address, VAULTS.wARS],
});
if (allowance < amount) {
  const approveHash = await walletClient.writeContract({
    address: TOKENS.wARS,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [VAULTS.wARS, amount],
  });
  await publicClient.waitForTransactionReceipt({ hash: approveHash });
}

// 2. Simulate first: catches reverts (for example, insufficient balance or allowance)
const { request, result: expectedShares } = await publicClient.simulateContract({
  account,
  address: VAULTS.wARS,
  abi: VAULT_ABI,
  functionName: 'deposit',
  args: [amount, account.address], // assets, receiver
});
if (expectedShares < minShares) throw new Error('Share price moved beyond tolerance');

// 3. Deposit, then record the shares actually minted, read from the receipt
const depositHash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash: depositHash });
const [deposit] = parseEventLogs({
  abi: VAULT_ABI,
  eventName: 'Deposit',
  logs: receipt.logs.filter((log) => log.address.toLowerCase() === VAULTS.wARS.toLowerCase()),
});
console.log('Shares minted:', deposit.args.shares);
```

> [!WARNING]
>
> ERC-4626 `deposit` and `redeem` have no on-chain minimum-out parameter. The check above runs off-chain, right before sending. If you credit users from an internal ledger, always record the `assets` and `shares` from the `Deposit` and `Withdraw` events in the receipt, never the simulated or previewed values.

> [!TIP]
>
> **One transaction instead of two.** wARS and wBRL support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permit`, so the user can sign the approval off-chain. Morpho's [Bundles](https://docs.morpho.org/developers/contracts/bundles) take that signature and run approve and deposit in a single transaction. The [Morpho SDK](https://docs.morpho.org/developers/earn/tutorials/assets-flow) builds these transactions for you. Morpho's previous Bundler3 and GeneralAdapter1 are deprecated: don't build new integrations on them.

### 4. Track the position

Shares don't rebase, so convert them to wFiat to show the balance:

```ts position.ts theme={null}
import { publicClient, walletClient } from './clients';
import { VAULTS } from './constants';
import { VAULT_ABI } from './abi';

const account = walletClient.account;

export const shares = await publicClient.readContract({
  address: VAULTS.wARS,
  abi: VAULT_ABI,
  functionName: 'balanceOf',
  args: [account.address],
});

// Current value of the position in wARS
const assets = await publicClient.readContract({
  address: VAULTS.wARS,
  abi: VAULT_ABI,
  functionName: 'convertToAssets',
  args: [shares],
});
```

To show earnings, store what the user deposited (from the `Deposit` events you recorded) and subtract it from `assets`.

### 5. Withdraw

Use `redeem` to exit fully (burns all shares) or `withdraw` for an exact amount of wFiat:

```ts withdraw.ts theme={null}
import { parseEventLogs, zeroAddress } from 'viem';
import { publicClient, walletClient } from './clients';
import { TOKENS, VAULTS } from './constants';
import { ERC20_ABI, VAULT_ABI } from './abi';
import { shares } from './position';

const account = walletClient.account;

// Liquidity for immediate withdrawals = idle wFiat + what the liquidity adapter can return.
// Neither KPK vault sets a liquidity adapter today, so idle is all of it. If one is set,
// read `liquidity` from the Morpho API instead.
const [idle, liquidityAdapter, expectedAssets] = await Promise.all([
  publicClient.readContract({
    address: TOKENS.wARS,
    abi: ERC20_ABI,
    functionName: 'balanceOf',
    args: [VAULTS.wARS],
  }),
  publicClient.readContract({
    address: VAULTS.wARS,
    abi: VAULT_ABI,
    functionName: 'liquidityAdapter',
  }),
  publicClient.readContract({
    address: VAULTS.wARS,
    abi: VAULT_ABI,
    functionName: 'previewRedeem',
    args: [shares],
  }),
]);
if (liquidityAdapter === zeroAddress && expectedAssets > idle) {
  // Not enough liquidity for a regular withdrawal: offer forceDeallocate (see below)
  // or a partial withdrawal instead of sending a transaction that reverts
  throw new Error('Not enough idle liquidity for a regular withdrawal');
}

// Full exit: burn all shares, send wARS to the user
const { request } = await publicClient.simulateContract({
  account,
  address: VAULTS.wARS,
  abi: VAULT_ABI,
  functionName: 'redeem',
  args: [shares, account.address, account.address], // shares, receiver, owner
});
const redeemHash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash: redeemHash });
const [withdrawal] = parseEventLogs({
  abi: VAULT_ABI,
  eventName: 'Withdraw',
  logs: receipt.logs.filter((log) => log.address.toLowerCase() === VAULTS.wARS.toLowerCase()),
});
console.log('wARS received:', withdrawal.args.assets);
```

> [!NOTE]
>
> On Morpho Vault V2, `maxDeposit`, `maxWithdraw`, `maxMint` and `maxRedeem` always return `0`. Don't use them to size a "Max" button: use the user's share balance and the liquidity check above, or the Morpho SDK's helpers.

A regular withdrawal is served from the vault's idle wFiat and, if one is set, the liquidity adapter. KPK targets keeping at least half of each vault reachable for withdrawals, counting idle funds and what can be pulled from markets. If a large exit exceeds what is available, the depositor (or an address they approved) can call [`forceDeallocate`](https://docs.morpho.org/developers/earn/tutorials/force-deallocate) to pull wFiat from a market that has unborrowed liquidity. It charges a penalty that is burned from the depositor's own shares:

* It is 0.01% today. Read the current value with `forceDeallocatePenalty(adapter)` instead of hardcoding it.
* The curator can change it **without a timelock**. KPK says it may raise it to 0.5% if it detects abuse, and the protocol maximum is 2%.

### 6. Claim rewards

Incentive rewards aren't added to the vault's share price. They accrue to the address that holds the shares and are claimed separately through [Merkl](https://merkl.xyz): either send users to the Merkl app, or read claimable amounts and build the claim transaction with the [Merkl API](https://docs.merkl.xyz). Rewards keep accruing even if the user never opens your app.

If you hold shares for your users in an omnibus wallet or a smart contract, make sure that address can call Merkl's claim function and that you pass the rewards on to your users. Otherwise most of the advertised yield can stay unclaimed.

## Monetize the integration

If you distribute these vaults to your users and want to earn a fee, KPK can deploy a **Morpho fee wrapper** for you: a vault on top of the KPK vault that charges your own performance or management fee and sends it to your fee recipient. Fee wrappers aren't listed in the Morpho app, and the Morpho API reports their APY net of your fee. See KPK's [integration guide](https://docs.kpk.io/vaults/vault-integration/integration) and contact them to set one up.

Before you put users behind a wrapper, confirm:

* **Who holds the wrapper's roles.** Know who the owner, curator and sentinel of the wrapper are.
* **The wrapper's gates are abdicated.** Morpho considers a wrapper fully non-custodial only when its receive-shares, send-shares and receive-assets gates are abdicated at deployment.
* **The underlying vault's gate.** The KPK vaults' `sendAssetsGate` isn't abdicated: it can be set after a 7-day timelock, and a gate there could block the wrapper's withdrawals.
* **How rewards reach your users.** Merkl rewards accrue to the wrapper's address, so agree with KPK and Merkl on how they are forwarded to wrapper depositors.

## Risks to disclose to your users

* **Issuer and peg risk.** wARS and wBRL are issued by Ripio and depend on its reserves backing them. The vault oracles value wARS and wBRL at a 1:1 reference price, so a depeg in the market isn't reflected in pricing: liquidations could fail and leave bad debt in the vault.
* **Smart contract risk.** Bugs in Morpho, the vault or the oracles can cause losses. Morpho and the vaults are audited, but audits don't remove the risk.
* **Market and liquidation risk.** If collateral prices drop faster than liquidations execute, markets can accrue bad debt that the vault absorbs. Liquidity of wFiat on-chain affects how smoothly liquidations work.
* **Oracle risk.** Markets price wFiat against the collateral through a USD/ARS or USD/BRL feed. KPK notes that the USD/ARS feed reflects a crypto-derived rate, not the official onshore rate. Every wARS market uses the same USD/ARS feed, so a feed failure hits all of them at once.
* **Currency risk.** wARS and wBRL track the ARS and BRL. Yield is earned in local currency stablecoins, so its value in USD moves with the exchange rate.
* **Withdrawal liquidity.** When utilization is high, a regular withdrawal can exceed the available liquidity and require `forceDeallocate`, whose penalty the curator can raise at any time, or waiting for borrowers to repay.
* **Curator and governance risk.** A 2-of-5 KPK multisig and KPK's agents move funds between markets instantly, with no timelock, and on-chain caps don't limit how much goes to each market. Only some changes, such as enabling new markets or changing fees, are timelocked and can be vetoed by the sentinel.
* **Variable yield.** Native APY depends on borrowing demand and can be near zero; incentive rewards are temporary.

KPK publishes its [risk framework](https://docs.kpk.io/vaults/infrastructure/risk-framework) and a per-vault risk assessment on each vault page. Also read their [disclaimer](https://docs.kpk.io/vaults/resources/legal/morpho-disclaimer) and confirm the product is available in your users' jurisdictions.

## Resources

* [KPK vault docs](https://docs.kpk.io/vaults/vaults/morpho) and [integration guide](https://docs.kpk.io/vaults/vault-integration/integration)
* [Morpho Vault V2 concepts](https://docs.morpho.org/learn/concepts/vault-v2)
* [Morpho vaults API](https://docs.morpho.org/developers/api/morpho-vaults)
* [Morpho Bundles](https://docs.morpho.org/developers/contracts/bundles)
* [wFiat contracts and networks](https://docs.ripio.com/wfiat/contracts.md)
* [Ripio Bridge developer guide](https://docs.ripio.com/wfiat/bridge.md)

Questions about wFiat? Write to [b2b@ripio.com](mailto:b2b@ripio.com). For vault-specific questions, contact [KPK](https://kpk.io/contact).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.