├── Base64.sol ├── CompcardV1.sol ├── CompcardV3.sol ├── CompcardV4.sol └── README.md /Base64.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /// [MIT License] 6 | /// @title Base64 7 | /// @notice Provides a function for encoding some bytes in base64 8 | /// @author Brecht Devos 9 | library Base64 { 10 | bytes internal constant TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 11 | 12 | /// @notice Encodes some bytes to the base64 representation 13 | function encode(bytes memory data) internal pure returns (string memory) { 14 | uint256 len = data.length; 15 | if (len == 0) return ""; 16 | 17 | // multiply by 4/3 rounded up 18 | uint256 encodedLen = 4 * ((len + 2) / 3); 19 | 20 | // Add some extra buffer at the end 21 | bytes memory result = new bytes(encodedLen + 32); 22 | 23 | bytes memory table = TABLE; 24 | 25 | assembly { 26 | let tablePtr := add(table, 1) 27 | let resultPtr := add(result, 32) 28 | 29 | for { 30 | let i := 0 31 | } lt(i, len) { 32 | 33 | } { 34 | i := add(i, 3) 35 | let input := and(mload(add(data, i)), 0xffffff) 36 | 37 | let out := mload(add(tablePtr, and(shr(18, input), 0x3F))) 38 | out := shl(8, out) 39 | out := add(out, and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)) 40 | out := shl(8, out) 41 | out := add(out, and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)) 42 | out := shl(8, out) 43 | out := add(out, and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)) 44 | out := shl(224, out) 45 | 46 | mstore(resultPtr, out) 47 | 48 | resultPtr := add(resultPtr, 4) 49 | } 50 | 51 | switch mod(len, 3) 52 | case 1 { 53 | mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) 54 | } 55 | case 2 { 56 | mstore(sub(resultPtr, 1), shl(248, 0x3d)) 57 | } 58 | 59 | mstore(result, encodedLen) 60 | } 61 | 62 | return string(result); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CompcardV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | 7 | import "./Base64.sol"; 8 | 9 | contract CompcardV1 is ERC721 { 10 | mapping (uint256 => string) private cards; 11 | 12 | /// The next claimable tokenId. 13 | uint256 public nextTokenId; 14 | 15 | error NotSupported(); 16 | 17 | constructor() ERC721("Compcard V1", "CCV1") { 18 | } 19 | 20 | /// Claim a new Compcard PFP. 21 | /// @param url A URL pointing to your image, preferably on a content-addressible URL, such as IPFS. 22 | /// @return tokenId The minted tokenId. 23 | function claim(string calldata url) external returns (uint256 tokenId) { 24 | tokenId = ++nextTokenId; 25 | cards[tokenId] = url; 26 | _safeMint(msg.sender, tokenId); 27 | } 28 | 29 | function tokenURI(uint256 tokenId) public override view returns (string memory) { 30 | return cards[tokenId]; 31 | } 32 | 33 | /// This can be used to convert an image into a data URL. 34 | /// Ideally this is used off-chain. 35 | function toDataURL(bytes calldata image) external pure returns (string memory) { 36 | bool jpeg; 37 | if (image[0] == 0xff && image[1] == 0xd8 && image[2] == 0xff) { 38 | jpeg = true; 39 | } else if (keccak256(image[0:8]) != keccak256(hex"89504e470d0a1a0a")) { 40 | revert NotSupported(); 41 | } 42 | 43 | return string(bytes.concat( 44 | "data:", 45 | jpeg ? bytes("image/jpeg;base64,") : bytes("image/png;base64,"), 46 | bytes(Base64.encode(image)) 47 | )); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /CompcardV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 8 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 9 | 10 | import "./Base64.sol"; 11 | 12 | /// This creates non-transferable immutable 1/1 NFTs. 13 | contract CompcardV3Factory { 14 | address public immutable implementation; 15 | 16 | event CompcardDeployed(string name, string symbol, address token, uint256 tokenId); 17 | 18 | error DeployFailed(); 19 | error NotSupported(); 20 | 21 | constructor() { 22 | // TODO: use salt for vanity address or pass in the implementation address 23 | implementation = address(new CompcardV3()); 24 | } 25 | 26 | /// Claim a new Compcard PFP. 27 | /// @param url A URL pointing to your image, preferably on a content-addressible URL, such as IPFS. 28 | /// @return token The minted token address. 29 | /// @return tokenId The minted tokenId (always 0). 30 | function claim(string calldata name, string calldata symbol, string calldata url) external returns (address token, uint256 tokenId) { 31 | if (bytes(name).length > 256) revert NotSupported(); 32 | if (bytes(symbol).length > 256) revert NotSupported(); 33 | 34 | uint256 payloadLength = 45 + 20 + 2 + bytes(name).length + bytes(symbol).length + bytes(url).length; 35 | 36 | bytes memory bytecode = abi.encodePacked( 37 | // TODO: use more efficient code (and vanity address) 38 | hex"3d61", 39 | uint8(payloadLength >> 8), 40 | uint8(payloadLength & 0xff), 41 | hex"80600b3d3981f3363d3d373d3d3d363d73", 42 | implementation, 43 | hex"5af43d82803e903d91602b57fd5bf3", 44 | msg.sender, 45 | uint8(bytes(name).length), 46 | uint8(bytes(symbol).length), 47 | name, 48 | symbol, 49 | url 50 | ); 51 | assembly { 52 | token := create(0, add(bytecode, 32), mload(bytecode)) 53 | } 54 | if (token == address(0)) { 55 | revert DeployFailed(); 56 | } 57 | emit CompcardDeployed(name, symbol, token, tokenId); 58 | } 59 | 60 | /// This can be used to convert an image into a data URL. 61 | /// Ideally this is used off-chain. 62 | function toDataURL(bytes calldata image) external pure returns (string memory) { 63 | bool jpeg; 64 | if (image[0] == 0xff && image[1] == 0xd8 && image[2] == 0xff) { 65 | jpeg = true; 66 | } else if (keccak256(image[0:8]) != keccak256(hex"89504e470d0a1a0a")) { 67 | revert NotSupported(); 68 | } 69 | 70 | return string(bytes.concat( 71 | "data:", 72 | jpeg ? bytes("image/jpeg;base64,") : bytes("image/png;base64,"), 73 | bytes(Base64.encode(image)) 74 | )); 75 | } 76 | } 77 | 78 | /// This creates non-transferable immutable 1/1 NFTs. 79 | /// 80 | /// Should transferability be preferred, the owner field should moved to be a storage item. 81 | /// 82 | /// The design is a proxy contract, which contains most of the details as "immutables" 83 | /// concatenated at the end of the contract. The layout is specific: 84 | /// - 20 bytes: owner 85 | /// - 1 byte: length of name 86 | /// - 1 byte: length of symbol 87 | /// - n bytes: name 88 | /// - n bytes: symbol 89 | /// - the remaining bytes: url 90 | contract CompcardV3 is IERC721, IERC721Metadata, IERC721Enumerable, ERC165 { 91 | uint256 private constant RUNTIME_CODE_LENGTH = 45; // TODO: add proper length 92 | 93 | error NotSupported(); 94 | error NotFound(); 95 | 96 | function readOwner() private view returns (address owner) { 97 | uint256 proxyLength = RUNTIME_CODE_LENGTH; 98 | assembly { 99 | // Read into the scratch space 100 | extcodecopy(address(), 0, proxyLength, 20) 101 | owner := shr(96, mload(0)) 102 | } 103 | } 104 | 105 | function readSettings() private view returns (string memory name, string memory symbol, string memory data) { 106 | uint256 proxyLength = RUNTIME_CODE_LENGTH; 107 | uint256 codeLength = address(this).code.length; 108 | uint256 length = codeLength - proxyLength; 109 | 110 | // These are wasting memory but we don't care. 111 | name = new string(256); 112 | symbol = new string(256); 113 | data = new string(length); 114 | 115 | assembly { 116 | // We skip the owner. 117 | let offset := add(proxyLength, 20) 118 | 119 | // Load sizes to scratch space. 120 | mstore(0, 0) 121 | extcodecopy(address(), 0, offset, 2) 122 | offset := add(offset, 2) 123 | 124 | let nameLength := byte(0, mload(0)) 125 | let symbolLength := byte(1, mload(0)) 126 | 127 | mstore(name, nameLength) 128 | extcodecopy(address(), add(name, 32), offset, nameLength) 129 | offset := add(offset, nameLength) 130 | 131 | mstore(symbol, symbolLength) 132 | extcodecopy(address(), add(symbol, 32), offset, symbolLength) 133 | offset := add(offset, symbolLength) 134 | 135 | length := sub(codeLength, offset) 136 | mstore(data, length) 137 | extcodecopy(address(), add(data, 32), offset, length) 138 | } 139 | } 140 | 141 | function name() public view virtual override returns (string memory ret) { 142 | // TODO: this is wasting memory by loading everything 143 | (ret, , ) = readSettings(); 144 | } 145 | 146 | function symbol() public view virtual override returns (string memory ret) { 147 | // TODO: this is wasting memory by loading everything 148 | (, ret, ) = readSettings(); 149 | } 150 | 151 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory ret) { 152 | if (tokenId != 0) revert NotFound(); 153 | // TODO: this is wasting memory by loading everything 154 | (, , ret) = readSettings(); 155 | } 156 | 157 | function balanceOf(address owner) external override view returns (uint256) { 158 | return (owner == readOwner()) ? 1 : 0; 159 | } 160 | 161 | function ownerOf(uint256 tokenId) external override view returns (address) { 162 | if (tokenId != 0) revert NotFound(); 163 | return readOwner(); 164 | } 165 | 166 | function safeTransferFrom( 167 | address from, 168 | address to, 169 | uint256 tokenId 170 | ) external override { 171 | revert NotSupported(); 172 | } 173 | 174 | function transferFrom( 175 | address from, 176 | address to, 177 | uint256 tokenId 178 | ) external override { 179 | revert NotSupported(); 180 | } 181 | 182 | function safeTransferFrom( 183 | address from, 184 | address to, 185 | uint256 tokenId, 186 | bytes calldata data 187 | ) external override { 188 | revert NotSupported(); 189 | } 190 | 191 | function approve(address to, uint256 tokenId) external override { 192 | revert NotSupported(); 193 | } 194 | 195 | function getApproved(uint256 tokenId) external override view returns (address operator) { 196 | revert NotSupported(); 197 | } 198 | 199 | function setApprovalForAll(address operator, bool _approved) external override { 200 | revert NotSupported(); 201 | } 202 | 203 | function isApprovedForAll(address owner, address operator) external override view returns (bool) { 204 | // False. 205 | } 206 | 207 | function totalSupply() external override pure returns (uint) { 208 | return 1; 209 | } 210 | 211 | function tokenOfOwnerByIndex(address owner, uint256 index) external override view returns (uint256 tokenId) { 212 | if ((owner != readOwner()) || (index != 0)) revert NotFound(); 213 | // The tokenId issued is 0. 214 | } 215 | 216 | function tokenByIndex(uint256 index) external view returns (uint256) { 217 | if (index != 0) revert NotFound(); 218 | // The tokenId issued is 0. 219 | } 220 | 221 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { 222 | return 223 | interfaceId == type(IERC721).interfaceId || 224 | interfaceId == type(IERC721Metadata).interfaceId || 225 | interfaceId == type(IERC721Enumerable).interfaceId || 226 | super.supportsInterface(interfaceId); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /CompcardV4.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 8 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 9 | 10 | import "./Base64.sol"; 11 | 12 | /// This creates transferable immutable 1/1 NFTs. 13 | contract CompcardV4Factory { 14 | address public immutable implementation; 15 | 16 | event CompcardDeployed(string name, string symbol, address token, uint256 tokenId); 17 | 18 | error DeployFailed(); 19 | error NotSupported(); 20 | 21 | constructor() { 22 | // TODO: use salt for vanity address or pass in the implementation address 23 | implementation = address(new CompcardV4()); 24 | } 25 | 26 | // Split out and split into multiple chunks to avoid stack too deep issues. 27 | function craftCode(string calldata name, string calldata symbol, string calldata url) private view returns (bytes memory bytecode) { 28 | uint256 payloadLength = 45 + 20 + 2 + bytes(name).length + bytes(symbol).length + bytes(url).length; 29 | 30 | bytecode = abi.encodePacked( 31 | hex"73", 32 | msg.sender, 33 | hex"3d55_3d61" 34 | ); 35 | bytecode = abi.encodePacked( 36 | bytecode, 37 | // TODO: use more efficient code (and vanity address) 38 | uint8(payloadLength >> 8), 39 | uint8(payloadLength & 0xff), 40 | hex"8060223d3981f3363d3d373d3d3d363d73", 41 | implementation, 42 | hex"5af43d82803e903d91602b57fd5bf3", 43 | uint8(bytes(name).length), 44 | uint8(bytes(symbol).length) 45 | ); 46 | bytecode = abi.encodePacked( 47 | bytecode, 48 | name, 49 | symbol, 50 | url 51 | ); 52 | } 53 | 54 | /// Claim a new Compcard PFP. 55 | /// @param url A URL pointing to your image, preferably on a content-addressible URL, such as IPFS. 56 | /// @return token The minted token address. 57 | /// @return tokenId The minted tokenId (always 0). 58 | function claim(string calldata name, string calldata symbol, string calldata url) external payable returns (address token, uint256 tokenId) { 59 | if (bytes(name).length > 256) revert NotSupported(); 60 | if (bytes(symbol).length > 256) revert NotSupported(); 61 | 62 | bytes memory bytecode = craftCode(name, symbol, url); 63 | 64 | assembly { 65 | token := create(0, add(bytecode, 32), mload(bytecode)) 66 | } 67 | if (token == address(0)) { 68 | revert DeployFailed(); 69 | } 70 | emit CompcardDeployed(name, symbol, token, tokenId); 71 | } 72 | 73 | /// This can be used to convert an image into a data URL. 74 | /// Ideally this is used off-chain. 75 | function toDataURL(bytes calldata image) external pure returns (string memory) { 76 | bool jpeg; 77 | if (image[0] == 0xff && image[1] == 0xd8 && image[2] == 0xff) { 78 | jpeg = true; 79 | } else if (keccak256(image[0:8]) != keccak256(hex"89504e470d0a1a0a")) { 80 | revert NotSupported(); 81 | } 82 | 83 | return string(bytes.concat( 84 | "data:", 85 | jpeg ? bytes("image/jpeg;base64,") : bytes("image/png;base64,"), 86 | bytes(Base64.encode(image)) 87 | )); 88 | } 89 | } 90 | 91 | /// This creates transferable immutable 1/1 NFTs. 92 | /// 93 | /// Should transferability be preferred, the owner field should moved to be a storage item. 94 | /// 95 | /// The design is a proxy contract, which contains most of the details as "immutables" 96 | /// concatenated at the end of the contract. The layout is specific: 97 | /// - 1 byte: length of name 98 | /// - 1 byte: length of symbol 99 | /// - n bytes: name 100 | /// - n bytes: symbol 101 | /// - the remaining bytes: url 102 | /// 103 | /// The owner is at storage slot 0. 104 | contract CompcardV4 is IERC721, IERC721Metadata, IERC721Enumerable, ERC165 { 105 | uint256 private constant RUNTIME_CODE_LENGTH = 45; // TODO: add proper length 106 | 107 | error NotSupported(); 108 | error NotFound(); 109 | 110 | function readOwner() private view returns (address owner) { 111 | assembly { 112 | owner := sload(0) 113 | } 114 | } 115 | 116 | function setOwner(address owner) private { 117 | assembly { 118 | sstore(0, owner) 119 | } 120 | } 121 | 122 | function readSettings() private view returns (string memory name, string memory symbol, string memory data) { 123 | uint256 proxyLength = RUNTIME_CODE_LENGTH; 124 | uint256 codeLength = address(this).code.length; 125 | uint256 length = codeLength - proxyLength; 126 | 127 | // These are wasting memory but we don't care. 128 | name = new string(256); 129 | symbol = new string(256); 130 | data = new string(length); 131 | 132 | assembly { 133 | let offset := proxyLength 134 | 135 | // Load sizes to scratch space. 136 | mstore(0, 0) 137 | extcodecopy(address(), 0, offset, 2) 138 | offset := add(offset, 2) 139 | 140 | let nameLength := byte(0, mload(0)) 141 | let symbolLength := byte(1, mload(0)) 142 | 143 | mstore(name, nameLength) 144 | extcodecopy(address(), add(name, 32), offset, nameLength) 145 | offset := add(offset, nameLength) 146 | 147 | mstore(symbol, symbolLength) 148 | extcodecopy(address(), add(symbol, 32), offset, symbolLength) 149 | offset := add(offset, symbolLength) 150 | 151 | length := sub(codeLength, offset) 152 | mstore(data, length) 153 | extcodecopy(address(), add(data, 32), offset, length) 154 | } 155 | } 156 | 157 | function name() public view virtual override returns (string memory ret) { 158 | // TODO: this is wasting memory by loading everything 159 | (ret, , ) = readSettings(); 160 | } 161 | 162 | function symbol() public view virtual override returns (string memory ret) { 163 | // TODO: this is wasting memory by loading everything 164 | (, ret, ) = readSettings(); 165 | } 166 | 167 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory ret) { 168 | if (tokenId != 0) revert NotFound(); 169 | // TODO: this is wasting memory by loading everything 170 | (, , ret) = readSettings(); 171 | } 172 | 173 | function balanceOf(address owner) external override view returns (uint256) { 174 | return (owner == readOwner()) ? 1 : 0; 175 | } 176 | 177 | function ownerOf(uint256 tokenId) external override view returns (address) { 178 | if (tokenId != 0) revert NotFound(); 179 | return readOwner(); 180 | } 181 | 182 | function safeTransferFrom( 183 | address from, 184 | address to, 185 | uint256 tokenId 186 | ) external override { 187 | revert NotSupported(); 188 | } 189 | 190 | function transferFrom( 191 | address from, 192 | address to, 193 | uint256 tokenId 194 | ) external override { 195 | if ((msg.sender != readOwner()) || (tokenId != 0)) revert NotFound(); 196 | setOwner(to); 197 | } 198 | 199 | function safeTransferFrom( 200 | address from, 201 | address to, 202 | uint256 tokenId, 203 | bytes calldata data 204 | ) external override { 205 | revert NotSupported(); 206 | } 207 | 208 | function approve(address to, uint256 tokenId) external override { 209 | revert NotSupported(); 210 | } 211 | 212 | function getApproved(uint256 tokenId) external override view returns (address operator) { 213 | revert NotSupported(); 214 | } 215 | 216 | function setApprovalForAll(address operator, bool _approved) external override { 217 | revert NotSupported(); 218 | } 219 | 220 | function isApprovedForAll(address owner, address operator) external override view returns (bool) { 221 | // False. 222 | } 223 | 224 | function totalSupply() external override pure returns (uint) { 225 | return 1; 226 | } 227 | 228 | function tokenOfOwnerByIndex(address owner, uint256 index) external override view returns (uint256 tokenId) { 229 | if ((owner != readOwner()) || (index != 0)) revert NotFound(); 230 | // The tokenId issued is 0. 231 | } 232 | 233 | function tokenByIndex(uint256 index) external view returns (uint256 tokenId) { 234 | if (index != 0) revert NotFound(); 235 | // The tokenId issued is 0. 236 | } 237 | 238 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { 239 | return 240 | interfaceId == type(IERC721).interfaceId || 241 | interfaceId == type(IERC721Metadata).interfaceId || 242 | interfaceId == type(IERC721Enumerable).interfaceId || 243 | super.supportsInterface(interfaceId); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compcard.xyz 2 | 3 | Compcard.xyz is an NFT project to store your avatar image in. --------------------------------------------------------------------------------