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
Creation tx
0x47d4825051…9676d3a841

Token

Name
YOWL
Symbol
YOWL
Decimals
18
Holders
92

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