diff --git a/.env.example b/.env.example index fb29ddb..4f3fddf 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,6 @@ AFFILIATE_PRIVATE_KEY=0x... RECIPIENT_ADDRESS=0x... CONTRACT_ADDRESS=0x... CLAIM_AMOUNT=100 -MERKLE_PROOF=0x... \ No newline at end of file +MERKLE_PROOF=0x... + +VOLUME_THRESHOLD=10000 \ No newline at end of file diff --git a/generator/src/createConfig.ts b/generator/src/createConfig.ts index 1496121..24a2dc5 100644 --- a/generator/src/createConfig.ts +++ b/generator/src/createConfig.ts @@ -17,12 +17,21 @@ export const createConfig = async () => { const merkleTree = new Generator(decimals, airdrop) // Reset rewards on rewards API - await axios.delete(process.env.REWARDS_API_URI, { - data: { campaign: "referral" }, - headers: { - Authorization: `Token ${process.env.REWARDS_API_TOKEN}` + try { + await axios.delete(process.env.REWARDS_API_URI, { + data: { campaign: "referral" }, + headers: { + Authorization: `Token ${process.env.REWARDS_API_TOKEN}` + } + }) + } catch (err) { + if (axios.isAxiosError(err)) { + const status = err.response?.status + const statusText = err.response?.statusText ?? err.message + throw new Error(`Error resetting rewards on the API (${status ?? "unknown"} ${statusText}).`) } - }) + throw err + } // Update rewards on rewards API for (const [wallet, amount] of Object.entries(airdrop)) { @@ -108,10 +117,20 @@ const fetchReferralRewards = async () => { } }` - const data = (await axios.post(process.env.SUBGRAPH, query )).data.data + const { data } = await axios.post( + process.env.SUBGRAPH as string, + { query }, + { + headers: { + "Content-Type": "application/json" + } + } + ) + + const { referralPositions, _meta } = data.data - res = data.referralPositions - lastBlockTimestamp = data._meta.block.timestamp + res = referralPositions + lastBlockTimestamp = _meta.block.timestamp res.forEach(({owner, totalRewardsPending}) => { rewardsPending[owner.id] = totalRewardsPending @@ -140,4 +159,4 @@ const fetchReferralRewards = async () => { totalRewards: totalRewards.toString(), top3Rewards } -} \ No newline at end of file +} diff --git a/package.json b/package.json index d845253..ec0390c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "deploy:mainnet": "bash -c 'source .env && forge script Deploy --rpc-url $MAINNET_RPC --broadcast --verify -vvvv'", "deploy:sepolia": "bash -c 'source .env && forge script Deploy --rpc-url $SEPOLIA_RPC --broadcast --verify -vvvv'", "deploy:bartio": "bash -c 'source .env && forge script Deploy --rpc-url $BARTIO_RPC --broadcast --legacy -vvvv'", + "deploy:bsc": "bash -c 'source .env && forge script Deploy --rpc-url $BSC_RPC --broadcast --verify -vvvv'", "upgrade:mainnet": "bash -c 'source .env && forge script UpgradeImplementation --rpc-url $MAINNET_RPC --broadcast --verify -vvvv'", - "upgrade:sepolia": "bash -c 'source .env && forge script UpgradeImplementation --rpc-url $SEPOLIA_RPC --broadcast --verify -vvvv'" + "upgrade:sepolia": "bash -c 'source .env && forge script UpgradeImplementation --rpc-url $SEPOLIA_RPC --broadcast --verify -vvvv'", + "upgrade:bsc": "bash -c 'source .env && forge script UpgradeImplementation --rpc-url $BSC_RPC --broadcast --verify -vvvv'" } -} \ No newline at end of file +} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 458f43e..6f3c602 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -7,9 +7,9 @@ import {ReferralList} from "src/ReferralList.sol"; import {ReferralListProxy} from "src/ProxyWrapper.sol"; abstract contract DeployReferralList is Script { - function _deploy() internal { + function _deploy() internal returns (address proxyAddress) { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - address owner_ = vm.envAddress("OWNER_ADDRESS"); + address owner_ = vm.addr(deployerPrivateKey); // Auto-derive owner from private key address _airdropper = vm.envAddress("AIRDROPPER_ADDRESS"); address _rewardToken = vm.envAddress("OV_CONTRACT"); address _verifyingAddress = vm.envAddress("VERIFIER_ADDRESS"); @@ -20,15 +20,36 @@ abstract contract DeployReferralList is Script { vm.startBroadcast(deployerPrivateKey); ReferralList impl = new ReferralList(); - new ReferralListProxy(address(impl), data); + ReferralListProxy proxy = new ReferralListProxy(address(impl), data); + proxyAddress = address(proxy); vm.stopBroadcast(); + + console2.log("Implementation deployed at:", address(impl)); + console2.log("Proxy deployed at:", proxyAddress); + console2.log("Initial owner (deployer):", owner_); } } contract Deploy is DeployReferralList { function run() external { - _deploy(); + address proxyAddress = _deploy(); + + // Transfer ownership to Safe if SAFE_ADDRESS is set + address safeAddress = vm.envOr("SAFE_ADDRESS", address(0)); + if (safeAddress != address(0)) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + console2.log("Transferring ownership to Safe:", safeAddress); + + vm.startBroadcast(deployerPrivateKey); + ReferralList(proxyAddress).transferOwnership(safeAddress); + vm.stopBroadcast(); + + console2.log("Ownership transferred to Safe:", safeAddress); + } else { + console2.log("SAFE_ADDRESS not set, skipping ownership transfer"); + } } } diff --git a/server/src/app.controller.ts b/server/src/app.controller.ts index 42fa164..be0961d 100644 --- a/server/src/app.controller.ts +++ b/server/src/app.controller.ts @@ -1,12 +1,26 @@ import { Controller, Get } from "@nestjs/common" +import { ConfigService } from "@nestjs/config" import { AppService } from "./app.service" @Controller() export class AppController { - constructor(private readonly appService: AppService) {} + constructor( + private readonly appService: AppService, + private readonly configService: ConfigService, + ) {} @Get() getHello(): string { return this.appService.getHello() } + + @Get("min-trading-volume") + getMinTradingVolume() { + const minTradingVolume = this.configService.get( + "referrals.minTradingVolume", + ) + return { + minTradingVolume: minTradingVolume.toString(), + } + } } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index e8f2706..e828c1e 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -2,10 +2,14 @@ import { Module } from "@nestjs/common" import { ConfigModule, ConfigService } from "@nestjs/config" import { MongooseModule } from "@nestjs/mongoose" import configuration from "./config/configuration" +import { AppController } from "./app.controller" +import { AppService } from "./app.service" import { AffiliatesController } from "./controllers/affiliates.controller" import { SignaturesController } from "./controllers/signatures.controller" import { AffiliateService } from "./services/affiliate.service" import { SignatureService } from "./services/signature.service" +import { SignaturesService } from "./signatures/signatures.service" +import { OnChainService } from "./utils/on-chain" import { Affiliate, AffiliateSchema } from "./schemas/affiliate.schema" import { Signature, SignatureSchema } from "./schemas/signature.schema" @@ -27,7 +31,13 @@ import { Signature, SignatureSchema } from "./schemas/signature.schema" { name: Signature.name, schema: SignatureSchema }, ]), ], - controllers: [AffiliatesController, SignaturesController], - providers: [AffiliateService, SignatureService], + controllers: [AppController, AffiliatesController, SignaturesController], + providers: [ + AppService, + AffiliateService, + SignatureService, + SignaturesService, + OnChainService, + ], }) export class AppModule {} diff --git a/server/src/config/configuration.ts b/server/src/config/configuration.ts index 7985048..5e35c4f 100644 --- a/server/src/config/configuration.ts +++ b/server/src/config/configuration.ts @@ -5,12 +5,13 @@ export default () => ({ port: parseInt(process.env.PORT) || 3000, signingKey: process.env.PRIVATE_KEY, subgraphUrl: - "https://api.studio.thegraph.com/query/49419/overlay-arb-sepolia/version/latest", + "https://api.goldsky.com/api/public/project_clyiptt06ifuv01ul9xiwfj28/subgraphs/overlay-bsc/prod/gn", referrals: { - minTradingVolume: ethers.parseEther("1000"), // 1000 OVL - contract: "0x1cee53AB89004b2a9E173edc6F51509f8eB32122", - chainId: 421614, + minTradingVolume: ethers.parseEther(process.env.VOLUME_THRESHOLD), // 10000 OVL + contract: "0xd36a37a5c116ef661a84bd2314b4ef59e1a0f307", + chainId: 56, }, + bscRpcUrl: process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org", isDevelopmentMode: process.env.NODE_ENV !== "production", mongoUri: process.env.MONGO_URI || "mongodb://localhost:27017/referral", }) diff --git a/server/src/controllers/signatures.controller.ts b/server/src/controllers/signatures.controller.ts index e277374..8cf9b12 100644 --- a/server/src/controllers/signatures.controller.ts +++ b/server/src/controllers/signatures.controller.ts @@ -10,11 +10,15 @@ import { BadRequestException, } from "@nestjs/common" import { SignatureService } from "../services/signature.service" +import { SignaturesService } from "../signatures/signatures.service" import { StoreSignatureDto } from "../dto/store-signature.dto" @Controller("signatures") export class SignaturesController { - constructor(private readonly signatureService: SignatureService) {} + constructor( + private readonly signatureService: SignatureService, + private readonly signaturesService: SignaturesService, + ) {} @Get("check/:trader") @HttpCode(HttpStatus.OK) @@ -28,6 +32,15 @@ export class SignaturesController { return { exists: !!signature, affiliate: signature?.affiliate ?? "" } } + @Get(":account") + async requestSignature(@Param("account") account: string) { + try { + return await this.signaturesService.requestSignature(account) + } catch (error) { + throw new BadRequestException(error.message) + } + } + @Post() @HttpCode(HttpStatus.CREATED) async store(@Body() storeSignatureDto: StoreSignatureDto) { diff --git a/server/src/services/affiliate.service.ts b/server/src/services/affiliate.service.ts index a4b514e..14dde0f 100644 --- a/server/src/services/affiliate.service.ts +++ b/server/src/services/affiliate.service.ts @@ -9,11 +9,13 @@ import { Affiliate } from "../schemas/affiliate.schema" import { CreateAffiliateDto } from "../dto/create-affiliate.dto" import { CreateAliasDto } from "../dto/create-alias.dto" import { getAddress, Hex, recoverTypedDataAddress } from "viem" +import { OnChainService } from "../utils/on-chain" @Injectable() export class AffiliateService { constructor( @InjectModel(Affiliate.name) private affiliateModel: Model, + private onChainService: OnChainService, ) {} async create(createAffiliateDto: CreateAffiliateDto): Promise { @@ -70,9 +72,17 @@ export class AffiliateService { throw new ConflictException("Alias already taken") } - const affiliate = await this.affiliateModel.findOne({ address }).exec() + let affiliate = await this.affiliateModel.findOne({ address }).exec() if (!affiliate) { - throw new BadRequestException("Affiliate not found") + const isAffiliate = await this.onChainService.isAffiliate(address) + if (!isAffiliate) { + throw new BadRequestException("Affiliate not found") + } + affiliate = await this.affiliateModel.findOneAndUpdate( + { address }, + { $setOnInsert: { address } }, + { upsert: true, new: true }, + ) } if (affiliate.alias) { throw new BadRequestException("Affiliate already has an alias") diff --git a/server/src/services/signature.service.ts b/server/src/services/signature.service.ts index 0e95cb6..3451969 100644 --- a/server/src/services/signature.service.ts +++ b/server/src/services/signature.service.ts @@ -7,6 +7,7 @@ import { import { InjectModel } from "@nestjs/mongoose" import { Model } from "mongoose" import { recoverTypedDataAddress, Hex } from "viem" +import { OnChainService } from "../utils/on-chain" import { Signature } from "../schemas/signature.schema" import { Affiliate } from "../schemas/affiliate.schema" import { StoreSignatureDto } from "../dto/store-signature.dto" @@ -17,6 +18,7 @@ export class SignatureService { constructor( @InjectModel(Signature.name) private signatureModel: Model, @InjectModel(Affiliate.name) private affiliateModel: Model, + private onChainService: OnChainService, ) {} async store( @@ -31,11 +33,21 @@ export class SignatureService { ) } - const affiliate = await this.affiliateModel + let affiliate = await this.affiliateModel .findOne({ address: storeSignatureDto.affiliate }) .exec() if (!affiliate) { - throw new NotFoundException("Affiliate not found") + const isAffiliate = await this.onChainService.isAffiliate( + storeSignatureDto.affiliate, + ) + if (!isAffiliate) { + throw new NotFoundException("Affiliate not found") + } + affiliate = await this.affiliateModel.findOneAndUpdate( + { address: storeSignatureDto.affiliate }, + { $setOnInsert: { address: storeSignatureDto.affiliate } }, + { upsert: true, new: true }, + ) } // Validate the signature diff --git a/server/src/utils/on-chain.ts b/server/src/utils/on-chain.ts new file mode 100644 index 0000000..f245df3 --- /dev/null +++ b/server/src/utils/on-chain.ts @@ -0,0 +1,64 @@ +import { + Injectable, + OnModuleInit, + ServiceUnavailableException, +} from "@nestjs/common" +import { ConfigService } from "@nestjs/config" +import { createPublicClient, getAddress, Hex, http } from "viem" +import { bsc } from "viem/chains" + +const USER_TIER_ABI = [ + { + inputs: [{ name: "", type: "address" }], + name: "userTier", + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const + +function createBscClient(rpcUrl: string) { + return createPublicClient({ + chain: bsc, + transport: http(rpcUrl, { timeout: 5000 }), + }) +} + +@Injectable() +export class OnChainService implements OnModuleInit { + private readContract: (typeof createBscClient extends ( + ...args: infer _ + ) => infer R + ? R + : never)["readContract"] + private contract: Hex + + constructor(private configService: ConfigService) {} + + onModuleInit() { + const rpcUrl = this.configService.get("bscRpcUrl") + this.contract = this.configService.get( + "referrals.contract", + ) as Hex + const client = createBscClient(rpcUrl) + this.readContract = client.readContract.bind(client) + } + + async isAffiliate(userAddress: string): Promise { + try { + const tier = await this.readContract({ + address: this.contract, + abi: USER_TIER_ABI, + functionName: "userTier", + args: [getAddress(userAddress)], + }) + + return tier > 0n + } catch (error) { + console.error("On-chain affiliate check failed:", error) + throw new ServiceUnavailableException( + "Unable to verify on-chain affiliate status", + ) + } + } +} diff --git a/src/ReferralList.sol b/src/ReferralList.sol index c59e385..33dcc3e 100644 --- a/src/ReferralList.sol +++ b/src/ReferralList.sol @@ -56,27 +56,6 @@ contract ReferralList is OwnableRoles, Initializable, UUPSUpgradeable, IReferral _saveReferralForTrader(msg.sender, _user); } - function addAffiliateOrKolOnBehalfOf(address _trader, address _affiliate, bytes calldata signature) public { - // validate EIP-712 signature: _trader adds _affiliate as referrer - bytes32 signedMessageHash = keccak256( - abi.encodePacked("\x19\x01", _DOMAIN_SEPARATOR, keccak256(abi.encode(_AFFILIATE_TO_TYPEHASH, _affiliate))) - ); - if (signedMessageHash.recover(signature) != _trader) revert InvalidSignature(); - _saveReferralForTrader(_trader, _affiliate); - } - - function batchAddAffiliateOrKolOnBehalfOf( - address[] calldata _traders, - address[] calldata _affiliates, - bytes[] calldata signatures - ) public { - uint256 totalSubmits = _traders.length; - if (_affiliates.length != totalSubmits || signatures.length != totalSubmits) revert LengthMismatch(); - for (uint16 i; i < totalSubmits; i++) { - addAffiliateOrKolOnBehalfOf(_traders[i], _affiliates[i], signatures[i]); - } - } - function _saveReferralForTrader(address _trader, address _affiliate) internal { if (msg.sender == _affiliate) { revert SelfReferralNotAllowed(); diff --git a/test/ReferralList.t.sol b/test/ReferralList.t.sol index a921f61..ab32e64 100644 --- a/test/ReferralList.t.sol +++ b/test/ReferralList.t.sol @@ -152,180 +152,6 @@ contract ReferralListTest is Test { referralList.addAffiliateOrKOL(affiliate2); } - function testAddAffiliateOrKolOnBehalfOf() public { - // create and allow affiliate - address affiliate = makeAddr("affiliate"); - vm.startPrank(AIRDROPPER); - referralList.allowKOL(affiliate); - - // create a valid message hash, and valid signature for USER - SigUtils.AffiliateTo memory affiliateTo = SigUtils.AffiliateTo({affiliate: affiliate}); - bytes32 msgHash = sigUtils.getTypedDataHash(affiliateTo); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(USER_PRIVATE_KEY, msgHash); - bytes memory signature = abi.encodePacked(r, s, v); - - vm.expectEmit(true, true, true, true); - emit AddAffiliateOrKOL(USER, affiliate); - - referralList.addAffiliateOrKolOnBehalfOf(USER, affiliate, signature); - } - - function testAddAffiliateOrKolOnBehalfOfInvalidDomain() public { - // create and allow affiliate - address affiliate = makeAddr("affiliate"); - vm.startPrank(AIRDROPPER); - referralList.allowKOL(affiliate); - - // setup invalid domain - EIP712Domain memory _invalidDomain = EIP712Domain("Overlay Referrals Invalid", "1.0"); - SigUtils invalidSigUtils = new SigUtils(hashDomain(_invalidDomain)); - - // create a message hash, and signature for USER - SigUtils.AffiliateTo memory affiliateTo = SigUtils.AffiliateTo({affiliate: affiliate}); - bytes32 msgHash = invalidSigUtils.getTypedDataHash(affiliateTo); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(USER_PRIVATE_KEY, msgHash); - bytes memory signature = abi.encodePacked(r, s, v); - - // expect to revert with InvalidSignature error - bytes4 selector = bytes4(keccak256("InvalidSignature()")); - vm.expectRevert(selector); - referralList.addAffiliateOrKolOnBehalfOf(USER, affiliate, signature); - } - - function testAddAffiliateOrKolOnBehalfOfInvalidSignature() public { - // create and allow affiliate - address affiliate = makeAddr("affiliate"); - vm.startPrank(AIRDROPPER); - referralList.allowKOL(affiliate); - - // create a valid message hash, and valid signature for USER - SigUtils.AffiliateTo memory affiliateTo = SigUtils.AffiliateTo({affiliate: affiliate}); - bytes32 msgHash = sigUtils.getTypedDataHash(affiliateTo); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(USER_PRIVATE_KEY, msgHash); - bytes memory signature = abi.encodePacked(r, s, v); - - // expect to revert with InvalidSignature error - bytes4 selector = bytes4(keccak256("InvalidSignature()")); - vm.expectRevert(selector); - // use invalid USER aka kaker - referralList.addAffiliateOrKolOnBehalfOf(makeAddr("kaker"), affiliate, signature); - } - - function testBatchAddAffiliateOrKolOnBehalfOf() public { - address[] memory affiliates = new address[](3); - address[] memory USERs = new address[](3); - bytes[] memory signatures = new bytes[](3); - - // create and allow affiliate1 - address affiliate = makeAddr("affiliate"); - affiliates[0] = affiliate; - vm.startPrank(AIRDROPPER); - referralList.allowKOL(affiliate); - // create and allow affiliate2 - address affiliate2 = makeAddr("affiliate2"); - affiliates[1] = affiliate2; - referralList.allowKOL(affiliate2); - // create and allow affiliate3 - address affiliate3 = makeAddr("affiliate3"); - affiliates[2] = affiliate3; - referralList.allowKOL(affiliate3); - - // create a valid message hash, and valid signature for USERs - uint32[3] memory users_pk = [0x0213456, 0x052436766, 0x02431657]; - uint256 amountOfIterations = users_pk.length; - for (uint256 i; i < amountOfIterations; i++) { - SigUtils.AffiliateTo memory affiliateTo = SigUtils.AffiliateTo({affiliate: affiliates[i]}); - bytes32 msgHash = sigUtils.getTypedDataHash(affiliateTo); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(users_pk[i], msgHash); - bytes memory signature = abi.encodePacked(r, s, v); - USERs[i] = vm.addr(users_pk[i]); - signatures[i] = signature; - } - vm.expectEmit(true, true, true, true); - emit AddAffiliateOrKOL(USERs[0], affiliates[0]); - vm.expectEmit(true, true, true, true); - emit AddAffiliateOrKOL(USERs[1], affiliates[1]); - vm.expectEmit(true, true, true, true); - emit AddAffiliateOrKOL(USERs[2], affiliates[2]); - referralList.batchAddAffiliateOrKolOnBehalfOf(USERs, affiliates, signatures); - } - - function testBatchAddAffiliateOrKolOnBehalfOfInvalidDomain() public { - address[] memory affiliates = new address[](2); - address[] memory USERs = new address[](2); - bytes[] memory signatures = new bytes[](2); - - // create and allow affiliate1 - address affiliate = makeAddr("affiliate"); - affiliates[0] = affiliate; - vm.startPrank(AIRDROPPER); - referralList.allowKOL(affiliate); - // create and allow affiliate2 - address affiliate2 = makeAddr("affiliate2"); - affiliates[1] = affiliate2; - referralList.allowKOL(affiliate2); - - // setup invalid domain - EIP712Domain memory _invalidDomain = EIP712Domain("Overlay Referrals Invalid", "1.0"); - SigUtils invalidSigUtils = new SigUtils(hashDomain(_invalidDomain)); - - // create a valid message hash, and valid signature for USERs - uint32[2] memory users_pk = [0x0213456, 0x052436766]; - uint256 amountOfIterations = users_pk.length; - for (uint256 i; i < amountOfIterations; i++) { - SigUtils.AffiliateTo memory affiliateTo = SigUtils.AffiliateTo({affiliate: affiliates[i]}); - bytes32 msgHash = invalidSigUtils.getTypedDataHash(affiliateTo); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(users_pk[i], msgHash); - bytes memory signature = abi.encodePacked(r, s, v); - USERs[i] = vm.addr(users_pk[i]); - signatures[i] = signature; - } - - // expect to revert with InvalidSignature error - bytes4 selector = bytes4(keccak256("InvalidSignature()")); - vm.expectRevert(selector); - referralList.batchAddAffiliateOrKolOnBehalfOf(USERs, affiliates, signatures); - } - - function testBatchAddAffiliateOrKolOnBehalfOfInvalidSignature() public { - address[] memory affiliates = new address[](3); - address[] memory USERs = new address[](3); - bytes[] memory signatures = new bytes[](3); - - // create and allow affiliate1 - address affiliate = makeAddr("affiliate"); - affiliates[0] = affiliate; - vm.startPrank(AIRDROPPER); - referralList.allowKOL(affiliate); - // create and allow affiliate2 - address affiliate2 = makeAddr("affiliate2"); - affiliates[1] = affiliate2; - referralList.allowKOL(affiliate2); - // create and allow affiliate3 - address affiliate3 = makeAddr("affiliate3"); - affiliates[2] = affiliate3; - referralList.allowKOL(affiliate3); - - // create a valid message hash, and valid signature for USERs - uint32[3] memory users_pk = [0x0213456, 0x052436766, 0x02431657]; - uint256 amountOfIterations = users_pk.length; - for (uint256 i; i < amountOfIterations; i++) { - SigUtils.AffiliateTo memory affiliateTo = SigUtils.AffiliateTo({affiliate: affiliates[i]}); - bytes32 msgHash = sigUtils.getTypedDataHash(affiliateTo); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(users_pk[i], msgHash); - bytes memory signature = abi.encodePacked(r, s, v); - USERs[i] = vm.addr(users_pk[i]); - signatures[i] = signature; - } - - USERs[0] = makeAddr("kaker"); - - // expect to revert with InvalidSignature error - bytes4 selector = bytes4(keccak256("InvalidSignature()")); - vm.expectRevert(selector); - referralList.batchAddAffiliateOrKolOnBehalfOf(USERs, affiliates, signatures); - } - function testSetRewardToken() public { vm.startPrank(AIRDROPPER); address token = makeAddr("token");