Loading…
Contract
Native balance
0 tZROAR
Live from the node · no monetary value
Activity
3 transactions
First seen block 16,982 · last · nonce 1
Contract
Verified — exact match
The compiled bytecode matches the deployed bytecode exactly, including metadata.0x0000000000000000000000008fd1accb358c6f2551c6ca0f590b709c0653e0e40000000000000000000000006da53af6a423ce52d6ed904ed4d4febbe25e153e000000000000000000000000f03683fa11e5d1fa9336b07d4a2f56ffd8af4269000000000000000000000000de8fc3b73e75987a7ff93300dc0b8abb6c26d4b1000000000000000000000000a26f455c2a998b005c3e868921d958ceff6c4f54
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IYieldStrategy} from "../IYieldStrategy.sol";
import {IPrincipalProtected} from "../IPrincipalProtected.sol";
import {IRoarLendHub, IRoarLendRToken} from "./IRoarLendHub.sol";
/**
* @title RoarLendStrategy
* @notice Routes CollateralVault's tZUSD into the ROARLend tZUSD reserve on Zroarus Devnet.
*
* @dev WHY ROARLEND
*
* Zroarus has no native staking and no Aave deployment, so the only
* lending market borrower collateral can earn in without leaving the
* chain is ROARLend's own CoreLiquidityHub. Supplying there is the same
* trade the retired Aave sleeve made on Base — an unsecured-in-practice
* claim on a smart contract, repayable only while the reserve holds idle
* liquidity — with two differences that matter:
*
* - Withdrawal reliability. ROARLend is young and its tZUSD reserve is
* thin: utilisation can sit at 0% (no yield at all) or spike to full
* (no liquidity to withdraw). The adapter treats both as normal
* operating states, never as surprises: a zero rate is reported, not
* hidden, and an illiquid reserve surfaces as a loud revert.
* - Share accounting. rTokens are ERC-4626-style shares, not 1:1
* rebasing balances, so every conversion floors twice (assets->shares
* on the way in, shares->assets on the way out). The adapter requests
* slightly more than it needs on withdrawal (see `_redeem`) so a
* rounding artefact can never brick a borrower's exit, and measures
* everything that moves rather than trusting any return value.
*
* The strategy never borrows, so it carries no debt and the hub's health
* checks never bind on its withdrawals. Rewards the hub accrues to this
* contract (if the RewardsController ever pays any) are NOT swept: there
* is deliberately no admin withdrawal path, so nothing here can become a
* backdoor later. Claiming them needs a future upgrade, not a hasty one.
*
* VERIFIED ZROARUS DEVNET ADDRESSES (see the constants below). They are
* constructor arguments so this contract is testable against a mock, but
* the constructor pins them on chain 305583 so a fat-fingered deploy
* cannot point borrower collateral at an arbitrary contract.
*/
contract RoarLendStrategy is IYieldStrategy, IPrincipalProtected, AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
/// @notice Held by CollateralVault (via TieredYieldStrategy). The only role that may move money.
bytes32 public constant VAULT_ROLE = keccak256("VAULT_ROLE");
uint256 private constant ZROARUS_DEVNET_CHAIN_ID = 305583;
/**
* tZUSD on Zroarus Devnet, 6 decimals.
* Verified against the canonical devnet record (decimals() answering 6,
* oracle quoting $1.00) and the Zroarus explorer token page.
*/
address internal constant ZROARUS_TZUSD = 0x8Fd1aCCB358c6f2551C6cA0F590B709c0653E0E4;
/**
* ROARLend CoreLiquidityHub on Zroarus Devnet.
* Verified against ROARLend's contracts/deployments/305583.json (`hub`)
* and by reading rTokenOf(tZUSD) live, which returns the receipt below.
*/
address internal constant ROAR_HUB = 0x6Da53aF6a423CE52d6eD904ED4D4FEBbe25e153E;
/**
* rtZUSD — the interest-bearing receipt for the tZUSD reserve.
* Verified against the same deployment record and by the hub's own
* rTokenOf(tZUSD) view.
*/
address internal constant ROAR_R_TZUSD = 0xf03683fA11e5D1fA9336b07d4a2f56FFD8aF4269;
/**
* rToken conversions floor on the way in AND on the way out, so a deposit
* can credit up to ~2 atomic units less than supplied purely from integer
* truncation (bounded while the liquidity index stays below 2 RAY, which
* at plausible supply rates is decades away). One unit of tZUSD is 1e-6
* dollars; tolerating two avoids a deposit that reverts for a rounding
* artefact, and it is far too small to hide a real shortfall.
*/
uint256 private constant ROUNDING_TOLERANCE = 2;
IERC20 public immutable usdc;
IRoarLendHub public immutable hub;
IRoarLendRToken public immutable rToken;
/// @notice Cost basis: tZUSD supplied by the vault, less what it has taken back.
uint256 public principalSupplied;
event Supplied(uint256 amount, uint256 principalAfter);
event Withdrawn(uint256 amount, uint256 principalAfter);
event Harvested(address indexed recipient, uint256 amount);
error ZeroAddress();
error ZeroAmount();
/// @dev A pinned devnet address did not match the verified constant.
error UnexpectedMarketAddress(address expected, address provided);
/// @dev The hub is wired to a different receipt than the strategy was given.
error ReceiptMismatch(address expected, address actual);
/// @dev The vault's tZUSD transfer delivered less than it promised.
error DepositShortfall(uint256 requested, uint256 received);
/// @dev The market took the tZUSD without crediting an equivalent share value.
error MarketCreditShortfall(uint256 expected, uint256 credited);
/// @dev More was asked for than this adapter's position is worth.
error InsufficientMarketBalance(uint256 requested, uint256 available);
/// @dev The market returned less than the full request. Never silently accepted.
error WithdrawalShortfall(uint256 requested, uint256 delivered);
/// @dev A harvest tried to reach past interest and into supplied principal.
error ExceedsAccruedYield(uint256 requested, uint256 available);
/// @dev Harvested interest may only be sent to a vault, never to an arbitrary address.
error InvalidHarvestRecipient(address recipient);
/**
* @param usdcAddress Underlying asset. Must be tZUSD on Zroarus Devnet.
* @param hubAddress ROARLend CoreLiquidityHub.
* @param rTokenAddress rToken for the tZUSD reserve.
* @param admin Holds DEFAULT_ADMIN_ROLE; grants and revokes VAULT_ROLE.
* @param vault TieredYieldStrategy. May be zero and granted later.
*/
constructor(
address usdcAddress,
address hubAddress,
address rTokenAddress,
address admin,
address vault
) {
if (usdcAddress == address(0) || hubAddress == address(0) || rTokenAddress == address(0)) {
revert ZeroAddress();
}
if (admin == address(0)) revert ZeroAddress();
// On Zroarus Devnet the market is not a deployer's choice. Anywhere
// else — a fork, a unit test — the arguments stand on their own,
// because there is no canonical ROARLend deployment to check against.
if (block.chainid == ZROARUS_DEVNET_CHAIN_ID) {
if (usdcAddress != ZROARUS_TZUSD) revert UnexpectedMarketAddress(ZROARUS_TZUSD, usdcAddress);
if (hubAddress != ROAR_HUB) {
revert UnexpectedMarketAddress(ROAR_HUB, hubAddress);
}
if (rTokenAddress != ROAR_R_TZUSD) {
revert UnexpectedMarketAddress(ROAR_R_TZUSD, rTokenAddress);
}
}
usdc = IERC20(usdcAddress);
hub = IRoarLendHub(hubAddress);
rToken = IRoarLendRToken(rTokenAddress);
// The hub names its own receipt for this reserve. If that is not the
// contract the strategy was given, the wiring is wrong and every share
// balance it reads would be someone else's position.
address hubReceipt = hub.rTokenOf(usdcAddress);
if (hubReceipt != rTokenAddress) revert ReceiptMismatch(hubReceipt, rTokenAddress);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
if (vault != address(0)) _grantRole(VAULT_ROLE, vault);
}
// ---------------------------------------------------------------- supply
/**
* @notice Pulls `amount` tZUSD from the vault and supplies it to ROARLend.
* @dev The approval goes to the RTOKEN vault, not the hub: hub.supply
* routes through RToken.hubDeposit, which pulls from the supplier.
* Approving the hub would leave a standing allowance a compromised hub
* could drain, and the supply would fail for lack of allowance to the
* contract actually pulling.
*
* Both the token transfer and the share credit are measured rather
* than assumed, for the same reason as the Aave sleeve this replaces:
* CollateralVault credits the borrower with exactly `amount`, so a
* partial deposit would corrupt their accounting.
*/
function deposit(uint256 amount)
external
override
onlyRole(VAULT_ROLE)
nonReentrant
returns (uint256)
{
if (amount == 0) revert ZeroAmount();
uint256 heldBefore = usdc.balanceOf(address(this));
usdc.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = usdc.balanceOf(address(this)) - heldBefore;
if (received < amount) revert DepositShortfall(amount, received);
uint256 valueBefore = _positionValue();
usdc.forceApprove(address(rToken), amount);
hub.supply(address(usdc), amount, address(this));
uint256 credited = _positionValue() - valueBefore;
if (credited + ROUNDING_TOLERANCE < amount) revert MarketCreditShortfall(amount, credited);
// A reverted-to-zero allowance costs one SSTORE and removes any
// standing approval if the rToken is ever upgraded.
usdc.forceApprove(address(rToken), 0);
principalSupplied += amount;
emit Supplied(amount, principalSupplied);
return amount;
}
// -------------------------------------------------------------- withdraw
/**
* @notice Redeems `amount` tZUSD from ROARLend and sends it to the vault.
* @dev MUST revert rather than deliver less than asked. CollateralVault
* decrements the borrower's `deployed` figure by the requested amount
* before this call returns, so a short withdrawal that returned
* quietly would leave the vault believing collateral is available that
* is in fact still in the market.
*
* Four distinct ways this can fail, all loud:
* 1. The request exceeds the position — caught here, with numbers.
* 2. The reserve is paused — the hub reverts; it bubbles up.
* 3. The reserve is illiquid — the hub reverts NotEnoughLiquidity.
* 4. The hub silently caps at the share balance (it never reverts
* for over-withdrawal) — the delivered amount is measured against
* the vault's own balance change and rejected if it falls short.
*/
function withdraw(uint256 amount)
external
override
onlyRole(VAULT_ROLE)
nonReentrant
returns (uint256)
{
if (amount == 0) revert ZeroAmount();
_redeem(amount, msg.sender);
// Withdrawals draw down principal first; anything beyond it is yield
// being taken alongside it, and principal simply floors at zero.
principalSupplied = amount >= principalSupplied ? 0 : principalSupplied - amount;
emit Withdrawn(amount, principalSupplied);
return amount;
}
/**
* @notice Sends accrued interest — and only interest — to a vault.
* @dev Same two constraints as the sleeve this replaces: the amount is
* bounded by `accruedYield()`, and the recipient must already hold
* VAULT_ROLE. An admin can decide *when* interest comes back, never
* *where* it goes.
*/
function harvest(address recipient, uint256 amount)
external
onlyRole(DEFAULT_ADMIN_ROLE)
nonReentrant
returns (uint256)
{
if (amount == 0) revert ZeroAmount();
if (!hasRole(VAULT_ROLE, recipient)) revert InvalidHarvestRecipient(recipient);
uint256 value = _positionValue();
uint256 available = value > principalSupplied ? value - principalSupplied : 0;
if (amount > available) revert ExceedsAccruedYield(amount, available);
_redeem(amount, recipient);
// principalSupplied deliberately untouched: only the surplus moved.
emit Harvested(recipient, amount);
return amount;
}
/**
* @dev Redeems from ROARLend and proves the recipient actually received it.
*
* The request is INFLATED past `amount` on purpose. Both rToken
* conversions floor, so asking for exactly `amount` can redeem a share
* balance worth `amount - 1` — and the hub would honour that silently,
* leaving this call to revert on its own measurement. Requesting the
* asset value of (shares-for-amount + 1 share) + 1 unit guarantees the
* hub computes at least shares-for-amount + 1, whose redemption is
* provably >= amount. Any over-delivery (at most a few atomic units)
* lands in the vault, which is where the borrower's money lives anyway.
*
* The delivered amount is measured as a balance delta on the recipient
* rather than read from the hub's return value, because a compromised
* or upgraded hub could return a number it did not honour.
*/
function _redeem(uint256 amount, address recipient) private {
uint256 position = _positionValue();
if (amount > position) revert InsufficientMarketBalance(amount, position);
uint256 sharesForAmount = rToken.convertToShares(amount);
uint256 request = rToken.convertToAssets(sharesForAmount + 1) + 1;
uint256 recipientBefore = usdc.balanceOf(recipient);
hub.withdraw(address(usdc), request, recipient);
uint256 delivered = usdc.balanceOf(recipient) - recipientBefore;
if (delivered < amount) revert WithdrawalShortfall(amount, delivered);
}
// ----------------------------------------------------------------- views
/// @notice Redeemable value of the whole position, in tZUSD.
/// @dev Shares marked at the hub's live liquidity index. This is a mark,
/// not a guarantee — it says nothing about whether the reserve
/// currently holds the liquidity to honour it.
function totalValue() external view override returns (uint256) {
return _positionValue();
}
/**
* @notice Interest earned above the supplied principal. Zero on a loss.
* @dev Floors at zero rather than reverting: CollateralVault and the
* servicing UI read this on paths that also release borrower funds,
* and a reverting view would turn a bad day for the market into an
* inability to operate the vault at all.
*/
function accruedYield() external view override returns (uint256) {
uint256 value = _positionValue();
uint256 basis = principalSupplied;
return value > basis ? value - basis : 0;
}
/// @inheritdoc IYieldStrategy
function marketName() external pure override returns (string memory) {
return "ROARLend tZUSD (Zroarus Devnet)";
}
/**
* @inheritdoc IPrincipalProtected
* @dev tZUSD is supplied to a lending market and never exchanged for
* another asset, so principal carries no price exposure. It is NOT
* risk-free: smart-contract failure and ROARLend bad debt can still
* take it below cost — and on a young devnet reserve, illiquidity is
* the likelier failure. This flag means "not sold into a traded
* market", which is the exposure CollateralVault is gating on.
*/
function isPrincipalProtected() external pure override returns (bool) {
return true;
}
/// @inheritdoc IPrincipalProtected
function principalAtMarketRisk() external pure override returns (uint256) {
return 0;
}
/// @dev Position marked in underlying at the live index. One line so every
/// reader — deposit, withdraw, harvest, views — marks identically.
function _positionValue() private view returns (uint256) {
return rToken.convertToAssets(rToken.balanceOf(address(this)));
}
}
Bytecode hash 0xeab99c6dc0e15d4806615b4ec129cb43c1e7fb37cbf3fb43bdd39699201d8fa4
[
{
"type": "constructor",
"inputs": [
{
"name": "usdcAddress",
"type": "address",
"internalType": "address"
},
{
"name": "hubAddress",
"type": "address",
"internalType": "address"
},
{
"name": "rTokenAddress",
"type": "address",
"internalType": "address"
},
{
"name": "admin",
"type": "address",
"internalType": "address"
},
{
"name": "vault",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "nonpayable"
},
{
"name": "AccessControlBadConfirmation",
"type": "error",
"inputs": []
},
{
"name": "AccessControlUnauthorizedAccount",
"type": "error",
"inputs": [
{
"name": "account",
"type": "address",
"internalType": "address"
},
{
"name": "neededRole",
"type": "bytes32",
"internalType": "bytes32"
}
]
},
{
"name": "DepositShortfall",
"type": "error",
"inputs": [
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "received",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "ExceedsAccruedYield",
"type": "error",
"inputs": [
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "available",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "InsufficientMarketBalance",
"type": "error",
"inputs": [
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "available",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "InvalidHarvestRecipient",
"type": "error",
"inputs": [
{
"name": "recipient",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "MarketCreditShortfall",
"type": "error",
"inputs": [
{
"name": "expected",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "credited",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "ReceiptMismatch",
"type": "error",
"inputs": [
{
"name": "expected",
"type": "address",
"internalType": "address"
},
{
"name": "actual",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ReentrancyGuardReentrantCall",
"type": "error",
"inputs": []
},
{
"name": "SafeERC20FailedOperation",
"type": "error",
"inputs": [
{
"name": "token",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "UnexpectedMarketAddress",
"type": "error",
"inputs": [
{
"name": "expected",
"type": "address",
"internalType": "address"
},
{
"name": "provided",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "WithdrawalShortfall",
"type": "error",
"inputs": [
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "delivered",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "ZeroAddress",
"type": "error",
"inputs": []
},
{
"name": "ZeroAmount",
"type": "error",
"inputs": []
},
{
"name": "Harvested",
"type": "event",
"inputs": [
{
"name": "recipient",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "RoleAdminChanged",
"type": "event",
"inputs": [
{
"name": "role",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "previousAdminRole",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "newAdminRole",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
}
],
"anonymous": false
},
{
"name": "RoleGranted",
"type": "event",
"inputs": [
{
"name": "role",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "sender",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "RoleRevoked",
"type": "event",
"inputs": [
{
"name": "role",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "sender",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "Supplied",
"type": "event",
"inputs": [
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "principalAfter",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "Withdrawn",
"type": "event",
"inputs": [
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "principalAfter",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "DEFAULT_ADMIN_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "VAULT_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "accruedYield",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "deposit",
"type": "function",
"inputs": [
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "getRoleAdmin",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "grantRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "harvest",
"type": "function",
"inputs": [
{
"name": "recipient",
"type": "address",
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "hasRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "hub",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IRoarLendHub"
}
],
"stateMutability": "view"
},
{
"name": "isPrincipalProtected",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "pure"
},
{
"name": "marketName",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "pure"
},
{
"name": "principalAtMarketRisk",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "pure"
},
{
"name": "principalSupplied",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "rToken",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IRoarLendRToken"
}
],
"stateMutability": "view"
},
{
"name": "renounceRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "callerConfirmation",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "revokeRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "supportsInterface",
"type": "function",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "totalValue",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "usdc",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IERC20"
}
],
"stateMutability": "view"
},
{
"name": "withdraw",
"type": "function",
"inputs": [
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
}
]