0xacfba7ce…4082·#25,798,772·view on Etherscan
o your chosen causes.</p></div></div>
<div class="arrow"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></div>
<div class="step"><span class="ico"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg></span>
<div><p>5. Withdraw</p><p>Your principal is always yours and withdrawable.</p></div></div>
</div>
</div>
<div class="card pane">
<div class="ov-hd" style="padding-bottom:1rem;margin-bottom:.25rem">
<h2>Recent Activity</h2>
<a id="lnkAll" target="_blank" rel="noreferrer" style="font-size:.8125rem;color:var(--pri);text-decoration:none;display:inline-flex;align-items:center;min-height:1.75rem;padding:0 .2rem">View All</a>
</div>
<div class="acty" id="acty"><p class="pos-empty">Loading…</p></div>
</div>
</div></div></section>
<!-- ---------------------------------------------------------------- CAUSES -->
<section id="causes"><div class="wrap">
<h2 class="sec-h">Causes</h2>
<p class="sec-sub">Permissionless: anyone may create one. Claiming pays the recipient, whoever calls it.</p>
<div class="cz-grid" id="czList"></div>
</div></section>
<!-- ---------------------------------------------------------- TRANSPARENCY -->
<section><div class="wrap"><div class="card" style="padding:1.75rem">
<h2 style="font-size:1.1875rem;font-weight:600">Protocol Transparency</h2>
<p class="sec-sub">Firstfruits is non-custodial and open source. Every number on this page is read from one of these.</p>
<div class="kv">
<div><span>Vault Contract</span><a id="lnkVault" target="_blank" rel="noreferrer"></a></div>
<div><span>rETH Token</span><a id="lnkReth" target="_blank" rel="noreferrer"></a></div>
<div><span>Chainlink ETH/USD</span><a id="lnkFeed" target="_blank" rel="noreferrer"></a></div>
<div><span>Rocket Pool Balances</span><a id="lnkNb" target="_blank" rel="noreferrer"></a></div>
<div><span>Multicall3</span><a id="lnkMc" target="_blank" rel="noreferrer"></a></div>
</div>
<p style="margin-top:1.25rem"><a href="https://github.com/unattended-backpack/firstfruits" target="_blank" rel="noreferrer" style="font-size:.875rem;color:var(--pri);text-decoration:none;display:inline-flex;align-items:center;min-height:1.75rem">View source on GitHub ↗</a></p>
</div></div></section>
<div class="band"><div class="wrap">
<div><b>Start giving what grows.</b><p class="mut">Stake ETH. Earn yield. Change the world.</p></div>
<button class="btn btn-pri btn-lg" id="connect2">Connect Wallet →</button>
</div></div>
</main>
<footer><div class="wrap">
<div class="foot-grid">
<div>
<span class="brand"><svg viewBox="0 0 24 24" aria-hidden="true"><g fill="currentColor"><path d="M20.5 2.5C21.6 11 17.5 18 11 21.5 10 13 14 6 20.5 2.5Z"/><path d="M2.2 8.5C7.6 9.8 10.6 14.4 10.9 21.4 5.6 20.1 2.5 15.5 2.2 8.5Z"/></g></svg><span>Firstfruits</span></span>
<p class="sec-sub" style="margin-top:.75rem;max-width:26rem">A charitable-giving vault built with rETH. Stake ETH, donate the yield, keep your principal.</p>
</div>
<div>
<h2>Resources</h2>
<ul>
<li><a href="https://docs.rocketpool.net/liquid-staking/overview" target="_blank" rel="noreferrer">RocketPool Docs</a></li>
<li><a href="https://github.com/unattended-backpack/firstfruits" target="_blank" rel="noreferrer">Contracts</a></li>
<li><a href="https://firstfruits-ui.vercel.app/" target="_blank" rel="noreferrer">Hosted UI</a></li>
</ul>
</div>
<div>
<h2>This page</h2>
<ul>
<li><span class="mut" id="pageWhere">—</span></li>
<li><span class="mut">No server, no indexer, no API</span></li>
<li><span class="mut" id="scanState">—</span></li>
<li id="newerLi" class="hide"><a id="newerLink" href="#">A newer version exists ↗</a></li>
</ul>
</div>
</div>
<div class="foot-end">
<span>VPL Copyleft 2026 Firstfruits. Non-custodial and open source.</span>
<span style="font-style:italic">“Honour the Lord with thy substance, and with the firstfruits of all thine increase.”</span>
</div>
</div></footer>
<div id="wcm" class="modal hide"><div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<b>WalletConnect</b><button class="btn btn-sm" id="wcX">Close</button></div>
<div id="wcQR"></div>
<p class="hint" style="text-align:center">Scan with a mobile wallet, or
<button class="btn btn-sm" id="wcCopy" style="margin-left:.35rem">copy link</button></p>
</div></div>
<div id="wpick" class="modal hide"><div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<b>Connect a wallet</b><button class="btn btn-sm" id="wpX">Close</button></div>
<div id="wpList"></div><p class="hint" id="wpNone"></p>
</div></div>
<script>
"use strict";
/* ------------------------------------------------------------ CONSTANTS */
const VAULT="0xff0000000674ffb069f73038b0a54c7937552088";
const RETH="0xae78736cd615f374d3085123a210448e74fc6393";
const MC3="0xca11bde05977b3631167028862be2a173976ca11";
/* Chainlink ETH/USD. The hosted UI takes its dollar figures from a price API;
this is the same number with an onchain source, so the page keeps its one
rule - it reads contracts, and nothing else. */
const FEED="0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419";
const FEED_STALE=172800;
/* RocketNetworkBalances. Its BalancesUpdated carries totalEth and rethSupply,
whose ratio IS the rETH exchange rate at that block - so the rate's history
is readable from logs, with no archive node and no API. That is where the
APY on the overview card comes from. */
const NETBAL="0x1d9f14c6bfd8358b589964bad8665add248e9473";
const T_BALANCES="0xdd27295717c4fbd48b1840f846e18be6f0b7bd6b55608e697e53b15848cecdf9";
/* The vault's first log - its OwnershipTransferred, at construction. Nothing
about the vault exists before this block, so no scan needs to look earlier. */
const FROM_BLOCK=25606349;
const MONTH_BLOCKS=216000;
const ZERO="0x0000000000000000000000000000000000000000";
const MAXU=(1n<<256n)-1n;
const CHAIN=1;
const C="eth_call",S="eth_sendTransaction",L="latest",I="eth_chainId";
const SCAN="https://etherscan.io/";
const F_NEXTID="2ee997b3",F_TPRINC="6dcf1bb2",F_TRETH="fb470ae9",F_TCLAIM="30b7262b";
const F_CAUSES="538a942a",F_PATRONS="2ef46320",F_WABLE="c6f27781",F_PEND="c7bf1980",F_ALLOCS="c3d38897";
const F_DEPOSIT="d0e30db0",F_DEPDES="8f18037a",F_DEPR="3f683618",F_DEPRDES="fc64748f";
const F_HARVEST="0e5c011e",F_WITHDRAW="2e1a7d4d",F_WITHDRAWR="545bbfab";
const F_WBANK="be2ff4a9",F_WBANKR="3ea428fc",F_CREATE="0d50288f",F_CLAIMR="fbf0c32c";
const F_RATE="e6aa216c",F_BALOF="70a08231",F_ALLOW="dd62ed3e",F_APPROVE="095ea7b3";
const F_ROUND="feaf968c";
/* Vault event topics, in the order the contract declares them. */
const EV={
"0x73a19dd210f1a7f902193214c0ee91dd35ee5b4d920cba8d519eca65a7b488ca":"Deposited",
"0x92ccf450a286a957af52509bc1c9939d1a6a481783e142e41e2499f0bb66ebc6":"Withdrawn",
"0x83d60b29d1376b78e2812b4cc06063732827c1a52185be1924c2da49bddbb7cb":"WithdrawnReth",
"0xb128f8ed07afeffa77252a341109d9b8385f636972cebf4cc805c719f62dc34b":"Designated",
"0x121c5042302bae5fc561fbc64368f297ca60a880878e1e3a7f7e9380377260bf":"Harvested",
"0x43ea0a5f330260fa4368797b43381bb76160b10aeaadc04b44a4ede0eaeb5405":"YieldAllocated",
"0xc2e35536eefd471aca99dde2e590c28ef94138fbbde462a6435a974ef13c3d1b":"YieldBanked",
"0x45100ac04e0460052b03b152802b617a82911c0f9e2cda3cca3ce2beabe084a5":"BankWithdrawn",
"0xbb86fa658458b7cfb6d419368d2a53ea0de9d8471bc613b7c0056dbe4274063d":"BankWithdrawnReth",
"0x70c09a3e66772b4e5784521a55d55e52685a4fe55e1ba0e75a12b00fcefece58":"ClaimedReth",
"0x47d233d25e187b6f6551b80ef65950d72ec02e429466b784516231e6042d134f":"ClaimedEther",
"0x03836492d478204a7f42dac4e381ea2786322654273cd6babf5cfe823fd586cc":"CauseCreated"};
/* -------------------------------------------------------------- ABI I/O */
const strip0x=h=>String(h==null?"":h).replace(/^0x/,"");
const pad32=h=>strip0x(h).padStart(64,"0");
const encUint=v=>BigInt(v).toString(16).padStart(64,"0");
const encAddr=a=>pad32(String(a).toLowerCase());
const encBool=b=>encUint(b?1:0);
const encBytes=b=>{let h="";for(const x of b)h+=x.toString(16).padStart(2,"0");
return encUint(b.length)+(h?h.padEnd(Math.ceil(h.length/64)*64,"0"):"")};
const encStr=s=>encBytes(new TextEncoder().encode(s));
const encAllocs=a=>encUint(a.length)+a.map(x=>encUint(x.id)+encUint(x.bps)).join("");
const word=(h,i)=>strip0x(h).slice(i*64,(i+1)*64);
const uword=(h,i)=>{const w=word(h,i);return w?BigInt("0x"+w):0n};
const aword=(h,i)=>"0x"+word(h,i).slice(24);
const hexNum=n=>"0x"+Number(n).toString(16);
const decStrAt=(h,byteOff)=>{const s=strip0x(h),i=byteOff*2;
const len=Number(BigInt("0x"+s.slice(i,i+64)));if(!len)return"";
const raw=s.slice(i+64,i+64+len*2),b=new Uint8Array(len);
for(let k=0;k<len;k++)b[k]=parseInt(raw.slice(k*2,k*2+2),16);
return new TextDecoder().decode(b)};
const encAgg3=cs=>{let head="",tail="",off=cs.length*32;
for(const c of cs){const d=strip0x(c.data),len=d.length/2;
const body=encAddr(c.to)+encBool(true)+encUint(96)+encUint(len)+d.padEnd(Math.ceil(d.length/64)*64,"0");
head+=encUint(off);tail+=body;off+=body.length/2}
return"0x82ad56cb"+pad32("20")+encUint(cs.length)+head+tail};
const decAgg3=hex=>{const h=strip0x(hex),at=i=>h.slice(i*64,(i+1)*64);
const n=Number(BigInt("0x"+at(1))),out=[];
for(let i=0;i<n;i++){const rel=Number(BigInt("0x"+at(2+i)))/32,w=2+rel;
const ok=BigInt("0x"+at(w))===1n,bo=Number(BigInt("0x"+at(w+1)))/32,lw=w+bo;
const len=Number(BigInt("0x"+at(lw)));
out.push(ok?"0x"+h.slice((lw+1)*64,(lw+1)*64+len*2):null)}
return out};
const mc3=async cs=>cs.length?decAgg3(await rpcRead(C,[{to:MC3,data:encAgg3(cs)},L])):[];
/* ------------------------------------------------------------- NUMBERS */
const fmt=(v,dp=4,dec=18)=>{
if(v==null)return"—";
v=BigInt(v);const neg=v<0n;if(neg)v=-v;
const base=10n**BigInt(dec);
let s=(v/base).toString();
let frac=(v%base).toString().padStart(dec,"0").slice(0,dp).replace(/0+$/,"");
if(v>0n&&v<base&&!frac){dp=Math.min(dec,8);frac=(v%base).toString().padStart(dec,"0").slice(0,dp).replace(/0+$/,"")}
s=s.replace(/\B(?=(\d{3})+(?!\d))/g,",");
return (neg?"-":"")+s+(frac?"."+frac:"")};
const parseAmt=(t,dec=18)=>{
t=String(t||"").trim().replace(/,/g,"");
if(!/^\d*\.?\d*$/.test(t)||t===""||t===".")return null;
const[i,f=""]=t.split(".");
if(f.length>dec)return null;
return BigInt((i||"0")+f.padEnd(dec,"0"))};
const shortAddr=a=>a?a.slice(0,6)+"…"+a.slice(-4):"";
const isAddr=a=>/^0x[0-9a-fA-F]{40}$/.test(String(a||"").trim());
const ago=t=>{
if(!t)return"";
const s=Math.max(0,Math.floor(Date.now()/1000)-t);
if(s<90)return"just now";
if(s<5400)return Math.round(s/60)+"m ago";
if(s<172800)return Math.round(s/3600)+"h ago";
return Math.round(s/86400)+"d ago"};
/* ------------------------------------------------- WALLETCONNECT (PORTED)
Lifted verbatim from zSwap v0.2: the ChaCha20-Poly1305 / X25519 / Ed25519
helpers, the relay client, and a QR encoder - about 16 KB of protocol,
carried so the page need not fetch a 635 KB bundle from a host at run time.
A page that can never be updated cannot depend on a CDN still being there.
It is self-contained: it touches no DOM and no state of this page, which is
what made it portable at all. */
const WC_PID="1e8390ef1c1d8a185e035912a1409749";
const WC_ICON='<svg viewBox="0 0 400 400" fill="none" aria-hidden="true"><circle cx="200" cy="200" r="200" fill="#3396ff"/><path fill="#fff" d="m122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022c2.14 2.086 2.14 5.469 0 7.555l-17.617 17.18c-1.07 1.043-2.804 1.043-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401c-1.07 1.043-2.804 1.043-3.874 0l-17.617-17.18c-2.14-2.086-2.14-5.469 0-7.555zm191.397 35.529 15.679 15.29c2.14 2.086 2.14 5.469 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931c-.535-.522-1.402-.522-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.7015-68.945c-2.1396-2.086-2.1396-5.469 0-7.555l15.6795-15.29c2.1396-2.087 5.6085-2.087 7.7481 0l50.1789 48.932c.535.522 1.402.522 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932c.535.522 1.402.522 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0z"/></svg>';
const WCU=(()=>{
const rl=(v,c)=>(v<<c|v>>>(32-c))>>>0;
const wds=b=>{const o=[];for(let i=0;i<b.length;i+=4)o.push((b[i]|b[i+1]<<8|b[i+2]<<16|b[i+3]<<24)>>>0);return o};
function cblk(x,k,n,c){
const s=[0x61707865,0x3320646e,0x79622d32,0x6b206574,
k[0],k[1],k[2],k[3],k[4],k[5],k[6],k[7],c,n[0],n[1],n[2]];
for(let i=0;i<16;i++)x[i]=s[i];
for(let r=0;r<10;r++){
const q=(a,b,cc,d)=>{
x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],16);
x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],12);
x[a]=(x[a]+x[b])>>>0;x[d]=rl(x[d]^x[a],8);
x[cc]=(x[cc]+x[d])>>>0;x[b]=rl(x[b]^x[cc],7)};
q(0,4,8,12);q(1,5,9,13);q(2,6,10,14);q(3,7,11,15);
q(0,5,10,15);q(1,6,11,12);q(2,7,8,13);q(3,4,9,14);}
for(let i=0;i<16;i++)x[i]=(x[i]+s[i])>>>0;}
function chacha(key,nonce,ctr,src){
const k=wds(key),n=wds(nonce),x=new Uint32Array(16),out=new Uint8Array(src.length);
for(let p=0;p<src.length;p+=64){cblk(x,k,n,ctr++);
for(let i=0;i<64&&p+i<src.length;i++)out[p+i]=src[p+i]^(x[i>>2]>>>((i&3)*8)&255);}
return out}
function poly(msg,key){
const P=(1n<<130n)-5n;
let r=0n,sk=0n;
for(let i=15;i>=0;i--)r=(r<<8n)|BigInt(key[i]);
r&=0x0ffffffc0ffffffc0ffffffc0fffffffn;
for(let i=31;i>=16;i--)sk=(sk<<8n)|BigInt(key[i]);
let a=0n;
for(let i=0;i<msg.length;i+=16){
const n=Math.min(16,msg.length-i);
let b=0n;
for(let j=n-1;j>=0;j--)b=(b<<8n)|BigInt(msg[i+j]);
a=((a+b+(1n<<BigInt(8*n)))*r)%P;}
a=(a+sk)&((1n<<128n)-1n);
const o=new Uint8Array(16);
for(let i=0;i<16;i++){o[i]=Number(a&255n);a>>=8n}
return o}
const p16=n=>n%16?16-n%16:0;
const le64=n=>{const b=new Uint8Array(8);for(let i=0;i<8;i++){b[i]=n&255;n=Math.floor(n/256)}return b};
function tagOf(ct,key,nonce){
const otk=chacha(key,nonce,0,new Uint8Array(32));
const pad=new Uint8Array(p16(ct.length));
const m=new Uint8Array(ct.length+pad.length+16);
m.set(ct);m.set(pad,ct.length);
m.set(le64(0),ct.length+pad.length);m.set(le64(ct.length),ct.length+pad.length+8);
return poly(m,otk)}
const seal=(key,nonce,pt)=>{
const ct=chacha(key,nonce,1,pt),tg=tagOf(ct,key,nonce);
const o=new Uint8Array(ct.length+16);o.set(ct);o.set(tg,ct.length);return o};
const open=(key,nonce,box)=>{
if(box.length<16)return null;
const ct=box.subarray(0,box.length-16),tg=box.subarray(box.length-16);
const w=tagOf(ct,key,nonce);
let bad=0;for(let i=0;i<16;i++)bad|=w[i]^tg[i];
return bad?null:chacha(key,nonce,1,ct)};
const hex=b=>{let s="";for(const x of b)s+=x.toString(16).padStart(2,"0");return s};
const unhex=h=>{const b=new Uint8Array(h.length/2);
for(let i=0;i<b.length;i++)b[i]=parseInt(h.substr(i*2,2),16);return b};
const bin=b=>{let s="";for(let i=0;i<b.length;i+=4096)s+=String.fromCharCode.apply(null,b.subarray(i,i+4096));return s};
const b64=b=>btoa(bin(b));
const unb64=s=>{const t=atob(s),b=new Uint8Array(t.length);
for(let i=0;i<t.length;i++)b[i]=t.charCodeAt(i);return b};
const b64u=b=>b64(b).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");
const utf8=s=>new TextEncoder().encode(s);
const B58="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const b58=b=>{let n=0n;for(const x of b)n=n*256n+BigInt(x);
let s="";while(n>0n){s=B58[Number(n%58n)]+s;n/=58n}
for(const x of b){if(x)break;s="1"+s}return s};
const rnd=n=>crypto.getRandomValues(new Uint8Array(n));
const sha256=async b=>new Uint8Array(await crypto.subtle.digest("SHA-256",b));
const topicOf=async k=>hex(await sha256(k));
const encode=(symKey,payload,senderPub)=>{
const iv=rnd(12),pt=utf8(JSON.stringify(payload));
const sealed=seal(symKey,iv,pt);
const t=senderPub?1:0;
const head=senderPub?1+32:1;
const out=new Uint8Array(head+12+sealed.length);
out[0]=t;
if(senderPub)out.set(senderPub,1);
out.set(iv,head);out.set(sealed,head+12);
return b64(out)};
const decode=(symKey,msg)=>{
const b=unb64(msg);
if(!b.length)return null;
const t=b[0];
if(t!==0&&t!==1)return null;
const head=t===1?33:1;
if(b.length<head+12+16)return null;
const iv=b.subarray(head,head+12),box=b.subarray(head+12);
const pt=open(symKey,iv,box);
if(!pt)return null;
try{return JSON.parse(new TextDecoder().decode(pt))}catch{return null}};
const didKey=pub=>{const b=new Uint8Array(34);b[0]=0xed;b[1]=0x01;b.set(pub,2);
return"did:key:z"+b58(b)};
async function relayJwt(aud){
let kp;
try{kp=await crypto.subtle.generateKey({name:"Ed25519"},true,["sign","verify"])}
catch{throw Error("This browser cannot do Ed25519. WalletConnect needs a newer "
+"browser; an extension wallet works here today.")}
const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey));
const iss=didKey(pub);
const iat=Math.floor(Date.now()/1e3);
const h=b64u(utf8(JSON.stringify({alg:"EdDSA",typ:"JWT"})));
const p=b64u(utf8(JSON.stringify({iss,sub:hex(rnd(32)),aud,iat,exp:iat+86400})));
const sig=new Uint8Array(await crypto.subtle.sign({name:"Ed25519"},kp.privateKey,utf8(h+"."+p)));
return h+"."+p+"."+b64u(sig)}
async function kex(){
let kp;
try{kp=await crypto.subtle.generateKey({name:"X25519"},true,["deriveBits"])}
catch{throw Error("This browser cannot do X25519. WalletConnect needs a newer "
+"browser; an extension wallet works here today.")}
const pub=new Uint8Array(await crypto.subtle.exportKey("raw",kp.publicKey));
return{priv:kp.privateKey,pub}}
async function sessionKey(priv,peerHex){
const peer=await crypto.subtle.importKey("raw",unhex(peerHex),{name:"X25519"},false,[]);
const bits=new Uint8Array(await crypto.subtle.deriveBits({name:"X25519",public:peer},priv,256));
const hk=await crypto.subtle.importKey("raw",bits,"HKDF",false,["deriveBits"]);
const out=await crypto.subtle.deriveBits(
{name:"HKDF",hash:"SHA-256",salt:new Uint8Array(0),info:new Uint8Array(0)},hk,256);
return new Uint8Array(out)}
return{seal,open,chacha,poly,hex,unhex,b64,unb64,b64u,b58,didKey,rnd,sha256,
topicOf,encode,decode,relayJwt,kex,sessionKey};
})();
const WCCLI=(U,WSC)=>{
const RELAY="wss://relay.walletconnect.org";
const T_PROPOSE=1100,T_APPROVE=1101,T_SETTLE=1102,T_SETTLE_RES=1103,T_REQ=1108,T_RES=1109;
const TTL=300,STTL=604800;
let sock=null,nextId=1,waiters={},subs={},onEvent=null,alive=false,gone="";
const rpc