Open Source Algorithms
Smart Contracts
Last Updated: March 3rd, 2025
For suggested improvements or requested changes, email community@aemula.com.
We are in process of setting up the community governance of the Aemula smart contracts, allowing community members to make pull requests of the source code from Github and propose changes for vote by the community.
Deployments
- Proxy Address: 0x6Ab1a35959303Cc43A9DE343E0B7F9F05e4aA2D4
- Implementation Address: 0x201CfF51d0Cc3c1d6aAd9284D2510286E93e21dC
AemulaV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
// OpenZeppelin imports
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
// set dependencies from OpenZeppelin contracts
contract AemulaV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable, AccessControlUpgradeable, PausableUpgradeable {
//**********************************************************************************************************
// STATE VARIABLE DECLARATIONS
//**********************************************************************************************************
// admin role declarations
bytes32 public constant UPGRADE_ADMIN_ROLE = keccak256("UPGRADE_ADMIN_ROLE");
bytes32 public constant SUBSCRIPTION_ADMIN_ROLE = keccak256("SUBSCRIPTION_ADMIN_ROLE");
bytes32 public constant OPERATOR_ADMIN_ROLE = keccak256("OPERATOR_ADMIN_ROLE");
bytes32 public constant GOVERNANCE_ADMIN_ROLE = keccak256("GOVERNANCE_ADMIN_ROLE");
bytes32 public constant GUARDIAN_ADMIN_ROLE = keccak256("GUARDIAN_ADMIN_ROLE");
// merkle root proof declarations
bytes32 public currentSubscriptionMerkleRoot;
bytes32 public currentReputationMerkleRoot;
bytes32 public currentArticleRankingMerkleRoot;
// state variable declarations
uint256 public trialPeriodDays;
uint256 public maxArticleDeleteBatchSize;
uint256 public maxAuthorArticlesPaginationLimit;
uint256 public maxNeutralBatchSize;
// mappings
// users to their subscriptionExpiry timestamp in seconds
mapping(address => uint256) public users;
// users to an array of their published articles by IPFS CID
mapping(address => string[]) public authorArticles;
// IPFS CIDs to their author's address
mapping(string => address) public articles;
//**********************************************************************************************************
// STORAGE GAP
//**********************************************************************************************************
// declare state variables in future upgrades here
// reduce storage gap size by the amount of slots taken up by new variable declarations
uint256[49] private __gap;
//**********************************************************************************************************
// EVENT DECLARATION
//**********************************************************************************************************
// user events
event NewUser(address indexed walletID, uint256 expiry);
event RemoveUser(address indexed walletID);
// article events
event NewArticle(string ipfsCID, address indexed authorID);
event UpdateArticle(string oldIpfsCID, string newIpfsCID);
event DeleteArticle(string ipfsCID);
event DeleteArticlesBatch(address indexed authorID, uint256 batchSize);
event DeleteAllArticlesComplete(address indexed authorID);
// interaction events
event ReadSupport(address indexed walletID, string ipfsCID);
event ReadDisagree(address indexed walletID, string ipfsCID);
event ReadNeutral(address indexed walletID, string ipfsCID);
event ReadReport(address indexed walletID, string ipfsCID);
// merkle root proof events
event NewSubscriptionMerkleRoot(bytes32 oldRoot, bytes32 newRoot);
event NewReputationMerkleRoot(bytes32 oldRoot, bytes32 newRoot);
event NewArticleRankingMerkleRoot(bytes32 oldRoot, bytes32 newRoot);
// subscription events
event SubscriptionStarted(address indexed walletID, uint256 expiry, bool isAnnual);
event SubscriptionEnded(address indexed walletID);
// upgradeability events
event UpgradedImplementation(address newImplementation);
// admin role events
event GrantedUpgradeAdminRole(address account, address sender);
event RevokedUpgradeAdminRole(address account, address sender);
event GrantedSubscriptionAdminRole(address account, address sender);
event RevokedSubscriptionAdminRole(address account, address sender);
event GrantedOperatorAdminRole(address account, address sender);
event RevokedOperatorAdminRole(address account, address sender);
event GrantedGovernanceAdminRole(address account, address sender);
event RevokedGovernanceAdminRole(address account, address sender);
event GrantedGuardianAdminRole(address account, address sender);
event RevokedGuardianAdminRole(address account, address sender);
// governance role events
event GovernanceRemoveUser(address indexed walletID, address sender);
event GovernanceDeleteArticle(string ipfsCID, address sender);
//**********************************************************************************************************
// CONTRACT FUNCTIONS
//**********************************************************************************************************
function initialize(
address upgradeAdminAddress,
address subscriptionAdminAddress,
address operatorAdminAddress,
address governanceAdminAddress,
address guardianAdminAddress
) public initializer {
__UUPSUpgradeable_init();
__Ownable_init(_msgSender());
__AccessControl_init();
__Pausable_init();
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
_grantRole(UPGRADE_ADMIN_ROLE, upgradeAdminAddress);
_grantRole(SUBSCRIPTION_ADMIN_ROLE, subscriptionAdminAddress);
_grantRole(OPERATOR_ADMIN_ROLE, operatorAdminAddress);
_grantRole(GOVERNANCE_ADMIN_ROLE, governanceAdminAddress);
_grantRole(GUARDIAN_ADMIN_ROLE, guardianAdminAddress);
_setRoleAdmin(UPGRADE_ADMIN_ROLE, GOVERNANCE_ADMIN_ROLE);
_setRoleAdmin(SUBSCRIPTION_ADMIN_ROLE, GOVERNANCE_ADMIN_ROLE);
_setRoleAdmin(OPERATOR_ADMIN_ROLE, GOVERNANCE_ADMIN_ROLE);
_setRoleAdmin(GOVERNANCE_ADMIN_ROLE, GOVERNANCE_ADMIN_ROLE);
_setRoleAdmin(GUARDIAN_ADMIN_ROLE, GOVERNANCE_ADMIN_ROLE);
trialPeriodDays = 30;
maxArticleDeleteBatchSize = 100;
maxAuthorArticlesPaginationLimit = 50;
maxNeutralBatchSize = 50;
}
function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADE_ADMIN_ROLE) {
emit UpgradedImplementation(newImplementation);
}
//**********************************************************************************************************
// USER FUNCTIONS
//**********************************************************************************************************
function createUser() external whenNotPaused {
require(!_isUser(msg.sender), "User already registered");
uint256 expiry = block.timestamp + (trialPeriodDays * 1 days);
users[msg.sender] = expiry;
emit NewUser(msg.sender, expiry);
}
function removeUser() external whenNotPaused {
require(_isUser(msg.sender), "User not registered to remove user");
delete users[msg.sender];
emit RemoveUser(msg.sender);
}
// HELPERS & GETTERS
function _isUser(address _walletID) internal view returns (bool) {
return users[_walletID] != 0;
}
function isUser(address _walletID) external view returns (bool) {
return _isUser(_walletID);
}
function _hasActiveSubscription(address _walletID) internal view returns (bool) {
return block.timestamp < users[_walletID];
}
function hasActiveSubscription(address _walletID) external view returns (bool) {
return _hasActiveSubscription(_walletID);
}
function userSubscriptionExpiry(address _walletID) external view returns (uint256) {
return users[_walletID];
}
//**********************************************************************************************************
// ARTICLE FUNCTIONS
//**********************************************************************************************************
function createArticle(string calldata _ipfsCID) external whenNotPaused {
require(_hasActiveSubscription(msg.sender), "No active subscription to publish article");
require(articles[_ipfsCID] == address(0), "Article already exists");
articles[_ipfsCID] = msg.sender;
authorArticles[msg.sender].push(_ipfsCID);
emit NewArticle(_ipfsCID, msg.sender);
}
function updateArticle(string calldata _oldIpfsCID, string calldata _newIpfsCID) external whenNotPaused {
require(_hasActiveSubscription(msg.sender), "No active subscription to update article");
require(articles[_oldIpfsCID] == msg.sender, "User is not the author of the article to be updated");
require(articles[_newIpfsCID] == address(0), "New article already exists and cannot be updated");
articles[_newIpfsCID] = msg.sender;
authorArticles[msg.sender].push(_newIpfsCID);
delete articles[_oldIpfsCID];
_softDeleteArticleInAuthorArray(_oldIpfsCID, msg.sender);
emit UpdateArticle(_oldIpfsCID, _newIpfsCID);
}
function _softDeleteArticleInAuthorArray(string memory _ipfsCID, address _user) internal {
string[] storage userArticles = authorArticles[_user];
for (uint i = 0; i < userArticles.length; i++) {
if (keccak256(abi.encodePacked(userArticles[i])) == keccak256(abi.encodePacked(_ipfsCID))) {
userArticles[i] = "";
break;
}
}
}
function softDeleteArticle(string calldata _ipfsCID) external whenNotPaused {
require(articles[_ipfsCID] == msg.sender, "User is not the author of the article to delete");
delete articles[_ipfsCID];
_softDeleteArticleInAuthorArray(_ipfsCID, msg.sender);
emit DeleteArticle(_ipfsCID);
}
function hardDeleteAllArticles() external whenNotPaused {
require(_isUser(msg.sender), "Caller is not a registered user");
uint256 totalArticles = authorArticles[msg.sender].length;
require(totalArticles > 0, "User has no articles to delete");
uint256 batchSize = totalArticles < maxArticleDeleteBatchSize ? totalArticles : maxArticleDeleteBatchSize;
for (uint256 i = 0; i < batchSize; i++) {
string memory articleCID = authorArticles[msg.sender][totalArticles - 1];
delete articles[articleCID];
authorArticles[msg.sender].pop();
totalArticles--;
}
emit DeleteArticlesBatch(msg.sender, batchSize);
if (authorArticles[msg.sender].length == 0) {
emit DeleteAllArticlesComplete(msg.sender);
}
}
// HELPERS & GETTERS
function getArticleAuthor(string calldata _ipfsCID) external view returns (address) {
return articles[_ipfsCID];
}
function getAuthorArticles(address _authorAddress) external view returns (string[] memory) {
require(authorArticles[_authorAddress].length <= maxAuthorArticlesPaginationLimit, "Article array is too big. Use getPaginatedAuthorArticles");
return authorArticles[_authorAddress];
}
function getPaginatedAuthorArticles(address _authorAddress, uint256 _offset, uint256 _limit) external view returns (string[] memory) {
require(_limit <= maxAuthorArticlesPaginationLimit, "Pagination limit is too large");
string[] storage articlesArray = authorArticles[_authorAddress];
uint256 totalArticles = articlesArray.length;
if (_offset >= totalArticles) {
return new string[](0);
}
uint256 end = _offset + _limit;
if (end > totalArticles) {
end = totalArticles;
}
uint256 resultSize = end - _offset;
string[] memory paginatedArticles = new string[](resultSize);
for (uint256 i = 0; i < resultSize; i++) {
paginatedArticles[i] = articlesArray[_offset + i];
}
return paginatedArticles;
}
//**********************************************************************************************************
// INTERACTION FUNCTIONS
//**********************************************************************************************************
function readSupport(string calldata ipfsCID) external {
require(_hasActiveSubscription(msg.sender), "Interaction denied: no active subscription");
emit ReadSupport(msg.sender, ipfsCID);
}
function readDisagree(string calldata ipfsCID) external {
require(_hasActiveSubscription(msg.sender), "Interaction denied: no active subscription");
emit ReadDisagree(msg.sender, ipfsCID);
}
function readReport(string calldata ipfsCID) external {
require(_hasActiveSubscription(msg.sender), "Interaction denied: no active subscription");
emit ReadReport(msg.sender, ipfsCID);
}
function readNeutral(string calldata ipfsCID) external {
require(_hasActiveSubscription(msg.sender), "Interaction denied: no active subscription");
emit ReadNeutral(msg.sender, ipfsCID);
}
//**********************************************************************************************************
// MERKLE ROOT PROOF FUNCTIONS
//**********************************************************************************************************
function updateSubscriptionMerkleRoot(bytes32 _newRoot) external {
require(hasRole(SUBSCRIPTION_ADMIN_ROLE, msg.sender), "Caller to update subscription root is not a subscription admin");
emit NewSubscriptionMerkleRoot(currentSubscriptionMerkleRoot, _newRoot);
currentSubscriptionMerkleRoot = _newRoot;
}
function updateReputationMerkleRoot(bytes32 _newRoot) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller to update reputation root is not an operator admin");
emit NewReputationMerkleRoot(currentReputationMerkleRoot, _newRoot);
currentReputationMerkleRoot = _newRoot;
}
function updateArticleRankingMerkleRoot(bytes32 _newRoot) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller to update article ranking root is not an operator admin");
emit NewArticleRankingMerkleRoot(currentArticleRankingMerkleRoot, _newRoot);
currentArticleRankingMerkleRoot = _newRoot;
}
//**********************************************************************************************************
// SUBSCRIPTION FUNCTIONS
//**********************************************************************************************************
function startSubscription(address _user, uint256 _expiry, bool _isAnnual) external {
require(hasRole(SUBSCRIPTION_ADMIN_ROLE, msg.sender), "Caller is not a subscription admin to start a subscription");
require(_isUser(_user), "User is not registered. Cannot start subscription.");
users[_user] = _expiry;
emit SubscriptionStarted(_user, _expiry, _isAnnual);
}
function endSubscription(address _user) external {
require(hasRole(SUBSCRIPTION_ADMIN_ROLE, msg.sender), "Caller is not a subscription admin to end a subscription");
require(_isUser(_user), "User is not registered. Cannot end subscription.");
users[_user] = block.timestamp;
emit SubscriptionEnded(_user);
}
function setTrialPeriod(uint256 _days) external {
require(hasRole(SUBSCRIPTION_ADMIN_ROLE, msg.sender), "Caller is not a subscription admin to set trial subscription period");
require(_days > 0 && _days <= 365, "Trial period must be between 1 and 365 days");
trialPeriodDays = _days;
}
// HELPERS & GETTERS
function getTrialPeriod() external view returns(uint256) {
return trialPeriodDays;
}
//**********************************************************************************************************
// BATCH LIMIT FUNCTIONS
//**********************************************************************************************************
function setMaxArticleDeleteBatchSize(uint256 _size) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to set max article delete batch size");
require(_size > 0 && _size <= 500, "Article delete batch size must be between 1 and 500");
maxArticleDeleteBatchSize = _size;
}
function setMaxAuthorArticlesPaginationLimit(uint256 _size) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to set author articles pagination limit");
require(_size > 0 && _size <= 100, "Author articles pagination limit must be between 1 and 100");
maxAuthorArticlesPaginationLimit = _size;
}
function setMaxNeutralBatchSize(uint256 _size) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to set max neutral batch size");
require(_size > 0 && _size <= 100, "Neutral batch size must be between 1 and 100");
maxNeutralBatchSize = _size;
}
// HELPERS & GETTERS
function getMaxArticleDeleteBatchSize() external view returns (uint256) {
return maxArticleDeleteBatchSize;
}
function getMaxAuthorArticlesPaginationLimit() external view returns (uint256) {
return maxAuthorArticlesPaginationLimit;
}
function getMaxNeutralBatchSize() external view returns (uint256) {
return maxNeutralBatchSize;
}
//**********************************************************************************************************
// ADMIN FUNCTIONS
//**********************************************************************************************************
function adminCreateUser(address _user) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to call adminCreateUser");
require(!_isUser(_user), "User already registered");
uint256 expiry = block.timestamp + (trialPeriodDays * 1 days);
users[_user] = expiry;
emit NewUser(_user, expiry);
}
function adminNeutral(address _walletID, string[] calldata _ipfsCIDs) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to batch process neutrals");
require(_isUser(_walletID), "User not registered to batch process neutrals");
require(_ipfsCIDs.length <= maxNeutralBatchSize, "Neutral batch size exceeds maximum allowed");
for (uint i = 0; i < _ipfsCIDs.length; i++) {
emit ReadNeutral(_walletID, _ipfsCIDs[i]);
}
}
function adminCreateArticle(string calldata _ipfsCID, address _authorAddress) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to create article");
require(_isUser(_authorAddress), "Author is not a registered user to call adminCreateArticle");
require(articles[_ipfsCID] == address(0), "New article already exists");
articles[_ipfsCID] = _authorAddress;
authorArticles[_authorAddress].push(_ipfsCID);
emit NewArticle(_ipfsCID, _authorAddress);
}
function adminUpdateArticle(address _authorAddress, string calldata _oldIpfsCID, string calldata _newIpfsCID) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to update article");
require(_isUser(_authorAddress), "Author is not a registered user to call adminUpdateArticle");
require(articles[_oldIpfsCID] == _authorAddress, "authorAddress is not the author of the article to be updated");
require(articles[_newIpfsCID] == address(0), "New article already exists and cannot be updated");
articles[_newIpfsCID] = _authorAddress;
authorArticles[_authorAddress].push(_newIpfsCID);
delete articles[_oldIpfsCID];
_softDeleteArticleInAuthorArray(_oldIpfsCID, _authorAddress);
emit UpdateArticle(_oldIpfsCID, _newIpfsCID);
}
function adminCleanAuthorArticleArray(address _authorAddress) external {
require(hasRole(OPERATOR_ADMIN_ROLE, msg.sender), "Caller is not an operator admin to clean author article array");
require(authorArticles[_authorAddress].length > 0, "User has no article array to clean");
string[] storage userArticles = authorArticles[_authorAddress];
uint256 length = userArticles.length;
uint256 validCount = 0;
for (uint256 i = 0; i < length; i++) {
if (bytes(userArticles[i]).length > 0) {
validCount++;
}
}
if (validCount == length) {
return;
}
string[] memory newArticles = new string[](validCount);
uint256 j = 0;
for (uint256 i = 0; i < length; i++) {
if (bytes(userArticles[i]).length > 0) {
newArticles[j] = userArticles[i];
j++;
}
}
delete authorArticles[_authorAddress];
for (uint256 i = 0; i < validCount; i++) {
authorArticles[_authorAddress].push(newArticles[i]);
}
}
//**********************************************************************************************************
// GOVERNANCE FUNCTIONS
//**********************************************************************************************************
function adminRemoveUser(address _user) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "User does not have governance role to remove user");
delete users[_user];
emit GovernanceRemoveUser(_user, msg.sender);
}
function adminSoftDeleteArticle(string calldata _ipfsCID, address _authorAddress) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "User does not have governance role to delete article");
require(articles[_ipfsCID] == _authorAddress, "authorAddress is not the author of the article to delete");
delete articles[_ipfsCID];
_softDeleteArticleInAuthorArray(_ipfsCID, _authorAddress);
emit GovernanceDeleteArticle(_ipfsCID, msg.sender);
}
//**********************************************************************************************************
// ROLE ASSIGNMENT FUNCTIONS
//**********************************************************************************************************
function grantUpgradeAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not governance admin to grant upgrade admin role");
grantRole(UPGRADE_ADMIN_ROLE, _account);
emit GrantedUpgradeAdminRole(_account, msg.sender);
}
function revokeUpgradeAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not governance admin to revoke upgrade admin role");
revokeRole(UPGRADE_ADMIN_ROLE, _account);
emit RevokedUpgradeAdminRole(_account, msg.sender);
}
function grantSubscriptionAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to grant subscription admin role");
grantRole(SUBSCRIPTION_ADMIN_ROLE, _account);
emit GrantedSubscriptionAdminRole(_account, msg.sender);
}
function revokeSubscriptionAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to revoke subscription admin role");
revokeRole(SUBSCRIPTION_ADMIN_ROLE, _account);
emit RevokedSubscriptionAdminRole(_account, msg.sender);
}
function grantOperatorAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to grant operator admin role");
grantRole(OPERATOR_ADMIN_ROLE, _account);
emit GrantedOperatorAdminRole(_account, msg.sender);
}
function revokeOperatorAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to revoke operator admin role");
revokeRole(OPERATOR_ADMIN_ROLE, _account);
emit RevokedOperatorAdminRole(_account, msg.sender);
}
function grantGovernanceAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to grant governance admin role");
grantRole(GOVERNANCE_ADMIN_ROLE, _account);
emit GrantedGovernanceAdminRole(_account, msg.sender);
}
function revokeGovernanceAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to revoke governance admin role");
require(_account != msg.sender, "Cannot revoke governance admin role from self");
revokeRole(GOVERNANCE_ADMIN_ROLE, _account);
emit RevokedGovernanceAdminRole(_account, msg.sender);
}
function grantGuardianAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to grant guardian admin role");
grantRole(GUARDIAN_ADMIN_ROLE, _account);
emit GrantedGuardianAdminRole(_account, msg.sender);
}
function revokeGuardianAdminRole(address _account) external {
require(hasRole(GOVERNANCE_ADMIN_ROLE, msg.sender), "Caller is not a governance admin to revoke guardian admin role");
revokeRole(GUARDIAN_ADMIN_ROLE, _account);
emit RevokedGuardianAdminRole(_account, msg.sender);
}
//**********************************************************************************************************
// GUARDIAN FUNCTIONS
//**********************************************************************************************************
function pause() external {
require(hasRole(GUARDIAN_ADMIN_ROLE, msg.sender), "Caller does not have guardian authority to pause");
_pause();
}
function unpause() external {
require(hasRole(GUARDIAN_ADMIN_ROLE, msg.sender), "Caller does not have guardian authority to unpause");
_unpause();
}
}