Skip to content

Commit

Permalink
feat: upgrade contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
Agilulfo1820 committed Sep 10, 2024
1 parent d2eab62 commit fb4dc89
Show file tree
Hide file tree
Showing 14 changed files with 1,843 additions and 37 deletions.
12 changes: 12 additions & 0 deletions config/contracts/envs/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,17 @@ export function createLocalConfig() {
// Migration
MIGRATION_ADDRESS: "0x865306084235Bf804c8Bba8a8d56890940ca8F0b", // 10th account from mnemonic of solo network
MIGRATION_AMOUNT: BigInt("3750000000000000000000000"), // 3.75 million B3TR tokens from pilot show

// X 2 Earn Rewards Pool
X_2_EARN_INITIAL_IMPACT_KEYS: [
"carbon",
"water",
"energy",
"waste_mass",
"education_time",
"timber",
"plastic",
"trees_planted",
],
})
}
3 changes: 3 additions & 0 deletions config/contracts/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ export type ContractsConfig = {
// Migration
MIGRATION_ADDRESS: string
MIGRATION_AMOUNT: bigint

// X 2 Earn Rewards Pool
X_2_EARN_INITIAL_IMPACT_KEYS: string[]
}
276 changes: 265 additions & 11 deletions contracts/X2EarnRewardsPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { IX2EarnApps } from "./interfaces/IX2EarnApps.sol";
import { IX2EarnRewardsPool } from "./interfaces/IX2EarnRewardsPool.sol";
import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

/**
* @title X2EarnRewardsPool
Expand All @@ -50,6 +51,7 @@ contract X2EarnRewardsPool is
{
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
bytes32 public constant CONTRACTS_ADDRESS_MANAGER_ROLE = keccak256("CONTRACTS_ADDRESS_MANAGER_ROLE");
bytes32 public constant IMPACT_KEY_MANAGER_ROLE = keccak256("IMPACT_KEY_MANAGER_ROLE");

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand All @@ -61,6 +63,8 @@ contract X2EarnRewardsPool is
IB3TR b3tr;
IX2EarnApps x2EarnApps;
mapping(bytes32 appId => uint256) availableFunds; // Funds that the app can use to reward users
mapping(string => uint256) impactKeyIndex; // Mapping from impact key to its index (1-based to distinguish from non-existent)
string[] allowedImpactKeys; // Array storing impact keys
}

// keccak256(abi.encode(uint256(keccak256("b3tr.storage.X2EarnRewardsPool")) - 1)) & ~bytes32(uint256(0xff))
Expand Down Expand Up @@ -99,6 +103,31 @@ contract X2EarnRewardsPool is
$.x2EarnApps = _x2EarnApps;
}

function initializeV2(address _impactKeyManager, string[] memory _initialImpactKeys) external reinitializer(2) {
require(_impactKeyManager != address(0), "X2EarnRewardsPool: impactKeyManager is the zero address");
require(_initialImpactKeys.length > 0, "X2EarnRewardsPool: initialImpactKeys is empty");

_grantRole(IMPACT_KEY_MANAGER_ROLE, _impactKeyManager);

X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage();

for (uint256 i; i < _initialImpactKeys.length; i++) {
_addImpactKey(_initialImpactKeys[i], $);
}
}

// ---------- Modifiers ---------- //
/**
* @notice Modifier to check if the user has the required role or is the DEFAULT_ADMIN_ROLE
* @param role - the role to check
*/
modifier onlyRoleOrAdmin(bytes32 role) {
if (!hasRole(role, msg.sender) && !hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
revert("X2EarnRewardsPool: sender is not an admin nor has the required role");
}
_;
}

// ---------- Authorizers ---------- //

function _authorizeUpgrade(address newImplementation) internal virtual override onlyRole(UPGRADER_ROLE) {}
Expand Down Expand Up @@ -155,32 +184,210 @@ contract X2EarnRewardsPool is
}

