Skip to main content

Best Practices for Entity Integration

Recommended patterns, security considerations, and optimization strategies for working with Endaoment entities.

Security Best Practices

Always Verify Entity Status

Before any transaction, confirm the entity is active:
async function safeEntityInteraction(entityAddress: string): Promise<boolean> {
  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
  const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
  
  // 1. Check if entity exists and is active
  const isActive = await registry.isActiveEntity(entityAddress);
  
  if (!isActive) {
    throw new Error(`Entity ${entityAddress} is not active or does not exist`);
  }
  
  // 2. Verify contract has code
  const code = await provider.getCode(entityAddress);
  if (code === '0x') {
    throw new Error('No contract at this address');
  }
  
  // 3. Optional: Verify it's the correct entity type
  const ENTITY_ABI = ['function entityType() external pure returns (uint8)'];
  const entity = new ethers.Contract(entityAddress, ENTITY_ABI, provider);
  const entityType = await entity.entityType();
  
  console.log(`✓ Entity verified: ${entityType === 0 ? 'Org' : 'Fund'}`);
  return true;
}

Validate Amounts

Always validate amounts before transactions:
function validateAmount(amount: string, decimals: number = 6): ethers.BigNumber {
  // 1. Check it's a valid number
  if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
    throw new Error('Invalid amount: must be a positive number');
  }
  
  // 2. Check precision
  const decimalPlaces = amount.split('.')[1]?.length || 0;
  if (decimalPlaces > decimals) {
    throw new Error(`Too many decimal places (max ${decimals})`);
  }
  
  // 3. Convert to Wei
  const amountWei = ethers.utils.parseUnits(amount, decimals);
  
  // 4. Sanity check (e.g., not more than $1B)
  const maxAmount = ethers.utils.parseUnits('1000000000', decimals);
  if (amountWei.gt(maxAmount)) {
    throw new Error('Amount too large');
  }
  
  return amountWei;
}

// Usage
try {
  const amount = validateAmount('1000.50');
  await entity.donate(amount);
} catch (error) {
  console.error(`Validation failed: ${error.message}`);
}

Check Permissions Before Operations

async function verifyPermissions(
  entityAddress: string,
  walletAddress: string,
  operation: 'donate' | 'transfer' | 'manager'
): Promise<boolean> {
  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
  const entity = new ethers.Contract(entityAddress, ENTITY_ABI, provider);
  
  if (operation === 'donate') {
    // Anyone can donate
    return true;
  }
  
  if (operation === 'transfer' || operation === 'manager') {
    // Check if wallet is manager
    const manager = await entity.manager();
    if (manager.toLowerCase() === walletAddress.toLowerCase()) {
      return true;
    }
    
    console.log('✗ Not the entity manager');
    return false;
  }
  
  return false;
}

Error Handling

Comprehensive Error Handler

class EntityOperationError extends Error {
  constructor(
    message: string,
    public code: string,
    public details?: any
  ) {
    super(message);
    this.name = 'EntityOperationError';
  }
}

async function safeDonation(
  entityAddress: string,
  amount: string
): Promise<ethers.ContractReceipt> {
  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  
  try {
    // 1. Validate inputs
    const amountWei = validateAmount(amount);
    
    // 2. Verify entity
    await safeEntityInteraction(entityAddress);
    
    // 3. Check USDC balance
    const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
    const balance = await usdc.balanceOf(signer.address);
    
    if (balance.lt(amountWei)) {
      throw new EntityOperationError(
        'Insufficient USDC balance',
        'INSUFFICIENT_BALANCE',
        {
          required: ethers.utils.formatUnits(amountWei, 6),
          available: ethers.utils.formatUnits(balance, 6)
        }
      );
    }
    
    // 4. Check allowance
    const allowance = await usdc.allowance(signer.address, entityAddress);
    
    if (allowance.lt(amountWei)) {
      console.log('Approving USDC...');
      const approveTx = await usdc.approve(entityAddress, amountWei);
      await approveTx.wait();
    }
    
    // 5. Execute donation
    const entity = new ethers.Contract(entityAddress, ENTITY_ABI, signer);
    const tx = await entity.donate(amountWei, {
      gasLimit: 200000
    });
    
    const receipt = await tx.wait();
    console.log(`✓ Donation successful: ${receipt.transactionHash}`);
    
    return receipt;
    
  } catch (error) {
    // Handle different error types
    if (error instanceof EntityOperationError) {
      console.error(`❌ ${error.message}`);
      console.error(`Code: ${error.code}`);
      if (error.details) {
        console.error('Details:', error.details);
      }
    } else if (error.code === 'INSUFFICIENT_FUNDS') {
      console.error('❌ Insufficient ETH for gas');
    } else if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
      console.error('❌ Transaction would fail. Check entity status and parameters.');
    } else {
      console.error('❌ Unexpected error:', error.message);
    }
    
    throw error;
  }
}

