Skip to content
This repository has been archived by the owner on Sep 19, 2024. It is now read-only.

penalty for reopened issue #462

Merged
merged 12 commits into from
Jul 31, 2023
60 changes: 60 additions & 0 deletions src/adapters/supabase/helpers/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { getAdapters, getLogger } from "../../../bindings";
import { Issue, UserProfile } from "../../../types";
import { Database } from "../types";
import { BigNumber, BigNumberish } from "ethers";
0xcodercrane marked this conversation as resolved.
Show resolved Hide resolved

/**
* @dev Creates a typescript client which will be used to interact with supabase platform
Expand Down Expand Up @@ -311,3 +312,62 @@ export const getMultiplierReason = async (username: string): Promise<string> =>
const { data } = await supabase.from("wallets").select("reason").eq("user_name", username).single();
return data?.reason;
};

export const addPenalty = async (username: string, repoName: string, tokenAddress: string, networkId: string, penalty: BigNumberish): Promise<void> => {
const { supabase } = getAdapters();
const logger = getLogger();

const { error } = await supabase.rpc("add_penalty", {
_username: username,
_repository_name: repoName,
_token_address: tokenAddress,
_network_id: networkId,
_penalty_amount: penalty.toString(),
});
logger.debug(`Adding penalty done, { data: ${JSON.stringify(error)}, error: ${JSON.stringify(error)} }`);

if (error) {
throw new Error(`Error adding penalty: ${error.message}`);
}
};

export const getPenalty = async (username: string, repoName: string, tokenAddress: string, networkId: string): Promise<BigNumber> => {
const { supabase } = getAdapters();
const logger = getLogger();

const { data, error } = await supabase
.from("penalty")
.select("amount")
.eq("username", username)
.eq("repository_name", repoName)
.eq("network_id", networkId)
.eq("token_address", tokenAddress);
logger.debug(`Getting penalty done, { data: ${JSON.stringify(error)}, error: ${JSON.stringify(error)} }`);

if (error) {
throw new Error(`Error getting penalty: ${error.message}`);
}

if (data.length === 0) {
return BigNumber.from(0);
}
return BigNumber.from(data[0].amount);
};

export const removePenalty = async (username: string, repoName: string, tokenAddress: string, networkId: string, penalty: BigNumberish): Promise<void> => {
const { supabase } = getAdapters();
const logger = getLogger();

const { error } = await supabase.rpc("remove_penalty", {
_username: username,
_repository_name: repoName,
_network_id: networkId,
_token_address: tokenAddress,
_penalty_amount: penalty.toString(),
});
logger.debug(`Removing penalty done, { data: ${JSON.stringify(error)}, error: ${JSON.stringify(error)} }`);

if (error) {
throw new Error(`Error removing penalty: ${error.message}`);
}
};
1 change: 0 additions & 1 deletion src/adapters/supabase/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./helpers";
export * from "./types";
19 changes: 18 additions & 1 deletion src/adapters/supabase/types/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,24 @@ export interface Database {
[_ in never]: never;
};
Functions: {
[_ in never]: never;
add_penalty: {
Args: {
username: string;
repository_name: string;
token_address: string;
penalty_amount: string;
};
Returns: string;
};
deduct_penalty: {
Args: {
username: string;
repository_name: string;
token_address: string;
penalty_amount: string;
};
Returns: string;
};
};
Enums: {
issue_status: "READY_TO_START" | "IN_PROGRESS" | "IN_REVIEW" | "DONE";
Expand Down
2 changes: 1 addition & 1 deletion src/bindings/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { getScalarKey, getWideConfig } from "../utils/private";

export const loadConfig = async (context: Context): Promise<BotConfig> => {
const {
privateKey,
baseMultiplier,
timeLabels,
privateKey,
priorityLabels,
commentElementPricing,
autoPayMode,
Expand Down
93 changes: 91 additions & 2 deletions src/handlers/comment/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@ import { unassign } from "./unassign";
import { registerWallet } from "./wallet";
import { setAccess } from "./allow";
import { multiplier } from "./multiplier";
import { addCommentToIssue, createLabel, addLabelToIssue, getLabel, upsertCommentToIssue } from "../../../helpers";
import { getBotConfig, getBotContext } from "../../../bindings";
import { BigNumber, ethers } from "ethers";
import { addPenalty } from "../../../adapters/supabase";
import {
addCommentToIssue,
createLabel,
addLabelToIssue,
getLabel,
upsertCommentToIssue,
getAllIssueComments,
getPayoutConfigByNetworkId,
getTokenSymbol,
getAllIssueAssignEvents,
} from "../../../helpers";
import { getBotConfig, getBotContext, getLogger } from "../../../bindings";
import { handleIssueClosed } from "../../payout";
import { query } from "./query";

Expand Down Expand Up @@ -86,6 +98,83 @@ export const issueCreatedCallback = async (): Promise<void> => {
}
};

/**
* Callback for issues reopened - Processor
*/

