CreditRamp uses the Blend Protocol SDK to interact with lending pools on Stellar. This document outlines the smart contract architecture, deployment process, and fee collection mechanism.
Key Insight: CreditRamp does NOT need to deploy a custom smart contract. Instead, we use:
- Blend Protocol Contracts (already deployed on Stellar)
- Atomic Transactions (native Stellar feature)
- Standard Payment Operations (for fee collection)
This approach is:
- ✅ More secure (no custom contract to audit)
- ✅ Lower cost (no contract deployment fees)
- ✅ Simpler to maintain
- ✅ Transparent to users
Package: @blend-capital/blend-sdk
pnpm add @blend-capital/blend-sdkKey Classes:
PoolContractV2- Interact with Blend lending poolsPoolFactoryContractV2- Deploy new pools (admin only)ReserveConfigV2- Configure pool reserves
Location: lib/contracts/*.contracts.json
Testnet Addresses (to be updated):
{
"poolFactoryV2": "CAQXEOMK7VKY5GGXVXVMXNXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV",
"oraclemock": "CBXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXV",
"creditRampFeeCollector": "GCREDITRAMPFEEADDRESS..."
}Setup Required:
- Create a dedicated Stellar account for fee collection
- Consider using multi-sig for security
- Update
creditRampFeeCollectorin contract configs
Recommended: Use a 2-of-3 multi-sig wallet with:
- CreditRamp operations key
- CreditRamp finance key
- Cold storage backup key
When a user supplies collateral to a Blend pool:
// 1. Calculate fee (3%)
const feeAmount = (amount * 300n) / 10000n;
const netAmount = amount - feeAmount;
// 2. Build atomic transaction
const tx = new TransactionBuilder(account, {
fee: '100',
networkPassphrase: network.passphrase,
})
// Operation 1: Transfer fee to CreditRamp
.addOperation(Operation.payment({
destination: CREDITRAMP_FEE_ADDRESS,
asset: asset,
amount: (Number(feeAmount) / 1e7).toString(),
}))
// Operation 2: Supply to Blend pool
.addOperation(poolContract.call(
'supply',
Address.fromString(user),
nativeToScVal(asset, { type: 'address' }),
nativeToScVal(netAmount, { type: 'i128' })
))
.setTimeout(30)
.build();
// 3. Sign and submit
const signedTx = await signTx(tx.toXDR(), user, network.passphrase);
const result = await server.submitTransaction(signedTx);Atomic Execution:
- Both operations succeed or both fail
- User never loses fee without successful supply
- No partial states possible
Transparency:
- User sees full transaction before signing
- Fee amount clearly visible
- Can verify on Stellar Explorer
Security:
- Uses battle-tested Blend contracts
- No custom contract attack surface
- Standard Stellar operations
pnpm installThis installs:
@blend-capital/blend-sdk- Blend Protocol SDK@stellar/stellar-sdk- Stellar SDK- Other dependencies
Update: lib/contracts/testnet.contracts.json
Get real addresses from:
{
"ids": {
"poolFactoryV2": "ACTUAL_POOL_FACTORY_ADDRESS",
"oraclemock": "ACTUAL_ORACLE_ADDRESS",
"creditRampFeeCollector": "YOUR_FEE_COLLECTION_ADDRESS"
}
}Create Account:
# Generate keypair
stellar keys generate creditramp-fees --network testnet
# Fund account (testnet)
curl "https://friendbot.stellar.org?addr=YOUR_PUBLIC_KEY"Configure Multi-Sig (Recommended):
import { Operation, TransactionBuilder } from '@stellar/stellar-sdk';
// Add signers
const tx = new TransactionBuilder(account, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(Operation.setOptions({
signer: {
ed25519PublicKey: SIGNER_2_PUBLIC_KEY,
weight: 1
}
}))
.addOperation(Operation.setOptions({
signer: {
ed25519PublicKey: SIGNER_3_PUBLIC_KEY,
weight: 1
}
}))
.addOperation(Operation.setOptions({
masterWeight: 1,
lowThreshold: 2,
medThreshold: 2,
highThreshold: 2
}))
.setTimeout(30)
.build();.env.local:
# Stellar Network
STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
# CreditRamp Fee Collection
CREDITRAMP_FEE_ADDRESS=GCREDITRAMP...
CREDITRAMP_FEE_BPS=300
# Blend Contracts
BLEND_POOL_FACTORY=CAQXEOMK...
BLEND_ORACLE=CBXVXVXV...import { loadMultiplePools } from '@/lib/blend';
const pools = await loadMultiplePools({
passphrase: Networks.TESTNET,
horizonUrl: 'https://horizon-testnet.stellar.org',
rpcUrl: 'https://soroban-testnet.stellar.org',
});
// Access pool data
pools.forEach(pool => {
console.log(`Pool: ${pool.name}`);
console.log(`TVL: $${Number(pool.totalValueLocked) / 1e7}`);
pool.reserves.forEach((reserve, symbol) => {
console.log(`${symbol}: ${reserve.supplyApr / 100}% APR`);
});
});import { supplyCollateral, CREDITRAMP_FEE_BPS } from '@/lib/blend';
const amount = BigInt(1000 * 1e7); // $1,000
const feeAmount = (amount * BigInt(CREDITRAMP_FEE_BPS)) / 10000n;
const netAmount = amount - feeAmount;
console.log(`Supplying: $${Number(amount) / 1e7}`);
console.log(`Fee (3%): $${Number(feeAmount) / 1e7}`);
console.log(`Net to pool: $${Number(netAmount) / 1e7}`);
const result = await supplyCollateral(
poolId,
USDC_ASSET,
amount,
userAddress,
network
);
console.log(`Transaction: ${result.hash}`);
console.log(`Fee collected: ${result.feeCollected}`);import { BlendDeploymentService } from '@/lib/blendDeployment';
const deployService = new BlendDeploymentService('testnet');
const poolData = {
name: 'CreditRamp SMB Pool',
network: 'testnet',
backstopTakeRate: 0.1,
maxPositions: 4,
minCollateral: 100,
selectedAssets: [
{
id: 'USDC',
symbol: 'USDC',
name: 'USD Coin',
address: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5',
decimals: 7
}
],
riskParameters: {
preset: 'balanced'
},
emissions: []
};
const result = await deployService.deployPool(poolData, adminKeypair);
console.log(`Pool deployed: ${result.poolAddress}`);import { Operation, TransactionBuilder } from '@stellar/stellar-sdk';
// Build withdrawal transaction
const tx = new TransactionBuilder(feeAccount, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(Operation.payment({
destination: TREASURY_ADDRESS,
asset: USDC_ASSET,
amount: withdrawAmount.toString()
}))
.setTimeout(30)
.build();
// Sign with required signers (multi-sig)
tx.sign(signer1Keypair);
tx.sign(signer2Keypair);
// Submit
const result = await server.submitTransaction(tx);// Query fee account balance
const account = await server.loadAccount(CREDITRAMP_FEE_ADDRESS);
account.balances.forEach(balance => {
if (balance.asset_type !== 'native') {
console.log(`${balance.asset_code}: ${balance.balance}`);
}
});
// Query recent payments
const payments = await server
.payments()
.forAccount(CREDITRAMP_FEE_ADDRESS)
.order('desc')
.limit(100)
.call();
let totalFeesCollected = 0;
payments.records.forEach(payment => {
if (payment.type === 'payment' && payment.to === CREDITRAMP_FEE_ADDRESS) {
totalFeesCollected += parseFloat(payment.amount);
}
});
console.log(`Total fees collected: $${totalFeesCollected}`);Best Practices:
- Use multi-sig (2-of-3 or 3-of-5)
- Store private keys in hardware wallets
- Implement withdrawal limits
- Regular security audits
- Monitor for suspicious activity
Before Submission:
// Validate inputs
if (amount <= 0) throw new Error('Invalid amount');
if (!isValidAddress(userAddress)) throw new Error('Invalid address');
// Check pool capacity
const pool = await loadPool(network, poolId);
const reserve = pool.reserves.get(assetSymbol);
if (!reserve) throw new Error('Asset not supported');
const availableCapacity = reserve.supply_cap - reserve.totalSupply;
if (netAmount > availableCapacity) {
throw new Error('Exceeds pool capacity');
}
// Verify fee calculation
const expectedFee = (amount * BigInt(CREDITRAMP_FEE_BPS)) / 10000n;
if (feeAmount !== expectedFee) {
throw new Error('Fee calculation mismatch');
}try {
const result = await supplyCollateral(...);
return { success: true, result };
} catch (error) {
if (error.message.includes('insufficient balance')) {
return { success: false, error: 'Insufficient balance' };
} else if (error.message.includes('pool capacity')) {
return { success: false, error: 'Pool at capacity' };
} else {
console.error('Supply error:', error);
return { success: false, error: 'Transaction failed' };
}
}import { calculateFee } from '@/lib/blendDeployment';
describe('Fee Calculation', () => {
it('should calculate 3% fee correctly', () => {
const amount = BigInt(1000 * 1e7);
const { feeAmount, netAmount } = calculateFee(amount);
expect(feeAmount).toBe(BigInt(30 * 1e7)); // 3%
expect(netAmount).toBe(BigInt(970 * 1e7)); // 97%
});
});# Test on testnet
pnpm test:integration
# Expected output:
# ✓ Connect wallet
# ✓ Load pools
# ✓ Calculate fees
# ✓ Submit supply transaction
# ✓ Verify fee collection
# ✓ Verify pool balanceTrack in Database:
CREATE TABLE fee_collections (
id SERIAL PRIMARY KEY,
transaction_hash VARCHAR(64) NOT NULL,
user_address VARCHAR(56) NOT NULL,
asset_code VARCHAR(12) NOT NULL,
gross_amount BIGINT NOT NULL,
fee_amount BIGINT NOT NULL,
net_amount BIGINT NOT NULL,
pool_id VARCHAR(56) NOT NULL,
timestamp TIMESTAMP DEFAULT NOW()
);
-- Query total fees
SELECT
asset_code,
SUM(fee_amount) / 1e7 as total_fees_usd
FROM fee_collections
GROUP BY asset_code;- Total fees collected (by asset)
- Daily/weekly/monthly revenue
- Average transaction size
- Number of unique users
- Pool utilization rates
- APR trends
1. "Contract not found"
- Verify contract addresses in
contracts/*.json - Check network configuration
- Ensure using correct network (testnet vs mainnet)
2. "Insufficient balance"
- User needs more funds
- Check asset balance, not just XLM
- Account for transaction fees
3. "Transaction failed"
- Check simulation results
- Verify all parameters
- Ensure pool has capacity
- Check asset is enabled in pool
4. "Fee address invalid"
- Update
creditRampFeeCollectorin config - Verify address format (starts with 'G')
- Ensure account exists and is funded
- Install Blend SDK:
pnpm install - Get Contract Addresses: From Blend docs/GitHub
- Create Fee Account: Generate and fund
- Update Configs: Set all addresses
- Test on Testnet: Run integration tests
- Deploy to Mainnet: After thorough testing
Status: Ready for implementation Dependencies: Blend SDK installation required Security: Multi-sig recommended for fee account