Skip to content

Contract Analysis

DISCLAIMER // NFA // DYOR

This analysis is based on observations of the contract behavior. We are not smart contract security experts. This document aims to explain what the contract appears to do based on the code. It should not be considered a comprehensive security audit or financial advice. Always verify critical information independently and consult with blockchain security professionals for important decisions.

⊙ generated by robots | curated by humans

Analysis Date: 2026-04-13


Metadata

Primary Contract

PROPERTY VALUE
Contract Address 0x00000000...768E22 (etherscan)
Network Ethereum Mainnet
Contract Type Standalone
Deployment Date 2026-03-05 08:21:59 UTC
Deployment Block 24590040
Contract Creator 0x1C0Aa8cC...855A20 (etherscan)
Creation TX 0xa49ef903...54d046 (tx)
Compiler Version Solidity 0.8.34 (optimizer on, 9,999,999 runs, Cancun EVM)
Total Functions 12 (4 user/receive, 7 admin, 3 view — staked(), slipBps(), owner(), plus auto-generated target(), asset(), holder())
External Contract Dependencies 1 hardcoded (Lido stETH) + up to 3 configurable (swap target, condition asset, condition holder)
Upgrade Mechanism ☒ None — Not Upgradable
Verification Status ☑ Verified on Etherscan (Exact Match)
Audit Status △ Reported "no valid findings" from a Zellic AI-assisted scan (README claim) — no traditional audit report observed
TYPE ADDRESS NOTES
Owner 0x1C0Aa8cC...855A20 (etherscan) EOA; also deployer of zRouter and zQuoter
Deployer Factory 0x5c2271fd...d21822 (etherscan) CREATE2 factory used to mine the vanity address
Lido stETH 0xae7ab965...d7fE84 (etherscan) Hardcoded constant — staking asset
Swap Target (current) 0xDC24316b...f67022 (etherscan) Curve stETH/ETH pool (Vyper); approved to pull unlimited stETH
Condition Asset (current) 0x00a6bA94...2dCb12 (etherscan) ZORG ("zOrg Shares") ERC-20
Condition Holder (current) 0xd14a07B5...5013aa (etherscan) Gnosis Safe (impl 0x41675C09...C7461a (etherscan)); current ZORG balance ≈ 27,811.6

Executive Summary

The LidoHarvester is a minimal (~100 line) standalone contract for harvesting yield from Lido liquid staking. A single owner deposits ETH or stETH; the contract stakes any ETH into Lido to receive stETH, and a running counter called staked records the principal basis at deposit time. Because stETH is a Rebasing Token, the contract's stETH balance grows over time independent of staked. The delta between balance and basis is the yield.

A permissionless harvest(bytes data) function lets anyone call out to a pre-approved swap target with arbitrary calldata, provided that (a) after the call the ETH balance has increased by at least yield * (10000 - slipBps) / 10000, and (b) the stETH balance is still within 2 wei of staked. The harvester currently points target at the Curve stETH/ETH pool, which implies the harvest is meant to execute a stETH→ETH swap of exactly the yield.

A companion withdraw(to, val, data, minGain) function — restricted to the owner — lets the owner spend harvested ETH on an arbitrary call, optionally gated by a minGain increase in a configured holder's balance of a configured asset. In the contract's current configuration, that gate reads as: "when the owner pushes ETH out, the zOrg Safe must end up with at least minGain more ZORG shares." Combined with target = Curve stETH/ETH, the observable flow is Lido yield → ETH → ZORG → zOrg Safe.

The contract has no pause, no timelock, no multisig on the harvester itself, and no upgrade path. It is immutable, but the owner retains broad discretion: the owner can change target, asset, holder, slippage, ownership, approve unlimited stETH to any address in setTarget, and call withdrawStETH to send the entire principal to any recipient. Trust in this contract is effectively trust in the single owner EOA. Given the stated design and the deployer's pattern of self-custodial tooling, this appears intentional.


Architecture

