Created
August 2, 2022 03:21
-
-
Save montycheese/1e12f287fca7f752a17d342b5e0ac1ff to your computer and use it in GitHub Desktop.
Solidity code sample
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: UNLICENSED | |
| // Creator: Montana Wong <montana@sprise.co> (https://sprise.co) | |
| pragma solidity 0.8.9; | |
| import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | |
| import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; | |
| import {IERC721MembershipOwnerChecker} from "./IERC721MembershipOwnerChecker.sol"; | |
| import {BFFSingleCharmRenderer} from "./BFFSingleCharmRenderer.sol"; | |
| /// This is a custom metadata renderer that returns "charms" attributes on a BFF friendship bracelet NFT | |
| /// https://opensea.io/collection/bff-friendship-bracelets | |
| /// It takes a dependency on an existing BFFSingleCharmRenderer, which holds | |
| /// state for the initially assigned charm - Charm ID 0 "You Charm" | |
| contract BFFMultiCharmRenderer is IERC721MembershipOwnerChecker { | |
| using Strings for uint256; | |
| address public bffBraceletContract; | |
| // Address that is allowed to call certain functions. Permissions are granted by | |
| // BFF bracelet membership owner | |
| address public authorizedCaller; | |
| BFFSingleCharmRenderer private singleCharmRenderer; | |
| // This contract only supports up to 256 charms per bracelet, which equals 2^256 different charm permutations | |
| // Mapping of a Bracelet's TokenID to its charm statuses, represented as a uint256 bitmap | |
| // The position of the boolean in the bitmap directly corresponds to which charm it is representing | |
| // An bitmap such as 1000010 would represent a bracelet that has Charms #1 and #6 attached to it. | |
| // Charm #0's state is stored in the SingleCharmRenderer dependency contract and should be managed directly from there. | |
| mapping(uint256 => uint256) private tokenIdToCharmStatuses; | |
| string public baseCharmURI = ""; | |
| event TokenURIUpdated(string uri); | |
| event AuthorizedCallerUpdated(address authorizedCaller); | |
| /// Constructor to initialize the charm renderer | |
| /// @param _bffBraceletContract Address of the BFF Bracelet contract. This | |
| /// is used for owner-related tasks | |
| /// @param _singleCharmRendererContract Address of the Single Charm renderer address. This | |
| /// is used to read status state for the very first charm that was added to the bracelet. | |
| /// @param _authorizedCallerAddress Address of the authorized caller who is given access | |
| /// to call certain functions on this contract in addition to the erc721 membership owner. | |
| constructor(address _bffBraceletContract, | |
| address _singleCharmRendererContract, | |
| address _authorizedCallerAddress | |
| ) { | |
| bffBraceletContract = _bffBraceletContract; | |
| singleCharmRenderer = BFFSingleCharmRenderer(_singleCharmRendererContract); | |
| authorizedCaller = _authorizedCallerAddress; | |
| } | |
| /// Only proceed if msg.sender is membership owner or the authorized operator | |
| /// @param membership membership whose owner we want to check | |
| modifier onlyMembershipOwnerOrAuthorizedCaller(address membership) { | |
| _onlyMembershipOwnerOrAuthorizedCaller(membership); | |
| _; | |
| } | |
| /// Updates token URI - only available to owner or authorized caller | |
| /// @param uri Base URI to use for a given charm permutation | |
| function updateBaseCharmURI(string memory uri) | |
| external | |
| onlyMembershipOwnerOrAuthorizedCaller(bffBraceletContract) | |
| { | |
| baseCharmURI = uri; | |
| emit TokenURIUpdated(uri); | |
| } | |
| /// Gets token URI for a given BFF Bracelet ID | |
| /// @return URI of a given bracelet based on the charm permutation | |
| /// The format of the token URI is <base uri>/<charm permutation id> | |
| function tokenURI(uint256 bffBraceletId) | |
| external | |
| view | |
| returns (string memory) | |
| { | |
| uint256 charmPermutationId = getCharmPermutationIdFromCharmStatuses(bffBraceletId); | |
| return string(abi.encodePacked(baseCharmURI, charmPermutationId.toString())); | |
| } | |
| /// Add a charm to one or more bracelets | |
| /// @param bffBraceletIds The IDs of the BFF bracelets to add a charm to | |
| /// @dev addCharm and removeCharm are separate to avoid needing to pass in | |
| /// an array of booleans representing the charm status (which would increase | |
| /// gas usage) | |
| function addCharms(uint256[] calldata bffBraceletIds, uint256 charmId) | |
| external | |
| onlyMembershipOwnerOrAuthorizedCaller(bffBraceletContract) { | |
| require(charmId > 0, "BFFMultiCharmRenderer: CharmID 0 is handled in the SingleCharmRenderer contract, cannot be set here."); | |
| // This for loop cannot realistically overflow due to the array length | |
| // (it would run out of gas before that point) | |
| unchecked { | |
| for (uint256 i = 0; i < bffBraceletIds.length; ++i) { | |
| _setCharm(bffBraceletIds[i], charmId, true); | |
| } | |
| } | |
| } | |
| function _setCharm(uint256 bffBraceletId, uint256 charmId, bool status) private { | |
| if (status) { | |
| tokenIdToCharmStatuses[bffBraceletId] = tokenIdToCharmStatuses[bffBraceletId] | uint256(1) << charmId; | |
| } else { | |
| tokenIdToCharmStatuses[bffBraceletId] = tokenIdToCharmStatuses[bffBraceletId] & ~(uint256(1) << charmId); | |
| } | |
| } | |
| /// Add one or more charms to one or bracelets | |
| /// @param bffBraceletIds The IDs of the BFF bracelets to add a charm to | |
| function addMultipleCharms(uint256[] calldata bffBraceletIds, uint256[] calldata charmIds) | |
| external | |
| onlyMembershipOwnerOrAuthorizedCaller(bffBraceletContract) { | |
| // This for loop cannot realistically overflow due to the array length | |
| // (it would run out of gas before that point) | |
| unchecked { | |
| for (uint256 i = 0; i < charmIds.length; ++i) { | |
| require(charmIds[i] > 0, "CharmID 0 is handled in the SingleCharmRenderer contract, cannot be set here."); | |
| for (uint256 j = 0; j < bffBraceletIds.length; ++j) { | |
| _setCharm(bffBraceletIds[j], charmIds[i], true); | |
| } | |
| } | |
| } | |
| } | |
| /// Remove a charm from one or more bracelets | |
| /// @param bffBraceletIds The IDs of the BFF bracelets to remove a charm from | |
| /// @dev addCharm and removeCharm are separate to avoid needing to pass in | |
| /// an array of booleans representing the charm status (which would increase | |
| /// gas usage) | |
| function removeCharms(uint256[] calldata bffBraceletIds, uint256 charmId) | |
| external | |
| onlyMembershipOwnerOrAuthorizedCaller(bffBraceletContract) { | |
| require(charmId > 0, "CharmID 0 is handled in the SingleCharmRenderer contract, cannot be set here."); | |
| // This for loop cannot realistically overflow due to the array length | |
| // (it would run out of gas before that point) | |
| unchecked { | |
| for (uint256 i = 0; i < bffBraceletIds.length; ++i) { | |
| _setCharm(bffBraceletIds[i], charmId, false); | |
| } | |
| } | |
| } | |
| // Get the current charm permutation id for a particular bracelet based on the statuses of its charms. | |
| function getCharmPermutationIdFromCharmStatuses(uint bffBraceletId) public view returns (uint256) { | |
| uint256 charmPermutationId = tokenIdToCharmStatuses[bffBraceletId]; | |
| // Factor in the charm status from the single charm renderer for CharmID 0 | |
| if (singleCharmRenderer.checkCharmStatus(bffBraceletId)) { | |
| charmPermutationId = charmPermutationId | uint256(1) << 0; | |
| } | |
| return charmPermutationId; | |
| } | |
| function getCharmStatusByIdForBracelet(uint256 bffBraceletId, uint256 charmId) public view returns (bool) { | |
| if (charmId == 0) { | |
| return singleCharmRenderer.checkCharmStatus(bffBraceletId); | |
| } | |
| uint256 flag = (tokenIdToCharmStatuses[bffBraceletId] >> charmId) & uint256(1); | |
| return (flag == 1 ? true : false); | |
| } | |
| /// Set an authorized caller that is allowed to call functions with the modifier | |
| /// onlyMembershipOwnerOrAuthorizedCaller | |
| /// @param _authorizedCaller The address of the authorized caller to set | |
| function setAuthorizedCaller(address _authorizedCaller) external onlyMembershipOwner(bffBraceletContract) { | |
| authorizedCaller = _authorizedCaller; | |
| emit AuthorizedCallerUpdated(_authorizedCaller); | |
| } | |
| /// Remove the current authorized caller | |
| function removeCurrentAuthorizedCaller() external onlyMembershipOwner(bffBraceletContract) { | |
| authorizedCaller = address(0); | |
| emit AuthorizedCallerUpdated(address(0)); | |
| } | |
| function _onlyMembershipOwnerOrAuthorizedCaller(address membership) internal view { | |
| require( | |
| msg.sender == Ownable(membership).owner() || msg.sender == authorizedCaller, | |
| "BFFMultiCharmRenderer: Caller not membership owner or authorized caller" | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment