SimpleContract¶
The SimpleContract provides a basic, single-oracle interface for development, testing, and simple production use cases. It offers direct oracle interaction without the complexity of reputation management or multi-oracle aggregation.
Overview¶
The SimpleContract is designed for: - Development and Testing: Quick setup for prototyping - Simple Applications: Minimal complexity for basic use cases - Fixed Oracle Setup: Direct connection to a specific oracle - Educational Purposes: Understanding basic Chainlink integration
Key Features¶
- Direct Oracle Connection: Configured with a specific oracle and job ID
- Bonus Payment System: Pays equal bonus to oracle on successful completion
- Class-based Filtering: Ensures only compatible oracles are used
- Timeout Handling: Automatic failure detection and manual finalization
- Cost Transparency: Simple 2× fee structure (base + bonus)
Contract Interface¶
Primary Function¶
function requestAIEvaluationWithApproval(
string[] memory cids,
string memory addendumText,
uint256, uint256, uint256, uint256, // Ignored parameters for compatibility
uint64 _requestedClass
) external returns (bytes32 requestId)
Parameters¶
Parameter | Type | Description |
---|---|---|
cids | string[] | IPFS content hashes containing evidence (max 10) |
addendumText | string | Additional context (max 1000 chars) |
_requestedClass | uint64 | Must match contract's configured class |
Note: The middle parameters are ignored for compatibility with other contracts.
View Functions¶
// Get evaluation result
function getEvaluation(bytes32 id)
external view returns (
uint256[] memory likelihoods,
string memory justificationCID,
bool exists
)
// Calculate maximum total fee (base + bonus)
function maxTotalFee(uint256) external view returns (uint256)
// Get contract configuration
function getContractConfig()
external view returns (
address oracleAddr,
address linkAddr,
bytes32 jobId,
uint256 currentFee
)
// Check if request failed
function isFailed(bytes32 requestId) external view returns (bool)
// Get timeout duration
function responseTimeoutSeconds() external view returns (uint256)
Configuration Functions¶
// Set response timeout (basic access control)
function setResponseTimeout(uint256 secs) external
// Owner-only functions (simplified in current implementation)
function withdrawLink(address payable to, uint256 amount) external
Constructor Parameters¶
constructor(
address _oracle, // ArbiterOperator contract address
bytes32 _jobId, // Job ID on the oracle
uint256 _fee, // LINK fee per request
address _link, // LINK token address
uint64 _requiredClass // Oracle class this contract accepts
)
Events¶
// Request submitted
event RequestAIEvaluation(bytes32 indexed requestId, string[] cids);
// Evaluation completed
event FulfillAIEvaluation(
bytes32 indexed requestId,
uint256[] likelihoods,
string justificationCID
);
// Internal fulfillment received
event FulfillmentReceived(
bytes32 indexed requestId,
address caller,
uint256 len,
string justificationCID
);
// Evaluation failed
event EvaluationFailed(bytes32 indexed requestId);
// Bonus payment to oracle
event BonusPaid(
bytes32 indexed requestId,
address oracle,
uint256 amount
);
Usage Examples¶
Basic Deployment¶
pragma solidity ^0.8.21;
import "./SimpleContract.sol";
contract SimpleContractDeployer {
SimpleContract public evaluator;
function deployEvaluator(
address oracleOperator,
bytes32 jobId,
address linkToken
) external returns (address) {
evaluator = new SimpleContract(
oracleOperator,
jobId,
0.01 ether, // 0.01 LINK fee
linkToken,
1 // Class 1 oracle
);
return address(evaluator);
}
function getEvaluatorInfo() external view returns (
address oracle,
address link,
bytes32 jobId,
uint256 fee,
uint256 maxCost
) {
(oracle, link, jobId, fee) = evaluator.getContractConfig();
maxCost = evaluator.maxTotalFee(0); // Parameter ignored
return (oracle, link, jobId, fee, maxCost);
}
}
Integration Example¶
pragma solidity ^0.8.21;
contract ContentModerator {
SimpleContract public evaluator;
IERC20 public linkToken;
struct ModerationRequest {
bytes32 requestId;
address requester;
string contentHash;
bool resolved;
bool approved;
}
mapping(bytes32 => ModerationRequest) public requests;
mapping(address => bool) public moderators;
event ContentSubmitted(bytes32 indexed requestId, address requester);
event ContentModerated(bytes32 indexed requestId, bool approved);
constructor(address _evaluator, address _linkToken) {
evaluator = SimpleContract(_evaluator);
linkToken = IERC20(_linkToken);
moderators[msg.sender] = true;
}
function submitContent(string memory contentIPFSHash) external returns (bytes32) {
// Prepare evidence array
string[] memory cids = new string[](1);
cids[0] = contentIPFSHash;
// Calculate and approve fees
uint256 totalFee = evaluator.maxTotalFee(0);
linkToken.transferFrom(msg.sender, address(this), totalFee);
linkToken.approve(address(evaluator), totalFee);
// Submit for evaluation
bytes32 requestId = evaluator.requestAIEvaluationWithApproval(
cids,
"Content moderation request",
0, 0, 0, 0, // Ignored parameters
1 // Moderation class
);
// Store request
requests[requestId] = ModerationRequest({
requestId: requestId,
requester: msg.sender,
contentHash: contentIPFSHash,
resolved: false,
approved: false
});
emit ContentSubmitted(requestId, msg.sender);
return requestId;
}
function processResult(bytes32 requestId) external {
ModerationRequest storage request = requests[requestId];
require(!request.resolved, "Already processed");
(uint256[] memory likelihoods, , bool exists) =
evaluator.getEvaluation(requestId);
require(exists, "Evaluation not complete");
// Approve if likelihood > 70%
bool approved = likelihoods[0] > 700;
request.resolved = true;
request.approved = approved;
emit ContentModerated(requestId, approved);
}
function handleTimeout(bytes32 requestId) external {
require(moderators[msg.sender], "Not authorized");
ModerationRequest storage request = requests[requestId];
require(!request.resolved, "Already processed");
// Check if evaluation failed
if (evaluator.isFailed(requestId)) {
// Manually reject on timeout
request.resolved = true;
request.approved = false;
emit ContentModerated(requestId, false);
}
}
}
Frontend Integration¶
import { ethers } from 'ethers';
class SimpleEvaluator {
constructor(contractAddress, signer) {
this.contract = new ethers.Contract(
contractAddress,
SIMPLE_CONTRACT_ABI,
signer
);
}
async getConfiguration() {
const [oracle, linkAddr, jobId, fee] =
await this.contract.getContractConfig();
const maxTotalFee = await this.contract.maxTotalFee(0);
const timeout = await this.contract.responseTimeoutSeconds();
return {
oracle,
linkToken: linkAddr,
jobId,
fee: ethers.formatEther(fee),
maxTotalFee: ethers.formatEther(maxTotalFee),
timeoutSeconds: Number(timeout)
};
}
async submitEvaluation(contentHash, description = "") {
try {
const config = await this.getConfiguration();
// Approve LINK tokens
const linkToken = new ethers.Contract(
config.linkToken,
LINK_ABI,
this.contract.signer
);
const totalFee = ethers.parseEther(config.maxTotalFee);
await linkToken.approve(this.contract.target, totalFee);
// Submit request
const tx = await this.contract.requestAIEvaluationWithApproval(
[contentHash],
description,
0, 0, 0, 0, // Ignored parameters
1 // Default class
);
console.log("Evaluation submitted:", tx.hash);
const receipt = await tx.wait();
// Extract request ID from events
const event = receipt.logs.find(
log => log.topics[0] === this.contract.interface.getEvent("RequestAIEvaluation").topicHash
);
const requestId = event.topics[1];
return {
requestId,
transactionHash: tx.hash,
estimatedCompletionTime: Date.now() + (config.timeoutSeconds * 1000)
};
} catch (error) {
console.error("Failed to submit evaluation:", error);
throw error;
}
}
async getResult(requestId) {
const [likelihoods, justificationCID, exists] =
await this.contract.getEvaluation(requestId);
if (!exists) {
const failed = await this.contract.isFailed(requestId);
return failed ?
{ status: 'failed', error: 'Evaluation timed out' } :
{ status: 'pending' };
}
return {
status: 'complete',
likelihoods: likelihoods.map(l => Number(l)),
justificationCID,
confidence: Math.max(...likelihoods.map(l => Number(l))) / 10 // Convert to percentage
};
}
watchEvaluation(requestId) {
return new Promise((resolve, reject) => {
const config = this.getConfiguration();
const timeout = setTimeout(() => {
cleanup();
reject(new Error('Evaluation timeout'));
}, config.timeoutSeconds * 1000);
const fulfillFilter = this.contract.filters.FulfillAIEvaluation(requestId);
const failFilter = this.contract.filters.EvaluationFailed(requestId);
const cleanup = () => {
this.contract.off(fulfillFilter);
this.contract.off(failFilter);
clearTimeout(timeout);
};
this.contract.once(fulfillFilter, async (reqId, likelihoods, justificationCID) => {
cleanup();
resolve({
status: 'complete',
likelihoods: likelihoods.map(l => Number(l)),
justificationCID
});
});
this.contract.once(failFilter, (reqId) => {
cleanup();
reject(new Error('Evaluation failed'));
});
});
}
}
// Usage example
async function moderateContent(contentHash) {
const evaluator = new SimpleEvaluator(CONTRACT_ADDRESS, signer);
try {
const submission = await evaluator.submitEvaluation(contentHash, "Moderation check");
console.log("Submitted:", submission.requestId);
const result = await evaluator.watchEvaluation(submission.requestId);
console.log("Result:", result);
return result.likelihoods[0] > 500; // Approve if > 50%
} catch (error) {
console.error("Moderation failed:", error);
return false; // Reject on error
}
}
Configuration and Deployment¶
Network-Specific Deployment¶
// Deployment script for different networks
const deploymentConfigs = {
'base-sepolia': {
linkToken: '0xE4aB69C077896252FAFBD49EFD26B5D171A32410',
oracle: '0x...', // Your ArbiterOperator address
jobId: '0x...', // Your job ID
fee: ethers.parseEther('0.01'),
requiredClass: 1
},
'sepolia': {
linkToken: '0x779877A7B0D9E8603169DdbD7836e478b4624789',
oracle: '0x...', // Your ArbiterOperator address
jobId: '0x...', // Your job ID
fee: ethers.parseEther('0.005'),
requiredClass: 1
}
};
async function deploySimpleContract(network) {
const config = deploymentConfigs[network];
if (!config) throw new Error(`No config for network: ${network}`);
const SimpleContract = await ethers.getContractFactory("SimpleContract");
const contract = await SimpleContract.deploy(
config.oracle,
config.jobId,
config.fee,
config.linkToken,
config.requiredClass
);
await contract.waitForDeployment();
console.log(`SimpleContract deployed to: ${contract.target}`);
console.log(`Configuration:`, config);
return contract;
}
Cost Structure¶
The SimpleContract uses a straightforward fee structure:
- Base Fee: The configured fee amount
- Bonus Fee: Equal to the base fee (paid on success)
- Total Maximum:
2 × fee
Example: If configured with 0.01 LINK fee: - User approves: 0.02 LINK - Oracle receives: 0.01 LINK immediately + 0.01 LINK bonus on completion
Comparison with Other Contracts¶
Feature | SimpleContract | ReputationSingleton | ReputationAggregator |
---|---|---|---|
Setup Complexity | Minimal | Medium | High |
Oracle Selection | Fixed | Dynamic (reputation-based) | Dynamic (multi-oracle) |
Cost | 2× fee | 2× fee | Up to 15× fee |
Response Time | ~30-60 seconds | ~30-60 seconds | ~5-15 minutes |
Security | Basic | Reputation-based | Multi-oracle consensus |
Use Cases | Testing, simple apps | Quick disputes | High-stakes disputes |
Best Practices¶
When to Use SimpleContract¶
✅ Good for: - Development and testing - Proof of concepts - Simple applications with trusted oracles - Educational purposes - Fixed oracle setups
❌ Avoid for: - Production applications requiring high security - Applications needing oracle redundancy - Dynamic oracle selection requirements - High-value dispute resolution
Security Considerations¶
Limitations: - Single Point of Failure: Only one oracle, no redundancy - No Reputation System: Cannot adapt to oracle performance - Fixed Configuration: Cannot change oracle without redeployment
Mitigations: - Oracle Selection: Choose high-reputation, reliable oracles - Monitoring: Implement external monitoring of oracle performance - Fallback Plans: Have manual override mechanisms for critical applications
Configuration Recommendations¶
// Development configuration
SimpleContract devContract = new SimpleContract(
testOracleAddress,
testJobId,
0.001 ether, // Low fee for testing
linkTokenAddress,
1 // Standard test class
);
// Production configuration (use with caution)
SimpleContract prodContract = new SimpleContract(
trustedOracleAddress,
productionJobId,
0.01 ether, // Reasonable fee
linkTokenAddress,
1 // Production class
);
Troubleshooting¶
Common Issues¶
"class mismatch"
// Check oracle's configured class matches contract
uint64 contractClass = 1; // Your configured class
uint64 requestClass = 2; // Class in request
// Must match: contractClass == requestClass
"CID count" / "CID len" / "Addendum len" - Max 10 CIDs per request - Each CID max 100 characters
- Addendum max 1000 characters
"bonus LINK xferFrom failed" - Ensure requester has sufficient LINK balance - Verify LINK token approval covers total fee - Check oracle address is valid
Testing and Debugging¶
// Test configuration
async function testSimpleContract(contractAddress) {
const contract = new ethers.Contract(contractAddress, ABI, provider);
const [oracle, link, jobId, fee] = await contract.getContractConfig();
const maxFee = await contract.maxTotalFee(0);
const timeout = await contract.responseTimeoutSeconds();
console.log('Configuration:', {
oracle,
linkToken: link,
jobId,
fee: ethers.formatEther(fee),
maxTotalFee: ethers.formatEther(maxFee),
timeoutSeconds: Number(timeout)
});
// Test with minimal request
const testCids = ['QmTest123'];
const linkToken = new ethers.Contract(link, LINK_ABI, signer);
await linkToken.approve(contractAddress, maxFee);
const tx = await contract.requestAIEvaluationWithApproval(
testCids, '', 0, 0, 0, 0, 1
);
console.log('Test request submitted:', tx.hash);
}