graph TB
    Owner[Owner EOA<br/>0x1C0Aa8cC...855A20]
    subgraph Harvester["LidoHarvester<br/>0x00000000...768E22"]
        Receive[receive<br/>ETH → stake]
        Stake[stake<br/>ETH → stETH]
        Deposit[deposit<br/>stETH in]
        Harvest[harvest<br/>yield → ETH]
        Withdraw[withdraw<br/>ETH out]
        WithdrawSt[withdrawStETH<br/>principal out]
    end
    Lido[Lido stETH<br/>0xae7ab965...d7fE84]
    Target[target<br/>Curve stETH/ETH<br/>0xDC24316b...f67022]
    Asset[asset<br/>ZORG token<br/>0x00a6bA94...2dCb12]
    Holder[holder<br/>zOrg Safe<br/>0xd14a07B5...5013aa]

    Owner -->|ETH| Receive
    Owner -->|stETH| Deposit
    Owner -->|admin| Stake
    Owner -->|calldata| Harvest
    Owner -->|calldata + val| Withdraw
    Owner -->|amt, to| WithdrawSt

    Receive -->|submit| Lido
    Stake -->|submit| Lido
    Deposit -->|transferFrom| Lido
    Harvest -->|swap yield stETH→ETH| Target
    Target -->|ETH back| Harvester
    Withdraw -->|"to.call{value}(data)"| Asset
    Withdraw -.->|measured balance| Holder

    Anyone[Anyone] -->|harvest| Harvest

System Overview

The LidoHarvester is a single-owner vault that holds Lido stETH as its principal position and treats any rebasing growth as harvestable yield. Its three operational primitives are:

  • Stake: accept ETH (via receive() or stake()) or stETH (via deposit()), record the resulting stETH balance delta into staked.
  • Harvest: anyone can call harvest(data) to invoke target.call(data); the contract requires that ETH balance grow by at least yield * (1 - slipBps/10000) and that stETH balance stay within 2 wei of staked.
  • Spend: only the owner can call withdraw(to, val, data, minGain) to push ETH out via an arbitrary call, optionally requiring that a configured holder's balance of a configured asset increase by at least minGain.

Notable observations:

  • The contract does not implement an ERC-20 vault share token. There are no depositor shares and no pro-rata redemption logic. There is one depositor: the owner.
  • The contract has no pause, no timelock, no multisig.
  • The stETH address is a compile-time constant — the contract is not portable to Lido L2 deployments or other liquid-staking tokens without a redeploy.
  • harvest() is the only function anyone can call. All other state-changing functions are owner-gated, except deposit() and receive() which permit any account to contribute principal (but attribute no claim on it).

Design Patterns Used

  • Basis-counter yield accounting: staked records principal; yield is derived as balanceOf(this) - staked. Identical pattern to ERC-4626 share-price tracking but without shares because there is only one beneficiary.
  • Transient storage reentrancy flag (EIP-1153): tstore(0, 1) during harvest() so that if the swap target sends ETH back into this contract, the fallback receive() early-returns instead of re-staking that ETH into Lido (which would inflate the staked balance mid-harvest).
  • Packed storage: slipBps (uint16) and owner (address) share slot 1, saving one SSTORE on each ownership or slippage change.
  • CREATE2 vanity deployment: deployed via factory so that msg.sender == factory, but tx.origin == deployer. The constructor uses tx.origin as the initial owner to survive the factory indirection.
  • Infinite approval to mutable target: setTarget(new) revokes approval on the old target (approve(old, 0)) and grants type(uint256).max to the new one. The current target is the Curve stETH/ETH pool and holds unlimited spend authority over the harvester's stETH.

Access Control

Roles & Permissions

ROLE ASSIGNED BY REVOKABLE CALL COUNT
Owner tx.origin at construction; later transferOwnership Yes — transferOwnership Unlimited
Keeper (anyone) N/A — permissionless N/A Unlimited
Depositor (anyone) N/A — permissionless N/A Unlimited

Permission Matrix

FUNCTION OWNER KEEPER DEPOSITOR ANYONE
receive() (send ETH)
deposit(amt)
harvest(data)
stake(amt)
withdraw(to, val, data, minGain)
withdrawStETH(to, amt)
setTarget(addr)
setSlippage(bps)
setCondition(asset, holder)
transferOwnership(addr)

Time Locks & Delays

ACTION TIME LOCK CAN CANCEL PURPOSE
Transfer ownership ☒ None N/A Immediate — single-step transfer
Change target (re-grants infinite stETH approval) ☒ None N/A Immediate
Change condition (asset + holder) ☒ None N/A Immediate
Change slippage ☒ None N/A Immediate; bounded to ≤10000 bps
Withdraw ETH ☒ None N/A Immediate
Withdraw stETH principal ☒ None N/A Immediate

Economic Model

Funding Sources & Sinks

Inflows:

  • ETH via receive() — staked into Lido immediately; staked counter incremented by resulting stETH balance delta.
  • stETH via deposit(amt) — pulled via transferFrom; staked incremented by actual balance delta.
  • ETH via stake(amt) (owner-only) — stakes either a specific amount or the full contract ETH balance; staked incremented.
  • ETH returned from the swap target during harvest() — not tracked in staked (it is yield-equivalent ETH).

