Skip to content

Commit

Permalink
Merge pull request #7 from RSSNext/feat/achievementNft
Browse files Browse the repository at this point in the history
Feat/achievement nft
  • Loading branch information
Atlasoin authored Nov 8, 2024
2 parents 9bb740f + 5fc1664 commit 5ad6392
Show file tree
Hide file tree
Showing 10 changed files with 475 additions and 16 deletions.
2 changes: 2 additions & 0 deletions deploy-config/local.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"proxyAdminOwner": "0x8AC80fa0993D95C9d6B8Cb494E561E6731038941",
"name": "POWER",
"symbol": "POWER",
"nftName": "FOLLOW ACHIEVEMENT",
"nftSymbol": "FOLLOW ACHIEVEMENT",
"appAdmin": "0x844e38Fae147aE933b0a930fF8EfDFF3767cEA24",
"dailyMintLimit": "100000000000000000000000"
}
6 changes: 6 additions & 0 deletions script/DeployConfig.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ contract DeployConfig is Script {

string public name;
string public symbol;

string public nftName;
string public nftSymbol;

address public proxyAdminOwner;
address public appAdmin;
uint256 public dailyMintLimit;
Expand All @@ -32,6 +36,8 @@ contract DeployConfig is Script {

name = stdJson.readString(_json, "$.name");
symbol = stdJson.readString(_json, "$.symbol");
nftName = stdJson.readString(_json, "$.nftName");
nftSymbol = stdJson.readString(_json, "$.nftSymbol");
proxyAdminOwner = stdJson.readAddress(_json, "$.proxyAdminOwner");
appAdmin = stdJson.readAddress(_json, "$.appAdmin");
dailyMintLimit = stdJson.readUint(_json, "$.dailyMintLimit");
Expand Down
68 changes: 68 additions & 0 deletions src/interfaces/IAchievement.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;

import {AchievementDetails} from "../../src/libraries/AchievementDataTypes.sol";

interface IAchievement {
/**
* @notice Initializes the contract. Setup token name, symbol and account with APP_ADMIN_ROLE.
* @param name_ The name of the token.
* @param symbol_ The symbol of the token.
* @param admin_ The account to be granted with APP_ADMIN_ROLE.
* @param powerToken_ The address of the power token.
*/
function initialize(
string calldata name_,
string calldata symbol_,
address admin_,
address powerToken_
) external;

/**
* @notice Only the APP_ADMIN_ROLE can set the achievement details.
* @param name Name of the achievement.
* @param description Description of the achievement.
* @param imageURL Image URL of the achievement.
*/
function setAchievement(
string calldata name,
string calldata description,
string calldata imageURL
) external;

/**
* @notice Mints a token to `account`.
* @param achievement Name of the achievement to mint.
* @return tokenId The new minted token id.
*/
function mint(string calldata achievement) external returns (uint256 tokenId);

/**
* @notice Returns total supply of tokens.
* @return Total supply of tokens.
*/
function totalSupply() external view returns (uint256);

/**
* @notice Returns the achievement details of all achievements.
* @return Achievement details.
*/
function getAllAchievements() external view returns (AchievementDetails[] memory);

/**
* @notice Returns whether the account has the achievement.
* @param account The address of the account.
* @param achievementName The name of the achievement.
* @return Whether the account has the achievement.
*/
function hasAchievement(address account, string calldata achievementName)
external
view
returns (bool);

/**
* @notice Returns the address of the PowerToken contract.
* @return Address of the PowerToken contract.
*/
function powerToken() external view returns (address);
}
13 changes: 13 additions & 0 deletions src/interfaces/IAchievementErrors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;

interface IErrors {
/// @dev receiver is empty.
error Unauthorized();

/// @dev achievement is not set.
error AchievementNotSet();

/// @dev achievement is already mint.
error AlreadyMinted(string achievementName);
}
6 changes: 6 additions & 0 deletions src/interfaces/IAchievementEvents.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;

interface IEvents {
event AchievementSet(string indexed name, string description, string imageURL);
}
8 changes: 8 additions & 0 deletions src/libraries/AchievementDataTypes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;

struct AchievementDetails {
string name;
string description;
string imageURL;
}
169 changes: 169 additions & 0 deletions src/misc/Achievement.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// SPDX-License-Identifier: MIT
// solhint-disable quotes
pragma solidity 0.8.22;

import {AccessControlEnumerableUpgradeable} from
"@openzeppelin-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import {ERC721Upgradeable} from "@openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol";

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

import {IAchievement} from "../interfaces/IAchievement.sol";
import {IErrors} from "../interfaces/IAchievementErrors.sol";
import {IEvents} from "../interfaces/IAchievementEvents.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {AchievementDetails} from "../libraries/AchievementDataTypes.sol";

contract Achievement is
IERC721,
AccessControlEnumerableUpgradeable,
ERC721Upgradeable,
IErrors,
IEvents,
IAchievement
{
using EnumerableSet for EnumerableSet.Bytes32Set;

bytes32 public constant APP_ADMIN_ROLE = keccak256("APP_ADMIN_ROLE");

bytes32 public constant APP_USER_ROLE = keccak256("APP_USER_ROLE");

uint256 internal _counter;
uint256 internal _totalSupply;

address internal _powerToken;

// achievement name => achievement details
EnumerableSet.Bytes32Set internal _achievements;

// mapping from bytes32(hash of achievement name) to AchievementDetails
mapping(bytes32 => AchievementDetails) internal _achievementDetails;

// tokenId => achievement name
mapping(uint256 => bytes32) internal _tokenIdToAchievements;

// if user has minted this type of achievement
mapping(address => mapping(bytes32 => bool)) internal _userAchievements;

/// @inheritdoc IAchievement
function initialize(
string calldata name_,
string calldata symbol_,
address admin_,
address powerToken_
) external override initializer {
__ERC721_init(name_, symbol_);

if (admin_ != address(0)) {
_grantRole(DEFAULT_ADMIN_ROLE, admin_);
_grantRole(APP_ADMIN_ROLE, admin_);
}

_powerToken = powerToken_;
}

/// @inheritdoc IAchievement
function setAchievement(
string calldata name,
string calldata description,
string calldata imageURL
) external override onlyRole(APP_ADMIN_ROLE) {
bytes32 nameHash = _getNameHash(name);
_achievements.add(nameHash);
_achievementDetails[nameHash] = AchievementDetails(name, description, imageURL);
emit AchievementSet(name, description, imageURL);
}

/// @inheritdoc IAchievement
function mint(string calldata achievementName) external override returns (uint256 tokenId) {
address account = msg.sender;

if (!AccessControlEnumerableUpgradeable(_powerToken).hasRole(APP_USER_ROLE, account)) {
revert Unauthorized();
}

bytes32 nameHash = _getNameHash(achievementName);
if (bytes(_achievementDetails[nameHash].imageURL).length == 0) {
revert AchievementNotSet();
}

// TODO: check if the user has already minted this type of achievement
if (_userAchievements[account][nameHash]) {
revert AlreadyMinted(achievementName);
}

_userAchievements[account][nameHash] = true;

tokenId = ++_counter;
_mint(account, tokenId);

_tokenIdToAchievements[tokenId] = nameHash;

++_totalSupply;
}

/// @inheritdoc ERC721Upgradeable
function tokenURI(uint256 id) public view override returns (string memory) {
AchievementDetails memory details = _achievementDetails[_tokenIdToAchievements[id]];
string memory json = string.concat(
'{"name": "Achievement: ',
details.name,
'", "description": "',
details.description,
'", "image": "',
details.imageURL,
'"}'
);

return json;
}

/// @inheritdoc IAchievement
function totalSupply() external view override returns (uint256) {
return _totalSupply;
}

/// @inheritdoc IAchievement
function getAllAchievements() external view override returns (AchievementDetails[] memory) {
uint256 length = _achievements.length();
AchievementDetails[] memory result = new AchievementDetails[](length);

for (uint256 i = 0; i < length; i++) {
bytes32 nameHash = _achievements.at(i);
result[i] = _achievementDetails[nameHash];
}

return result;
}

/// @inheritdoc IAchievement
function hasAchievement(address account, string calldata achievementName)
external
view
override
returns (bool)
{
return _userAchievements[account][_getNameHash(achievementName)];
}

/// @inheritdoc IAchievement
function powerToken() external view override returns (address) {
return _powerToken;
}

function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(AccessControlEnumerableUpgradeable, ERC721Upgradeable, IERC165)
returns (bool)
{
return super.supportsInterface(interfaceId);
}

function _getNameHash(string calldata name) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(name));
}
}
20 changes: 4 additions & 16 deletions test/PowerToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ contract PowerTokenTest is Utils, IErrors, IEvents, ERC20Upgradeable {
bytes32 public constant someFeedId2 = bytes32("someFeedId2");
bytes32 public constant someFeedId3 = bytes32("someFeedId3");

DeployConfig internal _cfg;
address public appAdmin;

PowerToken internal _token;
Expand All @@ -33,24 +32,13 @@ contract PowerTokenTest is Utils, IErrors, IEvents, ERC20Upgradeable {
function setUp() public {
// read config from local.json
string memory path = string.concat(vm.projectRoot(), "/deploy-config/", "local" ".json");
_cfg = new DeployConfig(path);
appAdmin = _cfg.appAdmin();

PowerToken tokenImpl = new PowerToken(appAdmin);
DeployConfig cfg = new DeployConfig(path);
appAdmin = cfg.appAdmin();

TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(tokenImpl),
_cfg.proxyAdminOwner(),
abi.encodeWithSelector(
IPowerToken.initialize.selector,
_cfg.name(),
_cfg.symbol(),
_cfg.appAdmin(),
_cfg.dailyMintLimit()
)
);
address proxy = deployPowerTokenProxy(cfg);

_token = PowerToken(address(proxy));
_token = PowerToken(proxy);
}

function testMintToTreasury(uint256 amount) public {
Expand Down
47 changes: 47 additions & 0 deletions test/helpers/Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
// solhint-disable comprehensive-interface
pragma solidity 0.8.22;

import {DeployConfig} from "../../script/DeployConfig.s.sol";
import {IPowerToken} from "../../src/interfaces/IPowerToken.sol";
import {TransparentUpgradeableProxy} from "../../src/upgradeability/TransparentUpgradeableProxy.sol";

import {PowerToken} from "../../src/PowerToken.sol";
import {Achievement} from "../../src/misc/Achievement.sol";
import "forge-std/Test.sol";
import "forge-std/Vm.sol";

Expand All @@ -25,4 +31,45 @@ contract Utils is Test {

vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData);
}

function deployPowerTokenProxy(DeployConfig cfg) public returns (address) {
address appAdmin = cfg.appAdmin();

PowerToken tokenImpl = new PowerToken(appAdmin);

TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(tokenImpl),
cfg.proxyAdminOwner(),
abi.encodeWithSelector(
IPowerToken.initialize.selector,
cfg.name(),
cfg.symbol(),
cfg.appAdmin(),
cfg.dailyMintLimit()
)
);

return address(proxy);
}

function deployAchievementProxy(DeployConfig cfg, address powerToken)
public
returns (address)
{
Achievement achievementImpl = new Achievement();

TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(achievementImpl),
cfg.proxyAdminOwner(),
abi.encodeWithSelector(
Achievement.initialize.selector,
cfg.nftName(),
cfg.nftSymbol(),
cfg.appAdmin(),
powerToken
)
);

return address(proxy);
}
}
Loading

0 comments on commit 5ad6392

Please sign in to comment.