NFTs, DAOs & Multisig

Metadata JSON (OpenSea convention): name, description, image, animation_url, attributes: [{trait_type, value}].

38.1 On-chain NFT metadata

Metadata JSON (OpenSea convention): name, description, image, animation_url, attributes: [{trait_type, value}]. Store off-chain media on IPFS or Arweave (content-addressed) rather than mutable HTTP servers. Royalties: ERC-2981 royaltyInfo (advisory; marketplaces may not enforce).

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

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

contract OnChainSVG is ERC721 {
    using Strings for uint256;
    uint256 public nextId;

    constructor() ERC721("OnChain Circles", "CIRC") {}

    function mint() external { _mint(msg.sender, nextId++); }

    function tokenURI(uint256 id) public view override returns (string memory) {
        _requireOwned(id);                                   // reverts for nonexistent tokens (OZ 5.x)
        uint256 hue = uint256(keccak256(abi.encode(id))) % 360;
        string memory svg = string.concat(
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">',
            '<circle cx="50" cy="50" r="40" fill="hsl(', hue.toString(), ',80%,60%)"/></svg>'
        );
        string memory json = string.concat(
            '{"name":"Circle #', id.toString(),
            '","description":"Fully on-chain","image":"data:image/svg+xml;base64,',
            Base64.encode(bytes(svg)), '"}'
        );
        return string
…

38.2 DAOs and governance

A DAO coordinates decisions through on-chain votes. OpenZeppelin's governance stack:

ERC20Votes / ERC721Votes voting power with checkpoints and delegation (snapshot at proposal block defeats flash-loan voting)

Governor + GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl proposals, voting periods, quorum, execution through a timelock delays execution so users can exit before changes take effect (Generate the exact override set for your OZ version with the Wizard — required overrides change between minor versions.)

Proposal lifecycle: propose → Pending (voting delay) → Active (castVote) → Succeeded/Defeated → queue (timelock) → execute. Off-chain voting with Snapshot + on-chain execution (e.g., Safe + oSnap) is a common cheaper alternative. Governance attacks: flash-loaned votes (mitigated by checkpoints), low quorum takeovers (Beanstalk 2022), malicious proposals with hidden payloads.

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

import {Governor} from "@openzeppelin/contracts/governance/Governor.sol";
import {GovernorSettings} from "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import {GovernorCountingSimple} from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotes} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import {GovernorVotesQuorumFraction} from "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import {GovernorTimelockControl} from "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";

contract MyGovernor is
    Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes,
    GovernorVotesQuorumFraction, GovernorTimelockControl
{
    constructor(IVotes token, TimelockController timelock)
        Governor("MyGovernor")
        GovernorSettings(7200 /* voting delay: ~1 day in blocks */, 50400 /* period: ~1 week */, 1000e18 /*
…

38.3 Multisig wallets

A multisig requires M-of-N owners to approve a transaction. Safe (formerly Gnosis Safe) is the standard for treasuries and admin keys. A simplified version:

Production lessons (e.g., the 2025 Bybit incident): multisig security also depends on signers verifying exactly what they sign — use hardware wallets, independent transaction decoding, and simulation.

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

contract MultiSigWallet {
    event Submit(uint256 indexed txId);
    event Confirm(address indexed owner, uint256 indexed txId);
    event Revoke(address indexed owner, uint256 indexed txId);
    event Execute(uint256 indexed txId);

    struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 confirmations; }

    address[] public owners;
    mapping(address => bool) public isOwner;
    uint256 public immutable required;
    Transaction[] public transactions;
    mapping(uint256 => mapping(address => bool)) public confirmed;

    error NotOwner();
    error TxNotFound();
    error AlreadyExecuted();
    error AlreadyConfirmed();
    error NotConfirmed();
    error NotEnoughConfirmations();
    error InvalidConfig();

    modifier onlyOwner() { if (!isOwner[msg.sender]) revert NotOwner(); _; }
    modifier exists(uint256 id) { if (id >= transactions.length) revert TxNotFound(); _; }
    modifier notExecuted(uint256 id) { if (transactions[id].executed) revert AlreadyExecuted(); _; }

    constructor(address[] memory _owners, uint256 _required) {
        if (_owners.length == 0 || _required == 0 || _
…