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

Feat: onchain impacts and proof generation #18

Merged
merged 2 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
Loading
Loading