Contract detail
Cc0StakingEscrow
Live Robinhood Chain account facts for 0xe4542b52…eeef35b2 with at least 14 observed transactions. The history total is updating as indexing continues.
- Address type
- Smart contract
- Transactions
- 14+ observed
- First indexed block
- #904242
- Latest indexed block
- #972785
- History coverage
- Recent activity available; total is catching up
- Indexed token transfers
- 2
- ERC-20 transfers
- 9
- Emitted events
- 3
- Verification
- Verified
- Compiler
- v0.8.28+commit.7893614a
- Verification match
- partial
- Creation transaction
- 0x55728618…a971dc81
- Created at block
- #904242
- Language
- solidity
- EVM version
- cancun
- License
- none
- Compiler optimization
- Enabled · 200 runs
Related activity
Transactions
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IClankerFeeLocker} from "./interfaces/IClankerFeeLocker.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title Cc0StakingEscrow
* @author cc0.company
* @notice Chain-local sink for the 15% protocol "staking" slice on chains with NO bridge path to Base
* yet (first use: Robinhood Chain, an Arbitrum-Orbit L2 — the OP Standard Bridge trick used by
* {Cc0EthStakingForwarder} on Ethereum L1 does not exist there).
*
* @dev cc0strategy staking ($cc0company) lives on BASE. On a chain where the 15% cannot be bridged to
* the Base {Cc0Staking} pool trustlessly at deploy time, this escrow is wired as the factory's
* enforced staking recipient and simply PARKS the accrued WETH until a bridge route exists:
*
* 1. Launch trading fees accrue in the chain's {ClankerFeeLocker} with this contract as the
* fee owner of the 15% slice.
* 2. `sync()` (permissionless) pulls the accrued WETH out of the locker into the escrow, so the
* parked balance is plainly visible on-chain (`weth.balanceOf(escrow)`).
* 3. Once a credible bridge adapter to the Base feeder exists, the owner wires it with
* `setTarget()` — ONE-TIME, must be a contract. It cannot be changed afterwards, which caps
* the owner's power at a single irreversible routing decision (mirroring the forwarder's
* immutable `baseFeeder`).
* 4. `forward()` (permissionless once wired) pushes ALL held WETH to the target — the caller
* gains nothing, same trust model as {Cc0EthStakingForwarder.forward}.
*
* The WETH is intentionally NOT unwrapped here (unlike the L1 forwarder): the future adapter picks
* the bridge's expected asset form. No ETH path, no receive() — WETH in, WETH out, one direction.
*
* Owner powers, complete list: `setTarget` (once) and `rescueERC20` for tokens that are NOT the
* reward WETH. There is NO way for the owner to extract the parked WETH to an EOA of their choice
* directly; the only exit is the one-time target, which is public and event-logged.
*/
contract Cc0StakingEscrow is Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
/// @notice This chain's ClankerFeeLocker; the escrow is the fee owner of the 15% slice.
IClankerFeeLocker public immutable feeLocker;
/// @notice Canonical WETH on this chain (the reward asset the locker accrues).
IERC20 public immutable weth;
/// @notice Future bridge adapter toward the Base staking pool. Zero until wired; settable ONCE.
address public target;
event Synced(uint256 amount);
event TargetSet(address indexed target);
event Forwarded(address indexed target, uint256 amount);
event ExcessRescued(address indexed token, address indexed to, uint256 amount);
error ZeroAddress();
error TargetAlreadySet();
error TargetNotAContract();
error TargetNotSet();
error NothingToForward();
error CannotRescueWeth();
constructor(address _feeLocker, address _weth, address _owner) Ownable(_owner) {
if (_feeLocker == address(0) || _weth == address(0)) revert ZeroAddress();
feeLocker = IClankerFeeLocker(_feeLocker);
weth = IERC20(_weth);
}
/**
* @notice Permissionless. Pull the accrued WETH (the 15% staking slice) out of the fee locker so
* it is parked HERE, visibly, instead of scattered as locker credit. Safe no-op when
* nothing is waiting.
*/
function sync() external nonReentrant {
uint256 available = feeLocker.availableFees(address(this), address(weth));
if (available == 0) return;
uint256 before = weth.balanceOf(address(this));
feeLocker.claim(address(this), address(weth));
emit Synced(weth.balanceOf(address(this)) - before);
}
/**
* @notice Wire the bridge adapter that will carry the parked WETH toward the Base staking pool.
* ONE-TIME and irreversible — the single routing decision the owner is trusted with.
* Must be a deployed contract (an EOA target would let the owner pocket the rewards).
*/
function setTarget(address _target) external onlyOwner {
if (target != address(0)) revert TargetAlreadySet();
if (_target == address(0)) revert ZeroAddress();
if (_target.code.length == 0) revert TargetNotAContract();
target = _target;
emit TargetSet(_target);
}
/**
* @notice Permissionless once the target is wired: drain the locker slice and push ALL held WETH
* to the target. The destination is fixed — the caller gains nothing.
*/
function forward() external nonReentrant {
address to = target;
if (to == address(0)) revert TargetNotSet();
if (feeLocker.availableFees(address(this), address(weth)) > 0) {
feeLocker.claim(address(this), address(weth));
}
uint256 bal = weth.balanceOf(address(this));
if (bal == 0) revert NothingToForward();
weth.safeTransfer(to, bal);
emit Forwarded(to, bal);
}
/**
* @notice Recover an unrelated token sent here by mistake. WETH (the parked reward asset) is
* excluded — it can only ever leave via `forward()` to the one-time target.
*/
function rescueERC20(address token, address to, uint256 amount) external onlyOwner {
if (token == address(weth)) revert CannotRescueWeth();
if (to == address(0)) revert ZeroAddress();
IERC20(token).safeTransfer(to, amount);
emit ExcessRescued(token, to, amount);
}
/// @notice WETH still waiting in the locker (not yet pulled here). Total parked = this + balanceOf.
function availableInLocker() external view returns (uint256) {
return feeLocker.availableFees(address(this), address(weth));
}
/// @notice Total 15%-slice WETH under this escrow's control (parked + still in the locker).
function totalParked() external view returns (uint256) {
return weth.balanceOf(address(this)) + feeLocker.availableFees(address(this), address(weth));
}
}