Retry Logic

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  delayMs: number = 1000
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) {
        throw error;
      }
      
      console.log(`Attempt ${i + 1} failed, retrying in ${delayMs}ms...`);
      await new Promise(resolve => setTimeout(resolve, delayMs));
      delayMs *= 2; // Exponential backoff
    }
  }
  
  throw new Error('Should not reach here');
}

// Usage
const receipt = await withRetry(() => 
  entity.donate(ethers.utils.parseUnits('100', 6))
);

Gas Optimization

Batch Operations

Use multicall pattern for reading multiple entities:
import { ethers } from 'ethers';

interface EntityData {
  address: string;
  balance: string;
  manager: string;
  entityType: 'org' | 'fund';
}

async function batchReadEntities(addresses: string[]): Promise<EntityData[]> {
  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
  
  // Batch all calls
  const calls = addresses.flatMap(address => {
    const entity = new ethers.Contract(address, ENTITY_ABI, provider);
    return [
      entity.balance(),
      entity.manager(),
      entity.entityType()
    ];
  });
  
  const results = await Promise.all(calls);
  
  // Parse results
  const entities: EntityData[] = [];
  for (let i = 0; i < addresses.length; i++) {
    const offset = i * 3;
    entities.push({
      address: addresses[i],
      balance: ethers.utils.formatUnits(results[offset], 6),
      manager: results[offset + 1],
      entityType: results[offset + 2] === 0 ? 'org' : 'fund'
    });
  }
  
  return entities;
}

Optimize Approvals

async function optimizedApproval(
  tokenAddress: string,
  spenderAddress: string,
  amount: ethers.BigNumber
): Promise<void> {
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
  
  // Check current allowance
  const currentAllowance = await token.allowance(signer.address, spenderAddress);
  
  // Only approve if needed
  if (currentAllowance.gte(amount)) {
    console.log('✓ Sufficient allowance already exists');
    return;
  }
  
  // If there's a non-zero allowance, reset to 0 first (for some tokens like USDT)
  if (currentAllowance.gt(0)) {
    console.log('Resetting allowance to 0...');
    await (await token.approve(spenderAddress, 0)).wait();
  }
  
  // Set new allowance
  console.log('Setting new allowance...');
  await (await token.approve(spenderAddress, amount)).wait();
  console.log('✓ Approval complete');
}

Integration Patterns

Pattern 1: Donation Widget

Complete donation flow with user feedback:
interface DonationResult {
  success: boolean;
  txHash?: string;
  error?: string;
  amountDonated?: string;
  amountReceived?: string;
  fee?: string;
}

