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 || _
…