CanopySplit
Turn Idle Capital into Urban Shade
Created on 9th November 2025
•
CanopySplit
Turn Idle Capital into Urban Shade
The problem CanopySplit solves
1. Passive Climate Impact Without Losing Principal
- Deposit WETH (or any ERC-20 asset) into yield-generating strategies
- Keep 100% of your principal - withdraw anytime
- All profits automatically fund climate projects - tree planting, MRV (monitoring/reporting/verification), maintenance
- No manual donations, no performance fees, no middlemen
2. Transparent, Epoch-Based Allocation
- Dynamic splits: Owner can adjust future epoch weights (e.g., 50% Planters, 30% MRV, 20% Maintenance)
- On-chain receipts: Every distribution emits
Distributedevents with full breakdown - Live tracking: Frontend shows pending donations, lifetime donated, current epoch policy
3. Multi-Source Yield Aggregation
- Aave v3 Integration: Deposits earn yield through aTokens (18-decimal WETH on Sepolia)
- ERC-4626 Wrapper: Standard vault interface for composability
- Uniswap v4 Hook (Local PoC): Swap fees can also donate to the same splitter
4. Role-Based Governance
- Management/Keeper: Can trigger
report()to realize profits → mint donation shares - Owner: Can adjust epoch weights and roll to new epochs
- Users: Just deposit/withdraw; yield flows automatically
Challenges I ran into
Challenge 1: Sepolia Gas Cap Hell (16.7M limit)
The Bug:
Error: server returned an error response: error code -32000:
transaction gas limit too high (cap: 16777216, tx: 25000000)
# Then when we lowered it:
Error: execution reverted: EvmError: OutOfGas (gas: 16711680)
# Foundry said "Deployed to: 0x..." but:
cast code 0xA487... --rpc-url $SEPOLIA_RPC_URL
# Returns: 0x (NO CODE!)
What Was Happening:
- Deploying
YieldDonatingTokenizedStrategyimplementation hit Sepolia's hard per-tx gas cap of 16,777,216 - Contract creation bytecode was too large even with optimizer enabled
- Foundry printed "Deployed to..." even when the tx reverted (misleading!)
- We tried 15.7M gas → OOG. Tried 16.7M → rejected by node. Tried 16.6M → OOG again.
How We Fixed It:
foundry.toml - AGGRESSIVE size reduction
[profile.default]
optimizer = true
optimizer_runs = 1 # Minimize runtime size
via_ir = false # Reduce init complexity
bytecode_hash = "none" # Remove metadata
cbor_metadata = false # Remove CBOR
# Force clean rebuild
forge clean && forge build -vvv
# Deploy just under cap with legacy tx
forge create YieldDonatingTokenizedStrategy \
--gas-limit 0xFD0000 \ # 16,646,144 (under cap)
--gas-price 2gwei \
--legacy \ # Avoid EIP-1559 estimation quirks
--broadcast -vvvv
VERIFY it actually exists:
cast code $IMPL --rpc-url $SEPOLIA_RPC_URL | wc -c # > 2 = success!
cast call $IMPL "apiVersion()(string)" # "1.0.0" ✅
Result: Successfully deployed implementation at 0xE668230D8F3289F9e252cA67Cc7dbEDaE9dB90E5
Challenge 2: USDC Supply Cap Full on Sepolia
The Bug:
Tried to deploy ATokenVault for USDC:
Error: call to non-contract address 0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e
Then when we used correct PoolAddressesProvider:
Deposits would fail with "supply cap reached" or liquidity issues
What Was Happening:
- Aave v3 Sepolia USDC market had supply cap limits
- Test USDC addresses on Sepolia were fragmented (multiple versions)
- PoolAddressesProvider address from mainnet docs doesn't exist on Sepolia
How We Fixed It - THE WETH PIVOT:
Switched to WETH (18 decimals, no supply cap issues)
export USDC_UNDERLYING=0xC558DBdd856501FCd9aaF1E62eae57A9F0629a3c # WETH!
export AAVE_ADDRESSES_PROVIDER=0x012bAC54348C0E635dCAc9D5FB99f06F24136C9A # Correct Sepolia
Deploy ATokenVault for WETH
forge script script/DeployVault.s.sol:DeployVault \
--rpc-url $SEPOLIA_RPC_URL --broadcast -vv
SUCCESS:
ATokenVault (proxy): 0x6938238e57CBe1b4B51Eb3B51389cEf8d3a88521
ProxyAdmin: 0x119e5211b41f44362e6681e7EDF5fC07a46D7A4a
Factory: 0x86b0e29414501643320B05AfF836D699681E450e
Challenge 3: Permit2 Allowance Spender Confusion (Uniswap v4)
The Bug:
Swap reverted with custom error:
Error: custom error 0xe450d38c # InsufficientAllowance-style error
Trace showed:
Currency0::transferFrom(deployer, PoolManager, 1e18)
└─ ← [Revert] custom error 0xe450d38c
What Was Happening:
- We granted Permit2 allowance to PoolManager as spender
- But the Router is the one that calls
permit2.transferFrom()during settle - Permit2 checks:
allowance[user][token][spender]where spender = Router, not PoolManager!
How We Fixed It:
// script/03_Swap.s.sol - BEFORE (wrong):
permit2.approve(address(token0), address(poolManager), type(uint160).max, type(uint48).max);
// AFTER (correct):
// 1. Grant ERC20 approval to Permit2
token0.approve(address(permit2), type(uint256).max);
// 2. Grant Permit2 allowance with Router as spender
permit2.approve(address(token0), address(swapRouter), type(uint160).max, type(uint48).max);
// 3. Also keep direct ERC20 approval to Router (fallback)
token0.approve(address(swapRouter), type(uint256).max);
Result: Swap executed successfully, hook emitted DonationExecuted, recipients received donations!
Tracks Applied (6)
Best use of a Yield Donating Strategy
Most creative use of Octant v2 for public goods
Best tutorial for Octant v2 (written/video)
Best public goods projects
Best use of Aave v3 (Aave Vaults)
Best Use of Uniswap v4 Hooks
Cheer Project
Cheering for a project means supporting a project you like with as little as 0.0025 ETH. Right now, you can Cheer using ETH on Arbitrum, Optimism and Base.
Discussion
Builders also viewed
See more projects on Devfolio
