Skip to content
Open

BSC #21

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ AFFILIATE_PRIVATE_KEY=0x...
RECIPIENT_ADDRESS=0x...
CONTRACT_ADDRESS=0x...
CLAIM_AMOUNT=100
MERKLE_PROOF=0x...
MERKLE_PROOF=0x...

VOLUME_THRESHOLD=10000
37 changes: 28 additions & 9 deletions generator/src/createConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -140,4 +159,4 @@ const fetchReferralRewards = async () => {
totalRewards: totalRewards.toString(),
top3Rewards
}
}
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
}
}
}
29 changes: 25 additions & 4 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
}
}
}

Expand Down
16 changes: 15 additions & 1 deletion server/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -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(),
}
}
}
14 changes: 12 additions & 2 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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 {}
9 changes: 5 additions & 4 deletions server/src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
15 changes: 14 additions & 1 deletion server/src/controllers/signatures.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
14 changes: 12 additions & 2 deletions server/src/services/affiliate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Affiliate>,
private onChainService: OnChainService,
) {}

async create(createAffiliateDto: CreateAffiliateDto): Promise<Affiliate> {
Expand Down Expand Up @@ -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")
Expand Down
16 changes: 14 additions & 2 deletions server/src/services/signature.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -17,6 +18,7 @@ export class SignatureService {
constructor(
@InjectModel(Signature.name) private signatureModel: Model<Signature>,
@InjectModel(Affiliate.name) private affiliateModel: Model<Affiliate>,
private onChainService: OnChainService,
) {}

async store(
Expand All @@ -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
Expand Down
64 changes: 64 additions & 0 deletions server/src/utils/on-chain.ts
Original file line number Diff line number Diff line change
@@ -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<string>("bscRpcUrl")
this.contract = this.configService.get<string>(
"referrals.contract",
) as Hex
const client = createBscClient(rpcUrl)
this.readContract = client.readContract.bind(client)
}

async isAffiliate(userAddress: string): Promise<boolean> {
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",
)
}
}
}
Loading