export const issueReopenedCallback = async (): Promise<void> => {
const { payload: _payload } = getBotContext();
const {
payout: { permitBaseUrl },
} = getBotConfig();
const logger = getLogger();
const issue = (_payload as Payload).issue;
const repository = (_payload as Payload).repository;
if (!issue) return;
try {
// find permit comment from the bot
const comments = await getAllIssueComments(issue.number);
const claimUrlRegex = new RegExp(`\\((${permitBaseUrl}\\?claim=\\S+)\\)`);
const permitCommentIdx = comments.findIndex((e) => e.user.type === "Bot" && e.body.match(claimUrlRegex));
if (permitCommentIdx === -1) {
return;
}

// extract permit amount and token
const permitComment = comments[permitCommentIdx];
const permitUrl = permitComment.body.match(claimUrlRegex);
if (!permitUrl || permitUrl.length < 2) {
logger.error(`Permit URL not found`);
return;
}
const url = new URL(permitUrl[1]);
const claimBase64 = url.searchParams.get("claim");
if (!claimBase64) {
logger.error(`Permit claim search parameter not found`);
return;
}
let networkId = url.searchParams.get("network");
if (!networkId) {
networkId = "1";
}
const { rpc } = getPayoutConfigByNetworkId(Number(networkId));
let claim;
try {
claim = JSON.parse(Buffer.from(claimBase64, "base64").toString("utf-8"));
} catch (err: unknown) {
logger.error(`Error parsing claim: ${err}`);
return;
}
const amount = BigNumber.from(claim.permit.permitted.amount);
const formattedAmount = ethers.utils.formatUnits(amount, 18);
const tokenAddress = claim.permit.permitted.token;
const tokenSymbol = await getTokenSymbol(tokenAddress, rpc);

// find latest assignment before the permit comment
const events = await getAllIssueAssignEvents(issue.number);
if (events.length === 0) {
logger.error(`No assignment found`);
return;
}
const assignee = events[0].assignee.login;

// write penalty to db
try {
await addPenalty(assignee, repository.full_name, tokenAddress, networkId.toString(), amount);
} catch (err) {
logger.error(`Error writing penalty to db: ${err}`);
return;
}

await addCommentToIssue(
`@${assignee} please be sure to review this conversation and implement any necessary fixes. Unless this is closed as completed, its payment of **${formattedAmount} ${tokenSymbol}** will be deducted from your next bounty.`,
issue.number
);
} catch (err: unknown) {
await addCommentToIssue(`Error: ${err}`, issue.number);
}
};