async function processDonation(
  entityAddress: string,
  amount: string,
  onProgress?: (step: string) => void
): Promise<DonationResult> {
  try {
    onProgress?.('Validating...');
    await safeEntityInteraction(entityAddress);
    const amountWei = validateAmount(amount);
    
    onProgress?.('Checking balance...');
    const signer = new ethers.Wallet(PRIVATE_KEY, provider);
    const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
    const balance = await usdc.balanceOf(signer.address);
    
    if (balance.lt(amountWei)) {
      return {
        success: false,
        error: 'Insufficient USDC balance'
      };
    }
    
    onProgress?.('Approving USDC...');
    await optimizedApproval(USDC_ADDRESS, entityAddress, amountWei);
    
    onProgress?.('Processing donation...');
    const entity = new ethers.Contract(entityAddress, ENTITY_ABI, signer);
    const tx = await entity.donate(amountWei);
    
    onProgress?.('Confirming transaction...');
    const receipt = await tx.wait();
    
    // Parse event for fee info
    const event = receipt.events?.find(e => e.event === 'EntityDonationReceived');
    
    return {
      success: true,
      txHash: receipt.transactionHash,
      amountDonated: amount,
      amountReceived: event?.args?.amountReceived 
        ? ethers.utils.formatUnits(event.args.amountReceived, 6)
        : undefined,
      fee: event?.args?.amountFee
        ? ethers.utils.formatUnits(event.args.amountFee, 6)
        : undefined
    };
    
  } catch (error) {
    return {
      success: false,
      error: error.message
    };
  }
}

// Usage
const result = await processDonation('0x...', '1000', (step) => {
  console.log(`Status: ${step}`);
});

if (result.success) {
  console.log(`✓ Donation successful!`);
  console.log(`  Transaction: ${result.txHash}`);
  console.log(`  Net to entity: ${result.amountReceived} USDC`);
  console.log(`  Fee: ${result.fee} USDC`);
} else {
  console.log(`✗ Donation failed: ${result.error}`);
}

Pattern 2: Grant Workflow

Structured grant recommendation and execution:
enum GrantStatus {
  PENDING = 'pending',
  APPROVED = 'approved',
  EXECUTED = 'executed',
  FAILED = 'failed'
}

interface Grant {
  id: string;
  fundAddress: string;
  orgAddress: string;
  orgName: string;
  amount: string;
  reason: string;
  status: GrantStatus;
  createdAt: number;
  executedAt?: number;
  txHash?: string;
  error?: string;
}

class GrantWorkflow {
  private grants: Map<string, Grant> = new Map();
  
