Skip to content

Commit

Permalink
Check Sufficient Bonds (#21)
Browse files Browse the repository at this point in the history
Introducing a new ENV var, `BOND_THRESHOLD`, that is used to compare the accounts remaining bond after drafting. If their bond is below threshold, this is included in the draft post.
  • Loading branch information
cowanator authored Jun 17, 2024
1 parent fc1b790 commit 37e68ea
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 16 deletions.
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ FEE_QUERY=3605385
ROLE_KEY=
## This is $3.50 with ETH at $3500
FINE_MIN=0.001
BOND_THRESHOLD=10.0
PAYMENT_QUERY=3742749
BOND_MAP="('0xa489faf6e337d997b8a23e2b6f3a8880b1b61e19', '0xfd39bc23d356a762cf80f60b7bc8d2a4b9bcfe67')"

Expand Down
1 change: 1 addition & 0 deletions src/abis.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const BILLING_CONTRACT_ABI = [
"function bonds(address) view returns (uint256)",
"function bill(address[] ids, uint256[] due, uint256 newPrice)",
"function draft(address id, uint256 amt)",
"function fine(address id, uint256 amt, address to)",
Expand Down
45 changes: 37 additions & 8 deletions src/accountManager.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { QueryRunner } from "./dune";
import { BillingContract } from "./billingContract";
import { Slack } from "./notify";
import { ethers } from "ethers";
import { DraftResults } from "./types";

const TEN_ETH = ethers.parseEther("1");

export class AccountManager {
private dataFetcher: QueryRunner;
private billingContract: BillingContract;
private slack: Slack;
scanUrl?: string;
bondThreshold: bigint;

constructor(
dataFetcher: QueryRunner,
billingContract: BillingContract,
slack: Slack,
bondThreshold: bigint = TEN_ETH,
scanUrl?: string,
) {
this.dataFetcher = dataFetcher;
Expand All @@ -21,21 +27,27 @@ export class AccountManager {
console.warn("running without scan URL, txHashes will be logged bare");
}
this.scanUrl = scanUrl;
this.bondThreshold = bondThreshold;
}

static async fromEnv(): Promise<AccountManager> {
const { BOND_THRESHOLD, SCAN_URL } = process.env;
const bondThreshold = BOND_THRESHOLD
? ethers.parseEther(BOND_THRESHOLD)
: TEN_ETH;
return new AccountManager(
QueryRunner.fromEnv(),
BillingContract.fromEnv(),
await Slack.fromEnv(),
bondThreshold,
SCAN_URL,
);
}

async runBilling() {
const today = todaysDate();
console.log("Running Biller for Date", today);
const billingResults = await this.dataFetcher.getBillingData(today);
// TODO - validate results!
const txHash =
await this.billingContract.updatePaymentDetails(billingResults);
await this.slack.post(
Expand All @@ -46,17 +58,34 @@ export class AccountManager {
async runDrafting() {
console.log("Running Drafter");
const paymentStatuses = await this.dataFetcher.getPaymentStatus();
const txHash =
const draftResults =
await this.billingContract.processPaymentStatuses(paymentStatuses);
if (txHash) {
await this.slack.post(
`MEV Drafting ran successfully: ${this.txLink(txHash)}`,
);
if (draftResults) {
await this.draftPost(draftResults);
} else {
console.log("No accounts drafted");
}
// TODO - check balances after drafting and notify if too low!
// May only need to query balances of those who were drafted.
}

async draftPost(draftResults: DraftResults): Promise<void> {
const { txHash, accounts } = draftResults;
let messages: string[] = [
`MEV Drafting ran successfully: ${this.txLink(txHash)}`,
];

for (const address of accounts) {
try {
const remainingBond = await this.billingContract.getBond(address);
if (remainingBond < this.bondThreshold) {
messages.push(
`Account ${address} bond (${ethers.formatEther(remainingBond)} ETH) below threshold!`,
);
}
} catch (error) {
messages.push(`Error reading bond value for ${address}: ${error}`);
}
}
await this.slack.post(messages.join("\n"));
}

private txLink(hash: string): string {
Expand Down
21 changes: 18 additions & 3 deletions src/billingContract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ethers, formatEther } from "ethers";
import { BillingData, LatestBillingStatus, PaymentStatus } from "./types";
import {
BillingData,
DraftResults,
LatestBillingStatus,
PaymentStatus,
} from "./types";
import { BILLING_CONTRACT_ABI, ROLE_MODIFIER_ABI } from "./abis";
import { getTxCostForGas, maxBigInt } from "./gas";
import { MetaTransaction, encodeMulti } from "ethers-multisend";
Expand Down Expand Up @@ -53,6 +58,7 @@ export class BillingContract {
const provider = new ethers.JsonRpcProvider(RPC_URL!);
const signer = new ethers.Wallet(BILLER_PRIVATE_KEY!, provider);
const minFine = FINE_MIN ? ethers.parseEther(FINE_MIN) : 0n;

return new BillingContract(
BILLING_CONTRACT_ADDRESS!,
provider,
Expand All @@ -77,7 +83,7 @@ export class BillingContract {

async processPaymentStatuses(
paymentStatuses: LatestBillingStatus[],
): Promise<string | undefined> {
): Promise<DraftResults | undefined> {
const unpaidRecords = paymentStatuses.filter((record) => {
const { account, status, paidAmount, billedAmount } = record;
if (status == PaymentStatus.UNPAID) {
Expand All @@ -93,6 +99,7 @@ export class BillingContract {
}
return false;
});
let draftedAccounts: `0x${string}`[] = [];
let drafts: MetaTransaction[] = [];
let fines: MetaTransaction[] = [];
if (unpaidRecords.length > 0) {
Expand All @@ -101,6 +108,7 @@ export class BillingContract {
drafts.push(
await this.buildDraft(rec.account, rec.billedAmount - rec.paidAmount),
);
draftedAccounts.push(rec.account);
}
const fineAmount = await this.evaluateFine(drafts);
for (const rec of unpaidRecords) {
Expand All @@ -112,13 +120,20 @@ export class BillingContract {
console.log(`Executing ${drafts.length} drafts & fines`);
const tx = await this.execWithRole([...drafts, ...fines], this.roleKey!);
await tx.wait();
return tx.hash;
return {
txHash: tx.hash as `0x${string}`,
accounts: draftedAccounts,
};
} else {
console.log("No Drafts to execute!");
}
return;
}

async getBond(account: `0x${string}`): Promise<bigint> {
return this.contract.bonds(account);
}

async evaluateFine(drafts: MetaTransaction[]): Promise<bigint> {
const metaTx = drafts.length > 1 ? encodeMulti(drafts) : drafts[0];
const gasEstimate =
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ export interface LatestBillingStatus {
paidAmount: bigint;
status: PaymentStatus;
}

export interface DraftResults {
txHash: `0x${string}`;
accounts: `0x${string}`[];
}
15 changes: 10 additions & 5 deletions tests/e2e/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,22 @@ describe("e2e - Sepolia", () => {
expect(logs!.length).toEqual(3);
});

it("Runs the drafting flow with mainnet data on Sepolia billing contract", async () => {
it.only("Runs the drafting flow with mainnet data on Sepolia billing contract", async () => {
const paymentStatus = await dataFetcher.getPaymentStatus();
const billingContract = BillingContract.fromEnv();
const draftHash =
const draftResults =
await billingContract.processPaymentStatuses(paymentStatus);

const provider = billingContract.contract.runner!.provider;
const receipt = await provider!.getTransactionReceipt(draftHash!);
let { txHash, accounts } = draftResults!;
const receipt = await provider!.getTransactionReceipt(txHash);
const logs = receipt?.logs!;
// 2 drafts + 2 fines + 2 safe module transactions.
const expectedLogs = 1 + 2 + 2 + 1;
expect(receipt?.logs!.length).toEqual(expectedLogs);
expect(logs.length).toEqual(1 + 2 + 2 + 1);
expect(accounts).toEqual([
"0x93699c88c427d1040f2839dffaaf0de0e8aae4b4",
"0x4efb61ffc5b81ce473b426e4bc9ffaf613574286",
]);
});

it.skip("e2e: successfully calls bill on BillingContract (with mock billing data)", async () => {
Expand Down

0 comments on commit 37e68ea

Please sign in to comment.