From 070f2b3b36b67c09db596a1ce76653e83c111b27 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 22 Jun 2024 14:48:04 +0200 Subject: [PATCH 01/20] feat: set x2EarnRewardsPool contract and appId in EcoEarn --- apps/contracts/contracts/EcoEarn.sol | 30 ++++++- .../interfaces/IX2EarnRewardsPool.sol | 83 +++++++++++++++++++ apps/contracts/scripts/deployEcoEarn.ts | 2 + packages/config-contract/config.ts | 2 + 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 apps/contracts/contracts/interfaces/IX2EarnRewardsPool.sol diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index eb1dc6c..f8256b3 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -27,14 +27,26 @@ pragma solidity ^0.8.19; import '@openzeppelin/contracts/access/AccessControl.sol'; import './interfaces/IToken.sol'; +import './interfaces/IX2EarnRewardsPool.sol'; /** * @title EcoEarn Contract * @dev This contract manages a reward system based on cycles. Participants can make valid submissions to earn rewards. + * Rewards are being distributed by interacting with the VeBetterDAO's X2EarnRewardsPool contract. + * + * @notice To distribute rewards this contract necesitates of a valid APP_ID provided by VeBetterDAO. This contract + * can be initially deployed without this information and DEFAULT_ADMIN_ROLE can update it later through {EcoEarn-setAppId}. */ contract EcoEarn is AccessControl { + // The reward erc20 token IToken public token; + // The X2EarnRewardsPool contract used to distribute rewards + IX2EarnRewardsPool public x2EarnRewardsPoolContract; + + // AppID given by the X2EarnApps contract of VeBetterDAO + bytes32 public appId; + // Mapping from cycle to total rewards mapping(uint256 => uint256) public rewards; @@ -68,14 +80,22 @@ contract EcoEarn is AccessControl { * @dev Constructor for the EcoEarn contract * @param _admin Address of the admin * @param _token Address of the token contract + * @param _x2EarnRewardsPoolContract Address of the X2EarnRewardsPool contract * @param _cycleDuration Duration of each cycle in blocks * @param _maxSubmissionsPerCycle Maximum submissions allowed per cycle + * @param _appId The appId generated by the X2EarnApps contract when app was added to VeBetterDAO */ - constructor(address _admin, address _token, uint256 _cycleDuration, uint256 _maxSubmissionsPerCycle) { + constructor(address _admin, address _token, address _x2EarnRewardsPoolContract, uint256 _cycleDuration, uint256 _maxSubmissionsPerCycle, bytes32 _appId) { + require(_admin !== address(0), "EcoEarn: _admin address cannot be the zero address"); + require(_token !== address(0), "EcoEarn: _token contract address cannot be the zero address"); + require(_x2EarnRewardsPoolContract !== address(0), "EcoEearn: x2EarnRewardsPool contract address cannot be the zero address"); + token = IToken(_token); + x2EarnRewardsPoolContract = IX2EarnRewardsPool(_x2EarnRewardsPoolContract); maxSubmissionsPerCycle = _maxSubmissionsPerCycle; cycleDuration = _cycleDuration; nextCycle = 1; + appId = _appId; _grantRole(DEFAULT_ADMIN_ROLE, _admin); } @@ -163,6 +183,14 @@ contract EcoEarn is AccessControl { nextCycle = _nextCycle; } + /** + * @dev Sets the appId provied by VeBetterDAO + * @param _appId The new app id + */ + function setAppId(bytes32 _appId) external onlyRole(DEFAULT_ADMIN_ROLE) { + appId = _appId; + } + // ---------------- GETTERS ---------------- // /** diff --git a/apps/contracts/contracts/interfaces/IX2EarnRewardsPool.sol b/apps/contracts/contracts/interfaces/IX2EarnRewardsPool.sol new file mode 100644 index 0000000..2246645 --- /dev/null +++ b/apps/contracts/contracts/interfaces/IX2EarnRewardsPool.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +/** + * @title IX2EarnRewardsPool + * @dev Interface designed to be used by a contract that allows x2Earn apps to reward users that performed sustainable actions. + * Funds can be deposited into this contract by specifying the app id that can access the funds. + * Admins of x2EarnApps can withdraw funds from the rewards pool, whihc are sent to the team wallet. + */ +interface IX2EarnRewardsPool { + /** + * @dev Event emitted when a new deposit is made into the rewards pool. + * + * @param amount The amount of $B3TR deposited. + * @param appId The ID of the app for which the deposit was made. + * @param depositor The address of the user that deposited the funds. + */ + event NewDeposit(uint256 amount, bytes32 indexed appId, address indexed depositor); + + /** + * @dev Event emitted when a team withdraws funds from the rewards pool. + * + * @param amount The amount of $B3TR withdrawn. + * @param appId The ID of the app for which the withdrawal was made. + * @param teamWallet The address of the team wallet that received the funds. + * @param withdrawer The address of the user that withdrew the funds. + * @param reason The reason for the withdrawal. + */ + event TeamWithdrawal(uint256 amount, bytes32 indexed appId, address indexed teamWallet, address withdrawer, string reason); + + /** + * @dev Event emitted when a reward is emitted by an app. + * + * @param amount The amount of $B3TR rewarded. + * @param appId The ID of the app that emitted the reward. + * @param receiver The address of the user that received the reward. + * @param proof The proof of the sustainable action that was performed. + * @param distributor The address of the user that distributed the reward. + */ + event RewardDistributed(uint256 amount, bytes32 indexed appId, address indexed receiver, string proof, address indexed distributor); + + /** + * @dev Retrieves the current version of the contract. + * + * @return The version of the contract. + */ + function version() external pure returns (string memory); + + /** + * @dev Function used by x2earn apps to deposit funds into the rewards pool. + * + * @param amount The amount of $B3TR to deposit. + * @param appId The ID of the app. + */ + function deposit(uint256 amount, bytes32 appId) external returns (bool); + + /** + * @dev Function used by x2earn apps to withdraw funds from the rewards pool. + * + * @param amount The amount of $B3TR to withdraw. + * @param appId The ID of the app. + * @param reason The reason for the withdrawal. + */ + function withdraw(uint256 amount, bytes32 appId, string memory reason) external; + + /** + * @dev Gets the amount of funds available for an app to reward users. + * + * @param appId The ID of the app. + */ + function availableFunds(bytes32 appId) external view returns (uint256); + + /** + * @dev Function used by x2earn apps to reward users that performed sustainable actions. + * + * @param appId the app id that is emitting the reward + * @param amount the amount of B3TR token the user is rewarded with + * @param receiver the address of the user that performed the sustainable action and is rewarded + * @param proof a JSON file uploaded on IPFS by the app that adds information on the type of action that was performed + */ + function distributeReward(bytes32 appId, uint256 amount, address receiver, string memory proof) external; +} diff --git a/apps/contracts/scripts/deployEcoEarn.ts b/apps/contracts/scripts/deployEcoEarn.ts index ae7a4c9..40b98f8 100644 --- a/apps/contracts/scripts/deployEcoEarn.ts +++ b/apps/contracts/scripts/deployEcoEarn.ts @@ -8,8 +8,10 @@ async function deployMugshot() { const ecoEarnInstance = await ecoEarn.deploy( owner, config.TOKEN_ADDRESS, + config.X2EARN_REWARDS_POOL, config.CYCLE_DURATION, config.MAX_SUBMISSIONS_PER_CYCLE, + config.APP_ID ); const ecoEarnAddress = await ecoEarnInstance.getAddress(); diff --git a/packages/config-contract/config.ts b/packages/config-contract/config.ts index 6875d3b..0d966ee 100644 --- a/packages/config-contract/config.ts +++ b/packages/config-contract/config.ts @@ -1,6 +1,8 @@ export const config = { "TOKEN_ADDRESS": "0x316Fb7a5D1461363037fca9f44B1631252669D6F", "CONTRACT_ADDRESS": "0xA5A7C5623fb234C961A9c8a50bDefaD2Cae68Fe8", + "X2EARN_REWARDS_POOL": "0xA5A7C5623fb234C961A9c8a50bDefaD2Cae68Fe8", + "APP_ID": "0x899de0d0f0b39e484c8835b2369194c4c102b230c813862db383d44a4efe14d3", "CYCLE_DURATION": 60480, "MAX_SUBMISSIONS_PER_CYCLE": 10 }; From c9733865f41a38610415f93693f06884d3dac171 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 22 Jun 2024 14:53:20 +0200 Subject: [PATCH 02/20] feat: distribute rewards through dao contract --- apps/contracts/contracts/EcoEarn.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index f8256b3..dd083ff 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -137,23 +137,26 @@ contract EcoEarn is AccessControl { * @param amount Amount of tokens to be allocated */ function claimAllocation(uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(amount <= token.balanceOf(msg.sender), 'EcoEarn: Insufficient balance'); + require(amount <= x2EarnRewardsPoolContract.availableFunds(appId), 'EcoEarn: Insufficient balance'); rewards[nextCycle] = amount; rewardsLeft[nextCycle] = amount; - require(token.transferFrom(msg.sender, address(this), amount)); + require(x2EarnRewardsPoolContract.distributeReward(appId, amount, msg.sender, "")); emit ClaimedAllocation(nextCycle, amount); } /** * @dev Withdraws remaining rewards of a specific cycle * @param cycle The cycle number to withdraw rewards from + * + * @notice to be able to perform this action this contract must be set as admin of the app + * in the X2EarnApps contract of VeBetterDAO. */ function withdrawRewards(uint256 cycle) public onlyRole(DEFAULT_ADMIN_ROLE) { require(rewards[cycle] > 0, 'EcoEarn: No rewards to withdraw'); require(cycle < getCurrentCycle(), 'EcoEarn: Cycle is not over'); uint256 amount = rewardsLeft[cycle]; rewardsLeft[cycle] = 0; - require(token.transfer(msg.sender, amount)); + require(x2EarnRewardsPoolContract.withdraw(amount, appId, "Withdraws remaining rewards of cycle nr." + cycle)); } // ---------------- SETTERS ---------------- // From 5f5ab077d0fcd6868e5ba66a7e7723b238636ded Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 22 Jun 2024 15:01:52 +0200 Subject: [PATCH 03/20] feat: use x2EarnContract to distribute token --- apps/contracts/contracts/EcoEarn.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index dd083ff..acae131 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -36,6 +36,7 @@ import './interfaces/IX2EarnRewardsPool.sol'; * * @notice To distribute rewards this contract necesitates of a valid APP_ID provided by VeBetterDAO. This contract * can be initially deployed without this information and DEFAULT_ADMIN_ROLE can update it later through {EcoEarn-setAppId}. + * This contract must me set as a `rewardDistributor` inside the X2EarnApps contract to be able to send rewards to users and withdraw. */ contract EcoEarn is AccessControl { // The reward erc20 token @@ -127,7 +128,7 @@ contract EcoEarn is AccessControl { rewardsLeft[getCurrentCycle()] -= amount; // Transfer the reward to the participant - require(token.transfer(participant, amount)); + require(x2EarnRewardsPoolContract.distributeReward(appId, amount, participant, "")); emit Submission(participant, amount); } @@ -147,9 +148,6 @@ contract EcoEarn is AccessControl { /** * @dev Withdraws remaining rewards of a specific cycle * @param cycle The cycle number to withdraw rewards from - * - * @notice to be able to perform this action this contract must be set as admin of the app - * in the X2EarnApps contract of VeBetterDAO. */ function withdrawRewards(uint256 cycle) public onlyRole(DEFAULT_ADMIN_ROLE) { require(rewards[cycle] > 0, 'EcoEarn: No rewards to withdraw'); From e2cd036feec3b39f7ffc321ff87248fe42e42776 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 22 Jun 2024 15:05:19 +0200 Subject: [PATCH 04/20] revert: claimAllocation --- apps/contracts/contracts/EcoEarn.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index acae131..3f5fde3 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -138,10 +138,10 @@ contract EcoEarn is AccessControl { * @param amount Amount of tokens to be allocated */ function claimAllocation(uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(amount <= x2EarnRewardsPoolContract.availableFunds(appId), 'EcoEarn: Insufficient balance'); + require(amount <= token.balanceOf(msg.sender), 'EcoEarn: Insufficient balance'); rewards[nextCycle] = amount; rewardsLeft[nextCycle] = amount; - require(x2EarnRewardsPoolContract.distributeReward(appId, amount, msg.sender, "")); + require(token.transferFrom(msg.sender, address(this), amount)); emit ClaimedAllocation(nextCycle, amount); } From 441c40c47c2dc7946374e94d7246d95626660c33 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 22 Jun 2024 15:08:11 +0200 Subject: [PATCH 05/20] feat: admin does not need to claim, but only set amount for next cycle --- apps/contracts/contracts/EcoEarn.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index 3f5fde3..a3f0c12 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -134,14 +134,13 @@ contract EcoEarn is AccessControl { } /** - * @dev Claims allocation for the next cycle + * @dev Set the allocation for the next cycle * @param amount Amount of tokens to be allocated */ - function claimAllocation(uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(amount <= token.balanceOf(msg.sender), 'EcoEarn: Insufficient balance'); + function setRewardsAmount(uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(amount <= x2EarnRewardsPoolContract.availableFunds(appId), 'EcoEarn: Insufficient balance on the X2EarnRewardsPool contract'); rewards[nextCycle] = amount; rewardsLeft[nextCycle] = amount; - require(token.transferFrom(msg.sender, address(this), amount)); emit ClaimedAllocation(nextCycle, amount); } From 619ffa167870fafbbb878521f48becd9ce31ba2d Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Sat, 22 Jun 2024 15:09:13 +0200 Subject: [PATCH 06/20] feat: remove IToken from EcoEarn contract (not needed anymore) --- apps/contracts/contracts/EcoEarn.sol | 17 +---------------- apps/contracts/scripts/deployEcoEarn.ts | 1 - 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index a3f0c12..cbe51e6 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -26,7 +26,6 @@ pragma solidity ^0.8.19; import '@openzeppelin/contracts/access/AccessControl.sol'; -import './interfaces/IToken.sol'; import './interfaces/IX2EarnRewardsPool.sol'; /** @@ -39,9 +38,6 @@ import './interfaces/IX2EarnRewardsPool.sol'; * This contract must me set as a `rewardDistributor` inside the X2EarnApps contract to be able to send rewards to users and withdraw. */ contract EcoEarn is AccessControl { - // The reward erc20 token - IToken public token; - // The X2EarnRewardsPool contract used to distribute rewards IX2EarnRewardsPool public x2EarnRewardsPoolContract; @@ -80,18 +76,15 @@ contract EcoEarn is AccessControl { /** * @dev Constructor for the EcoEarn contract * @param _admin Address of the admin - * @param _token Address of the token contract * @param _x2EarnRewardsPoolContract Address of the X2EarnRewardsPool contract * @param _cycleDuration Duration of each cycle in blocks * @param _maxSubmissionsPerCycle Maximum submissions allowed per cycle * @param _appId The appId generated by the X2EarnApps contract when app was added to VeBetterDAO */ - constructor(address _admin, address _token, address _x2EarnRewardsPoolContract, uint256 _cycleDuration, uint256 _maxSubmissionsPerCycle, bytes32 _appId) { + constructor(address _admin, address _x2EarnRewardsPoolContract, uint256 _cycleDuration, uint256 _maxSubmissionsPerCycle, bytes32 _appId) { require(_admin !== address(0), "EcoEarn: _admin address cannot be the zero address"); - require(_token !== address(0), "EcoEarn: _token contract address cannot be the zero address"); require(_x2EarnRewardsPoolContract !== address(0), "EcoEearn: x2EarnRewardsPool contract address cannot be the zero address"); - token = IToken(_token); x2EarnRewardsPoolContract = IX2EarnRewardsPool(_x2EarnRewardsPoolContract); maxSubmissionsPerCycle = _maxSubmissionsPerCycle; cycleDuration = _cycleDuration; @@ -167,14 +160,6 @@ contract EcoEarn is AccessControl { maxSubmissionsPerCycle = _maxSubmissionsPerCycle; } - /** - * @dev Sets the token address - * @param _token New token contract address - */ - function setToken(address _token) external onlyRole(DEFAULT_ADMIN_ROLE) { - token = IToken(_token); - } - /** * @dev Sets the next cycle number * @param _nextCycle New next cycle number diff --git a/apps/contracts/scripts/deployEcoEarn.ts b/apps/contracts/scripts/deployEcoEarn.ts index 40b98f8..5622b99 100644 --- a/apps/contracts/scripts/deployEcoEarn.ts +++ b/apps/contracts/scripts/deployEcoEarn.ts @@ -7,7 +7,6 @@ async function deployMugshot() { const ecoEarnInstance = await ecoEarn.deploy( owner, - config.TOKEN_ADDRESS, config.X2EARN_REWARDS_POOL, config.CYCLE_DURATION, config.MAX_SUBMISSIONS_PER_CYCLE, From 8ada0f57a3125ddf696c9712bae18cd4dd1e669a Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Wed, 17 Jul 2024 18:23:56 +0200 Subject: [PATCH 07/20] fix: compile contract --- apps/contracts/contracts/EcoEarn.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index cbe51e6..d54ac44 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -32,8 +32,8 @@ import './interfaces/IX2EarnRewardsPool.sol'; * @title EcoEarn Contract * @dev This contract manages a reward system based on cycles. Participants can make valid submissions to earn rewards. * Rewards are being distributed by interacting with the VeBetterDAO's X2EarnRewardsPool contract. - * - * @notice To distribute rewards this contract necesitates of a valid APP_ID provided by VeBetterDAO. This contract + * + * @notice To distribute rewards this contract necesitates of a valid APP_ID provided by VeBetterDAO. This contract * can be initially deployed without this information and DEFAULT_ADMIN_ROLE can update it later through {EcoEarn-setAppId}. * This contract must me set as a `rewardDistributor` inside the X2EarnApps contract to be able to send rewards to users and withdraw. */ @@ -41,7 +41,7 @@ contract EcoEarn is AccessControl { // The X2EarnRewardsPool contract used to distribute rewards IX2EarnRewardsPool public x2EarnRewardsPoolContract; - // AppID given by the X2EarnApps contract of VeBetterDAO + // AppID given by the X2EarnApps contract of VeBetterDAO bytes32 public appId; // Mapping from cycle to total rewards @@ -82,8 +82,8 @@ contract EcoEarn is AccessControl { * @param _appId The appId generated by the X2EarnApps contract when app was added to VeBetterDAO */ constructor(address _admin, address _x2EarnRewardsPoolContract, uint256 _cycleDuration, uint256 _maxSubmissionsPerCycle, bytes32 _appId) { - require(_admin !== address(0), "EcoEarn: _admin address cannot be the zero address"); - require(_x2EarnRewardsPoolContract !== address(0), "EcoEearn: x2EarnRewardsPool contract address cannot be the zero address"); + require(_admin != address(0), 'EcoEarn: _admin address cannot be the zero address'); + require(_x2EarnRewardsPoolContract != address(0), 'EcoEearn: x2EarnRewardsPool contract address cannot be the zero address'); x2EarnRewardsPoolContract = IX2EarnRewardsPool(_x2EarnRewardsPoolContract); maxSubmissionsPerCycle = _maxSubmissionsPerCycle; @@ -120,8 +120,8 @@ contract EcoEarn is AccessControl { // Decrease the rewards left rewardsLeft[getCurrentCycle()] -= amount; - // Transfer the reward to the participant - require(x2EarnRewardsPoolContract.distributeReward(appId, amount, participant, "")); + // Transfer the reward to the participant, will revert if the transfer fails + x2EarnRewardsPoolContract.distributeReward(appId, amount, participant, ''); emit Submission(participant, amount); } @@ -131,7 +131,7 @@ contract EcoEarn is AccessControl { * @param amount Amount of tokens to be allocated */ function setRewardsAmount(uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(amount <= x2EarnRewardsPoolContract.availableFunds(appId), 'EcoEarn: Insufficient balance on the X2EarnRewardsPool contract'); + require(amount <= x2EarnRewardsPoolContract.availableFunds(appId), 'EcoEarn: Insufficient balance on the X2EarnRewardsPool contract'); rewards[nextCycle] = amount; rewardsLeft[nextCycle] = amount; emit ClaimedAllocation(nextCycle, amount); @@ -146,7 +146,7 @@ contract EcoEarn is AccessControl { require(cycle < getCurrentCycle(), 'EcoEarn: Cycle is not over'); uint256 amount = rewardsLeft[cycle]; rewardsLeft[cycle] = 0; - require(x2EarnRewardsPoolContract.withdraw(amount, appId, "Withdraws remaining rewards of cycle nr." + cycle)); + require(x2EarnRewardsPoolContract.withdraw(amount, appId, 'Withdraws remaining rewards of cycle nr.' + cycle)); } // ---------------- SETTERS ---------------- // From 2f26f3edf955755e487dacf6aac24d2fa0f6f485 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 07:52:59 +0200 Subject: [PATCH 08/20] feat: added mock contracts --- apps/contracts/contracts/EcoEarn.sol | 5 +- apps/contracts/contracts/Token.sol | 14 - apps/contracts/contracts/mock/B3TR_Mock.sol | 20 + .../contracts/mock/X2EarnAppsMock.sol | 364 ++++++++++++++++++ .../contracts/mock/X2EarnRewardsPoolMock.sol | 213 ++++++++++ .../mock/interfaces/IX2EarnAppsMock.sol | 167 ++++++++ .../mock/interfaces/X2EarnAppsDataTypes.sol | 19 + 7 files changed, 787 insertions(+), 15 deletions(-) delete mode 100644 apps/contracts/contracts/Token.sol create mode 100644 apps/contracts/contracts/mock/B3TR_Mock.sol create mode 100644 apps/contracts/contracts/mock/X2EarnAppsMock.sol create mode 100644 apps/contracts/contracts/mock/X2EarnRewardsPoolMock.sol create mode 100644 apps/contracts/contracts/mock/interfaces/IX2EarnAppsMock.sol create mode 100644 apps/contracts/contracts/mock/interfaces/X2EarnAppsDataTypes.sol diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index d54ac44..dffad9d 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -27,6 +27,7 @@ pragma solidity ^0.8.19; import '@openzeppelin/contracts/access/AccessControl.sol'; import './interfaces/IX2EarnRewardsPool.sol'; +import '@openzeppelin/contracts/utils/Strings.sol'; /** * @title EcoEarn Contract @@ -146,7 +147,9 @@ contract EcoEarn is AccessControl { require(cycle < getCurrentCycle(), 'EcoEarn: Cycle is not over'); uint256 amount = rewardsLeft[cycle]; rewardsLeft[cycle] = 0; - require(x2EarnRewardsPoolContract.withdraw(amount, appId, 'Withdraws remaining rewards of cycle nr.' + cycle)); + + // will revert if the withdraw fails + x2EarnRewardsPoolContract.withdraw(amount, appId, string.concat('Withdraws remaining rewards of cycle nr.', Strings.toString(cycle))); } // ---------------- SETTERS ---------------- // diff --git a/apps/contracts/contracts/Token.sol b/apps/contracts/contracts/Token.sol deleted file mode 100644 index 3aefa26..0000000 --- a/apps/contracts/contracts/Token.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; -import '@openzeppelin/contracts/access/Ownable.sol'; -import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol'; - -contract Token is ERC20, Ownable, ERC20Permit { - constructor(address initialOwner) ERC20('Token', 'TKN') Ownable(initialOwner) ERC20Permit('Token') {} - - function mint(address to, uint256 amount) public onlyOwner { - _mint(to, amount); - } -} diff --git a/apps/contracts/contracts/mock/B3TR_Mock.sol b/apps/contracts/contracts/mock/B3TR_Mock.sol new file mode 100644 index 0000000..b2de792 --- /dev/null +++ b/apps/contracts/contracts/mock/B3TR_Mock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @title B3TR_Mock + * @dev Mock contract for the B3TR token. + */ +contract B3TR_Mock is ERC20 { + // Mint 10,000,000 B3TR tokens to the deployer + constructor() ERC20("B3TR", "B3TR") { + _mint(msg.sender, 10000000 * 10 ** decimals()); + } + + // Public function to mint tokens + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/apps/contracts/contracts/mock/X2EarnAppsMock.sol b/apps/contracts/contracts/mock/X2EarnAppsMock.sol new file mode 100644 index 0000000..bca35fb --- /dev/null +++ b/apps/contracts/contracts/mock/X2EarnAppsMock.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {IX2EarnAppsMock} from "./interfaces/IX2EarnAppsMock.sol"; +import {X2EarnAppsDataTypes} from "./interfaces/X2EarnAppsDataTypes.sol"; + +/** + * @title X2EarnApps + * + * @notice DEV MOCKED VERSION of the VeBetterDAO's X2EarnApps contract. + * @dev This contract can be used to add an app, set the admin of the app, add reward distributors to the app, and remove reward distributors from the app. + * It has a function to check the existence of an app, check if an account is the admin of the app, and check if an account is a reward distributor of the app. + * This is the minimum required functionality for the VeBetterDAO's X2EarnRewardsPool contract to function. + * + */ +contract X2EarnAppsMock is IX2EarnAppsMock { + uint256 public constant MAX_MODERATORS = 100; + uint256 public constant MAX_REWARD_DISTRIBUTORS = 100; + + /// @custom:storage-location erc7201:b3tr.storage.X2EarnApps.AppsStorage + struct AppsStorageStorage { + // Mapping from app ID to app + mapping(bytes32 appId => X2EarnAppsDataTypes.App) _apps; + // List of app IDs to enable retrieval of all _apps + bytes32[] _appIds; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.X2EarnApps.AppsStorage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant AppsStorageStorageLocation = + 0xb6909058bd527140b8d55a44344c5e42f1f148f1b3b16df7641882df8dd72900; + + function _getAppsStorageStorage() + internal + pure + returns (AppsStorageStorage storage $) + { + assembly { + $.slot := AppsStorageStorageLocation + } + } + + /// @custom:storage-location erc7201:b3tr.storage.X2EarnApps.Administration + struct AdministrationStorage { + mapping(bytes32 appId => address) _admin; + mapping(bytes32 appId => address[]) _moderators; + mapping(bytes32 appId => address[]) _rewardDistributors; // addresses that can distribute rewards from X2EarnRewardsPool + mapping(bytes32 appId => address) _teamWalletAddress; + mapping(bytes32 appId => uint256) _teamAllocationPercentage; // by default this is 0 and all funds are sent to the X2EarnRewardsPool + mapping(bytes32 appId => string) _metadataURI; + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.X2EarnApps.Administration")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant AdministrationStorageLocation = + 0x5830f0e95c01712d916c34d9e2fa42e9f749b325b67bce7382d70bb99c623500; + + function _getAdministrationStorage() + internal + pure + returns (AdministrationStorage storage $) + { + assembly { + $.slot := AdministrationStorageLocation + } + } + + constructor() {} + + // ---------- Modifiers ------------ // + + /** + * @dev Throws if called by any account that is not an app admin. + * @param appId the app ID + */ + modifier onlyAppAdmin(bytes32 appId) { + if (!isAppAdmin(appId, msg.sender)) { + revert X2EarnUnauthorizedUser(msg.sender); + } + _; + } + + // ---------- Getters ------------ // + /** + * @dev See {IX2EarnApps-hashAppName}. + */ + function hashAppName(string memory appName) public pure returns (bytes32) { + return keccak256(abi.encodePacked(appName)); + } + + /** + * @dev See {IX2EarnApps-appExists}. + */ + function appExists(bytes32 appId) public view returns (bool) { + AppsStorageStorage storage $ = _getAppsStorageStorage(); + + return $._apps[appId].createdAtTimestamp != 0; + } + + /** + * @dev Check if an account is the admin of the app + * + * @param appId the hashed name of the app + * @param account the address of the account + */ + function isAppAdmin( + bytes32 appId, + address account + ) public view returns (bool) { + AdministrationStorage storage $ = _getAdministrationStorage(); + + return $._admin[appId] == account; + } + + /** + * @dev Returns true if an account is a reward distributor of the app + * + * @param appId the hashed name of the app + * @param account the address of the account + */ + function isRewardDistributor( + bytes32 appId, + address account + ) public view returns (bool) { + AdministrationStorage storage $ = _getAdministrationStorage(); + + address[] memory distributors = $._rewardDistributors[appId]; + for (uint256 i; i < distributors.length; i++) { + if (distributors[i] == account) { + return true; + } + } + + return false; + } + + /** + * @dev Get the address where the x2earn app receives allocation funds + * + * @param appId the hashed name of the app + */ + function teamWalletAddress( + bytes32 appId + ) public view override returns (address) { + AdministrationStorage storage $ = _getAdministrationStorage(); + + return $._teamWalletAddress[appId]; + } + + // ---------- Overrides ------------ // + + /** + * @dev See {IX2EarnApps-addApp}. + * + * DEV-TESTNET: Everyone can add an app for testing purposes. + */ + function addApp( + address _teamWalletAddress, + address _admin, + string memory _appName + ) public { + _addApp(_teamWalletAddress, _admin, _appName); + } + + /** + * @dev See {IX2EarnApps-setAppAdmin}. + */ + function setAppAdmin( + bytes32 _appId, + address _newAdmin + ) public onlyAppAdmin(_appId) { + _setAppAdmin(_appId, _newAdmin); + } + + /** + * @dev See {IX2EarnApps-addRewardDistributor}. + */ + function addRewardDistributor( + bytes32 _appId, + address _distributor + ) public onlyAppAdmin(_appId) { + _addRewardDistributor(_appId, _distributor); + } + + /** + * @dev See {IX2EarnApps-removeRewardDistributor}. + */ + function removeRewardDistributor( + bytes32 _appId, + address _distributor + ) public onlyAppAdmin(_appId) { + _removeRewardDistributor(_appId, _distributor); + } + + /** + * @dev Create app. + * The id of the app is the hash of the app name. + * Will be eligible for voting by default from the next round and + * the team allocation percentage will be 0%. + * + * @param _teamWalletAddress the address where the app should receive allocation funds + * @param _admin the address of the admin + * @param _appName the name of the app + * + * Emits a {AppAdded} event. + */ + function _addApp( + address _teamWalletAddress, + address _admin, + string memory _appName + ) internal { + if (_teamWalletAddress == address(0)) { + revert X2EarnInvalidAddress(_teamWalletAddress); + } + if (_admin == address(0)) { + revert X2EarnInvalidAddress(_admin); + } + + AppsStorageStorage storage $ = _getAppsStorageStorage(); + bytes32 id = hashAppName(_appName); + + if (appExists(id)) { + revert X2EarnAppAlreadyExists(id); + } + + // Store the new app + $._apps[id] = X2EarnAppsDataTypes.App(id, _appName, block.timestamp); + $._appIds.push(id); + _setAppAdmin(id, _admin); + _updateTeamWalletAddress(id, _teamWalletAddress); + + emit AppAdded(id, _teamWalletAddress, _appName, true); + } + + /** + * @dev Get the app data saved in storage + * + * @param appId the if of the app + */ + function _getAppStorage( + bytes32 appId + ) internal view returns (X2EarnAppsDataTypes.App memory) { + if (!appExists(appId)) { + revert X2EarnNonexistentApp(appId); + } + + AppsStorageStorage storage $ = _getAppsStorageStorage(); + return $._apps[appId]; + } + + /** + * @dev Internal function to set the admin address of the app + * + * @param appId the hashed name of the app + * @param newAdmin the address of the new admin + */ + function _setAppAdmin(bytes32 appId, address newAdmin) internal { + if (!appExists(appId)) { + revert X2EarnNonexistentApp(appId); + } + + if (newAdmin == address(0)) { + revert X2EarnInvalidAddress(newAdmin); + } + + AdministrationStorage storage $ = _getAdministrationStorage(); + + emit AppAdminUpdated(appId, $._admin[appId], newAdmin); + + $._admin[appId] = newAdmin; + } + + /** + * @dev Internal function to add a reward distributor to the app + * + * @param appId the hashed name of the app + * @param distributor the address of the reward distributor + */ + function _addRewardDistributor( + bytes32 appId, + address distributor + ) internal { + if (distributor == address(0)) { + revert X2EarnInvalidAddress(distributor); + } + + if (!appExists(appId)) { + revert X2EarnNonexistentApp(appId); + } + + AdministrationStorage storage $ = _getAdministrationStorage(); + + if ($._rewardDistributors[appId].length >= MAX_REWARD_DISTRIBUTORS) { + revert X2EarnMaxRewardDistributorsReached(appId); + } + + $._rewardDistributors[appId].push(distributor); + + emit RewardDistributorAddedToApp(appId, distributor); + } + + /** + * @dev Internal function to remove a reward distributor from the app + * + * @param appId the hashed name of the app + * @param distributor the address of the reward distributor + */ + function _removeRewardDistributor( + bytes32 appId, + address distributor + ) internal { + if (distributor == address(0)) { + revert X2EarnInvalidAddress(distributor); + } + + if (!appExists(appId)) { + revert X2EarnNonexistentApp(appId); + } + + if (!isRewardDistributor(appId, distributor)) { + revert X2EarnNonexistentRewardDistributor(appId, distributor); + } + + AdministrationStorage storage $ = _getAdministrationStorage(); + + address[] storage distributors = $._rewardDistributors[appId]; + for (uint256 i; i < distributors.length; i++) { + if (distributors[i] == distributor) { + distributors[i] = distributors[distributors.length - 1]; + distributors.pop(); + emit RewardDistributorRemovedFromApp(appId, distributor); + break; + } + } + } + + /** + * @dev Update the address where the x2earn app receives allocation funds + * + * @param appId the hashed name of the app + * @param newTeamWalletAddress the address of the new wallet where the team will receive the funds + */ + function _updateTeamWalletAddress( + bytes32 appId, + address newTeamWalletAddress + ) internal { + if (newTeamWalletAddress == address(0)) { + revert X2EarnInvalidAddress(newTeamWalletAddress); + } + + if (!appExists(appId)) { + revert X2EarnNonexistentApp(appId); + } + + AdministrationStorage storage $ = _getAdministrationStorage(); + address oldTeamWalletAddress = $._teamWalletAddress[appId]; + $._teamWalletAddress[appId] = newTeamWalletAddress; + + emit TeamWalletAddressUpdated( + appId, + oldTeamWalletAddress, + newTeamWalletAddress + ); + } +} diff --git a/apps/contracts/contracts/mock/X2EarnRewardsPoolMock.sol b/apps/contracts/contracts/mock/X2EarnRewardsPoolMock.sol new file mode 100644 index 0000000..6e498d6 --- /dev/null +++ b/apps/contracts/contracts/mock/X2EarnRewardsPoolMock.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; +import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; +import {IToken} from '../interfaces/IToken.sol'; +import {IX2EarnAppsMock} from './interfaces/IX2EarnAppsMock.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'; + +/** + * Mock contract forked from vebetterdao-contracts. + * + * @title X2EarnRewardsPool + * @dev This contract is used by x2Earn apps to reward users that performed sustainable actions. + * The XAllocationPool contract or other contracts/users can deposit funds into this contract by specifying the app + * that can access the funds. + * Admins of x2EarnApps can withdraw funds from the rewards pool, whihch are sent to the team wallet. + * Reward distributors of a x2Earn app can distribute rewards to users that performed sustainable actions or withdraw funds + * to the team wallet. + */ +contract X2EarnRewardsPoolMock is IX2EarnRewardsPool, AccessControl, ReentrancyGuard { + bytes32 public constant CONTRACTS_ADDRESS_MANAGER_ROLE = keccak256('CONTRACTS_ADDRESS_MANAGER_ROLE'); + + /// @custom:storage-location erc7201:b3tr.storage.X2EarnRewardsPool + struct X2EarnRewardsPoolStorage { + IToken b3tr; + IX2EarnAppsMock x2EarnApps; + mapping(bytes32 appId => uint256) availableFunds; // Funds that the app can use to reward users + } + + // keccak256(abi.encode(uint256(keccak256("b3tr.storage.X2EarnRewardsPool")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant X2EarnRewardsPoolStorageLocation = 0x7c0dcc5654efea34bf150fefe2d7f927494d4026026590e81037cb4c7a9cdc00; + + function _getX2EarnRewardsPoolStorage() private pure returns (X2EarnRewardsPoolStorage storage $) { + assembly { + $.slot := X2EarnRewardsPoolStorageLocation + } + } + + constructor(address _admin, IToken _b3tr, IX2EarnAppsMock _x2EarnApps) { + require(_admin != address(0), 'X2EarnRewardsPool: admin is the zero address'); + require(address(_b3tr) != address(0), 'X2EarnRewardsPool: b3tr is the zero address'); + require(address(_x2EarnApps) != address(0), 'X2EarnRewardsPool: x2EarnApps is the zero address'); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + $.b3tr = _b3tr; + $.x2EarnApps = _x2EarnApps; + } + + // ---------- Setters ---------- // + + /** + * @dev See {IX2EarnRewardsPool-deposit} + */ + function deposit(uint256 amount, bytes32 appId) external returns (bool) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + + // check that app exists + require($.x2EarnApps.appExists(appId), 'X2EarnRewardsPool: app does not exist'); + + // increase available amount for the app + $.availableFunds[appId] += amount; + + // transfer tokens to this contract + require($.b3tr.transferFrom(msg.sender, address(this), amount), 'X2EarnRewardsPool: deposit transfer failed'); + + emit NewDeposit(amount, appId, msg.sender); + + return true; + } + + /** + * @dev See {IX2EarnRewardsPool-withdraw} + */ + function withdraw(uint256 amount, bytes32 appId, string memory reason) external nonReentrant { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + + require($.x2EarnApps.appExists(appId), 'X2EarnRewardsPool: app does not exist'); + + require( + $.x2EarnApps.isAppAdmin(appId, msg.sender) || $.x2EarnApps.isRewardDistributor(appId, msg.sender), + 'X2EarnRewardsPool: not an app admin nor a reward distributor' + ); + + // check if the app has enough available funds to withdraw + 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'); + + // Get the team wallet address + address teamWalletAddress = $.x2EarnApps.teamWalletAddress(appId); + + // transfer the rewards to the team wallet + $.availableFunds[appId] -= amount; + require($.b3tr.transfer(teamWalletAddress, amount), 'X2EarnRewardsPool: Allocation transfer to app failed'); + + emit TeamWithdrawal(amount, appId, teamWalletAddress, msg.sender, reason); + } + + /** + * @dev See {IX2EarnRewardsPool-distributeReward} + */ + function distributeReward(bytes32 appId, uint256 amount, address receiver, string memory proof) external nonReentrant { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + + 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 + 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 + $.availableFunds[appId] -= amount; + require($.b3tr.transfer(receiver, amount), 'X2EarnRewardsPool: Allocation transfer to app failed'); + + // emit event + emit RewardDistributed(amount, appId, receiver, proof, msg.sender); + } + + /** + * @dev Sets the X2EarnApps contract address. + * + * @param _x2EarnApps the new X2EarnApps contract + */ + function setX2EarnApps(IX2EarnAppsMock _x2EarnApps) external onlyRole(CONTRACTS_ADDRESS_MANAGER_ROLE) { + require(address(_x2EarnApps) != address(0), 'X2EarnRewardsPool: x2EarnApps is the zero address'); + + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + $.x2EarnApps = _x2EarnApps; + } + + // ---------- Getters ---------- // + + /** + * @dev See {IX2EarnRewardsPool-availableFunds} + */ + function availableFunds(bytes32 appId) external view returns (uint256) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.availableFunds[appId]; + } + + /** + * @dev See {IX2EarnRewardsPool-version} + */ + function version() external pure virtual returns (string memory) { + return '1'; + } + + /** + * @dev Retrieves the B3TR token contract. + */ + function b3tr() external view returns (IToken) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.b3tr; + } + + /** + * @dev Retrieves the X2EarnApps contract. + */ + function x2EarnApps() external view returns (IX2EarnAppsMock) { + X2EarnRewardsPoolStorage storage $ = _getX2EarnRewardsPoolStorage(); + return $.x2EarnApps; + } + + // ---------- Fallbacks ---------- // + + /** + * @dev Transfers of VET to this contract are not allowed. + */ + receive() external payable virtual { + revert('X2EarnRewardsPool: contract does not accept VET'); + } + + /** + * @dev Contract does not accept calls/data. + */ + fallback() external payable { + revert('X2EarnRewardsPool: contract does not accept calls/data'); + } + + /** + * @dev Transfers of ERC721 tokens to this contract are not allowed. + * + * @notice supported only when safeTransferFrom is used + */ + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + revert('X2EarnRewardsPool: contract does not accept ERC721 tokens'); + } + + /** + * @dev Transfers of ERC1155 tokens to this contract are not allowed. + */ + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { + revert('X2EarnRewardsPool: contract does not accept ERC1155 tokens'); + } + + /** + * @dev Transfers of ERC1155 tokens to this contract are not allowed. + */ + function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) public virtual returns (bytes4) { + revert('X2EarnRewardsPool: contract does not accept batch transfers of ERC1155 tokens'); + } +} diff --git a/apps/contracts/contracts/mock/interfaces/IX2EarnAppsMock.sol b/apps/contracts/contracts/mock/interfaces/IX2EarnAppsMock.sol new file mode 100644 index 0000000..578da20 --- /dev/null +++ b/apps/contracts/contracts/mock/interfaces/IX2EarnAppsMock.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {X2EarnAppsDataTypes} from "./X2EarnAppsDataTypes.sol"; + +/** + * Mocked interface updated with only a subset of the functions that should mimic some + * functionality of the VeBetterDAO X2EarnApps contract. + * + * @title IX2EarnApps + * @notice Interface for the X2EarnApps contract. + */ +interface IX2EarnAppsMock { + /** + * @dev The `appId` doesn't exist. + */ + error X2EarnNonexistentApp(bytes32 appId); + + /** + * @dev The `addr` is not valid (eg: is the ZERO ADDRESS). + */ + error X2EarnInvalidAddress(address addr); + + /** + * @dev An app with the specified `appId` already exists. + */ + error X2EarnAppAlreadyExists(bytes32 appId); + + /** + * @dev The user is not authorized to perform the action. + */ + error X2EarnUnauthorizedUser(address user); + + /** + * @dev The maximum number of reward distributors has been reached. + */ + error X2EarnMaxRewardDistributorsReached(bytes32 appId); + + /** + * @dev The `distributorAddress` is not valid. + */ + error X2EarnNonexistentRewardDistributor( + bytes32 appId, + address distributorAddress + ); + + /** + * @dev Event fired when a new app is added. + */ + event AppAdded( + bytes32 indexed id, + address addr, + string name, + bool appAvailableForAllocationVoting + ); + + /** + * @dev Event fired when the admin adds a new reward distributor to the app. + */ + event RewardDistributorAddedToApp( + bytes32 indexed appId, + address distributorAddress + ); + + /** + * @dev Event fired when the admin removes a reward distributor from the app. + */ + event RewardDistributorRemovedFromApp( + bytes32 indexed appId, + address distributorAddress + ); + + /** + * @dev Event fired when the admin of an app changes. + */ + event AppAdminUpdated( + bytes32 indexed appId, + address oldAdmin, + address newAdmin + ); + + /** + * @dev Event fired when the address where the x2earn app receives allocation funds is changed. + */ + event TeamWalletAddressUpdated( + bytes32 indexed appId, + address oldTeamWalletAddress, + address newTeamWalletAddress + ); + + /** + * @dev Get the address where the x2earn app receives allocation funds. + * + * @param appId the id of the app + */ + function teamWalletAddress(bytes32 appId) external view returns (address); + + /** + * @dev Add a new app to the x2earn apps. + * + * @param teamWalletAddress the address where the app should receive allocation funds + * @param admin the address of the admin that will be able to manage the app and perform all administration actions + * @param appName the name of the app + * + * Emits a {AppAdded} event. + */ + function addApp( + address teamWalletAddress, + address admin, + string memory appName + ) external; + + /** + * @dev Check if an account is the admin of the app + * + * @param appId the hashed name of the app + * @param account the address of the account + */ + function isAppAdmin( + bytes32 appId, + address account + ) external view returns (bool); + + /** + * @dev Add a new reward distributor to the app. + * + * @param appId the id of the app + * @param distributorAddress the address of the reward distributor + * + * Emits a {RewardDistributorAddedToApp} event. + */ + function addRewardDistributor( + bytes32 appId, + address distributorAddress + ) external; + + /** + * @dev Remove a reward distributor from the app. + * + * @param appId the id of the app + * @param distributorAddress the address of the reward distributor + * + * Emits a {RewardDistributorRemovedFromApp} event. + */ + function removeRewardDistributor( + bytes32 appId, + address distributorAddress + ) external; + + /** + * @dev Returns true if an account is a reward distributor of the app + * + * @param appId the id of the app + * @param distributorAddress the address of the account + */ + function isRewardDistributor( + bytes32 appId, + address distributorAddress + ) external view returns (bool); + + /** + * @dev Check if there is an app with the specified `appId`. + * + * @param appId the id of the app + */ + function appExists(bytes32 appId) external view returns (bool); +} diff --git a/apps/contracts/contracts/mock/interfaces/X2EarnAppsDataTypes.sol b/apps/contracts/contracts/mock/interfaces/X2EarnAppsDataTypes.sol new file mode 100644 index 0000000..48ad101 --- /dev/null +++ b/apps/contracts/contracts/mock/interfaces/X2EarnAppsDataTypes.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +library X2EarnAppsDataTypes { + struct App { + bytes32 id; + string name; + uint256 createdAtTimestamp; + } + + struct AppWithDetailsReturnType { + bytes32 id; + address teamWalletAddress; + string name; + string metadataURI; + uint256 createdAtTimestamp; + bool appAvailableForAllocationVoting; + } +} From ea3002c63f7cb126c7364a7daaed2f738588af8f Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 08:31:51 +0200 Subject: [PATCH 09/20] feat: added new deploy script --- README.md | 27 ++++++-- apps/contracts/package.json | 92 ++++++++++++------------- apps/contracts/scripts/deploy.ts | 76 ++++++++++++++++++++ apps/contracts/scripts/deployEcoEarn.ts | 31 --------- apps/contracts/scripts/deployToken.ts | 25 ------- apps/contracts/scripts/index.ts | 15 ++++ packages/config-contract/config.ts | 7 +- 7 files changed, 159 insertions(+), 114 deletions(-) create mode 100644 apps/contracts/scripts/deploy.ts delete mode 100644 apps/contracts/scripts/deployEcoEarn.ts delete mode 100644 apps/contracts/scripts/deployToken.ts create mode 100644 apps/contracts/scripts/index.ts diff --git a/README.md b/README.md index ea992fb..3b0ebe0 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,17 @@ Ensure your development environment is set up with the following: - **Hardhat (for smart contracts):** [Getting Started with Hardhat](https://hardhat.org/hardhat-runner/docs/getting-started) ⛑️ ## Project Structure + ### Frontend (apps/frontend) 🌐 A blazing-fast React application powered by Vite: + - **Vechain dapp-kit:** Streamline wallet connections and interactions. [Learn more](https://docs.vechain.org/developer-resources/sdks-and-providers/dapp-kit) ### Backend (apps/backend) πŸ”™ An Express server crafted with TypeScript for robust API development: + - **Vechain SDK:** Seamlessly fetch and perform transactions with the VechainThor blockchain. [Learn more](https://docs.vechain.org/developer-resources/sdks-and-providers/sdk) - **OpenAI GPT-Vision-Preview:** Integrate image analysis capabilities. [Explore here](https://platform.openai.com/docs/guides/vision) @@ -59,29 +62,35 @@ Configure your environment variables for seamless integration: ### Frontend Place your `.env` files in `apps/frontend`: + - **VITE_RECAPTCHA_V3_SITE_KEY:** [Request your RecaptchaV3 site keys](https://developers.google.com/recaptcha/docs/v3) ### Backend Store your environment-specific `.env` files in `apps/backend`. `.env.development.local` & `.env.production.local` allow for custom environment variables based on the environment: + - **OPENAI_API_KEY:** [Get your GPT-4 OpenAI key](https://platform.openai.com/api-keys) (Enable GPT-4 [here](https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4-gpt-4-turbo-and-gpt-4o)) - **RECAPTCHA_SECRET_KEY:** [Request your RecaptchaV3 site keys](https://developers.google.com/recaptcha/docs/v3) ### Contracts Manage deployment parameters and network configurations in `hardhat.config.js` under `apps/contracts`: + - **MNEMONIC:** Mnemonic of the deploying wallet ## Getting Started 🏁 Clone the repository and install dependencies with ease: + ```bash yarn install # Run this at the root level of the project ``` ## Deploying Contracts πŸš€ -Deploy your contracts effortlessly on either the Testnet or Solo networks: +Deploy your contracts effortlessly on either the Testnet or Solo networks. +If you are deploying on the Testnet, ensure you have the correct addresses in the `config-contracts` package. +When deploying on the SOLO network the script will deploy for you mocked VeBetterDAO contracts. ### Deploying on Solo Network @@ -98,25 +107,29 @@ yarn contracts:deploy:testnet ## Running on Solo Network πŸ”§ Run the Frontend and Backend, connected to the Solo network and pointing to your deployed contracts. Ensure all environment variables are properly configured: + ```bash -yarn dev +yarn dev ``` ### Setting up rewards -Run vechain devpal + +Run vechain devpal + ```bash npx @vechain/devpal http://localhost:8669 ``` Open the `Inspector` tab and perform the following transactions: + - **Add Contracts:** Add the EcoEarn contract and the Token contract deployed previously. Addresses can be found in the `config-contracts` package. ABIs can be found in the artifacts folder of the `contracts` app. -![image](https://github.com/vechain/x-app-template/assets/64158778/e288ada4-5973-4428-9e72-a362388b1826) + ![image](https://github.com/vechain/x-app-template/assets/64158778/e288ada4-5973-4428-9e72-a362388b1826) - **Approve token:** Approve the EcoEarn contract to spend your tokens -![image](https://github.com/vechain/x-app-template/assets/64158778/70787d8d-ae60-40ea-b277-87359aaca4ee) + ![image](https://github.com/vechain/x-app-template/assets/64158778/70787d8d-ae60-40ea-b277-87359aaca4ee) - **Claim rewards:** Claim rewards for the EcoEarn contract -![image](https://github.com/vechain/x-app-template/assets/64158778/834437e5-8de1-4802-9ed7-dca6fe4df332) + ![image](https://github.com/vechain/x-app-template/assets/64158778/834437e5-8de1-4802-9ed7-dca6fe4df332) - **Trigger cycle:** Trigger the cycle for the EcoEarn contract -![image](https://github.com/vechain/x-app-template/assets/64158778/00236dcd-5b64-4493-9acd-55c6a7f0981f) + ![image](https://github.com/vechain/x-app-template/assets/64158778/00236dcd-5b64-4493-9acd-55c6a7f0981f) ## Disclaimer ⚠️ diff --git a/apps/contracts/package.json b/apps/contracts/package.json index 0ed604a..51c8454 100644 --- a/apps/contracts/package.json +++ b/apps/contracts/package.json @@ -1,50 +1,46 @@ { - "name": "@repo/contracts", - "version": "1.0.0", - "scripts": { - "test": "npx hardhat test --network hardhat ", - "test:solo": "npx hardhat test --network vechain_solo", - "compile": "npx hardhat compile", - "deploy:token:solo": "npx hardhat run scripts/deployToken.ts --network vechain_solo", - "deploy:ecoearn:solo": "npx hardhat run scripts/deployEcoEarn.ts --network vechain_solo", - "deploy:token:testnet": "npx hardhat run scripts/deployToken.ts --network vechain_testnet", - "deploy:ecoearn:testnet": "npx hardhat run scripts/deployEcoEarn.ts --network vechain_testnet", - "deploy:solo": "yarn solo-up && yarn deploy:token:solo && yarn deploy:ecoearn:solo", - "deploy:testnet": "yarn deploy:token:testnet && yarn deploy:ecoearn:testnet", - "solo-up": "make solo-up" - }, - "dependencies": { - "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", - "@nomicfoundation/hardhat-ethers": "^3.0.4", - "@nomicfoundation/hardhat-network-helpers": "^1.0.0", - "@nomiclabs/hardhat-etherscan": "^3.0.0", - "@nomiclabs/hardhat-truffle5": "^2.0.7", - "@nomiclabs/hardhat-web3": "^2.0.0", - "@openzeppelin/contracts": "^5.0.1", - "@repo/config-contract": "*", - "@typechain/hardhat": "^8.0.0", - "@types/chai": "^4.2.0", - "@types/mocha": ">=9.1.0", - "@vechain/hardhat-ethers": "^0.1.4", - "@vechain/hardhat-vechain": "^0.1.4", - "@vechain/web3-providers-connex": "^1.1.2", - "chai": "^4.2.0", - "dayjs": "^1.11.10", - "hardhat": "^2.12.6", - "hardhat-gas-reporter": "^1.0.8", - "react-slot-counter": "^2.2.5", - "solidity-coverage": "^0.8.1", - "ts-node": ">=8.0.0", - "typechain": "^8.1.0", - "typescript": ">=4.5.0" - }, - "devDependencies": { - "@nomicfoundation/hardhat-toolbox": "^3.0.0", - "@nomicfoundation/hardhat-verify": "^1.1.1", - "@typechain/ethers-v6": "^0.4.3", - "@vechain/sdk-hardhat-plugin": "^1.0.0-beta.12", - "ethers": "^6.7.1", - "ganache-cli": "^6.12.2", - "hardhat": "^2.19.1" - } + "name": "@repo/contracts", + "version": "1.0.0", + "scripts": { + "test": "npx hardhat test --network hardhat ", + "test:solo": "npx hardhat test --network vechain_solo", + "compile": "npx hardhat compile", + "deploy:solo": "npx hardhat run scripts --network vechain_solo", + "deploy:testnet": "npx hardhat run scripts --network vechain_testnet", + "solo-up": "make solo-up" + }, + "dependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", + "@nomicfoundation/hardhat-ethers": "^3.0.4", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomiclabs/hardhat-etherscan": "^3.0.0", + "@nomiclabs/hardhat-truffle5": "^2.0.7", + "@nomiclabs/hardhat-web3": "^2.0.0", + "@openzeppelin/contracts": "^5.0.1", + "@repo/config-contract": "*", + "@typechain/hardhat": "^8.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "@vechain/hardhat-ethers": "^0.1.4", + "@vechain/hardhat-vechain": "^0.1.4", + "@vechain/web3-providers-connex": "^1.1.2", + "chai": "^4.2.0", + "dayjs": "^1.11.10", + "hardhat": "^2.12.6", + "hardhat-gas-reporter": "^1.0.8", + "react-slot-counter": "^2.2.5", + "solidity-coverage": "^0.8.1", + "ts-node": ">=8.0.0", + "typechain": "^8.1.0", + "typescript": ">=4.5.0" + }, + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.1.1", + "@typechain/ethers-v6": "^0.4.3", + "@vechain/sdk-hardhat-plugin": "^1.0.0-beta.12", + "ethers": "^6.7.1", + "ganache-cli": "^6.12.2", + "hardhat": "^2.19.1" + } } diff --git a/apps/contracts/scripts/deploy.ts b/apps/contracts/scripts/deploy.ts new file mode 100644 index 0000000..4823ce5 --- /dev/null +++ b/apps/contracts/scripts/deploy.ts @@ -0,0 +1,76 @@ +import { ethers, network } from 'hardhat'; +import { updateConfig, config } from '@repo/config-contract'; + +export async function deploy() { + const deployer = (await ethers.getSigners())[0]; + console.log(`Deploying on ${network.name} with wallet ${deployer.address}...`); + + let REWARD_TOKEN_ADDRESS = config.TOKEN_ADDRESS; + let X2EARN_REWARDS_POOL = config.X2EARN_REWARDS_POOL; + let X2EARN_APPS = config.X2EARN_APPS; + let APP_ID = config.APP_ID; + + // If we are running on the solo network, we need to deploy the mock contracts + // and generate the appID + if (network.name === 'vechain_solo') { + console.log(`Deploying mock RewardToken...`); + const RewardTokenContract = await ethers.getContractFactory('B3TR_Mock'); + const rewardToken = await RewardTokenContract.deploy(); + await rewardToken.waitForDeployment(); + REWARD_TOKEN_ADDRESS = await rewardToken.getAddress(); + console.log(`RewardToken deployed to ${REWARD_TOKEN_ADDRESS}`); + + console.log('Deploying X2EarnApps mock contract...'); + const X2EarnAppsContract = await ethers.getContractFactory('X2EarnAppsMock'); + const x2EarnApps = await X2EarnAppsContract.deploy(); + await x2EarnApps.waitForDeployment(); + X2EARN_APPS = await x2EarnApps.getAddress(); + console.log(`X2EarnApps deployed to ${await x2EarnApps.getAddress()}`); + + console.log('Deploying X2EarnRewardsPool mock contract...'); + const X2EarnRewardsPoolContract = await ethers.getContractFactory('X2EarnRewardsPoolMock'); + const x2EarnRewardsPool = await X2EarnRewardsPoolContract.deploy(deployer.address, REWARD_TOKEN_ADDRESS, await x2EarnApps.getAddress()); + await x2EarnRewardsPool.waitForDeployment(); + X2EARN_REWARDS_POOL = await x2EarnRewardsPool.getAddress(); + console.log(`X2EarnRewardsPool deployed to ${await x2EarnRewardsPool.getAddress()}`); + + console.log('Adding app in X2EarnApps...'); + await x2EarnApps.addApp(deployer.address, deployer.address, 'EcoEarn'); + const appID = await x2EarnApps.hashAppName('EcoEarn'); + APP_ID = appID; + console.log(`AppID: ${appID}`); + + console.log(`Funding contract...`); + await rewardToken.approve(await x2EarnRewardsPool.getAddress(), ethers.parseEther('10000')); + await x2EarnRewardsPool.deposit(ethers.parseEther('2000'), appID); + console.log('Funded'); + } + + console.log('Deploying EcoEarn contract...'); + const ecoEarn = await ethers.getContractFactory('EcoEarn'); + const ecoEarnInstance = await ecoEarn.deploy( + deployer.address, + X2EARN_REWARDS_POOL, // mock in solo, from config in testnet/mainnet + config.CYCLE_DURATION, + config.MAX_SUBMISSIONS_PER_CYCLE, + APP_ID, // mock in solo, from config in testnet/mainnet + ); + await ecoEarnInstance.waitForDeployment(); + const ecoEarnAddress = await ecoEarnInstance.getAddress(); + console.log(`EcoEarn deployed to: ${ecoEarnAddress}`); + + // In solo network, we need to add the EcoEarn contract as a distributor + if (network.name === 'vechain_solo') { + console.log('Add EcoEarn contracts as distributor...'); + const x2EarnApps = await ethers.getContractAt('X2EarnAppsMock', X2EARN_APPS); + await x2EarnApps.addRewardDistributor(APP_ID, ecoEarnAddress); + console.log('Added'); + } + + updateConfig({ + ...config, + CONTRACT_ADDRESS: ecoEarnAddress, + }); + + console.log(`Done`); +} diff --git a/apps/contracts/scripts/deployEcoEarn.ts b/apps/contracts/scripts/deployEcoEarn.ts deleted file mode 100644 index 5622b99..0000000 --- a/apps/contracts/scripts/deployEcoEarn.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { updateConfig, config } from '@repo/config-contract'; -import { ethers } from 'hardhat'; -async function deployMugshot() { - const [owner] = await ethers.getSigners(); - - const ecoEarn = await ethers.getContractFactory('EcoEarn'); - - const ecoEarnInstance = await ecoEarn.deploy( - owner, - config.X2EARN_REWARDS_POOL, - config.CYCLE_DURATION, - config.MAX_SUBMISSIONS_PER_CYCLE, - config.APP_ID - ); - - const ecoEarnAddress = await ecoEarnInstance.getAddress(); - - console.log(`EcoEarn deployed to: ${ecoEarnAddress}`); - - updateConfig({ - ...config, - CONTRACT_ADDRESS: ecoEarnAddress, - }); -} - -deployMugshot() - .then(() => process.exit(0)) - .catch(error => { - console.error(error); - process.exit(1); - }); diff --git a/apps/contracts/scripts/deployToken.ts b/apps/contracts/scripts/deployToken.ts deleted file mode 100644 index 189827d..0000000 --- a/apps/contracts/scripts/deployToken.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { updateConfig, config } from '@repo/config-contract'; -import { ethers } from 'hardhat'; - -async function deployToken() { - const [owner] = await ethers.getSigners(); - - const token = await ethers.getContractFactory('Token'); - const tokenInstance = await token.deploy(owner); - - const tokenAddress = await tokenInstance.getAddress(); - - console.log(`Token deployed to: ${tokenAddress}`); - - updateConfig({ - ...config, - TOKEN_ADDRESS: tokenAddress, - }); -} - -deployToken() - .then(() => process.exit(0)) - .catch(error => { - console.error(error); - process.exit(1); - }); diff --git a/apps/contracts/scripts/index.ts b/apps/contracts/scripts/index.ts new file mode 100644 index 0000000..ffbd7fc --- /dev/null +++ b/apps/contracts/scripts/index.ts @@ -0,0 +1,15 @@ +// We recommend this pattern to be able to use async/await everywhere + +import { deploy } from './deploy'; + +// and properly handle errors. +const execute = async () => { + await deploy(); +}; + +execute() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/packages/config-contract/config.ts b/packages/config-contract/config.ts index 0d966ee..dd2e8fe 100644 --- a/packages/config-contract/config.ts +++ b/packages/config-contract/config.ts @@ -1,7 +1,8 @@ export const config = { - "TOKEN_ADDRESS": "0x316Fb7a5D1461363037fca9f44B1631252669D6F", - "CONTRACT_ADDRESS": "0xA5A7C5623fb234C961A9c8a50bDefaD2Cae68Fe8", - "X2EARN_REWARDS_POOL": "0xA5A7C5623fb234C961A9c8a50bDefaD2Cae68Fe8", + "TOKEN_ADDRESS": "0xbf64cf86894Ee0877C4e7d03936e35Ee8D8b864F", + "CONTRACT_ADDRESS": "0x0519a9b1871f0f74fe0f8e304a259693861125ab", + "X2EARN_REWARDS_POOL": "0x5F8f86B8D0Fa93cdaE20936d150175dF0205fB38", + "X2EARN_APPS": "0xcB23Eb1bBD5c07553795b9538b1061D0f4ABA153", "APP_ID": "0x899de0d0f0b39e484c8835b2369194c4c102b230c813862db383d44a4efe14d3", "CYCLE_DURATION": 60480, "MAX_SUBMISSIONS_PER_CYCLE": 10 From 5b8b140fd8815e06f37da75140c250fa1e00f6c0 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 08:52:23 +0200 Subject: [PATCH 10/20] feat: updated test deploy helper --- apps/contracts/package.json | 1 + apps/contracts/test/helpers/allocation.ts | 10 +++++-- apps/contracts/test/helpers/deploy.ts | 35 +++++++++++++++++++---- package.json | 4 ++- packages/config-contract/config.ts | 2 +- turbo.json | 28 ++++++++---------- 6 files changed, 55 insertions(+), 25 deletions(-) diff --git a/apps/contracts/package.json b/apps/contracts/package.json index 51c8454..45348aa 100644 --- a/apps/contracts/package.json +++ b/apps/contracts/package.json @@ -4,6 +4,7 @@ "scripts": { "test": "npx hardhat test --network hardhat ", "test:solo": "npx hardhat test --network vechain_solo", + "contracts:test": "yarn test:solo", "compile": "npx hardhat compile", "deploy:solo": "npx hardhat run scripts --network vechain_solo", "deploy:testnet": "npx hardhat run scripts --network vechain_testnet", diff --git a/apps/contracts/test/helpers/allocation.ts b/apps/contracts/test/helpers/allocation.ts index 8413079..89ee04b 100644 --- a/apps/contracts/test/helpers/allocation.ts +++ b/apps/contracts/test/helpers/allocation.ts @@ -1,8 +1,14 @@ import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; -import { EcoEarn, Token } from '../../typechain-types'; +import { EcoEarn, B3TR_Mock } from '../../typechain-types'; import { ethers } from 'ethers'; -export const receiveAllocations = async (mugshot: EcoEarn, token: Token, owner: HardhatEthersSigner, admin: HardhatEthersSigner, amount: string) => { +export const receiveAllocations = async ( + mugshot: EcoEarn, + token: B3TR_Mock, + owner: HardhatEthersSigner, + admin: HardhatEthersSigner, + amount: string, +) => { await token.connect(owner).mint(admin, ethers.parseEther(amount)); await token.connect(admin).approve(await mugshot.getAddress(), ethers.parseEther(Number.MAX_SAFE_INTEGER.toString())); diff --git a/apps/contracts/test/helpers/deploy.ts b/apps/contracts/test/helpers/deploy.ts index e9c0db0..96a10aa 100644 --- a/apps/contracts/test/helpers/deploy.ts +++ b/apps/contracts/test/helpers/deploy.ts @@ -10,11 +10,36 @@ export const getAndDeployContracts = async () => { // Contracts are deployed using the first signer/account by default const [owner, admin, account3, account4, ...otherAccounts] = await ethers.getSigners(); - const tokenContract = await ethers.getContractFactory('Token'); - const token = await tokenContract.deploy(owner); + const RewardTokenContract = await ethers.getContractFactory('B3TR_Mock'); + const rewardToken = await RewardTokenContract.deploy(); + await rewardToken.waitForDeployment(); - const ecoEarnContract = await ethers.getContractFactory('EcoEarn'); - const ecoearn = await ecoEarnContract.deploy(admin, await token.getAddress(), CYCLE_DURATION, MAX_SUBMISSIONS_PER_CYCLE); + const X2EarnAppsContract = await ethers.getContractFactory('X2EarnAppsMock'); + const x2EarnApps = await X2EarnAppsContract.deploy(); + await x2EarnApps.waitForDeployment(); - return { token, ecoearn, owner, admin, account3, account4, otherAccounts }; + const X2EarnRewardsPoolContract = await ethers.getContractFactory('X2EarnRewardsPoolMock'); + const x2EarnRewardsPool = await X2EarnRewardsPoolContract.deploy(owner.address, await rewardToken.getAddress(), await x2EarnApps.getAddress()); + await x2EarnRewardsPool.waitForDeployment(); + + await x2EarnApps.addApp(owner.address, owner.address, 'EcoEarn'); + const APP_ID = await x2EarnApps.hashAppName('EcoEarn'); + + await rewardToken.approve(await x2EarnRewardsPool.getAddress(), ethers.parseEther('10000')); + await x2EarnRewardsPool.deposit(ethers.parseEther('2000'), APP_ID); + + const ecoEarn = await ethers.getContractFactory('EcoEarn'); + const ecoEarnInstance = await ecoEarn.deploy( + owner.address, + await x2EarnRewardsPool.getAddress(), + CYCLE_DURATION, + MAX_SUBMISSIONS_PER_CYCLE, + APP_ID, + ); + await ecoEarnInstance.waitForDeployment(); + const ecoEarnAddress = await ecoEarnInstance.getAddress(); + + await x2EarnApps.addRewardDistributor(APP_ID, ecoEarnAddress); + + return { token: rewardToken, ecoearn: ecoEarnInstance, x2EarnApps, x2EarnRewardsPool, owner, admin, account3, account4, otherAccounts }; }; diff --git a/package.json b/package.json index d533aa0..0ae8e70 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "lint:fix": "turbo lint:fix", "test": "turbo test", "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "contracts:compile": "turbo run compile", + "contracts:test": "turbo run contracts:test", "contracts:deploy:solo": "turbo run deploy:solo", "contracts:deploy:testnet": "turbo run deploy:testnet" }, @@ -20,4 +22,4 @@ "apps/*", "packages/*" ] -} \ No newline at end of file +} diff --git a/packages/config-contract/config.ts b/packages/config-contract/config.ts index dd2e8fe..482230e 100644 --- a/packages/config-contract/config.ts +++ b/packages/config-contract/config.ts @@ -1,6 +1,6 @@ export const config = { "TOKEN_ADDRESS": "0xbf64cf86894Ee0877C4e7d03936e35Ee8D8b864F", - "CONTRACT_ADDRESS": "0x0519a9b1871f0f74fe0f8e304a259693861125ab", + "CONTRACT_ADDRESS": "0x0d515384440b09ebc7247eff9e864c75a96ad450", "X2EARN_REWARDS_POOL": "0x5F8f86B8D0Fa93cdaE20936d150175dF0205fB38", "X2EARN_APPS": "0xcB23Eb1bBD5c07553795b9538b1061D0f4ABA153", "APP_ID": "0x899de0d0f0b39e484c8835b2369194c4c102b230c813862db383d44a4efe14d3", diff --git a/turbo.json b/turbo.json index 4b095d5..637f421 100644 --- a/turbo.json +++ b/turbo.json @@ -2,12 +2,8 @@ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "dist/**" - ] + "dependsOn": ["^build"], + "outputs": ["dist/**"] }, "lint": {}, "lint:fix": {}, @@ -16,20 +12,20 @@ "persistent": true }, "test": { - "dependsOn": [ - "^lint" - ], + "dependsOn": ["^lint"], "cache": false }, "deploy:solo": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "deploy:testnet": { - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] + }, + "compile": { + "dependsOn": ["^build"] + }, + "contracts:test": { + "dependsOn": ["^build"] } } -} \ No newline at end of file +} From e3e82b88236651958df3fae9c83c6d677a47555b Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 08:54:24 +0200 Subject: [PATCH 11/20] chore: add docs in readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3b0ebe0..0a3d2ba 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Unlock the potential of decentralized application development on Vechain with our X-App template for VeBetterDAO. Designed for the Vechain Thor blockchain, this template integrates cutting-edge technologies such as React, TypeScript, Hardhat, and Express, ensuring a seamless and efficient DApp development experience. 🌟 +Read more about the implementation and key features of this template in our [Developer Docs](https://docs.vebetterdao.org/developer-guides/integration-examples/pattern-2-use-smart-contracts-and-backend). + ## Requirements Ensure your development environment is set up with the following: From 5ce629a4dc8e4f93593b0effc590cf79c106aa23 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 09:07:09 +0200 Subject: [PATCH 12/20] fix: tests --- apps/contracts/package.json | 2 +- apps/contracts/test/EcoEarn.test.ts | 44 ++++++++++++----------- apps/contracts/test/helpers/allocation.ts | 9 +++-- apps/contracts/test/helpers/deploy.ts | 26 +++++++++----- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/apps/contracts/package.json b/apps/contracts/package.json index 45348aa..b169c0f 100644 --- a/apps/contracts/package.json +++ b/apps/contracts/package.json @@ -4,7 +4,7 @@ "scripts": { "test": "npx hardhat test --network hardhat ", "test:solo": "npx hardhat test --network vechain_solo", - "contracts:test": "yarn test:solo", + "contracts:test": "yarn test", "compile": "npx hardhat compile", "deploy:solo": "npx hardhat run scripts --network vechain_solo", "deploy:testnet": "npx hardhat run scripts --network vechain_testnet", diff --git a/apps/contracts/test/EcoEarn.test.ts b/apps/contracts/test/EcoEarn.test.ts index d042927..658c79d 100644 --- a/apps/contracts/test/EcoEarn.test.ts +++ b/apps/contracts/test/EcoEarn.test.ts @@ -14,18 +14,17 @@ describe('ecoearn', () => { describe('Allocations', () => { it('Should track allocations for a cycle correctly', async () => { - const { ecoearn, token, owner, admin } = await getAndDeployContracts(); + const { ecoearn, token, x2EarnRewardsPool, appId, owner, admin } = await getAndDeployContracts(); // Simulate receiving tokens from X Allocations Round await token.connect(owner).mint(admin, ethers.parseEther('6700')); // Allowance - await token.connect(admin).approve(await ecoearn.getAddress(), ethers.parseEther(Number.MAX_SAFE_INTEGER.toString())); + await token.connect(admin).approve(await x2EarnRewardsPool.getAddress(), ethers.parseEther('6700')); + await x2EarnRewardsPool.connect(admin).deposit(ethers.parseEther('6700'), appId); - // Claim allocation for the current cycle - await ecoearn.connect(admin).claimAllocation(await token.balanceOf(admin.address)); - - expect(await token.balanceOf(await ecoearn.getAddress())).to.equal(ethers.parseEther('6700')); + // Set rewards for the current cycle + await ecoearn.connect(admin).setRewardsAmount(await token.balanceOf(admin.address)); await waitForNextCycle(ecoearn); // Assure cycle can be triggered @@ -35,9 +34,9 @@ describe('ecoearn', () => { }); it('Should track allocations for multiple cycles correctly', async () => { - const { ecoearn, token, owner, admin } = await getAndDeployContracts(); + const { ecoearn, token, x2EarnRewardsPool, appId, owner, admin } = await getAndDeployContracts(); - await receiveAllocations(ecoearn, token, owner, admin, '6700'); + await receiveAllocations(ecoearn, token, owner, admin, '6700', x2EarnRewardsPool, appId); await waitForNextCycle(ecoearn); @@ -45,7 +44,7 @@ describe('ecoearn', () => { expect(await ecoearn.getCurrentCycle()).to.equal(1); - await receiveAllocations(ecoearn, token, owner, admin, '6700'); + await receiveAllocations(ecoearn, token, owner, admin, '6700', x2EarnRewardsPool, appId); await waitForNextCycle(ecoearn); @@ -57,9 +56,9 @@ describe('ecoearn', () => { describe('Rewards', () => { it('Should track valid submissions correctly', async () => { - const { ecoearn, owner, admin, account3, token } = await getAndDeployContracts(); + const { ecoearn, owner, admin, account3, token, x2EarnRewardsPool, appId } = await getAndDeployContracts(); - await receiveAllocations(ecoearn, token, owner, admin, '6700'); + await receiveAllocations(ecoearn, token, owner, admin, '6700', x2EarnRewardsPool, appId); await waitForNextCycle(ecoearn); @@ -79,21 +78,24 @@ describe('ecoearn', () => { }); it('Should be able to receive expected rewards', async () => { - const { ecoearn, token, owner, account3, admin } = await getAndDeployContracts(); + const { ecoearn, token, owner, account3, account4, admin, x2EarnRewardsPool, appId } = await getAndDeployContracts(); - await receiveAllocations(ecoearn, token, owner, admin, '6700'); + await receiveAllocations(ecoearn, token, owner, admin, '6700', x2EarnRewardsPool, appId); await waitForNextCycle(ecoearn); await ecoearn.connect(admin).triggerCycle(); - await ecoearn.connect(admin).registerValidSubmission(owner.address, ethers.parseEther('1')); + expect(await token.balanceOf(account4.address)).to.equal(ethers.parseEther('0')); + expect(await token.balanceOf(account3.address)).to.equal(ethers.parseEther('0')); + + await ecoearn.connect(admin).registerValidSubmission(account4.address, ethers.parseEther('1')); await ecoearn.connect(admin).registerValidSubmission(account3.address, ethers.parseEther('1')); - await ecoearn.connect(admin).registerValidSubmission(owner.address, ethers.parseEther('1')); + await ecoearn.connect(admin).registerValidSubmission(account4.address, ethers.parseEther('1')); - expect(await token.balanceOf(owner.address)).to.equal( + expect(await token.balanceOf(account4.address)).to.equal( ethers.parseEther('2'), // Received 2 tokens ); @@ -103,9 +105,9 @@ describe('ecoearn', () => { }); it('Should calculate correctly rewards left', async () => { - const { ecoearn, token, owner, account3, admin, account4, otherAccounts } = await getAndDeployContracts(); + const { ecoearn, token, owner, account3, admin, account4, otherAccounts, x2EarnRewardsPool, appId } = await getAndDeployContracts(); - await receiveAllocations(ecoearn, token, owner, admin, '50'); // Receive 50 tokens + await receiveAllocations(ecoearn, token, owner, admin, '50', x2EarnRewardsPool, appId); // Receive 50 tokens await waitForNextCycle(ecoearn); @@ -132,9 +134,9 @@ describe('ecoearn', () => { describe('Withdrawals', () => { it("Should be able to withdraw if user's did not claim all their rewards", async () => { - const { ecoearn, token, owner, admin, account3 } = await getAndDeployContracts(); + const { ecoearn, token, owner, admin, account3, x2EarnRewardsPool, appId } = await getAndDeployContracts(); - await receiveAllocations(ecoearn, token, owner, admin, '6700'); + await receiveAllocations(ecoearn, token, owner, admin, '6700', x2EarnRewardsPool, appId); await waitForNextCycle(ecoearn); @@ -148,7 +150,7 @@ describe('ecoearn', () => { await waitForNextCycle(ecoearn); - await receiveAllocations(ecoearn, token, owner, admin, '6700'); + await receiveAllocations(ecoearn, token, owner, admin, '6700', x2EarnRewardsPool, appId); await waitForNextCycle(ecoearn); diff --git a/apps/contracts/test/helpers/allocation.ts b/apps/contracts/test/helpers/allocation.ts index 89ee04b..17719be 100644 --- a/apps/contracts/test/helpers/allocation.ts +++ b/apps/contracts/test/helpers/allocation.ts @@ -1,5 +1,5 @@ import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; -import { EcoEarn, B3TR_Mock } from '../../typechain-types'; +import { EcoEarn, B3TR_Mock, X2EarnRewardsPoolMock } from '../../typechain-types'; import { ethers } from 'ethers'; export const receiveAllocations = async ( @@ -8,10 +8,13 @@ export const receiveAllocations = async ( owner: HardhatEthersSigner, admin: HardhatEthersSigner, amount: string, + x2EarnRewardsPool: X2EarnRewardsPoolMock, + appId: string, ) => { await token.connect(owner).mint(admin, ethers.parseEther(amount)); - await token.connect(admin).approve(await mugshot.getAddress(), ethers.parseEther(Number.MAX_SAFE_INTEGER.toString())); + await token.connect(admin).approve(await x2EarnRewardsPool.getAddress(), ethers.parseEther(Number.MAX_SAFE_INTEGER.toString())); + await x2EarnRewardsPool.connect(admin).deposit(ethers.parseEther(amount), appId); - await mugshot.connect(admin).claimAllocation(ethers.parseEther(amount)); + await mugshot.connect(admin).setRewardsAmount(ethers.parseEther(amount)); }; diff --git a/apps/contracts/test/helpers/deploy.ts b/apps/contracts/test/helpers/deploy.ts index 96a10aa..39871fb 100644 --- a/apps/contracts/test/helpers/deploy.ts +++ b/apps/contracts/test/helpers/deploy.ts @@ -19,18 +19,15 @@ export const getAndDeployContracts = async () => { await x2EarnApps.waitForDeployment(); const X2EarnRewardsPoolContract = await ethers.getContractFactory('X2EarnRewardsPoolMock'); - const x2EarnRewardsPool = await X2EarnRewardsPoolContract.deploy(owner.address, await rewardToken.getAddress(), await x2EarnApps.getAddress()); + const x2EarnRewardsPool = await X2EarnRewardsPoolContract.deploy(admin.address, await rewardToken.getAddress(), await x2EarnApps.getAddress()); await x2EarnRewardsPool.waitForDeployment(); - await x2EarnApps.addApp(owner.address, owner.address, 'EcoEarn'); + await x2EarnApps.addApp(admin.address, admin.address, 'EcoEarn'); const APP_ID = await x2EarnApps.hashAppName('EcoEarn'); - await rewardToken.approve(await x2EarnRewardsPool.getAddress(), ethers.parseEther('10000')); - await x2EarnRewardsPool.deposit(ethers.parseEther('2000'), APP_ID); - const ecoEarn = await ethers.getContractFactory('EcoEarn'); const ecoEarnInstance = await ecoEarn.deploy( - owner.address, + admin.address, await x2EarnRewardsPool.getAddress(), CYCLE_DURATION, MAX_SUBMISSIONS_PER_CYCLE, @@ -39,7 +36,18 @@ export const getAndDeployContracts = async () => { await ecoEarnInstance.waitForDeployment(); const ecoEarnAddress = await ecoEarnInstance.getAddress(); - await x2EarnApps.addRewardDistributor(APP_ID, ecoEarnAddress); - - return { token: rewardToken, ecoearn: ecoEarnInstance, x2EarnApps, x2EarnRewardsPool, owner, admin, account3, account4, otherAccounts }; + await x2EarnApps.connect(admin).addRewardDistributor(APP_ID, ecoEarnAddress); + + return { + token: rewardToken, + ecoearn: ecoEarnInstance, + x2EarnApps, + x2EarnRewardsPool, + appId: APP_ID, + owner, + admin, + account3, + account4, + otherAccounts, + }; }; From 7e1fec3766c45e26d6a148848f145c94e50741ba Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 09:11:40 +0200 Subject: [PATCH 13/20] fix: yarn lint --- apps/frontend/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 9ce9909..578a85f 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -5,7 +5,7 @@ import react from "@vitejs/plugin-react"; import { nodePolyfills } from "vite-plugin-node-polyfills"; import { resolve } from "path"; -export default defineConfig(({ mode }) => { +export default defineConfig(() => { return { plugins: [nodePolyfills(), react()], build: { From af3f05596fc2e551c8974509a9574079e31554df Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 09:45:27 +0200 Subject: [PATCH 14/20] fix: warn devs to use the test sandbox --- README.md | 10 ++++++++-- apps/contracts/contracts/EcoEarn.sol | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0a3d2ba..59c364e 100644 --- a/README.md +++ b/README.md @@ -88,11 +88,17 @@ Clone the repository and install dependencies with ease: yarn install # Run this at the root level of the project ``` +To distribute rewards this contract necesitates of a valid APP_ID provided by VeBetterDAO when joining the ecosystem. +In testnet you can generate the APP_ID by using the [VeBetterDAO sandbox](https://dev.testnet.governance.vebetterdao.org/). +This contract can be initially deployed without this information and DEFAULT_ADMIN_ROLE can update it later through {EcoEarn-setAppId}. + +This contract must me set as a `rewardDistributor` inside the X2EarnApps contract to be able to send rewards to users and withdraw. + ## Deploying Contracts πŸš€ Deploy your contracts effortlessly on either the Testnet or Solo networks. -If you are deploying on the Testnet, ensure you have the correct addresses in the `config-contracts` package. -When deploying on the SOLO network the script will deploy for you mocked VeBetterDAO contracts. +If you are deploying on the Testnet, ensure you have the correct addresses in the `config-contracts` package (generated on the [VeBetterDAO sandbox](https://dev.testnet.governance.vebetterdao.org/)). +When deploying on the SOLO network the script will deploy for you the mocked VeBetterDAO contracts and generate an APP_ID. ### Deploying on Solo Network diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index dffad9d..9ae4171 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -34,9 +34,11 @@ import '@openzeppelin/contracts/utils/Strings.sol'; * @dev This contract manages a reward system based on cycles. Participants can make valid submissions to earn rewards. * Rewards are being distributed by interacting with the VeBetterDAO's X2EarnRewardsPool contract. * - * @notice To distribute rewards this contract necesitates of a valid APP_ID provided by VeBetterDAO. This contract - * can be initially deployed without this information and DEFAULT_ADMIN_ROLE can update it later through {EcoEarn-setAppId}. - * This contract must me set as a `rewardDistributor` inside the X2EarnApps contract to be able to send rewards to users and withdraw. + * @notice To distribute rewards this contract necesitates of a valid APP_ID provided by VeBetterDAO when joining the ecosystem. + * In testnet you can generate the APP_ID by using the VeBetterDAO sandbox at https://dev.testnet.governance.vebetterdao.org/. + * This contract can be initially deployed without this information and DEFAULT_ADMIN_ROLE can update it later through {EcoEarn-setAppId}. + * + * @notice This contract must me set as a `rewardDistributor` inside the X2EarnApps contract to be able to send rewards to users and withdraw. */ contract EcoEarn is AccessControl { // The X2EarnRewardsPool contract used to distribute rewards From fd9e7ad4fe14969629fd8e55cee2cb05a5f6ef0f Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 09:47:42 +0200 Subject: [PATCH 15/20] chore: added comment regarding sustainability proof --- apps/contracts/contracts/EcoEarn.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/contracts/contracts/EcoEarn.sol b/apps/contracts/contracts/EcoEarn.sol index 9ae4171..af73265 100644 --- a/apps/contracts/contracts/EcoEarn.sol +++ b/apps/contracts/contracts/EcoEarn.sol @@ -124,6 +124,9 @@ contract EcoEarn is AccessControl { rewardsLeft[getCurrentCycle()] -= amount; // Transfer the reward to the participant, will revert if the transfer fails + // The last parameter is the proof of the sustainable action the user is rewarded for. + // It is optional and can be left empty, but it is recommended to provide a proof for transparency and to avoid disputes. + // Read more about proofs in the VeBetterDAO documentation: https://docs.vebetterdao.org/developer-guides/sustainability-proofs x2EarnRewardsPoolContract.distributeReward(appId, amount, participant, ''); emit Submission(participant, amount); From 0bfad3096687fec37611374c02b5d914109ba7ac Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 09:52:44 +0200 Subject: [PATCH 16/20] feat: updated readme --- README.md | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 59c364e..9c7e466 100644 --- a/README.md +++ b/README.md @@ -122,22 +122,9 @@ yarn dev ### Setting up rewards -Run vechain devpal +Read the [VeBetterDAO documentation](https://docs.vebetterdao.org/developer-guides/test-environmnet) to learn how to set up rewards for your users and use the Testnet environment. -```bash -npx @vechain/devpal http://localhost:8669 -``` - -Open the `Inspector` tab and perform the following transactions: - -- **Add Contracts:** Add the EcoEarn contract and the Token contract deployed previously. Addresses can be found in the `config-contracts` package. ABIs can be found in the artifacts folder of the `contracts` app. - ![image](https://github.com/vechain/x-app-template/assets/64158778/e288ada4-5973-4428-9e72-a362388b1826) -- **Approve token:** Approve the EcoEarn contract to spend your tokens - ![image](https://github.com/vechain/x-app-template/assets/64158778/70787d8d-ae60-40ea-b277-87359aaca4ee) -- **Claim rewards:** Claim rewards for the EcoEarn contract - ![image](https://github.com/vechain/x-app-template/assets/64158778/834437e5-8de1-4802-9ed7-dca6fe4df332) -- **Trigger cycle:** Trigger the cycle for the EcoEarn contract - ![image](https://github.com/vechain/x-app-template/assets/64158778/00236dcd-5b64-4493-9acd-55c6a7f0981f) +
## Disclaimer ⚠️ From aec30a82bfe625de425598bb605c450f8b76c646 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 09:53:26 +0200 Subject: [PATCH 17/20] chore: added testnet env link --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9c7e466..79a5043 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ yarn dev Read the [VeBetterDAO documentation](https://docs.vebetterdao.org/developer-guides/test-environmnet) to learn how to set up rewards for your users and use the Testnet environment. +Test environment: [https://dev.testnet.governance.vebetterdao.org/](https://dev.testnet.governance.vebetterdao.org/) +
## Disclaimer ⚠️ From cfaae321d850afa053e8f476a4d65d58a08d588c Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 11:18:32 +0200 Subject: [PATCH 18/20] chore: updated readme --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index 79a5043..602b3ae 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,14 @@ Unlock the potential of decentralized application development on Vechain with ou Read more about the implementation and key features of this template in our [Developer Docs](https://docs.vebetterdao.org/developer-guides/integration-examples/pattern-2-use-smart-contracts-and-backend). +This template uses the VeBetterDAO ecosystem to distribute rewards to users. To learn more about VeBetterDAO, visit our [documentation](https://docs.vebetterdao.org/developer-guides/integration-examples). + +When using the solo node you can import the following mnemonic into your wallet and have access to 10 pre-funded accounts: + +``` +denial kitchen pet squirrel other broom bar gas better priority spoil cross +``` + ## Requirements Ensure your development environment is set up with the following: @@ -122,12 +130,48 @@ yarn dev ### Setting up rewards +## Testnet + Read the [VeBetterDAO documentation](https://docs.vebetterdao.org/developer-guides/test-environmnet) to learn how to set up rewards for your users and use the Testnet environment. Test environment: [https://dev.testnet.governance.vebetterdao.org/](https://dev.testnet.governance.vebetterdao.org/)
+## Solo Network + +Since the Solo network is a local network with mocked VeBetterDAO contracts you can use the following steps to set up available rewards to distribute to users: + +0. Ensure you are using a wallet with imported pre-funded accounts mnemonic into your wallet. Mnemoninc: + +``` +denial kitchen pet squirrel other broom bar gas better priority spoil cross +``` + +1. Copy the `APP_ID` generated by the `contracts:deploy:solo` script and logged in the console. +2. Run `devpal`, a frontend tool to interact with your contracts: + +```bash +npx @vechain/devpal http://localhost:8669 +``` + +3. Open the `Inspector` tab and perform the following actions: +4. Add the B3TR_Mock contract (get the address from the console logs and ABI from the `apps/contracts/artifacts/contracts/mock/B3TR_Mock.sol/B3TR_Mock.json` file) + ![image](https://i.ibb.co/6Zrj7Nx/SCR-20240723-jorq.png) +5. Add the X2EarnRewardsPool contract (get the address from the console logs and ABI from the `apps/contracts/artifacts/contracts/mock/X2EarnRewardsPoolMock.sol/X2EarnRewardsPoolMock.json` file) + ![image](https://i.ibb.co/yYjLw9v/SCR-20240723-jozk.png) +6. You should now have the following setup: + ![image](https://i.ibb.co/w4XWyh9/SCR-20240723-jpbc.png) +7. To recharge the rewards pool you will need to mint some mocked B3TR tokens, then deposit them into the rewards pool. Perform the following actions: + - Mint some tokens by calling the `mint` function on the B3TR_Mock contract + ![image](https://i.ibb.co/XCQ7LNR/SCR-20240723-kgll.png) + - Approve the X2EarnRewards contract to spend the tokens by calling the `approve` function on the B3TR_Mock contract + ![image](hhttps://i.ibb.co/7WvQL15/SCR-20240723-kedb.png) + - Deposit the tokens into the rewards pool by calling the `deposit` function on the X2EarnRewardsPool contract + ![image](https://i.ibb.co/X7Txx7Y/SCR-20240723-keuu.png) + +NB: Values are in wei, use this tool to convert to VET: [https://eth-converter.com/](https://eth-converter.com/) + ## Disclaimer ⚠️ This template serves as a foundational starting point and should be thoroughly reviewed and customized to suit your project’s specific requirements. Pay special attention to configurations, security settings, and environment variables to ensure a secure and efficient deployment. From 75e857c619896584501e12739470a95b4caa16a9 Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 11:21:27 +0200 Subject: [PATCH 19/20] fix: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 602b3ae..cb563cc 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ npx @vechain/devpal http://localhost:8669 - Mint some tokens by calling the `mint` function on the B3TR_Mock contract ![image](https://i.ibb.co/XCQ7LNR/SCR-20240723-kgll.png) - Approve the X2EarnRewards contract to spend the tokens by calling the `approve` function on the B3TR_Mock contract - ![image](hhttps://i.ibb.co/7WvQL15/SCR-20240723-kedb.png) + ![image](https://i.ibb.co/X7Txx7Y/SCR-20240723-keuu.png) - Deposit the tokens into the rewards pool by calling the `deposit` function on the X2EarnRewardsPool contract ![image](https://i.ibb.co/X7Txx7Y/SCR-20240723-keuu.png) From 75fa6edab12ee37196b2fcdc8f39f4ca63d665db Mon Sep 17 00:00:00 2001 From: Dan Rusnac Date: Tue, 23 Jul 2024 11:32:53 +0200 Subject: [PATCH 20/20] feat: added more steps in readme --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index cb563cc..ad78b1e 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,21 @@ Test environment: [https://dev.testnet.governance.vebetterdao.org/](https://dev.
+Thanks to the test environment you will be able to mint and deposit B3TR tokens int the rewards pool that you will use to distribute rewards to users. + +Now you just need to trigger cycles and set amount of rewards per cycle on your EcoEarn contract. + +1. Go to our online [inspector app](https://solid-funicular-1wmop55.pages.github.io/#/contracts) that you can use to interact with your contracts. Be sure to select the correct network (Testnet). + +2. Add the `EcoEarn` contract to the inspector app. Get the address from `config-contracts` package and the ABI from the `apps/contracts/artifacts/contracts/EcoEarn.sol/EcoEarn.json` file. + ![image](https://i.ibb.co/TK8519c/SCR-20240723-kjid.png) + +3. Set how many rewards you want to distribute per cycle: + ![image](https://i.ibb.co/qpJnL5x/SCR-20240723-kkti.png) + +4. Trigger a cycle: + ![image](https://i.ibb.co/47V2Zjb/SCR-20240723-kkxx.png) + ## Solo Network Since the Solo network is a local network with mocked VeBetterDAO contracts you can use the following steps to set up available rewards to distribute to users: @@ -169,6 +184,16 @@ npx @vechain/devpal http://localhost:8669 ![image](https://i.ibb.co/X7Txx7Y/SCR-20240723-keuu.png) - Deposit the tokens into the rewards pool by calling the `deposit` function on the X2EarnRewardsPool contract ![image](https://i.ibb.co/X7Txx7Y/SCR-20240723-keuu.png) +8. Now you just need to set how many rewards you want to distribute per cycle and trigger the start of the cycle + +- Add the `EcoEarn` contract to the inspector app. Get the address from `config-contracts` package and the ABI from the `apps/contracts/artifacts/contracts/EcoEarn.sol/EcoEarn.json` file. + ![image](https://i.ibb.co/TK8519c/SCR-20240723-kjid.png) + +- Set how many rewards you want to distribute per cycle: + ![image](https://i.ibb.co/qpJnL5x/SCR-20240723-kkti.png) + +- Trigger a cycle: + ![image](https://i.ibb.co/47V2Zjb/SCR-20240723-kkxx.png) NB: Values are in wei, use this tool to convert to VET: [https://eth-converter.com/](https://eth-converter.com/)