Chirps

0x84a508e988e7e4897dfdaf68efdaedeb9c5e5545

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
8,629 bytes
ABI entries
64
26 read · 16 write
License
none

Contract information

Address
0x84a508e988e7e4897dfdaf68efdaedeb9c5e5545
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x13d35679f6…fb9abebafb

Token

Name
Chirps
Symbol
CHIRPS
Decimals
18
Holders
322

Read contract (26)

ChirpsTotalCount()uint256
Holder(uint256)address
HolderNumber(address)uint256
HoldersCount()uint256
INITIAL_SUPPLY()uint256
IsHolder(address)bool
OwnerChirp(address, uint256)tuple
OwnerChirpIndex(address, uint256)uint256
OwnerChirpsCount(address)uint256
OwnerChirpsPage(address, uint256, uint256)tuple[]
OwnerOwns(address, uint256)bool
UNIT_PER_CHIRP()uint256
allowance(address, address)uint256
balanceOf(address)uint256
decimals()uint8
hook()address
imageParams()address
isStarted()bool
maxBuy()uint256
name()string
owner()address
ownershipHandoverExpiresAt(address)uint256
pool()address
randomSeedProvider()address
symbol()string
totalSupply()uint256

Events (8)

ApprovalOnChirpBurnedOnChirpMintedOnChirpTransferOwnershipHandoverCanceledOwnershipHandoverRequestedOwnershipTransferredTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AddressCanNotReceiveChirp",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ChirpIndexOutOfRange",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "allowance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientAllowance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSpender",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NewOwnerIsZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoHandoverRequest",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotChirpOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Unauthorized",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      }
    ],
    "name": "OnChirpBurned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      }
    ],
    "name": "OnChirpMinted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      }
    ],
    "name": "OnChirpTransfer",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "pendingOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipHandoverCanceled",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "pendingOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipHandoverRequested",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "ChirpsTotalCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "Holder",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "HolderNumber",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "HoldersCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "INITIAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "IsHolder",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "OwnerChirp",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "id",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "seed",
            "type": "uint256"
          }
        ],
        "internalType": "struct ChirpSeedData",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      }
    ],
    "name": "OwnerChirpIndex",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnerChirpsCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "page",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "pageSize",
        "type": "uint256"
      }
    ],
    "name": "OwnerChirpsPage",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "id",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "seed",
            "type": "uint256"
          }
        ],
        "internalType": "struct ChirpSeedData[]",
        "name": "chirps",
        "type": "tuple[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      }
    ],
    "name": "OwnerOwns",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "newIndex",
        "type": "uint256"
      }
    ],
    "name": "ReorderChirp",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "newIndex",
        "type": "uint256"
      }
    ],
    "name": "ReorderChirpPair",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "UNIT_PER_CHIRP",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cancelOwnershipHandover",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pendingOwner",
        "type": "address"
      }
    ],
    "name": "completeOwnershipHandover",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "imageParams",
    "outputs": [
      {
        "internalType": "contract IImageParams",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "isStarted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "result",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pendingOwner",
        "type": "address"
      }
    ],
    "name": "ownershipHandoverExpiresAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "result",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "randomSeedProvider",
    "outputs": [
      {
        "internalType": "contract IRandomSeedProvider",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "requestOwnershipHandover",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newHook",
        "type": "address"
      }
    ],
    "name": "setHook",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "imageParamsProvider",
        "type": "address"
      }
    ],
    "name": "setImageParamsProvider",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_randomSeedProvider",
        "type": "address"
      }
    ],
    "name": "setRandomSeedProvider",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newPool",
        "type": "address"
      }
    ],
    "name": "start",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "chirpId",
        "type": "uint256"
      }
    ],
    "name": "transferChirp",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256[]",
        "name": "chirpIds",
        "type": "uint256[]"
      }
    ],
    "name": "transferChirpsList",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./chirps/Chirp.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "solady/src/auth/Ownable.sol";
import {IStartableToken} from "./chirps/IStartableToken.sol";