/**
* @dev See {IX2EarnRewardsPool-distributeReward}
* @dev Distribute rewards to a user with a self provided proof.
* @notice This function is deprecated and kept for backwards compatibility, will be removed in future versions.
*/
function distributeReward(
function distributeRewardDeprecated(bytes32 appId, uint256 amount, address receiver, string memory proof) external {
// emit event with provided json proof
emit RewardDistributed(amount, appId, receiver, proof, msg.sender);

_distributeReward(appId, amount, receiver);
}

/**
* @dev {IX2EarnRewardsPool-distributeReward}
* @notice the proof argument is unused but kept for backwards compatibility
*/
function distributeReward(bytes32 appId, uint256 amount, address receiver, string memory /*proof*/) external {
// emit event with empty proof
emit RewardDistributed(amount, appId, receiver, "", msg.sender);

_distributeReward(appId, amount, receiver);
}

/**
* @dev See {IX2EarnRewardsPool-distributeRewardWithProof}
*/
function distributeRewardWithProof(
bytes32 appId,
uint256 amount,
address receiver,
string memory proof
) external nonReentrant {
string[] memory proofTypes,
string[] memory proofValues,
string[] memory impactCodes,
uint256[] memory impactValues,
string memory description
) external {
_emitProof(appId, amount, receiver, proofTypes, proofValues, impactCodes, impactValues, description);
_distributeReward(appId, amount, receiver);
}

/**
* @dev See {IX2EarnRewardsPool-distributeReward}
* @notice The impact is an array of integers and codes that represent the impact of the action.
* Each index of the array represents a different impact.
* The codes are predefined and the values are the impact values.
* Example: ["carbon", "water", "energy"], [100, 200, 300]
*/
function _distributeReward(bytes32 appId, uint256 amount, address receiver) internal nonReentrant {
X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage();

// check authorization
require($.x2EarnApps.appExists(appId), "X2EarnRewardsPool: app does not exist");

require($.x2EarnApps.isRewardDistributor(appId, msg.sender), "X2EarnRewardsPool: not a reward distributor");

// check if the app has enough available funds to reward users
// check if the app has enough available funds to distribute
require($.availableFunds[appId] >= amount, "X2EarnRewardsPool: app has insufficient funds");

// check if the contract has enough funds
require($.b3tr.balanceOf(address(this)) >= amount, "X2EarnRewardsPool: insufficient funds on contract");

// transfer the rewards to the receiver
// Transfer the rewards to the receiver
$.availableFunds[appId] -= amount;
require($.b3tr.transfer(receiver, amount), "X2EarnRewardsPool: Allocation transfer to app failed");
}

/**
* @dev Emits the RewardDistributed event with the provided proofs and impacts.
*/
function _emitProof(
bytes32 appId,
uint256 amount,
address receiver,
string[] memory proofTypes,
string[] memory proofValues,
string[] memory impactCodes,
uint256[] memory impactValues,
string memory description
) internal {
// Build the JSON proof string from the proof and impact data
string memory jsonProof = buildProof(proofTypes, proofValues, impactCodes, impactValues, description);

// emit event
emit RewardDistributed(amount, appId, receiver, proof, msg.sender);
emit RewardDistributed(amount, appId, receiver, jsonProof, msg.sender);
}

/**
* @dev see {IX2EarnRewardsPool-buildProof}
*/
function buildProof(
string[] memory proofTypes,
string[] memory proofValues,
string[] memory impactCodes,
uint256[] memory impactValues,
string memory description
) public view virtual returns (string memory) {
bool hasProof = proofTypes.length > 0 && proofValues.length > 0;
bool hasImpact = impactCodes.length > 0 && impactValues.length > 0;
bool hasDescription = bytes(description).length > 0;

// If neither proof nor impact is provided, return an empty string
if (!hasProof && !hasImpact) {
return "";
}

// Initialize an empty JSON bytes array with version
bytes memory json = abi.encodePacked('{"version": 2');

// Add description if available
if (hasDescription) {
json = abi.encodePacked(json, ',"description": "', description, '"');
}

// Add proof if available
if (hasProof) {
bytes memory jsonProof = _buildProofJson(proofTypes, proofValues);

json = abi.encodePacked(json, ',"proof": ', jsonProof);
}

// Add impact if available
if (hasImpact) {
bytes memory jsonImpact = _buildImpactJson(impactCodes, impactValues);

json = abi.encodePacked(json, ',"impact": ', jsonImpact);
}

// Close the JSON object
json = abi.encodePacked(json, "}");

return string(json);
}

/**
* @dev Builds the proof JSON string from the proof data.
* @param proofTypes the proof types
* @param proofValues the proof values
*/
function _buildProofJson(
string[] memory proofTypes,
string[] memory proofValues
) internal pure returns (bytes memory) {
require(proofTypes.length == proofValues.length, "X2EarnRewardsPool: Mismatched input lengths for Proof");

bytes memory json = abi.encodePacked("{");

for (uint256 i; i < proofTypes.length; i++) {
if (_isValidProofType(proofTypes[i])) {
json = abi.encodePacked(json, '"', proofTypes[i], '":', '"', proofValues[i], '"');
if (i < proofTypes.length - 1) {
json = abi.encodePacked(json, ",");
}
} else {
revert("X2EarnRewardsPool: Invalid proof type");
}
}

json = abi.encodePacked(json, "}");

return json;
}

/**
* @dev Builds the impact JSON string from the impact data.
*
* @param impactCodes the impact codes
* @param impactValues the impact values
*/
function _buildImpactJson(
string[] memory impactCodes,
uint256[] memory impactValues
) internal view returns (bytes memory) {
require(impactCodes.length == impactValues.length, "X2EarnRewardsPool: Mismatched input lengths for Impact");

bytes memory json = abi.encodePacked("{");

for (uint256 i; i < impactValues.length; i++) {
if (_isAllowedImpactKey(impactCodes[i])) {
json = abi.encodePacked(json, '"', impactCodes[i], '":', Strings.toString(impactValues[i]));
if (i < impactValues.length - 1) {
json = abi.encodePacked(json, ",");
}
} else {
revert("X2EarnRewardsPool: Invalid impact key");
}
}

json = abi.encodePacked(json, "}");

return json;
}

/**
* @dev Checks if the key is allowed.
*/
function _isAllowedImpactKey(string memory key) internal view returns (bool) {
X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage();
return $.impactKeyIndex[key] > 0;
}

/**
* @dev Checks if the proof type is valid.
*/
function _isValidProofType(string memory proofType) internal pure returns (bool) {
return
keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("image")) ||
keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("link")) ||
keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("text")) ||
keccak256(abi.encodePacked(proofType)) == keccak256(abi.encodePacked("video"));
}

