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
Distributed
events 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
YieldDonatingTokenizedStrategy
implementation 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.