Outflows:

  • stETH via withdrawStETH(to, amt) — owner pulls principal directly; staked decremented.
  • stETH via setTarget infinite approval — any address that becomes target can pull all stETH under the harvester's balance via transferFrom. Effectively a custodial grant.
  • ETH via withdraw(to, val, data, minGain) — owner calls an arbitrary address with arbitrary calldata and any ETH in the contract.
  • stETH consumed during harvest() — the target pulls up to the yield amount; the contract re-checks that stETH balance is still ≥ staked - 2 after the call.

Economic Invariants

The contract attempts to enforce the following on each harvest():

  • ETH gain ≥ yield * (10000 - slipBps) / 10000 — the swap must deliver at least the expected ETH for the yield, minus allowed slippage. Note this compares pre/post contract ETH balance, not a price oracle.
  • Remaining stETH ≥ staked - 2 — the swap is not allowed to consume principal, with a 2-wei tolerance to accommodate Lido's known 1-wei rounding per transfer.

The contract does not enforce that the target.call(data) actually performs a stETH→ETH swap. The economic safety relies on those two balance deltas alone. A target that returned the right amount of ETH from any source (e.g., paid in by the caller) would satisfy the invariants.

Fee Structure

No explicit protocol fees. slipBps is a slippage allowance on the ETH return, not a fee paid to any party. Any ETH above the slippage floor is captured by the contract.


Summary of Observations

The LidoHarvester is a compact, purpose-built yield harvester. The on-chain fingerprint is consistent with the stated README: a single-depositor vault that converts Lido rebase yield to ETH and then to some destination (in its current configuration, ZORG shares delivered to the zOrg Gnosis Safe). The following stand out:

Design choices that appear deliberate and self-consistent:

  • staked is a simple scalar rather than a share ledger — the contract never pretends to support multiple LPs, so it does not need share accounting.
  • ☑ Transient storage reentrancy flag (tstore(0, 1)) is the right primitive for this shape — cheaper than a storage SLOAD/SSTORE and automatically cleared at transaction end.
  • ☑ The post-harvest invariant stETH + 2 ≥ staked captures Lido's known per-transfer rounding loss.
  • harvest() is permissionless, which enables keeper bots. The invariant-gated design means a malicious keeper cannot cause loss beyond the configured slippage.
  • ☑ Infinite approval is revoked on the old target in setTarget, reducing lingering spend authority when the owner rotates venues.
  • ☑ No upgrade path — the contract is immutable once deployed. Owner rotation is possible but logic is fixed.

Trade-offs and trust assumptions:

  • △ The owner is the single trust anchor. withdrawStETH can drain the entire principal, withdraw can push arbitrary ETH to any address with any calldata, and setTarget grants unlimited stETH approval to a new address. This is appropriate for a self-custodial tool; it is not suitable as a pooled product without further controls.
  • target holds an unlimited stETH approval while set. A compromised or malicious target (or a bug in the target's swap logic) could drain stETH up to the full balance. The post-harvest invariant blocks this only when harvesting — it does not prevent the target from independently pulling stETH via its approval outside a harvest call.
  • △ The harvest() ETH-gain check does not reference an external price oracle. It guards against self-inflicted bad trades but not against a mispriced venue. If target is a thinly liquid pool, a legitimate keeper call can still satisfy the invariant while taking substantial MEV or slippage loss within the allowed bps.
  • △ Constructor uses tx.origin as the initial owner. This is required because the contract is deployed via a CREATE2 factory, but tx.origin is unusual for authorization and warrants attention when reading the code.
  • setCondition can be used to sanity-check withdrawals into a specific downstream recipient (e.g., "this push to zRouter must deliver ZORG to the Safe"). This is useful but does not guarantee correct pricing — minGain is set per call by the owner.
  • ◇ A value field is accepted on every owner-gated setter (they are declared payable). This appears to be a gas-optimization pattern rather than a functional requirement.

This document is produced for educational purposes. It reflects what the code does, not whether the code is safe to trust with funds or what it will do under all possible future configurations. Treat it as a starting point for your own review, not as a security audit.


References

RESOURCE NOTES
Etherscan — LidoHarvester source Verified source used as primary artifact
github.com/z0r0z/lido-harvester Author's repo — README describes intent, tests, and deployment target
Lido stETH contract Hardcoded dependency
Curve stETH/ETH pool Current harvest target
EIP-1153 — Transient storage Mechanism used for the harvest() reentrancy flag
zRouter Contract Analysis Same author; used as the to for withdraw() in observed transactions
zFi Project Overview DNZN research index and known infrastructure for the zFi ecosystem

Change Log

DATE AUTHOR NOTES
2026-04-13 Artificial. Generated by robots. Gas: 80 tok
2026-04-14 Denizen. Reviewed, edited, and curated by humans.