Skip to content

Instantly share code, notes, and snippets.

@montycheese
Created August 2, 2022 03:21
Show Gist options
  • Select an option

  • Save montycheese/1e12f287fca7f752a17d342b5e0ac1ff to your computer and use it in GitHub Desktop.

Select an option

Save montycheese/1e12f287fca7f752a17d342b5e0ac1ff to your computer and use it in GitHub Desktop.
Solidity code sample
// 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