DopplerDN404
0x062939bb32eabd47a522fe48872f41e04017a095
Verification
Verified
0.8.26+commit.8a97fa7a
Type
Contract
9,012 bytes
ABI entries
57
16 read · 10 write
License
none
Contract information
- Address
- 0x062939bb32eabd47a522fe48872f41e04017a095
- Chain
- Robinhood Chain (4663)
- Compiler
- 0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x37A9Fa204a…76C854Bc9d
- Creation tx
- 0x47d4825051…9676d3a841
Token
- Name
- YOWL
- Symbol
- YOWL
- Decimals
- 18
- Holders
- 92
- Market
- View token page →
Read contract (16)
allowance(address, address) → uint256
balanceOf(address) → uint256
baseURI() → string
decimals() → uint8
frozenBalances(address) → uint256
getSkipNFT(address) → bool
isPoolUnlocked() → bool
mirrorERC721() → address
name() → string
owner() → address
pool() → address
symbol() → string
tokenByIndex(uint256) → uint256
tokenOfOwnerByIndex(address, uint256) → uint256
totalSupply() → uint256
unit() → uint256
Events (4)
ApprovalOwnershipTransferredSkipNFTSetTransfer
ABI
[
{
"inputs": [
{
"internalType": "string",
"name": "name_",
"type": "string"
},
{
"internalType": "string",
"name": "symbol_",
"type": "string"
},
{
"internalType": "uint256",
"name": "initialTokenSupply",
"type": "uint256"
},
{
"internalType": "address",
"name": "initialSupplyOwner",
"type": "address"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
},
{
"internalType": "string",
"name": "baseURI_",
"type": "string"
},
{
"internalType": "uint256",
"name": "unit_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ApprovalCallerNotOwnerNorApproved",
"type": "error"
},
{
"inputs": [],
"name": "DNAlreadyInitialized",
"type": "error"
},
{
"inputs": [],
"name": "DNNotInitialized",
"type": "error"
},
{
"inputs": [],
"name": "FnSelectorNotRecognized",
"type": "error"
},
{
"inputs": [],
"name": "GlobalIndexOutOfBounds",
"type": "error"
},
{
"inputs": [],
"name": "InitialSupplyNotMultipleOfUnit",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientAllowance",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientBalance",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientBalanceToFreeze",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientUnfrozenBalance",
"type": "error"
},
{
"inputs": [],
"name": "InvalidUnit",
"type": "error"
},
{
"inputs": [],
"name": "LinkMirrorContractFailed",
"type": "error"
},
{
"inputs": [],
"name": "MirrorAddressIsZero",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "OwnerIndexOutOfBounds",
"type": "error"
},
{
"inputs": [],
"name": "PoolLocked",
"type": "error"
},
{
"inputs": [],
"name": "SenderNotMirror",
"type": "error"
},
{
"inputs": [],
"name": "TokenDoesNotExist",
"type": "error"
},
{
"inputs": [],
"name": "TokenIDIndexOutOfBounds",
"type": "error"
},
{
"inputs": [],
"name": "TotalSupplyOverflow",
"type": "error"
},
{
"inputs": [],
"name": "TransferCallerNotOwnerNorApproved",
"type": "error"
},
{
"inputs": [],
"name": "TransferFromIncorrectOwner",
"type": "error"
},
{
"inputs": [],
"name": "TransferToZeroAddress",
"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": "amount",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "status",
"type": "bool"
}
],
"name": "SkipNFTSet",
"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": "amount",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"stateMutability": "payable",
"type": "fallback"
},
{
"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": "amount",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "baseURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256[]",
"name": "tokenIDIndexes",
"type": "uint256[]"
}
],
"name": "freezeTokenIDsByIndex",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "frozenBalances",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "getSkipNFT",
"outputs": [
{
"internalType": "bool",
"name": "result",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "isPoolUnlocked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pool_",
"type": "address"
}
],
"name": "lockPool",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "mirrorERC721",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "baseURI_",
"type": "string"
}
],
"name": "setBaseURI",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "skipNFT",
"type": "bool"
}
],
"name": "setSkipNFT",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"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": "amount",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"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": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "unit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "unlockPool",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;
import { Ownable } from "@openzeppelin/access/Ownable.sol";
import { DN404 } from "lib/dn404/src/DN404.sol";
import { LibString } from "solady/utils/LibString.sol";
import { DopplerDN404Mirror } from "src/tokens/DopplerDN404Mirror.sol";
/// @dev Thrown when trying to transfer tokens into the pool while it is locked
error PoolLocked();
error InvalidUnit();
error InitialSupplyNotMultipleOfUnit();
error InsufficientBalanceToFreeze();
error TokenIDIndexOutOfBounds();
error InsufficientUnfrozenBalance();
error OwnerIndexOutOfBounds();
error GlobalIndexOutOfBounds();
/// @title DopplerDN404
/// @notice DN404-based asset token used by the Doppler protocol. From Doppler's point of view,
/// this behaves like a standard ERC20, while also exposing an ERC721 mirror.
contract DopplerDN404 is DN404, Ownable {
uint256 private immutable UNIT;
// ERC20 metadata
string private _name;
string private _symbol;
// ERC721 metadata base URI
string private _baseURI;
/// @notice Address of the liquidity pool used for migration locking
address public pool;
/// @notice Whether the pool can receive tokens (unlocked) or not
bool public isPoolUnlocked;
mapping(address => uint256) public frozenBalances;
constructor(
string memory name_,
string memory symbol_,
uint256 initialTokenSupply,
address initialSupplyOwner,
address owner_,
string memory baseURI_,
uint256 unit_
) Ownable(owner_) {
if (unit_ == 0) revert InvalidUnit();
if (initialTokenSupply % unit_ != 0) revert InitialSupplyNotMultipleOfUnit();
_name = name_;
_symbol = symbol_;
_baseURI = baseURI_;
UNIT = unit_;
address mirror = address(new DopplerDN404Mirror(msg.sender));
_initializeDN404(initialTokenSupply, initialSupplyOwner, mirror);
}
// ---- ERC20 metadata ----
function name() public view override returns (string memory) {
return _name;
}
function symbol() public view override returns (string memory) {
return _symbol;
}
function unit() public view returns (uint256) {
return UNIT;
}
function baseURI() public view returns (string memory) {
return _baseURI;
}
// ---- ERC721 metadata ----
function _tokenURI(uint256 tokenId) internal view override returns (string memory result) {
if (bytes(_baseURI).length != 0) {
result = string(abi.encodePacked(_baseURI, LibString.toString(tokenId)));
}
}
/// @notice Updates the base URI for NFT metadata
function setBaseURI(string calldata baseURI_) external onlyOwner {
_baseURI = baseURI_;
}
/// @notice Locks the pool, preventing it from receiving tokens
/// @param pool_ Address of the pool to lock
function lockPool(address pool_) external onlyOwner {
pool = pool_;
isPoolUnlocked = false;
}
function _unit() internal view override returns (uint256) {
return UNIT;
}
/// @notice Unlocks the pool, allowing it to receive tokens
function unlockPool() external onlyOwner {
isPoolUnlocked = true;
}
function freezeTokenIDsByIndex(uint256[] calldata tokenIDIndexes) external {
uint256 amountToFreeze = tokenIDIndexes.length * UNIT;
uint256 currentFrozen = frozenBalances[msg.sender];
if (balanceOf(msg.sender) < currentFrozen + amountToFreeze) {
revert InsufficientBalanceToFreeze();
}
// Number of NFTs already frozen corresponds to the first `startIndex` slots.
uint256 startIndex = currentFrozen / UNIT;
frozenBalances[msg.sender] = currentFrozen + amountToFreeze;
DN404Storage storage $ = _getDN404Storage();
AddressData storage fromAddressData = $.addressData[msg.sender];
Uint32Map storage fromOwned = $.owned[msg.sender];
Uint32Map storage oo = $.oo;
uint256 fromIndex = fromAddressData.ownedLength;
// Move the selected token IDs to the beginning of the owned list, in-order.
// DN404 burns/transfers from the end of the list first, so front items are protected.
for (uint256 i = 0; i < tokenIDIndexes.length; i++) {
uint256 tokenIDIndex = tokenIDIndexes[i];
if (tokenIDIndex >= fromIndex) revert TokenIDIndexOutOfBounds();
if (tokenIDIndex < startIndex) continue; // Already frozen.
if (tokenIDIndex != startIndex) {
uint32 tokenIDAtIndex = _get(fromOwned, tokenIDIndex);
uint32 tokenIDAtStartIndex = _get(fromOwned, startIndex);
// Swap positions in owned list.
_set(fromOwned, tokenIDIndex, tokenIDAtStartIndex);
_set(fromOwned, startIndex, tokenIDAtIndex);
// Update owned index metadata for both swapped token IDs.
_set(oo, _ownedIndex(tokenIDAtIndex), uint32(startIndex));
_set(oo, _ownedIndex(tokenIDAtStartIndex), uint32(tokenIDIndex));
}
unchecked {
startIndex++;
}
}
}
/// @inheritdoc DN404
function _transfer(address from, address to, uint256 amount) internal override {
if (to == pool && isPoolUnlocked == false) revert PoolLocked();
if (from != address(0)) {
uint256 balance = balanceOf(from);
uint256 frozen = frozenBalances[from];
if (frozen > balance || amount > balance - frozen) revert InsufficientUnfrozenBalance();
}
super._transfer(from, to, amount);
}
/// @dev Override direct NFT transfers to adjust frozen balance if a frozen ID is moved.
/// Maintains the invariant that the first `frozenBalances[from]/UNIT` indices remain the
/// frozen set (minus the transferred one), with minimal swaps:
/// - Move the last frozen token into the index of the transferred frozen token.
/// - Move the list's last element (that super() moved into the transferred index) to the
/// first unfrozen index (i.e., the new boundary) if it exists.
function _transferFromNFT(address from, address to, uint256 id, address msgSender) internal override {
DN404Storage storage $ = _getDN404Storage();
uint256 F = frozenBalances[from] / UNIT;
bool wasFrozen;
uint256 i; // index of `id` in `from`'s owned list before transfer.
if (F != 0) {
i = _get($.oo, _ownedIndex(id));
wasFrozen = i < F;
}
super._transferFromNFT(from, to, id, msgSender);
if (wasFrozen) {
// Compute target index for last frozen before decrement.
uint256 j = F - 1;
// New owned length after super.
uint256 newLen = $.addressData[from].ownedLength;
// Perform the minimal swaps to preserve the frozen prefix (minus `id`).
// Only operate on indices that are < newLen (valid part of the list).
if (j < newLen) {
Uint32Map storage fromOwned = $.owned[from];
Uint32Map storage oo = $.oo;
// The element currently at `i` is what super() moved from the previous last index.
// If `i` is within range, swap `i` <-> `j`.
if (i < newLen) {
if (i != j) {
uint32 idAtI = _get(fromOwned, i);
uint32 idAtJ = _get(fromOwned, j);
_set(fromOwned, i, idAtJ);
_set(fromOwned, j, idAtI);
_set(oo, _ownedIndex(idAtI), uint32(j));
_set(oo, _ownedIndex(idAtJ), uint32(i));
}
}
}
unchecked {
frozenBalances[from] -= UNIT;
}
}
}
/// @notice Returns the token ID owned by `owner` at `index` in their ordered NFT list.
/// Mirrors the behavior of ERC721Enumerable's `tokenOfOwnerByIndex` using DN404's
/// internal owned array. Reverts if `index` is out of bounds.
/// @param owner The owner address to query.
/// @param index Zero-based index into the owner's ordered NFT list.
/// @return id The token ID at the given index.
function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 id) {
DN404Storage storage $ = _getDN404Storage();
uint256 len = $.addressData[owner].ownedLength;
if (index >= len) revert OwnerIndexOutOfBounds();
Uint32Map storage owned = $.owned[owner];
id = _get(owned, index);
}
/// @notice Returns the token ID at the given global `index` across all existing NFTs.
/// Mirrors the behavior of ERC721Enumerable's `tokenByIndex`, iterating over the
/// DN404 ownership map in token ID order. Reverts if `index` is out of bounds.
/// @dev Intended for off-chain/indexer reads; cost is linear in the minted token ID space.
/// @param index Zero-based index into the global ordered list of existing token IDs.
/// @return id The token ID at the given global index.
function tokenByIndex(uint256 index) external view returns (uint256 id) {
DN404Storage storage $ = _getDN404Storage();
uint256 total = $.totalNFTSupply;
if (index >= total) revert GlobalIndexOutOfBounds();
// Iterate over the possible token ID space [start .. start + idLimit - 1]
// and count existing tokens until we reach `index`.
uint256 start = _toUint(_useOneIndexed());
uint256 idLimit = $.totalSupply / _unit();
Uint32Map storage oo = $.oo;
uint256 found;
// Scan at most `idLimit` slots, which bounds the NFT ID space.
for (uint256 i = 0; i < idLimit; i++) {
uint256 candidate = start + i;
if (_get(oo, _ownershipIndex(candidate)) != 0) {
if (found == index) {
return candidate;
}
unchecked {
++found;
}
}
}
revert GlobalIndexOutOfBounds();
}
}
Chain explorer5942msChain node88ms