Feature: Integrate Yield Safe With Overnight USD+
Feature: Integrate Yield Safe with Overnight USD+
Overview
The Yield Safe integration with Overnight USD+ is a crucial feature for users to earn yield on their idle USDC within their Safe wallet. This feature will provide a one-click method for users to deposit and withdraw USDC, while maintaining transparency regarding risks associated with rebasing tokens and peg risks.
Goal
The primary goal of this feature is to provide users with a seamless and secure way to earn yield on their idle USDC. The flow must:
- Never require on-chain signatures from the user (Privy handles this).
- Be fully gas-sponsored.
- Clearly display all important states (loading, pending, success, error, APR, accrued yield).
- Maintain transparency regarding risks (rebasing token, peg risk, withdrawal latency).
Constants
export const OVERNIGHT = {
USDC: '0x833589fcD6eDb6E08f4C7C32D4f71b54bda02913', // Base USDC
USD_PLUS: '0x236eEC6359fb44cCe8F97e99387AA7f8cC06bd8F', // Overnight USD+ on Base
EXCHANGE: '0x6B3712943a913eB9A22B71d4210DE6158c519970', // Overnight Exchange contract for mint/redeem on Base
ABI_MINT: 'function mint(address asset, uint256 amount, uint256 minUsdPlus, address referral)',
ABI_REDEEM: 'function redeem(address asset, uint256 amountUsdPlus, uint256 minAsset, address referral)',
}
Drizzle Schema Additions
A new table is required to track user positions:
// In packages/web/src/db/schema.ts
export const yieldPosition = pgTable('yield_position', {
userId: text('user_id').primaryKey().references(() => users.id), // Assuming a users table exists
yieldSafeAddress: text('yield_safe_address').notNull(), // Address of the specific yield safe
depositedUsdc: numeric('deposited_usdc').default('0').notNull(), // Principal deposited in USDC (scaled)
currentUsdPlus: numeric('current_usd_plus').default('0').notNull(), // Current USD+ balance (scaled, rebasing)
lastUpdatedAt: timestamp('last_updated_at').defaultNow().notNull(), // Timestamp of the last update
createdAt: timestamp('created_at').defaultNow().notNull(),
});
Action Required: Generate and run database migrations after adding this schema.
Backend Service Layer
4.1 Helper: buildUsdPlusBatch
A helper function to construct the Safe transaction batch for depositing or withdrawing from Overnight.
import { encodeFunctionData, parseUnits, type Address, zeroAddress as ADDRESS_ZERO } from 'viem';
import Safe from '@safe-global/protocol-kit';
import { base } from 'viem/chains';
import { buildPrevalidatedSig } from '@/utils/safe-utils'; // Assuming utils exist
import { erc20Abi } from 'viem';
// Helper to build an ERC20 approve transaction
const approveTx = (token: Address, spender: Address, amount: bigint) => ({
to: token value: '0',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [spender, amount],
}),
});
// Define ABI items for clarity
const MINT_ABI_ITEM = parseAbiItem(OVERNIGHT.ABI_MINT);
const REDEEM_ABI_ITEM = parseAbiItem(OVERNIGHT.ABI_REDEEM);
export async function buildUsdPlusBatch(
safeAddr: Address,
owner: Address,
amount: bigint, // For deposit: USDC amount (6 decimals), For withdraw: USD+ amount (18 decimals)
direction: 'deposit' | 'withdraw',
rpcUrl: string = process.env.NEXT_PUBLIC_BASE_RPC_URL || 'https://mainnet.base.org'
) {
const minReturn = 0n; // Slippage control - set to 0 for MVP
const txs = direction === 'deposit'
? [
// 1. Approve USDC spending by the Overnight Exchange contract
approveTx(OVERNIGHT.USDC, OVERNIGHT.EXCHANGE, amount),
// 2. Mint USD+ using the approved USDC
{
to: OVERNIGHT.EXCHANGE,
value: '0',
data: encodeFunctionData({
abi: [MINT_ABI_ITEM],
functionName: 'mint',
args: [OVERNIGHT.USDC, amount, minReturn, ADDRESS_ZERO], // referral = zero address
}),
},
]
: [
// 1. Redeem USD+ for USDC
{
to: OVERNIGHT.EXCHANGE,
value: '0',
data: encodeFunctionData({
abi: [REDEEM_ABI_ITEM],
functionName: 'redeem',
args: [OVERNIGHT.USDC, amount, minReturn, ADDRESS_ZERO], // referral = zero address
}),
},
];
// Initialize Safe SDK (read-only)
const sdk = await Safe.init({ provider: rpcUrl, safeAddress: safeAddr });
// Create the multi-send transaction object
const safeTx = await sdk.createTransaction({ transactions: txs });
// Estimate gas - ~350k should cover approve + mint/redeem
safeTx.data.safeTxGas = '350000';
// Add pre-validated signature (owner == msg.sender, no EOA sig needed via Privy relay)
const prevalidatedSig = buildPrevalidatedSig(owner);
safeTx.addSignature({ signer: owner, data: prevalidatedSig, isContractSignature: true } as any);
// Encode the final execTransaction call
const contractManager = await sdk.getContractManager();
const safeContract = contractManager.safeContract_v1_4_1; // Ensure correct version
if (!safeContract) {
throw new Error('Failed to get Safe contract instance for encoding.');
}
const execData = safeContract.encode('execTransaction', [
safeTx.data.to,
safeTx.data.value,
safeTx.data.data,
safeTx.data.operation,
safeTx.data.safeTxGas,
safeTx.data.baseGas,
safeTx.data.gasPrice,
safeTx.data.gasToken,
safeTx.data.refundReceiver,
safeTx.encodedSignatures(),
]) as `0x${string}`;
return { to: safeAddr, data:Data, value: 0n }; // Return object ready for sendTransaction
}
tRPC Router: yieldRouter
A new tRPC router to handle yield operations.
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../create-router'; // Adjust path
import { TRPCError } from '@trpc/server';
import { parseUnits, formatUnits, createPublicClient, http, type Address } from 'viem';
import { base } from 'viem/chains';
import { erc20Abi } from 'viem';
import { api } from '@/trpc/react'; // Assuming client-side usage for stats later
import { buildUsdPlusBatch } from '@/server/services/overnight-service'; // Adjust path
import { db } from '@/db'; // Adjust path
import { yieldPosition } from '@/db/schema'; // Adjust path
import { eq } from 'drizzle-orm';
import { OVERNIGHT } from '@/lib/constants'; // Adjust path
// Helper to load user's primary safe (replace with actual logic)
async function getPrimarySafeAndOwner(userId: string): Promise<{ safeAddress: Address, ownerAddress: Address }> {
// This needs to fetch the primary safe address associated with the user
// and the corresponding owner address (likely the Privy smart wallet address)
// Example placeholder:
const safes = await db.query.userSafes.findMany({ // Assuming a userSafes table
where: eq(userSafes.userId, userId),
});
const primarySafe = safes.find(s => s.safeType === 'primary');
const privyAccount = user.linkedAccounts.find(a => a.type === 'smart_wallet'); // Fetch from ctx.privy
if (!primarySafe || !primarySafe.safeAddress) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Primary Safe not found.' });
}
if (!privyAccount || !privyAccount.address) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Privy smart wallet owner not found.' });
}
return {
safeAddress: primarySafe.safeAddress as Address,
ownerAddress: privyAccount.address as Address,
};
}
const publicClient = createPublicClient({
chain: base,
transport: http(process.env.NEXT_PUBLIC_BASE_RPC_URL),
});
export const yieldRouter = createTRPCRouter({
stats: protectedProcedure.query(async ({ ctx }) => {
const { safeAddress } = await getPrimarySafeAndOwner(ctx.user.id);
// Fetch on-chain USD+ balance
const usdPlusBalanceRaw = await publicClient.readContract({
address: OVERNIGHT.USD_PLUS,
abi: erc20Abi,
functionName: 'balanceOf',
args: [safeAddress],
});
const currentUsdPlus = formatUnits(usdPlusBalanceRaw, 18); // USD<br/>
**Q&A: Integrate Yield Safe with Overnight USD+**
**Q: What is Yield Safe and how does it work?**
A: Yield Safe is a feature that allows users to earn yield on their idle USDC within their Safe wallet. It integrates with Overnight USD+, a rebasing token that maintains a $1 peg, to provide a seamless and secure way to earn yield.
**Q: What are the benefits of using Yield Safe with Overnight USD+?**
A: The benefits of using Yield Safe with Overnight USD+ include:
* **Earn yield on idle USDC**: Users can earn yield on their idle USDC without having to actively manage their funds.
* **Seamless and secure experience**: The integration with Overnight USD+ provides a seamless and secure way to earn yield, with no need for on-chain signatures or gas payments.
* **Transparency regarding risks**: The feature clearly displays all important states, including loading, pending, success, error, APR, and accrued yield, while maintaining transparency regarding risks associated with rebasing tokens and peg risks.
**Q: How does the feature handle gas sponsorship?**
A: The feature is fully gas-sponsored, meaning that users do not need to pay for gas to use the feature. The gas is sponsored by the Paymaster, ensuring a seamless and secure experience for users.
**Q: What are the risks associated with rebasing tokens and peg risks?**
A: Rebased tokens, like Overnight USD+, can be subject to peg risks, which occur when the token's price deviates from its pegged value. This can result in losses for users who hold the token. The feature clearly displays all important states, including loading, pending, success, error, APR, and accrued yield, while maintaining transparency regarding these risks.
**Q: How does the feature handle user positions?**
A: The feature uses a new table in the database to track user positions, including the deposited USDC, current USD+ balance, and last updated timestamp.
**Q: What are the technical requirements for implementing the feature?**
A: The technical requirements for implementing the feature include:
* **Drizzle schema additions**: A new table is required to track user positions.
* **Backend service layer**: A helper function is required to construct the Safe transaction batch for depositing or withdrawing from Overnight.
* **tRPC router**: A new tRPC router is required to handle yield operations.
* **UI + UX**: A new component is required to display the yield card, including the APR, your balance, accrued yield, and PnL.
**Q: What are the future hooks and considerations for the feature?**
A: The future hooks and considerations for the feature include:
* **Auto-deposit rule**: Logic to automatically deposit USDC from the main safe to the yield safe if balance > threshold (post-allocation).
* **Strategy switching**: Abstract `OVERNIGHT.EXCHANGE` and related ABIs to support other yield protocols (Yearn, Velodrome) later.
* **Risk controls**: Implement DB-driven limits (max deposit per user, total protocol TVL cap) and protocol allow-lists.
* **Accurate PnL**: Use historical data or integrate Overnight SDK for more precise PnL calculation considering rebasing.
* **APR fetching**: Integrate Overnight's `/stats` API endpoint display real-time APR.
* **DB updates**: Refine when DB updates occur (optimistic vs. confirmed).
**Q: What are the testing requirements for the feature?**
A: The testing requirements for the feature include:
* **Deposit 1 USDC**: Verify ~1 USD+ minted in safe (check `balanceOf`).
* **Simulate rebase**: Verify UI PnL updates based on `stats` query.
* **Withdraw full USD+ balance**: Verify original USDC (minus small swap fee) returned to safe.
* **Verify gas sponsorship**: Check transaction details on Basescan (Gas paid by user should be 0 or near 0, sponsored by Paymaster).
* **UI States**: Verify disabled buttons for insufficient balance, no safe, etc.
* **Input validation**: Test non-numeric, negative, or zero inputs - ensure Zod blocks in backend, and UI prevents submission.
* **Error Handling**: Simulate network errors, paymaster errors, rejected transactions - verify appropriate toasts/messages.