  async createGrant(
    fundAddress: string,
    orgAddress: string,
    orgName: string,
    amount: string,
    reason: string
  ): Promise<string> {
    const grantId = `grant-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    
    const grant: Grant = {
      id: grantId,
      fundAddress,
      orgAddress,
      orgName,
      amount,
      reason,
      status: GrantStatus.PENDING,
      createdAt: Date.now()
    };
    
    // Validate grant
    await this.validateGrant(grant);
    
    this.grants.set(grantId, grant);
    console.log(`✓ Grant ${grantId} created`);
    
    return grantId;
  }
  
  private async validateGrant(grant: Grant): Promise<void> {
    const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
    const fund = new ethers.Contract(grant.fundAddress, ENTITY_ABI, provider);
    const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
    
    // Check org is active
    const isActive = await registry.isActiveEntity(grant.orgAddress);
    if (!isActive) {
      throw new Error('Recipient organization is not active');
    }
    
    // Check fund balance
    const balance = await fund.balance();
    const amountWei = ethers.utils.parseUnits(grant.amount, 6);
    
    if (balance.lt(amountWei)) {
      throw new Error('Insufficient fund balance');
    }
  }
  
  async approveGrant(grantId: string): Promise<void> {
    const grant = this.grants.get(grantId);
    if (!grant) throw new Error('Grant not found');
    
    grant.status = GrantStatus.APPROVED;
    console.log(`✓ Grant ${grantId} approved`);
  }
  
  async executeGrant(grantId: string): Promise<ethers.ContractReceipt> {
    const grant = this.grants.get(grantId);
    if (!grant) throw new Error('Grant not found');
    if (grant.status !== GrantStatus.APPROVED) {
      throw new Error('Grant must be approved before execution');
    }
    
    try {
      const signer = new ethers.Wallet(PRIVATE_KEY, provider);
      const fund = new ethers.Contract(grant.fundAddress, ENTITY_ABI, signer);
      const amountWei = ethers.utils.parseUnits(grant.amount, 6);
      
      console.log(`Executing grant ${grantId}...`);
      const tx = await fund.transferToEntity(grant.orgAddress, amountWei);
      const receipt = await tx.wait();
      
      grant.status = GrantStatus.EXECUTED;
      grant.executedAt = Date.now();
      grant.txHash = receipt.transactionHash;
      
      console.log(`✓ Grant ${grantId} executed: ${receipt.transactionHash}`);
      return receipt;
      
    } catch (error) {
      grant.status = GrantStatus.FAILED;
      grant.error = error.message;
      console.error(`✗ Grant ${grantId} failed: ${error.message}`);
      throw error;
    }
  }
  
  getGrant(grantId: string): Grant | undefined {
    return this.grants.get(grantId);
  }
  
  listGrants(status?: GrantStatus): Grant[] {
    const grants = Array.from(this.grants.values());
    return status ? grants.filter(g => g.status === status) : grants;
  }
}

// Usage
const workflow = new GrantWorkflow();

// Create grant recommendation
const grantId = await workflow.createGrant(
  '0x...', // fund
  '0x...', // org
  'Example Charity',
  '5000',
  'Annual education program support'
);

// Review and approve
await workflow.approveGrant(grantId);

// Execute
await workflow.executeGrant(grantId);

Testing

Local Testing with Hardhat

import { ethers } from 'hardhat';
import { expect } from 'chai';

describe('Entity Integration Tests', () => {
  let entity, usdc, signer;
  
  beforeEach(async () => {
    // Fork mainnet
    await network.provider.request({
      method: 'hardhat_reset',
      params: [{
        forking: {
          jsonRpcUrl: RPC_URL,
          blockNumber: 18000000
        }
      }]
    });
    
    [signer] = await ethers.getSigners();
    
    // Get contracts
    entity = await ethers.getContractAt('Entity', ENTITY_ADDRESS);
    usdc = await ethers.getContractAt('ERC20', USDC_ADDRESS);
    
    // Get USDC from whale
    await network.provider.request({
      method: 'hardhat_impersonateAccount',
      params: [USDC_WHALE]
    });
    
    const whale = await ethers.getSigner(USDC_WHALE);
    await usdc.connect(whale).transfer(
      signer.address,
      ethers.utils.parseUnits('10000', 6)
    );
  });
  
  it('should donate USDC successfully', async () => {
    const amount = ethers.utils.parseUnits('100', 6);
    
    await usdc.approve(entity.address, amount);
    
    const balanceBefore = await entity.balance();
    await entity.donate(amount);
    const balanceAfter = await entity.balance();
    
    expect(balanceAfter.sub(balanceBefore)).to.be.gt(0);
  });
  
  it('should handle insufficient balance', async () => {
    const amount = ethers.utils.parseUnits('100000', 6); // More than we have
    
    await usdc.approve(entity.address, amount);
    
    await expect(entity.donate(amount)).to.be.reverted;
  });
});

Monitoring & Maintenance

Health Check Script

async function healthCheck(entityAddress: string): Promise<{
  healthy: boolean;
  issues: string[];
}> {
  const issues: string[] = [];
  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
  
  try {
    // 1. Check entity is active
    const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
    const isActive = await registry.isActiveEntity(entityAddress);
    if (!isActive) {
      issues.push('Entity is not active');
    }
    
    // 2. Check balance is reasonable
    const entity = new ethers.Contract(entityAddress, ENTITY_ABI, provider);
    const balance = await entity.balance();
    if (balance.isZero()) {
      issues.push('Entity balance is zero');
    }
    
    // 3. Check manager is set
    const manager = await entity.manager();
    if (manager === ethers.constants.AddressZero) {
      issues.push('No manager set');
    }
    
    // 4. Check contract code hasn't changed
    const code = await provider.getCode(entityAddress);
    if (code === '0x') {
      issues.push('No contract code at address');
    }
    
    console.log(`\n🏥 Health Check for ${entityAddress}:`);
    if (issues.length === 0) {
      console.log('✅ All checks passed');
      return { healthy: true, issues: [] };
    } else {
      console.log('⚠️  Issues found:');
      issues.forEach(issue => console.log(`  - ${issue}`));
      return { healthy: false, issues };
    }
    
  } catch (error) {
    issues.push(`Health check failed: ${error.message}`);
    return { healthy: false, issues };
  }
}

// Run health check
await healthCheck('0x...');

Next Steps