Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cachable-nft): support auto cache #117

Merged
merged 3 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions contracts/story-nft/CachableNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,51 @@ pragma solidity ^0.8.26;
// two mode passthrough and cache
// passthrough will just forward the call to the nft contract

// cache contrat has two modes
// cache contrat has three modes
// 1. cache mode
// 2. passthrough mode
// 3. auto mode
// cache mode will cache the nft data and return it
// passthrough mode will forward the call to the nft contract
// auto mode will cache the nft data if the basefee is greater than the threshold
import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
abstract contract CachableNFT is OwnableUpgradeable {
using EnumerableMap for EnumerableMap.UintToAddressMap;

/// @dev Enum for cache mode.
enum CacheMode {
sebsadface marked this conversation as resolved.
Show resolved Hide resolved
Passthrough,
Cache,
Auto
}

/// @dev Storage structure for the CacheableNFT
/// @custom:storage-location erc7201:story-protocol-periphery.CacheableNFT
struct CacheableNFTStorage {
// tokenId => ipId
EnumerableMap.UintToAddressMap cache;
bool cacheMode;
bool DEPRECATED_cacheMode;
sebsadface marked this conversation as resolved.
Show resolved Hide resolved
CacheMode mode;
uint256 autoCacheBaseFeeThreshold;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.CacheableNFT")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant CacheableNFTStorageLocation =
0xb2c28ba4bb2a3f74a63ac2785b5af0c41313804d8b65acc69c0b2736a57e5f00;

/// @notice Sets the cache mode.
/// @param useCache The new cache mode, true for cache mode, false for passthrough mode.
function setCacheMode(bool useCache) external onlyOwner {
/// @param mode The new cache mode.
function setCacheMode(CacheMode mode) external onlyOwner {
CacheableNFTStorage storage $ = _getCacheableNFTStorage();
$.cacheMode = useCache;
$.mode = mode;
}

/// @notice Sets the auto cache base fee threshold.
/// @param threshold The new auto cache base fee threshold.
function setCacheModeAutoThreshold(uint256 threshold) external onlyOwner {
sebsadface marked this conversation as resolved.
Show resolved Hide resolved
CacheableNFTStorage storage $ = _getCacheableNFTStorage();
$.autoCacheBaseFeeThreshold = threshold;
}

/// @notice Mints NFTs to the cache.
Expand All @@ -51,8 +70,8 @@ abstract contract CachableNFT is OwnableUpgradeable {

/// @notice Returns the cache mode.
/// @return The cache mode, true for cache mode, false for passthrough mode.
function getCacheMode() external view returns (bool) {
return _getCacheableNFTStorage().cacheMode;
function getCacheMode() external view returns (CacheMode) {
return _getCacheableNFTStorage().mode;
}

/// @notice Returns the NFT at the given index in the cache.
Expand All @@ -69,9 +88,14 @@ abstract contract CachableNFT is OwnableUpgradeable {
/// @return ipId The IP ID of the transferred NFT.
function _transferFromCache(address recipient) internal returns (uint256 tokenId, address ipId) {
CacheableNFTStorage storage $ = _getCacheableNFTStorage();
if (!$.cacheMode || $.cache.length() == 0) {
if ($.mode == CacheMode.Passthrough || $.cache.length() == 0) {
return (0, address(0));
} else if ($.mode == CacheMode.Auto) {
if (block.basefee <= $.autoCacheBaseFeeThreshold * 1 gwei) {
return (0, address(0));
}
}

(tokenId, ipId) = $.cache.at(0);
$.cache.remove(tokenId);

Expand Down
27 changes: 25 additions & 2 deletions test/story-nft/StoryBadgeNFT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IStoryBadgeNFT } from "../../contracts/interfaces/story-nft/IStoryBadge
import { IOrgStoryNFT } from "../../contracts/interfaces/story-nft/IOrgStoryNFT.sol";
import { IStoryNFT } from "../../contracts/interfaces/story-nft/IStoryNFT.sol";
import { StoryBadgeNFT } from "../../contracts/story-nft/StoryBadgeNFT.sol";
import { CachableNFT } from "../../contracts/story-nft/CachableNFT.sol";

// test
import { BaseTest } from "../utils/BaseTest.t.sol";
Expand Down Expand Up @@ -224,7 +225,7 @@ contract StoryBadgeNFTTest is BaseTest {
assertEq(rootOrgStoryNft.cacheSize(), 1); // 1 cached
rootOrgStoryNft.mintToCache(100);
assertEq(rootOrgStoryNft.cacheSize(), 101); // 100 cached + 1 minted
rootOrgStoryNft.setCacheMode(true); // enable cache mode
rootOrgStoryNft.setCacheMode(CachableNFT.CacheMode.Cache); // enable cache mode
vm.stopPrank();

signature = _signAddress(rootOrgStoryNftSignerSk, u.carl);
Expand All @@ -235,7 +236,7 @@ contract StoryBadgeNFTTest is BaseTest {
assertEq(rootOrgStoryNft.cacheSize(), 100); // cache size is reduced by 1

vm.startPrank(rootOrgStoryNftOwner);
rootOrgStoryNft.setCacheMode(false); // disable cache mode
rootOrgStoryNft.setCacheMode(CachableNFT.CacheMode.Passthrough); // disable cache mode
vm.stopPrank();

signature = _signAddress(rootOrgStoryNftSignerSk, u.bob);
Expand All @@ -244,6 +245,28 @@ contract StoryBadgeNFTTest is BaseTest {
assertEq(rootOrgStoryNft.ownerOf(tokenId), u.bob); // minted directly
vm.stopPrank();
assertEq(rootOrgStoryNft.cacheSize(), 100); // cache size is unchanged

vm.startPrank(rootOrgStoryNftOwner);
rootOrgStoryNft.setCacheMode(CachableNFT.CacheMode.Auto); // disable cache mode
rootOrgStoryNft.setCacheModeAutoThreshold(100);
vm.stopPrank();

vm.fee(20 gwei);
signature = _signAddress(rootOrgStoryNftSignerSk, u.dan);
vm.startPrank(u.dan);
(tokenId, ) = rootOrgStoryNft.mint(u.dan, signature);
assertEq(rootOrgStoryNft.ownerOf(tokenId), u.dan); // minted directly
vm.stopPrank();
assertEq(rootOrgStoryNft.cacheSize(), 100); // cache size is unchanged

vm.fee(200 gwei);
address eva = vm.addr(0x123456);
signature = _signAddress(rootOrgStoryNftSignerSk, eva);
vm.startPrank(eva);
(tokenId, ) = rootOrgStoryNft.mint(eva, signature);
assertEq(rootOrgStoryNft.ownerOf(tokenId), eva); // minted from cache
vm.stopPrank();
assertEq(rootOrgStoryNft.cacheSize(), 99); // cache size is unchanged
}

function test_StoryBadgeNFT_revert_setSigner_CallerIsNotOwner() public {
Expand Down
Loading