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 Type | Inbound Fee | Note |
|---|
| Org | 1.5% (150 bp) | 100 USDC → 1.5 USDC fee, 98.5 USDC net |
| Fund | 0.05-0.50% | Tiered: 0.50% on first 250k,decliningto0.05100M |
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
| Error | Fix |
|---|
EntityInactive | Entity not active. Check: registry.isActiveEntity(entityAddress) |
ERC20: insufficient allowance | Increase approval: usdc.approve(entity, amount) |
InvalidAction | Swap 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