/**
Expand All @@ -195,6 +402,45 @@ contract X2EarnRewardsPool is
$.x2EarnApps = _x2EarnApps;
}

/**
* @dev Adds a new allowed impact key.
* @param newKey the new key to add
*/
function addImpactKey(string memory newKey) external onlyRoleOrAdmin(IMPACT_KEY_MANAGER_ROLE) {
X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage();
_addImpactKey(newKey, $);
}

/**
* @dev Internal function to add a new allowed impact key.
* @param key the new key to add
* @param $ the storage pointer
*/
function _addImpactKey(string memory key, X2EarnRewardsPoolStorage storage $) internal {
require($.impactKeyIndex[key] == 0, "X2EarnRewardsPool: Key already exists");
$.allowedImpactKeys.push(key);
$.impactKeyIndex[key] = $.allowedImpactKeys.length; // Store 1-based index
}

/**
* @dev Removes an allowed impact key.
* @param keyToRemove the key to remove
*/
function removeImpactKey(string memory keyToRemove) external onlyRoleOrAdmin(IMPACT_KEY_MANAGER_ROLE) {
X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage();
uint256 index = $.impactKeyIndex[keyToRemove];
require(index > 0, "X2EarnRewardsPool: Key not found");

// Move the last element into the place to delete
string memory lastKey = $.allowedImpactKeys[$.allowedImpactKeys.length - 1];
$.allowedImpactKeys[index - 1] = lastKey;
$.impactKeyIndex[lastKey] = index; // Update the index of the last key

// Remove the last element
$.allowedImpactKeys.pop();
delete $.impactKeyIndex[keyToRemove];
}

// ---------- Getters ---------- //

/**
Expand All @@ -209,7 +455,7 @@ contract X2EarnRewardsPool is
* @dev See {IX2EarnRewardsPool-version}
*/
function version() external pure virtual returns (string memory) {
return "1";
return "2";
}

/**
Expand All @@ -228,6 +474,14 @@ contract X2EarnRewardsPool is
return $.x2EarnApps;
}

/**
* @dev Retrieves the allowed impact keys.
*/
function getAllowedImpactKeys() external view returns (string[] memory) {
X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage();
return $.allowedImpactKeys;
}

// ---------- Fallbacks ---------- //

/**
Expand Down
File renamed without changes.
Loading

0 comments on commit fb4dc89

Please sign in to comment.