/**
* Default callback for slash commands
*
Expand Down
88 changes: 83 additions & 5 deletions src/handlers/payout/action.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,82 @@
import { getWalletAddress, getWalletMultiplier } from "../../adapters/supabase";
import { BigNumber, ethers } from "ethers";
import { getPenalty, getWalletAddress, getWalletMultiplier, removePenalty } from "../../adapters/supabase";
import { getBotConfig, getBotContext, getLogger } from "../../bindings";
import { addLabelToIssue, deleteLabel, generatePermit2Signature, getAllIssueComments, getTokenSymbol } from "../../helpers";
import {
addLabelToIssue,
deleteLabel,
generatePermit2Signature,
getAllIssueAssignEvents,
getAllIssueComments,
getTokenSymbol,
wasIssueReopened,
} from "../../helpers";
import { UserType, Payload, StateReason } from "../../types";
import { shortenEthAddress } from "../../utils";
import { bountyInfo } from "../wildcard";

export const handleIssueClosed = async () => {
const context = getBotContext();
const {
payout: { paymentToken, rpc },
payout: { paymentToken, rpc, permitBaseUrl, networkId },
mode: { autoPayMode },
} = getBotConfig();
const logger = getLogger();
const payload = context.payload as Payload;
const issue = payload.issue;
if (!issue) return;

const comments = await getAllIssueComments(issue.number);

const wasReopened = await wasIssueReopened(issue.number);
const claimUrlRegex = new RegExp(`\\((${permitBaseUrl}\\?claim=\\S+)\\)`);
0xcodercrane marked this conversation as resolved.
Show resolved Hide resolved
const permitCommentIdx = comments.findIndex((e) => e.user.type === "Bot" && e.body.match(claimUrlRegex));

if (wasReopened && permitCommentIdx !== -1) {
const permitComment = comments[permitCommentIdx];
const permitUrl = permitComment.body.match(claimUrlRegex);
if (!permitUrl || permitUrl.length < 2) {
logger.error(`Permit URL not found`);
return;
}
const url = new URL(permitUrl[1]);
const claimBase64 = url.searchParams.get("claim");
if (!claimBase64) {
logger.error(`Permit claim search parameter not found`);
return;
}
let networkId = url.searchParams.get("network");
if (!networkId) {
networkId = "1";
}
let claim;
try {
claim = JSON.parse(Buffer.from(claimBase64, "base64").toString("utf-8"));
} catch (err: unknown) {
logger.error(`${err}`);
return;
}
const amount = BigNumber.from(claim.permit.permitted.amount);
const tokenAddress = claim.permit.permitted.token;

// extract assignee
const events = await getAllIssueAssignEvents(issue.number);
if (events.length === 0) {
logger.error(`No assignment found`);
return;
}
const assignee = events[0].assignee.login;

try {
await removePenalty(assignee, payload.repository.full_name, tokenAddress, networkId, amount);
} catch (err) {
logger.error(`Failed to remove penalty: ${err}`);
return;
}

logger.info(`Penalty removed`);
return;
}

if (issue.state_reason !== StateReason.COMPLETED) {
logger.info("Permit generation skipped because the issue was not closed as completed");
return "Permit generation skipped because the issue was not closed as completed";
Expand Down Expand Up @@ -54,24 +115,41 @@ export const handleIssueClosed = async () => {
}

// TODO: add multiplier to the priceInEth
const priceInEth = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString();
let priceInEth = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString();
if (!recipient || recipient?.trim() === "") {
logger.info(`Recipient address is missing`);
return;
}

// if bounty hunter has any penalty then deduct it from the bounty
const penaltyAmount = await getPenalty(assignee.login, payload.repository.full_name, paymentToken, networkId.toString());
if (penaltyAmount.gt(0)) {
logger.info(`Deducting penalty from bounty`);
const bountyAmount = ethers.utils.parseUnits(priceInEth, 18);
const bountyAmountAfterPenalty = bountyAmount.sub(penaltyAmount);
if (bountyAmountAfterPenalty.lte(0)) {
await removePenalty(assignee.login, payload.repository.full_name, paymentToken, networkId.toString(), bountyAmount);
const msg = `Permit generation skipped because bounty amount after penalty is 0`;
0xcodercrane marked this conversation as resolved.
Show resolved Hide resolved
logger.info(msg);
return msg;
}
priceInEth = ethers.utils.formatUnits(bountyAmountAfterPenalty, 18);
}

const payoutUrl = await generatePermit2Signature(recipient, priceInEth, issue.node_id);
const tokenSymbol = await getTokenSymbol(paymentToken, rpc);
const shortenRecipient = shortenEthAddress(recipient, `[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]`.length);
logger.info(`Posting a payout url to the issue, url: ${payoutUrl}`);
const comment = `### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```";
const comments = await getAllIssueComments(issue.number);
const permitComments = comments.filter((content) => content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot);
if (permitComments.length > 0) {
logger.info(`Skip to generate a permit url because it has been already posted`);
return `Permit generation skipped because it was already posted to this issue.`;
}
await deleteLabel(issueDetailed.priceLabel);
await addLabelToIssue("Permitted");
if (penaltyAmount.gt(0)) {
await removePenalty(assignee.login, payload.repository.full_name, paymentToken, networkId.toString(), penaltyAmount);
}
return comment;
};
7 changes: 6 additions & 1 deletion src/handlers/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { closePullRequestForAnIssue, commentWithAssignMessage } from "./assign";
import { pricingLabelLogic, validatePriceLabels } from "./pricing";
import { checkBountiesToUnassign, collectAnalytics, checkWeeklyUpdate } from "./wildcard";
import { nullHandler } from "./shared";
import { handleComment, issueClosedCallback } from "./comment";
import { handleComment, issueClosedCallback, issueReopenedCallback } from "./comment";
import { checkPullRequests } from "./assign/auto";
import { createDevPoolPR } from "./pull-request";
import { runOnPush } from "./push";
Expand All @@ -15,6 +15,11 @@ export const processors: Record<string, Handler> = {
action: [nullHandler], // SHOULD not set `issueCreatedCallback` until the exploit issue resolved. https://github.com/ubiquity/ubiquibot/issues/535
post: [nullHandler],
},
[GithubEvent.ISSUES_REOPENED]: {
pre: [nullHandler],
action: [issueReopenedCallback],
post: [nullHandler],
},
[GithubEvent.ISSUES_LABELED]: {
pre: [validatePriceLabels],
action: [pricingLabelLogic],
Expand Down
Loading