Skip to main content

Making Donations

Send USDC or other tokens to entities onchain.

Overview

  • Direct USDC: Approve and donate
  • Token Swap: Any ERC20 → USDC via swap wrapper
  • Native ETH: Wrapped to WETH → USDC
  • Donation fees apply, EntityDonationReceived event emitted
  • Permissionless - anyone can donate

Direct USDC Donations

Single call: invoke donate(amount) on the entity contract.

TypeScript

const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
const entity = new ethers.Contract(ENTITY_ADDRESS, ENTITY_ABI, signer);

const amount = ethers.utils.parseUnits('100', 6);

// Approve entity to spend USDC (required)
await usdc.approve(ENTITY_ADDRESS, amount);
await entity.donate(amount);

Python

w3 = Web3(Web3.HTTPProvider(RPC_URL))
account = Account.from_key(PRIVATE_KEY)
usdc = w3.eth.contract(address=USDC_ADDRESS, abi=USDC_ABI)
entity = w3.eth.contract(address=ENTITY_ADDRESS, abi=ENTITY_ABI)

amount = 100 * (10 ** 6)  # 100 USDC

# Approve entity to spend USDC (required)
approve_tx = usdc.functions.approve(ENTITY_ADDRESS, amount).build_transaction({
    'from': account.address,
    'nonce': w3.eth.get_transaction_count(account.address)
})
signed = account.sign_transaction(approve_tx)
w3.eth.send_raw_transaction(signed.rawTransaction)

# Donate
tx = entity.functions.donate(amount).build_transaction({
    'from': account.address,
    'nonce': w3.eth.get_transaction_count(account.address)
})
signed_tx = account.sign_transaction(tx)
w3.eth.send_raw_transaction(signed_tx.rawTransaction)

Token Swap + Donation

Swap any ERC20 to USDC and donate:
const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const ENTITY_ADDRESS = '0x...';  // Your entity
const SWAP_WRAPPER_ADDRESS = '0x60303F43028AD7198bc4bDF96ccBf734A87444df';

const weth = new ethers.Contract(WETH_ADDRESS, WETH_ABI, signer);
const entity = new ethers.Contract(ENTITY_ADDRESS, ENTITY_ABI, signer);
const amount = ethers.utils.parseEther('1');

// Hardcoded for example - in practice, this should be fetched from
// smart order router / Endaoment's backend
const path = ethers.utils.solidityPack(
  ['address', 'uint24', 'address'],
  [WETH_ADDRESS, 3000, USDC_ADDRESS]
);
const minOutput = ethers.utils.parseUnits('1800', 6);  // Min USDC with slippage
const deadline = Math.floor(Date.now() / 1000) + 600;  // 10 min
const swapData = ethers.utils.defaultAbiCoder.encode(
  ['bytes', 'uint256', 'uint256'],
  [path, minOutput, deadline]
);

await weth.approve(ENTITY_ADDRESS, amount);
await entity.swapAndDonate(SWAP_WRAPPER_ADDRESS, WETH_ADDRESS, amount, swapData);

ETH Donation

const ETH_PLACEHOLDER = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
const amount = ethers.utils.parseEther('0.5');

// Encode swap path (WETH -> USDC)
const path = ethers.utils.solidityPack(
  ['address', 'uint24', 'address'],
  [WETH_ADDRESS, 3000, USDC_ADDRESS]
);
const minOutput = ethers.utils.parseUnits('900', 6);
const deadline = Math.floor(Date.now() / 1000) + 600;
const swapData = ethers.utils.defaultAbiCoder.encode(
  ['bytes', 'uint256', 'uint256'],
  [path, minOutput, deadline]
);

await entity.swapAndDonate(
  SWAP_WRAPPER_ADDRESS,
  ETH_PLACEHOLDER,
  amount,
  swapData,
  { value: amount }
);

Reconcile Balance

Process USDC sent directly to entity:
await entity.reconcileBalance();

Donation Fees

Entity TypeInbound FeeNote
Org1.5% (150 bp)100 USDC → 1.5 USDC fee, 98.5 USDC net
Fund0.05-0.50%Tiered: 0.50% on first 250k,decliningto0.05250k, declining to 0.05% over 100M
Funds also have 1.0% outbound fee on grants. Check exact fee: registry.getDonationFee(entityAddress)
Fee timing and compounding: inbound donation fees apply when the donation is processed (e.g., donate() or swapAndDonate()), reducing the entity’s USDC balance by the fee immediately. Outbound fees apply when funds are transferred via grants. If you donate and then later transfer, each step applies its respective fee (they do not “compound” at one step, but both fees apply across the two actions). Always verify exact fees via the Registry before large transactions.

Common Errors

ErrorFix
EntityInactiveEntity not active. Check: registry.isActiveEntity(entityAddress)
ERC20: insufficient allowanceIncrease approval: usdc.approve(entity, amount)
InvalidActionSwap wrapper not approved. Check: registry.isSwapperSupported(wrapperAddress) before using the wrapper

Advanced Patterns

Calculate Fees

const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
const feeInBp = await registry.getDonationFee(entityAddress);
const feeAmount = amount.mul(feeInBp).div(10000);
const netAmount = amount.sub(feeAmount);

Batch Donations

for (const {address, amount} of recipients) {
  const entity = new ethers.Contract(address, ENTITY_ABI, signer);
  await entity.donate(ethers.utils.parseUnits(amount, 6));
}

Contract Integration

contract DonationHelper {
    function donateToEntity(address entity, uint256 amount) external {
        IERC20(usdc).transferFrom(msg.sender, address(this), amount);
        IERC20(usdc).approve(entity, amount);
        IEntity(entity).donate(amount);
    }
}

Next Steps