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.
Payable fallback. Any raw ETH transfer to the contract is staked into Lido as stETH, and staked is incremented by the actual stETH balance delta. During an active harvest(), this fallback short-circuits via a transient storage flag so ETH returned by the swap target does not get re-staked.
ATTRIBUTE
VALUE
Selector
— (fallback)
Parameters
none
Access
external, payable, permissionless
FLAG
OBSERVATION
☑
Uses transient storage (tload(0)) to detect re-entry from harvest() and early-return — prevents double-accounting
☑
Delta is computed from actual balance, so it tolerates Lido's 1-wei rounding
△
Any address can push ETH in and inflate the principal; the sender has no claim on it
Deposit existing stETH. Pulls amt stETH from msg.sender and increments staked by the actual balance delta. Allows anyone to contribute principal; no accounting is produced to track who deposited what.
ATTRIBUTE
VALUE
Selector
0xb6b55f25
Parameters
amt — stETH amount (in wei) to pull
Access
external, payable, permissionless
FLAG
OBSERVATION
☑
Uses balance-delta rather than amt to update staked, handling Lido's rounding
△
No claim or share token is issued — depositors have no way to withdraw
Convert accrued yield stETH (balance above staked) to ETH by calling the configured target with caller-supplied data. Permissionless so that keeper bots can trigger it, but gated by two on-chain invariants that bound how much can be lost to slippage or misdirection in a single call.
ATTRIBUTE
VALUE
Selector
0xce5c7f61
Parameters
data — arbitrary calldata forwarded to target
Access
external, payable, permissionless
Returns
yield — stETH yield at call time (before the swap)
FLAG
OBSERVATION
☑
Sets transient flag tstore(0, 1) before the call so receive() does not re-stake returned ETH, then clears it
☑
Post-call invariant: ETH balance must grow by at least yield * (1 - slipBps/10000)
☑
Post-call invariant: stETH must remain within 2 wei of staked (protects principal)
△
Invariant compares self balances only — does not consult an external price oracle
△
target already holds unlimited stETH approval, so the swap does not need a per-call allowance
sequenceDiagram
participant Keeper
participant H as LidoHarvester
participant T as target (Curve)
participant S as stETH
Keeper->>H: harvest(data)
H->>S: balanceOf(this)
H->>H: yield = stethBal - staked
H->>H: tstore(0, 1)
H->>T: call(data)
T->>S: transferFrom(this, …, yield)
T-->>H: ETH return (via call)
Note over H: receive() short-circuits<br/>because tload(0) = 1
H->>H: tstore(0, 0)
H->>H: require ETH gain ≥ yield × (1 - slipBps)
H->>H: require stETH + 2 ≥ staked
Admin Functions
Function: stake(uint256 amt)
Stake the contract's ETH into Lido. If amt == 0, stakes the entire ETH balance; otherwise stakes amt. Useful when ETH has accumulated in the contract (e.g., post-harvest) and the owner wants to roll it back into the principal.
Function: withdraw(address to, uint256 val, bytes calldata data, uint256 minGain)
Call an arbitrary address with arbitrary calldata and any ETH value, optionally gated by a minimum increase in a configured asset/holder balance. Intended for the owner to push harvested ETH into downstream destinations (e.g., calling a DEX router that delivers tokens to a multisig).
ATTRIBUTE
VALUE
Selector
0x9483e91a
Parameters
to — call target; val — ETH to attach; data — calldata; minGain — required increase in asset held by holder
Access
onlyOwner, payable
FLAG
OBSERVATION
△
to, val, and data are entirely owner-controlled — this is an unrestricted external call
☑
The minGain guard is optional: only enforced when holder != address(0)
☑
Condition supports native ETH (asset == address(0)) or any ERC-20 token
◇
The current to observed on-chain is the zRouter (0x000000...F600e4), buying ZORG for the zOrg Safe
CONDITION
REQUIREMENT
Caller is owner
msg.sender == owner else Unauthorized()
STEP
ACTION
1
Load holder and asset into memory
2
If holder != 0, record balBefore = holder's asset balance (native ETH if asset == 0)
3
(bool ok,) = to.call{value: val}(data)
4
If conditioned, require balAfter >= balBefore + minGain else ConditionUnmet()
VARIABLE
CHANGE
ETH balance
-= val (plus any ETH the target pulls via the call)
Send amtstETH from the contract to to, decrementing staked by the same amount. The primary principal exit for the owner.
ATTRIBUTE
VALUE
Selector
0xc9f78f5a
Parameters
to — recipient; amt — stETH amount
Access
onlyOwner, payable
FLAG
OBSERVATION
△
Decrements staked by amt before the transfer — if amt > staked, this underflows in unchecked-arithmetic context. The surrounding block is checked (Solidity 0.8.x), so it would revert on underflow
△
Owner can withdraw all principal at any time with no delay
Set the swap target used by harvest(). Revokes the stETH approval granted to the previous target, then grants type(uint256).max stETH approval to the new target.
ATTRIBUTE
VALUE
Selector
0x776d1a01
Parameters
_target — new swap target (zero address disables)
Access
onlyOwner, payable
FLAG
OBSERVATION
△
Grants unlimited stETH allowance to _target — any bug or compromise of the target risks the entire principal
☑
Revokes old target's approval before setting the new one
☑
Setting _target = address(0) disables future harvests (receive/transferFrom would fail on a zero-call without allowance logic)
Set the optional withdraw() post-call condition. When _holder is non-zero, each withdraw() must grow the holder's balance of _asset (or native ETH, if _asset == 0) by at least minGain.
ATTRIBUTE
VALUE
Selector
0xbcc79256
Parameters
_asset — ERC-20 (or 0 for ETH); _holder — address to measure