|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +pragma solidity =0.8.9; |
| 3 | + |
| 4 | +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; |
| 5 | +import {Base64} from "@devprotocol/util-contracts/contracts/utils/Base64.sol"; |
| 6 | +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; |
| 7 | +import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; |
| 8 | + |
| 9 | +import {ISBT} from "./interfaces/ISBT.sol"; |
| 10 | + |
| 11 | +contract SBT is ISBT, ERC721EnumerableUpgradeable { |
| 12 | + using Base64 for bytes; |
| 13 | + using Strings for uint256; |
| 14 | + |
| 15 | + /// @dev Account with proxy adming rights. |
| 16 | + address private _proxyAdmin; |
| 17 | + /// @dev EOA with rights to allow(add)/disallow(remove) minter. |
| 18 | + address private _minterUpdater; |
| 19 | + |
| 20 | + /// @dev EOA with minting rights. |
| 21 | + mapping(address => bool) private _minters; |
| 22 | + /// @dev Holds the encoded metadata of a SBT token. |
| 23 | + mapping(uint256 => bytes) private _sbtdata; |
| 24 | + |
| 25 | + modifier onlyMinter() { |
| 26 | + require(_minters[_msgSender()], "Illegal access"); |
| 27 | + _; |
| 28 | + } |
| 29 | + |
| 30 | + modifier onlyMinterUpdater() { |
| 31 | + require(_msgSender() == _minterUpdater, "Not minter updater"); |
| 32 | + _; |
| 33 | + } |
| 34 | + |
| 35 | + function _setTokenURI(uint256 tokenId, bytes memory metadata) private { |
| 36 | + _sbtdata[tokenId] = metadata; |
| 37 | + emit SetSBTTokenURI(tokenId, metadata); |
| 38 | + } |
| 39 | + |
| 40 | + function _beforeTokenTransfer( |
| 41 | + address from, |
| 42 | + address to, |
| 43 | + uint256 tokenId, |
| 44 | + uint256 batchSize |
| 45 | + ) internal virtual override { |
| 46 | + if (from == address(0)) { |
| 47 | + // allow mint |
| 48 | + super._beforeTokenTransfer(from, to, tokenId, batchSize); |
| 49 | + } else if (to == address(0)) { |
| 50 | + // disallow burn |
| 51 | + revert("SBT can not burn"); |
| 52 | + } else if (to != from) { |
| 53 | + // disallow transfer |
| 54 | + revert("SBT can not transfer"); |
| 55 | + } else { |
| 56 | + // disallow other |
| 57 | + revert("Illegal operation"); |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + function initialize( |
| 62 | + address minterUpdater, |
| 63 | + address[] memory minters |
| 64 | + ) external initializer { |
| 65 | + __ERC721_init("Dev Protocol SBT V1", "DEV-SBT-V1"); |
| 66 | + |
| 67 | + _minterUpdater = minterUpdater; |
| 68 | + for (uint256 i = 0; i < minters.length; i++) { |
| 69 | + _minters[minters[i]] = true; |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + function setProxyAdmin(address proxyAdmin) external { |
| 74 | + require(_proxyAdmin == address(0), "Already set"); |
| 75 | + _proxyAdmin = proxyAdmin; |
| 76 | + emit SetProxyAdmin(proxyAdmin); |
| 77 | + } |
| 78 | + |
| 79 | + function addMinter(address minter) external override onlyMinterUpdater { |
| 80 | + _minters[minter] = true; |
| 81 | + emit MinterAdded(minter); |
| 82 | + } |
| 83 | + |
| 84 | + function removeMinter(address minter) external override onlyMinterUpdater { |
| 85 | + _minters[minter] = false; |
| 86 | + emit MinterRemoved(minter); |
| 87 | + } |
| 88 | + |
| 89 | + function setTokenURI( |
| 90 | + uint256 tokenId, |
| 91 | + bytes memory metadata |
| 92 | + ) external override onlyMinter { |
| 93 | + require(tokenId < currentIndex(), "Token not found"); |
| 94 | + _setTokenURI(tokenId, metadata); |
| 95 | + } |
| 96 | + |
| 97 | + function mint( |
| 98 | + address to, |
| 99 | + bytes memory metadata |
| 100 | + ) external override onlyMinter returns (uint256 tokenId_) { |
| 101 | + uint256 currentId = currentIndex(); |
| 102 | + _mint(to, currentId); |
| 103 | + emit Minted(currentId, to); |
| 104 | + _setTokenURI(currentId, metadata); |
| 105 | + return currentId; |
| 106 | + } |
| 107 | + |
| 108 | + function _tokenURI(uint256 tokenId) private view returns (string memory) { |
| 109 | + ( |
| 110 | + string memory name, |
| 111 | + string memory description, |
| 112 | + string memory tokenUriImage, |
| 113 | + StringAttribute[] memory stringAttributes, |
| 114 | + NumberAttribute[] memory numberAttributes |
| 115 | + ) = abi.decode( |
| 116 | + _sbtdata[tokenId], |
| 117 | + (string, string, string, StringAttribute[], NumberAttribute[]) |
| 118 | + ); |
| 119 | + |
| 120 | + bool isStringDataPresent = false; |
| 121 | + string memory sbtAttributes = ""; |
| 122 | + |
| 123 | + for (uint256 i = 0; i < stringAttributes.length; i++) { |
| 124 | + string memory attributeInString = string( |
| 125 | + abi.encodePacked( |
| 126 | + // solhint-disable-next-line quotes |
| 127 | + '{"trait_type": "', |
| 128 | + stringAttributes[i].trait_type, |
| 129 | + // solhint-disable-next-line quotes |
| 130 | + '",', |
| 131 | + // solhint-disable-next-line quotes |
| 132 | + ' "value": "', |
| 133 | + stringAttributes[i].value, |
| 134 | + // solhint-disable-next-line quotes |
| 135 | + '"}' |
| 136 | + ) |
| 137 | + ); |
| 138 | + |
| 139 | + if (i == 0) { |
| 140 | + isStringDataPresent = true; |
| 141 | + sbtAttributes = attributeInString; |
| 142 | + } else { |
| 143 | + sbtAttributes = string( |
| 144 | + abi.encodePacked(sbtAttributes, ", ", attributeInString) |
| 145 | + ); |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + for (uint256 i = 0; i < numberAttributes.length; i++) { |
| 150 | + string memory attributeInString = string( |
| 151 | + abi.encodePacked( |
| 152 | + // solhint-disable-next-line quotes |
| 153 | + '{"trait_type": "', |
| 154 | + numberAttributes[i].trait_type, |
| 155 | + // solhint-disable-next-line quotes |
| 156 | + '",', |
| 157 | + // solhint-disable-next-line quotes |
| 158 | + ' "display_type": "', |
| 159 | + numberAttributes[i].display_type, |
| 160 | + // solhint-disable-next-line quotes |
| 161 | + '",', |
| 162 | + // solhint-disable-next-line quotes |
| 163 | + ' "value": "', |
| 164 | + numberAttributes[i].value.toString(), |
| 165 | + // solhint-disable-next-line quotes |
| 166 | + '"}' |
| 167 | + ) |
| 168 | + ); |
| 169 | + |
| 170 | + if (i == 0 && !isStringDataPresent) { |
| 171 | + sbtAttributes = attributeInString; |
| 172 | + } else { |
| 173 | + sbtAttributes = string( |
| 174 | + abi.encodePacked(sbtAttributes, ", ", attributeInString) |
| 175 | + ); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + sbtAttributes = string(abi.encodePacked("[", sbtAttributes, "]")); |
| 180 | + |
| 181 | + return |
| 182 | + string( |
| 183 | + abi.encodePacked( |
| 184 | + "data:application/json;base64,", |
| 185 | + abi |
| 186 | + .encodePacked( |
| 187 | + // solhint-disable-next-line quotes |
| 188 | + '{"name":"', |
| 189 | + name, |
| 190 | + // solhint-disable-next-line quotes |
| 191 | + '", "description":"', |
| 192 | + description, |
| 193 | + // solhint-disable-next-line quotes |
| 194 | + '", "image": "', |
| 195 | + tokenUriImage, |
| 196 | + // solhint-disable-next-line quotes |
| 197 | + '", "attributes":', |
| 198 | + sbtAttributes, |
| 199 | + "}" |
| 200 | + ) |
| 201 | + .encode() |
| 202 | + ) |
| 203 | + ); |
| 204 | + } |
| 205 | + |
| 206 | + function encodeMetadata( |
| 207 | + string memory name, |
| 208 | + string memory description, |
| 209 | + StringAttribute[] memory stringAttributes, |
| 210 | + NumberAttribute[] memory numberAttributes, |
| 211 | + string memory tokenUriImage |
| 212 | + ) public pure override returns (bytes memory) { |
| 213 | + return |
| 214 | + abi.encode( |
| 215 | + name, |
| 216 | + description, |
| 217 | + tokenUriImage, |
| 218 | + stringAttributes, |
| 219 | + numberAttributes |
| 220 | + ); |
| 221 | + } |
| 222 | + |
| 223 | + function tokenURI( |
| 224 | + uint256 tokenId |
| 225 | + ) public view override returns (string memory) { |
| 226 | + return _tokenURI(tokenId); |
| 227 | + } |
| 228 | + |
| 229 | + function currentIndex() public view override returns (uint256) { |
| 230 | + return super.totalSupply(); |
| 231 | + } |
| 232 | + |
| 233 | + function owner() external view returns (address) { |
| 234 | + return ProxyAdmin(_proxyAdmin).owner(); |
| 235 | + } |
| 236 | + |
| 237 | + function tokensOfOwner( |
| 238 | + address tokenOwner |
| 239 | + ) external view override returns (uint256[] memory) { |
| 240 | + uint256 length = super.balanceOf(tokenOwner); |
| 241 | + uint256[] memory tokens = new uint256[](length); |
| 242 | + for (uint256 i = 0; i < length; i++) { |
| 243 | + tokens[i] = super.tokenOfOwnerByIndex(tokenOwner, i); |
| 244 | + } |
| 245 | + return tokens; |
| 246 | + } |
| 247 | +} |
0 commit comments