diff --git a/README.md b/README.md index df2674c..4ba24af 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ NODE_ENV=main npx hardhat deployBnbXImpl --network NODE_ENV=main npx hardhat deployStakeManagerProxy --network NODE_ENV=main npx hardhat upgradeStakeManagerProxy --network NODE_ENV=main npx hardhat deployStakeManagerImpl --network + +NODE_ENV=main npx hardhat deployReferralContract --network +NODE_ENV=main npx hardhat upgradeReferralContract --network ``` ## Verifying on etherscan diff --git a/contracts/campaigns/KOLReferral.sol b/contracts/campaigns/KOLReferral.sol new file mode 100644 index 0000000..badf328 --- /dev/null +++ b/contracts/campaigns/KOLReferral.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@opengsn/contracts/src/ERC2771Recipient.sol"; + +contract KOLReferral is Initializable, ERC2771Recipient { + address public admin; + + mapping(address => string) public walletToReferralId; + mapping(string => address) public referralIdToWallet; + mapping(address => string) public userReferredBy; + mapping(address => address[]) private _kolToUsers; + + address[] private _users; + address[] private _kols; + + modifier onlyAdmin() { + require(_msgSender() == admin, "Only Admin"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address admin_, address trustedForwarder_) + external + initializer + { + require(admin_ != address(0), "zero address"); + require(trustedForwarder_ != address(0), "zero address"); + admin = admin_; + _setTrustedForwarder(trustedForwarder_); + } + + function registerKOL(address wallet, string memory referralId) + external + onlyAdmin + { + require( + referralIdToWallet[referralId] == address(0), + "ReferralId is already taken" + ); + require( + bytes(walletToReferralId[wallet]).length == 0, + "ReferralId is already assigned for this wallet" + ); + walletToReferralId[wallet] = referralId; + referralIdToWallet[referralId] = wallet; + _kols.push(wallet); + } + + function storeUserInfo(string memory referralId) external { + require( + referralIdToWallet[referralId] != address(0), + "Invalid ReferralId" + ); + require( + bytes(userReferredBy[_msgSender()]).length == 0, + "User is already referred before" + ); + userReferredBy[_msgSender()] = referralId; + _users.push(_msgSender()); + + address kolWallet = referralIdToWallet[referralId]; + + require(_msgSender() != kolWallet, "kol not allowed as user"); + _kolToUsers[kolWallet].push(_msgSender()); + } + + function queryUserReferrer(address user) + external + view + returns (address _referrer) + { + string memory referralId = userReferredBy[user]; + return referralIdToWallet[referralId]; + } + + function getKOLUserList(address kol) + external + view + returns (address[] memory) + { + return _kolToUsers[kol]; + } + + function getKOLs() external view returns (address[] memory) { + return _kols; + } + + function getUsers(uint256 startIdx, uint256 maxNumUsers) + external + view + returns (uint256 numUsers, address[] memory userList) + { + require(startIdx < _users.length, "invalid startIdx"); + + if (startIdx + maxNumUsers > _users.length) { + maxNumUsers = _users.length - startIdx; + } + + userList = new address[](maxNumUsers); + for ( + numUsers = 0; + startIdx < _users.length && numUsers < maxNumUsers; + numUsers++ + ) { + userList[numUsers] = _users[startIdx++]; + } + + return (numUsers, userList); + } + + function getKOLCount() external view returns (uint256) { + return _kols.length; + } + + function getUserCount() external view returns (uint256) { + return _users.length; + } + + function getKOLRefCount(address kol) external view returns (uint256) { + return _kolToUsers[kol].length; + } + + function setAdmin(address admin_) external onlyAdmin { + require(admin_ != address(0), "zero address"); + require(admin_ != admin, "old admin == new admin"); + admin = admin_; + } + + function setTrustedForwarder(address trustedForwarder_) external onlyAdmin { + require(trustedForwarder_ != address(0), "zero address"); + _setTrustedForwarder(trustedForwarder_); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 03b0c8f..42a32af 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -77,6 +77,21 @@ task( await deployDirect(hre, "StakeManager"); }); +task("deployReferralContract", "Deploy KOL Referral Contract") + .addPositionalParam("admin") + .addPositionalParam("trustedForwarder") + .setAction( + async ({ admin, trustedForwarder }, hre: HardhatRuntimeEnvironment) => { + await deployProxy(hre, "KOLReferral", admin, trustedForwarder); + } + ); + +task("upgradeReferralContract", "Upgrade KOL Referral Contract") + .addPositionalParam("proxyAddress") + .setAction(async ({ proxyAddress }, hre: HardhatRuntimeEnvironment) => { + await upgradeProxy(hre, "KOLReferral", proxyAddress); + }); + const config: HardhatUserConfig = { defaultNetwork: "hardhat", solidity: { diff --git a/kol-referral-info.json b/kol-referral-info.json new file mode 100644 index 0000000..460d40f --- /dev/null +++ b/kol-referral-info.json @@ -0,0 +1,13 @@ +{ + "testnet": { + "contract": "0x50C15cb832dcBFB9ee4b95d101608b664F06D7f7", + "admin": "0x8e39FBBA48014E8a36b36dad183d2A00E9c750cC", + "trustedForwarder": "0x61456BF1715C1415730076BB79ae118E806E74d2" + }, + "beta": { + "contract": "0x3629787a59418732B388ED398928c5Bb9f4E74f1", + "proxyAdmin": "0xcb507d421540f1ec1b8adcaf81995c7c89f4213e", + "admin": "0x8e39FBBA48014E8a36b36dad183d2A00E9c750cC", + "trustedForwarder": "0x86C80a8aa58e0A4fa09A69624c31Ab2a6CAD56b8" + } +} diff --git a/package-lock.json b/package-lock.json index 38315b3..4550d24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "bnbx", "version": "1.0.0", "license": "ISC", + "dependencies": { + "@opengsn/contracts": "^3.0.0-beta.1" + }, "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.6", "@nomiclabs/hardhat-etherscan": "^3.0.4", @@ -1612,6 +1615,19 @@ "hardhat": "^2.0.0" } }, + "node_modules/@opengsn/contracts": { + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@opengsn/contracts/-/contracts-3.0.0-beta.1.tgz", + "integrity": "sha512-ZQrpe8F45ONSyLnCeEbbSXWqUpP5EIjnOsIMHIudYDT+0c689EpzsZIMluX3RZhYrUMvc7JW05zx/sbCBeEhug==", + "dependencies": { + "@openzeppelin/contracts": "^4.2.0" + } + }, + "node_modules/@openzeppelin/contracts": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.0.tgz", + "integrity": "sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw==" + }, "node_modules/@openzeppelin/contracts-upgradeable": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.1.tgz", @@ -25216,6 +25232,19 @@ "@types/web3": "1.0.19" } }, + "@opengsn/contracts": { + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@opengsn/contracts/-/contracts-3.0.0-beta.1.tgz", + "integrity": "sha512-ZQrpe8F45ONSyLnCeEbbSXWqUpP5EIjnOsIMHIudYDT+0c689EpzsZIMluX3RZhYrUMvc7JW05zx/sbCBeEhug==", + "requires": { + "@openzeppelin/contracts": "^4.2.0" + } + }, + "@openzeppelin/contracts": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.0.tgz", + "integrity": "sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw==" + }, "@openzeppelin/contracts-upgradeable": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.1.tgz", diff --git a/package.json b/package.json index ec9a26e..faca1eb 100644 --- a/package.json +++ b/package.json @@ -52,5 +52,8 @@ "ts-node": "^10.8.1", "typechain": "^5.2.0", "typescript": "^4.7.3" + }, + "dependencies": { + "@opengsn/contracts": "^3.0.0-beta.1" } } diff --git a/test/KOLReferral.spec.ts b/test/KOLReferral.spec.ts new file mode 100644 index 0000000..0b452f9 --- /dev/null +++ b/test/KOLReferral.spec.ts @@ -0,0 +1,67 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; +import { KOLReferral } from "../typechain"; + +describe("KOL referral Contract", () => { + let admin: SignerWithAddress; + let kol1: SignerWithAddress; + let trustedForwarder: SignerWithAddress; + let users: SignerWithAddress[]; + let kolContract: KOLReferral; + + beforeEach(async () => { + [admin, kol1, trustedForwarder, ...users] = await ethers.getSigners(); + + kolContract = (await upgrades.deployProxy( + await ethers.getContractFactory("KOLReferral"), + [admin.address, trustedForwarder.address] + )) as KOLReferral; + await kolContract.deployed(); + }); + + it("register a kol", async () => { + let referralId1: string = "kol_1_ref_id"; + + expect(await kolContract.walletToReferralId(kol1.address)).be.eq(""); + expect(await kolContract.referralIdToWallet(referralId1)).be.eq( + ethers.constants.AddressZero + ); + expect(await kolContract.getKOLCount()).be.eq(0); + + await kolContract.registerKOL(kol1.address, referralId1); + expect(await kolContract.walletToReferralId(kol1.address)).be.eq( + referralId1 + ); + expect(await kolContract.referralIdToWallet(referralId1)).be.eq( + kol1.address + ); + expect(await kolContract.getKOLCount()).be.eq(1); + }); + + it("store user info", async () => { + let referralId1: string = "kol_1_ref_id"; + await kolContract.registerKOL(kol1.address, referralId1); + + expect(await kolContract.getUserCount()).to.be.eq(0); + expect(await kolContract.getKOLRefCount(kol1.address)).be.eq(0); + + let u1kolContract = kolContract.connect(users[0]); + await u1kolContract.storeUserInfo(referralId1); + + expect(await kolContract.queryUserReferrer(users[0].address)).to.be.eq( + kol1.address + ); + expect(await kolContract.getKOLRefCount(kol1.address)).be.eq(1); + + const totalUsers = await kolContract.getUserCount(); + const { numUsers, userList } = await kolContract.getUsers(0, totalUsers); + expect(userList[0]).to.be.eq(users[0].address); + expect(numUsers).to.be.eq(1); + expect(numUsers).to.be.eq(totalUsers); + + await expect( + kolContract.connect(kol1).storeUserInfo(referralId1) + ).be.revertedWith("kol not allowed as user"); + }); +});