/// @title Chirps token
/// @notice ERC20 token that syncs Chirps (pixel robin NFTs) with balances.
/// OZ ERC20 (scanner-safe) + solady Ownable (renounce pattern).
/// - Mints Chirps on transfers from the pool (with generated art traits)
/// - Burns Chirps when sending to the pool or zero address
/// - Transfers Chirps 1:1 on user-to-user transfers
contract Chirps is Chirp, ERC20, Ownable, IStartableToken {
    error AddressCanNotReceiveChirp();

    uint256 public constant UNIT_PER_CHIRP = 1e18;
    uint256 public constant INITIAL_SUPPLY = 10_000e18;

    uint256 constant MAX_BUY_PRECISION = 100000;
    uint256 constant _startMaxBuyCount = (INITIAL_SUPPLY * 500) / MAX_BUY_PRECISION;
    uint256 constant _addMaxBuyPercentPerSec = 3;

    address immutable _notMintableAccount;
    uint256 _startTime;
    address public hook;
    address public pool;

    constructor(address owner_) ERC20("Chirps", "CHIRPS") {
        _initializeOwner(owner_);
        _mint(owner_, INITIAL_SUPPLY);
        _notMintableAccount = owner_;
    }

    modifier canReceiveChirps(address addr) {
        if (!_canReceiveChirps(addr)) revert AddressCanNotReceiveChirp();
        _;
    }

    modifier onlyHook() {
        require(msg.sender == hook, "only for hook");
        _;
    }

    function setHook(address newHook) external onlyOwner {
        hook = newHook;
    }

    function setImageParamsProvider(address imageParamsProvider) public onlyOwner {
        _setImageParamsProvider(imageParamsProvider);
    }

    function setRandomSeedProvider(address _randomSeedProvider) public onlyOwner {
        _setRandomSeedProvider(_randomSeedProvider);
    }

    function start(address newPool) external onlyHook {
        pool = newPool;
        _startTime = block.timestamp;
    }

    function isStarted() public view returns (bool) {
        return pool != address(0);
    }

    function maxBuy() public view returns (uint256) {
        if (!isStarted()) return 0;
        uint256 count = _startMaxBuyCount +
            (INITIAL_SUPPLY * (block.timestamp - _startTime) * _addMaxBuyPercentPerSec) /
            MAX_BUY_PRECISION;
        if (count > INITIAL_SUPPLY) count = INITIAL_SUPPLY;
        return count;
    }

    // ---- ERC20 hooks (OZ uses _update) ----

    function _update(address from, address to, uint256 amount) internal override {
        super._update(from, to, amount);
        if (pool == address(0)) return;
        _onTokenTransfer(from, to, amount, createRandom());
    }

    function _onTokenTransfer(
        address from, address to, uint256 amount, Random memory random
    ) internal {
        if (pool == address(0)) return;

        // pool -> user: mint chirps with generated traits
        if (from == pool && to != address(0)) {
            if (_notMintableAccount == to) return;
            require(isStarted(), "not started");
            require(amount <= maxBuy(), "buy limit");
            _mintChirps(to, amount / UNIT_PER_CHIRP, random);
            return;
        }

        uint256 from_max_allowed = balanceOf(from) / UNIT_PER_CHIRP;
        uint256 from_count = this.OwnerChirpsCount(from);
        uint256 from_remove_count = from_count > from_max_allowed
            ? from_count - from_max_allowed
            : 0;

        uint256 to_max_allowed = balanceOf(to) / UNIT_PER_CHIRP;
        uint256 to_count = this.OwnerChirpsCount(to);
        uint256 to_receive_allowed = to_count < to_max_allowed
            ? to_max_allowed - to_count
            : 0;

        if (from_remove_count == 0) return;

        if (!_canReceiveChirps(to)) {
            to_receive_allowed = 0;
        }

        uint256 move_qty = from_remove_count < to_receive_allowed
            ? from_remove_count
            : to_receive_allowed;
        if (move_qty > 0) _moveChirps(from, to, move_qty);
        _burnChirps(from, from_remove_count - move_qty);
    }

    function _mintChirps(address user, uint256 qty, Random memory random) internal {
        for (uint256 i = 0; i < qty; i++) {
            _mintChirp(user, random);
        }
    }

    function _burnChirps(address user, uint256 qty) internal {
        for (uint256 i = 0; i < qty; i++) {
            uint count = this.OwnerChirpsCount(user);
            if (count == 0) break;
            uint lastIdx = count - 1;
            uint chirpId = this.OwnerChirp(user, lastIdx).id;
            _burnChirp(user, chirpId);
        }
    }

    function _moveChirps(address from, address to, uint256 qty) internal canReceiveChirps(to) {
        for (uint256 i = 0; i < qty; i++) {
            uint count = this.OwnerChirpsCount(from);
            if (count == 0) break;
            uint lastIdx = count - 1;
            uint chirpId = this.OwnerChirp(from, lastIdx).id;
            _transferChirp(from, to, chirpId);
        }
    }

    function _canReceiveChirps(address addr) internal view returns (bool) {
        return addr != pool && addr != address(0);
    }

    function transferChirp(address to, uint chirpId) external override {
        require(_owns[msg.sender][chirpId], "not owner");
        _transferChirp(msg.sender, to, chirpId);
        _transfer(msg.sender, to, UNIT_PER_CHIRP);
    }

    function transferChirpsList(address to, uint[] calldata chirpIds) external override {
        for (uint i = 0; i < chirpIds.length; i++) {
            require(_owns[msg.sender][chirpIds[i]], "not owner");
            _transferChirp(msg.sender, to, chirpIds[i]);
        }
        _transfer(msg.sender, to, UNIT_PER_CHIRP * chirpIds.length);
    }
}
Chain explorer1096msChain node88ms