Integration Guide
This guide provides step-by-step instructions for integrating with the Opals Protocol, from environment setup through production deployment.
Environment Setup
Prerequisites
Required Tools:
Install Foundry:
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Verify installation
forge --version
cast --version
anvil --versionClone Repository:
git clone https://github.com/OpalsProtocol/opals-contracts.git
cd opals-contracts
# Install dependencies
forge install
yarn install # Optional, for deployment scriptsConfiguration
Create .env file:
# Private key for deployments (NEVER commit this)
PRIVATE_KEY=0x...
# RPC endpoints
BASE_SEPOLIA_RPC=https://sepolia.base.org
MONAD_TESTNET_RPC=https://testnet-rpc.monad.xyz
# Etherscan API key (for verification)
ETHERSCAN_API_KEY=your_key_here
# Admin addresses
DEPLOYER_ADDRESS=0x...
TREASURY_ADDRESS=0x...Load environment:
source .envBuild and Test
# Compile all contracts
forge build
# Run full test suite (375 tests)
forge test
# Run with verbosity for debugging
forge test -vvvv
# Run specific test
forge test --match-test testPatronClaimRewards
# Run specific test file
forge test --match-path test/templates/core/PatronClaim.t.sol
# Gas reporting
forge test --gas-report
# Coverage analysis
forge coverageDeployment Patterns
Pattern 1: Recipe-Based Deployment (Recommended)
Deploy a complete project ecosystem in a single transaction using OpalsRecipe.
Use Case: Standard token launch with crowdfunding, staking, and liquidity.
Code Example:
// Get factory and recipe contracts
OpalsFactory factory = OpalsFactory(FACTORY_ADDRESS);
OpalsRecipe recipe = OpalsRecipe(RECIPE_ADDRESS);
// Define market configuration
OpalsRecipe.MarketConfig memory marketConfig = OpalsRecipe.MarketConfig({
basePrice: 0.01 ether, // Starting price
priceIncrement: 0.001 ether, // Price increase per batch
itemsPerPackage: 10, // Items per batch
numberOfPackages: 100, // Total batches
maxSupply: 1000, // Total NFTs
patronCardBaseUri: "ipfs://...", // Metadata URI
launcherAllocation: 5000 // 50% to liquidity
});
// Define launcher configuration
OpalsRecipe.LauncherConfig memory launcherConfig = OpalsRecipe.LauncherConfig({
tokenSupplyForLP: 500_000 * 1e18, // Tokens for LP
uniswapRouter: UNISWAP_ROUTER, // Router address
uniswapFactory: UNISWAP_FACTORY // Factory address
});
// Deploy complete project
(
address project,
address market,
address patronClaim,
address vaultClaim,
address launcher,
address distributor
) = recipe.deployProject(
"MyProject", // Project name
admin, // Admin address
marketConfig,
launcherConfig
);
console.log("Project deployed:", project);
console.log("Market deployed:", market);
console.log("PatronClaim deployed:", patronClaim);What Gets Deployed:
Project (coordination hub)
Token (ERC20 with 1e27 supply)
Operator (access control)
SteppedMarket (NFT sales)
PatronClaim (10x multiplier rewards)
VaultClaim (0-5x flexible staking)
Patron Card (ERC721 for PatronClaim)
Vault Card (ERC721 for VaultClaim)
LiquidityLauncher (Uniswap integration)
Distributor (protocol fee distribution)
Gas Cost: 2M gas ($200 at 100 gwei)
Pattern 2: Custom Template Deployment
Deploy specific components for custom configurations.
Use Case: Unique economic models or integration with existing infrastructure.
Code Example:
OpalsFactory factory = OpalsFactory(FACTORY_ADDRESS);
// Deploy project
bytes memory projectInit = abi.encode("ProjectName", admin);
address project = factory.deployContract(
keccak256("PROJECT"),
projectInit
);
// Deploy token
bytes memory tokenInit = abi.encode("MyToken", "TKN", project);
address token = factory.deployContract(
keccak256("TOKEN"),
tokenInit
);
// Deploy custom market with specific parameters
bytes memory marketInit = abi.encodeWithSignature(
"init(address,address,uint256,uint256,uint256,uint256,uint256,string,uint256)",
project,
card,
0.05 ether, // basePrice
0.01 ether, // priceIncrement
5, // itemsPerPackage
20, // numberOfPackages
100, // maxSupply
"ipfs://...", // baseUri
7500 // launcherAllocation (75%)
);
address market = factory.deployContract(
keccak256("STEPPED_MARKET"),
marketInit
);
// Wire components together
Project(project).addMarket(market, card);
Project(project).addClaim(patronClaim, card);Benefits:
Maximum control over parameters
Custom component selection
Integration with existing contracts
Granular gas optimization
Pattern 3: Integration with Existing Projects
Add Opals features to existing projects.
Use Case: Existing NFT collection wants to add staking rewards.
Code Example:
// Existing NFT contract
IERC721 existingNFT = IERC721(0x...);
// Deploy PatronClaim for existing NFT
bytes memory claimInit = abi.encode(
project, // New project for tracking
existingNFT, // Existing NFT contract
lpToken // LP token to distribute
);
address patronClaim = factory.deployContract(
keccak256("PATRON_CLAIM"),
claimInit
);
// Add card set for existing NFT collection
IPatronClaim(patronClaim).addCardSet(
address(existingNFT), // Card contract
1000, // Weight per card
10000 // Max supply
);
// Set LP tokens for distribution
IPatronClaim(patronClaim).setLPTokensForCardSet(
0, // Card set index
lpTokenAmount // LP tokens to allocate
);
// Users can now claim rewards
uint256[] memory tokenIds = new uint256[](1);
tokenIds[0] = myTokenId;
IPatronClaim(patronClaim).claimTokensForNFTs(tokenIds, tokenToWithdraw);Benefits:
No migration needed for existing NFTs
Adds value to existing community
Gradual feature rollout possible
Maintains existing contracts
Configuration Best Practices
Market Configuration
Stepped Market Pricing:
// Bot-resistant configuration
MarketConfig({
basePrice: 0.01 ether, // Entry price
priceIncrement: 0.005 ether, // 50% increase per batch
itemsPerPackage: 10, // Batch size
numberOfPackages: 50, // Total batches
maxSupply: 500, // Total supply
patronCardBaseUri: "ipfs://Qm...",
launcherAllocation: 5000 // 50% to LP
});Rationale:
Small batches (10) reduce bot advantage
Large price increments (50%) discourage speculation
50% to LP ensures healthy initial liquidity
500 total supply balances scarcity with accessibility
Fixed Market Pricing:
// Simple membership model
FixedMarketConfig({
price: 0.1 ether, // Fixed price
maxSupply: 1000, // Total members
patronCardBaseUri: "ipfs://Qm...",
launcherAllocation: 6000 // 60% to LP
});Liquidity Configuration
Conservative (High Liquidity):
LauncherConfig({
tokenSupplyForLP: 600_000 * 1e18, // 60% of supply
uniswapRouter: router,
uniswapFactory: factory
});Benefits: Lower slippage, better price stability, less volatility
Aggressive (Low Liquidity):
LauncherConfig({
tokenSupplyForLP: 200_000 * 1e18, // 20% of supply
uniswapRouter: router,
uniswapFactory: factory
});Benefits: Higher price appreciation potential, more tokens for rewards/team
Recommended: 40-60% of supply to LP
Staking Configuration
VaultClaim Lock Periods:
// Allow flexible staking
uint256[] memory lockPeriods = [
7 days, // 0.024x multiplier
30 days, // 0.104x multiplier
90 days, // 0.308x multiplier
180 days, // 0.616x multiplier
365 days, // 1.25x multiplier
730 days, // 2.5x multiplier
1460 days // 5x multiplier
];
// Users choose lock duration for desired multiplier
vaultClaim.stakeLP(amount, lockPeriods[userChoice]);Design Considerations:
Longer locks = higher multipliers (0-5x)
Early exit available with 0-50% penalty
Penalties redistribute to diamond hands
Permanent locks get 10x multiplier
Event Listening and Monitoring
Critical Events to Monitor
Project Lifecycle:
const ethers = require('ethers');
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
// Monitor factory for new projects
const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, provider);
factory.on("ContractCreated", async (owner, addr, template, event) => {
// Decode template ID
const templateId = await factory.getContractTemplateId(addr);
if (templateId === ethers.utils.id("PROJECT")) {
console.log(`New project created by ${owner}: ${addr}`);
// Initialize monitoring for this project
await monitorProject(addr);
}
});
async function monitorProject(projectAddr) {
const project = new ethers.Contract(projectAddr, PROJECT_ABI, provider);
// Get all components
const token = await project.token();
const markets = await project.getMarkets();
const claims = await project.getClaims();
const launcher = await project.launcher();
console.log(`Project components:`, {
token,
markets,
claims,
launcher
});
// Set up monitoring for each component
monitorMarket(markets[0]);
monitorLauncher(launcher);
monitorClaim(claims[0]);
}Market Sales:
async function monitorMarket(marketAddr) {
const market = new ethers.Contract(marketAddr, MARKET_ABI, provider);
market.on("Collected", (buyer, tokenId, price, referrer, event) => {
console.log(`Sale: NFT #${tokenId} to ${buyer} for ${ethers.utils.formatEther(price)} ETH`);
// Update analytics
updateSalesMetrics({
buyer,
tokenId: tokenId.toString(),
price: ethers.utils.formatEther(price),
referrer,
timestamp: event.blockNumber
});
});
market.on("Finalized", async (totalRaised, lpAmount, treasuryAmount) => {
console.log(`Market finalized: ${ethers.utils.formatEther(totalRaised)} ETH raised`);
// Notify that trading will open soon
await notifyTradingOpening(marketAddr, totalRaised);
});
}Liquidity Launch:
async function monitorLauncher(launcherAddr) {
const launcher = new ethers.Contract(launcherAddr, LAUNCHER_ABI, provider);
launcher.on("LiquidityLaunched", async (pair, tokenAmount, ethAmount, lpTokens) => {
console.log(`Liquidity launched!`);
console.log(`Pair: ${pair}`);
console.log(`Tokens: ${ethers.utils.formatEther(tokenAmount)}`);
console.log(`ETH: ${ethers.utils.formatEther(ethAmount)}`);
console.log(`LP: ${ethers.utils.formatEther(lpTokens)}`);
// Enable trading interface
await enableTrading(pair);
// Start price tracking
await trackPairPrice(pair);
});
}Claim Activity:
async function monitorClaim(claimAddr) {
const claim = new ethers.Contract(claimAddr, CLAIM_ABI, provider);
claim.on("TokensDeposited", (token, amount, event) => {
console.log(`Rewards deposited: ${ethers.utils.formatEther(amount)} of ${token}`);
});
claim.on("TokensClaimed", (claimer, token, amount, tokenIds) => {
console.log(`${claimer} claimed ${ethers.utils.formatEther(amount)} with NFTs ${tokenIds}`);
// Update user balances
updateUserRewards(claimer, token, amount);
});
claim.on("LPStaked", (staker, tokenId, amount, lockEndTime) => {
console.log(`${staker} staked ${ethers.utils.formatEther(amount)} until ${new Date(lockEndTime * 1000)}`);
});
claim.on("EarlyExit", (user, tokenId, lpReturned, penalty) => {
console.log(`${user} exited early, penalty: ${ethers.utils.formatEther(penalty)}`);
// Track penalty for redistribution
trackPenalty(user, tokenId, penalty);
});
}Testing Strategies
Unit Testing
Test individual contract functions in isolation:
// test/templates/markets/SteppedMarket.t.sol
contract SteppedMarketTest is Test {
SteppedMarket market;
address admin = address(1);
address buyer = address(2);
function setUp() public {
// Deploy market
market = new SteppedMarket();
market.init(
project,
card,
0.01 ether, // basePrice
0.001 ether, // priceIncrement
10, // itemsPerPackage
10, // numberOfPackages
100, // maxSupply
"ipfs://", // baseUri
5000 // launcherAllocation
);
}
function testCollect() public {
vm.deal(buyer, 1 ether);
vm.prank(buyer);
uint256 price = market.getCurrentPrice();
market.collect{value: price}(address(0));
assertEq(market.totalSold(), 1);
}
function testPriceIncreases() public {
uint256 initialPrice = market.getCurrentPrice();
// Buy entire first batch
for (uint i = 0; i < 10; i++) {
buyOne();
}
uint256 newPrice = market.getCurrentPrice();
assertGt(newPrice, initialPrice, "Price should increase after batch");
}
}Integration Testing
Test cross-contract interactions:
// test/integration/ProjectFlow.t.sol
contract ProjectFlowTest is DeployProjectSetup {
function testCompleteProjectFlow() public {
// 1. Users purchase NFTs
uint256 price = market.getCurrentPrice();
vm.deal(user1, 10 ether);
vm.prank(user1);
market.collect{value: price}(address(0));
// 2. Market reaches success threshold
buyUntilSuccess();
// 3. Liquidity launches
launcher.launch();
assertTrue(launcher.isLaunched());
// 4. Users can claim rewards
vm.prank(user1);
uint256[] memory tokenIds = new uint256[](1);
tokenIds[0] = 0;
patronClaim.claimTokensForNFTs(tokenIds, address(token));
// 5. Verify user received rewards
assertGt(token.balanceOf(user1), 0);
}
}Fuzz Testing
Test with random inputs to find edge cases:
function testFuzz_StakeLP(uint256 amount, uint256 lockDuration) public {
// Bound inputs to valid ranges
amount = bound(amount, 1e18, 1000e18);
lockDuration = bound(lockDuration, 7 days, 4 * 365 days);
// Setup
vm.deal(user, amount);
vm.startPrank(user);
lpToken.approve(address(vaultClaim), amount);
// Test staking
vaultClaim.stakeLP(amount, lockDuration);
// Verify state
assertEq(vaultClaim.getStakedAmount(0), amount);
}Production Deployment Checklist
Pre-Deployment
Deployment
# Deploy to testnet first
forge script script/Deploy.s.sol \
--rpc-url $BASE_SEPOLIA_RPC \
--broadcast \
--verify \
--private-key $PRIVATE_KEY
# Test on testnet for 1-2 weeks
# - Full user flow testing
# - Load testing
# - Monitor for issues
# Deploy to mainnet with multi-sig
forge script script/Deploy.s.sol \
--rpc-url $BASE_MAINNET_RPC \
--broadcast \
--verify \
--ledger \
--sender $MULTISIG_ADDRESSPost-Deployment
Common Integration Patterns
Pattern: Progressive Rewards
Gradually increase rewards over time:
// Deposit rewards incrementally
for (uint i = 0; i < 12; i++) {
// Monthly deposits
uint256 monthlyRewards = totalRewards / 12;
patronClaim.depositRewards{value: monthlyRewards}();
wait(30 days);
}Pattern: Multi-Tier Membership
Different NFT tiers with different weights:
// Bronze tier: 100 weight
patronClaim.addCardSet(bronzeCard, 100, 1000);
// Silver tier: 250 weight (2.5x bronze)
patronClaim.addCardSet(silverCard, 250, 500);
// Gold tier: 500 weight (5x bronze)
patronClaim.addCardSet(goldCard, 500, 100);Pattern: Referral Rewards
Track and reward referrers:
// Market automatically tracks referrers
market.collect{value: price}(referrerAddress);
// Referrer receives 0.3% of sale (15% of 2% fee)
// Automatically distributed via EthRewarderTroubleshooting
Issue: Transaction Reverts on collect()
Possible causes:
Insufficient ETH sent
Market sold out
Paused state
Debug:
uint256 currentPrice = market.getCurrentPrice();
uint256 totalSold = market.totalSold();
uint256 maxSupply = market.maxSupply();
console.log("Price:", currentPrice);
console.log("Sold:", totalSold);
console.log("Max:", maxSupply);Issue: Liquidity launch fails
Possible causes:
Market not successful
Already launched
Insufficient token balance
Debug:
bool isSuccessful = market.isSuccessful();
bool isLaunched = launcher.isLaunched();
uint256 tokenBalance = token.balanceOf(address(launcher));
require(isSuccessful, "Market not successful");
require(!isLaunched, "Already launched");
require(tokenBalance > 0, "No tokens for LP");Issue: Claims not working
Possible causes:
No rewards deposited
NFT not eligible
Card set not configured
Debug:
uint256 claimable = patronClaim.getClaimableAmountForNFT(tokenId, rewardToken);
uint256 cardSetCount = patronClaim.getCardSetCount();
uint256 totalLP = patronClaim.getTotalLPTokens();
console.log("Claimable:", claimable);
console.log("Card sets:", cardSetCount);
console.log("Total LP:", totalLP);Next Steps
Review Contracts Reference for detailed function documentation
Study Security for security best practices
Explore Architecture Overview for system design
Join Discord for developer support
You're now ready to integrate with Opals Protocol and launch your project!
Last updated