Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ipfs tools #7

Merged
merged 4 commits into from
May 17, 2023
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.env

dist
node_modules
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# BGD Labs <> Aave CLI
23 changes: 14 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@bgd-labs/report-engine",
"version": "0.0.11",
"description": "",
"name": "@bgd-labs/aave-cli",
"version": "0.0.1",
"description": "A cli to perform various aave governance related tasks",
"main": "./dist/index.js",
"module": "./dist/index.cjs",
"private": false,
"bin": {
"aave-report-engine": "dist/cli.cjs"
"aave-cli": "dist/cli.cjs"
},
"scripts": {
"build": "tsup",
Expand All @@ -25,10 +25,11 @@
"homepage": "https://github.com/bgd-labs/report-engine#readme",
"type": "module",
"devDependencies": {
"@types/yargs": "^17.0.23",
"@types/node-fetch": "^2.6.4",
"@types/yargs": "^17.0.24",
"tsup": "^6.7.0",
"typescript": "^5.0.2",
"vitest": "^0.29.3"
"typescript": "^5.0.4",
"vitest": "^0.31.0"
},
"exports": {
"default": "./dist/index.cjs",
Expand All @@ -40,11 +41,15 @@
"access": "public"
},
"dependencies": {
"bs58": "^5.0.0",
"dotenv": "^16.0.3",
"gray-matter": "^4.0.3",
"ipfs-only-hash": "^4.0.0",
"json-bigint": "^1.0.0",
"node-fetch": "^2.6.9",
"object-hash": "^3.0.0",
"viem": "^0.3.0",
"yargs": "^17.7.1",
"viem": "^0.3.27",
"yargs": "^17.7.2",
"zod": "^3.21.4"
}
}
37 changes: 8 additions & 29 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,14 @@
#!/usr/bin/env node
import * as dotenv from "dotenv"; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
dotenv.config();

import yargs from "yargs/yargs";
import { hideBin } from "yargs/helpers";
import { diffReports } from "./diffReports";
import { readJson } from "./utils/json";
import fs from "fs";
import * as ipfsCmd from "./commands/ipfs-upload";
import * as diffSnapshot from "./commands/diff-snaphots";

yargs(hideBin(process.argv))
.scriptName("aave-report-engine")
.usage("$0 <cmd> [args]")
.command<{ from: string; to: string; out: string }>(
"diff [from] [to] [out]",
"diffs two json reports",
(yargs) => {
yargs.positional("from", {
type: "string",
describe: "the initial json",
});
yargs.positional("to", {
type: "string",
describe: "the final json",
});
yargs.positional("out", {
type: "string",
describe: "the output path & filename",
});
},
async function (argv) {
const from = readJson(argv.from);
const to = readJson(argv.to);
const content = await diffReports(from, to);
fs.writeFileSync(argv.out, content);
}
)
.command(ipfsCmd)
.command(diffSnapshot)
.demandCommand()
.help().argv;
34 changes: 34 additions & 0 deletions src/commands/diff-snaphots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { diffReports } from "../reports/diff-reports";
import { readJsonString, readJsonFile } from "../utils/json";
import fs from "fs";

export const command = "diff-snapshots [from] [to]";

export const describe =
"diffs two json snapshots and generates a markdown report";

export const builder = (yargs) =>
yargs
.option("out", {
type: "string",
describe: "output path",
alias: "o",
})
.option("stringMode", {
type: "boolean",
describe: "assumes in/out to be string instead of files",
default: false,
});

export const handler = async function (argv) {
const from = argv.stringMode
? readJsonString(argv.from)
: readJsonFile(argv.from);
const to = argv.stringMode ? readJsonString(argv.to) : readJsonFile(argv.to);
const content = await diffReports(from, to);
if (argv.out) {
fs.writeFileSync(argv.out, content);
} else {
console.log(content);
}
};
89 changes: 89 additions & 0 deletions src/commands/ipfs-upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import fs from "fs";
import path from "path";
import Hash from "ipfs-only-hash";
import bs58 from "bs58";
import { create } from "ipfs-http-client";
import { validateAIPHeader } from "../ipfs/aip-validation";

// https://ethereum.stackexchange.com/questions/44506/ipfs-hash-algorithm
async function getHash(data: string) {
return Hash.of(data);
}

async function uploadToPinata(source: string) {
const PINATA_KEY = process.env.PINATA_KEY;
if (!PINATA_KEY) throw new Error("PINATA_KEY env must be set");
const PINATA_SECRET = process.env.PINATA_SECRET;
if (!PINATA_SECRET) throw new Error("PINATA_SECRET env must be set");
const data = new FormData();
data.append("file", new Blob([source]));
const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
method: "POST",
body: data,
headers: {
pinata_api_key: PINATA_KEY,
pinata_secret_api_key: PINATA_SECRET,
},
});

if (!res.ok) {
throw Error(await res.text());
}

const result = await res.json();

if (result.error) throw { message: result.error };
return result;
}

async function uploadToTheGraph(source: string) {
const data = new FormData();
data.append("file", new Blob([source]));
const res = await fetch("https://api.thegraph.com/ipfs/api/v0/add", {
method: "POST",
body: data,
});
return await res.json();
}

export const command = "ipfs <source>";

export const describe = "generates the ipfs hash for specified source";

export const builder = (yargs) =>
yargs
.option("upload", {
describe: "upload to ipfs",
default: false,
alias: "u",
type: "boolean",
})
.option("verbose", {
default: false,
type: "boolean",
});

export const handler = async function (argv) {
const filePath = path.join(process.cwd(), argv.source);
const content = fs.readFileSync(filePath, "utf8");
validateAIPHeader(content);

const hash = await getHash(content);
const bs58Hash = `0x${Buffer.from(bs58.decode(hash))
.slice(2)
.toString("hex")}`;

if (argv.upload) {
const [pinata, thegraph] = await Promise.all([
uploadToPinata(content),
uploadToTheGraph(content),
]);
if (argv.verbose) {
console.log("pinata response", pinata);
console.log("thegraph response", thegraph);
}
}

// log as hex to console so foundry can read the content
console.log(bs58Hash);
};
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./diffReports";
export * from "./reports/diff-reports";
21 changes: 21 additions & 0 deletions src/ipfs/aip-validation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect } from "vitest";
import { validateAIPHeader } from "./aip-validation";

describe("validateAIP", () => {
it("should succeed when all keys are present", () => {
const header = `---
title: TestTitle
discussions: TestDiscussion
author: TestAuthor
---`;
expect(validateAIPHeader(header)).toBe("TestTitle");
});

it("should throw when required key is missing", () => {
const header = `---
title: TestTitle
discussions: testDiscussion
---`;
expect(() => validateAIPHeader(header)).toThrow();
});
});
19 changes: 19 additions & 0 deletions src/ipfs/aip-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { z } from "zod";
import matter from "gray-matter";

const aipType = z.object({
discussions: z.string(),
title: z.string(),
author: z.string(),
});

/**
* Validates the aip header and returns the aip title
* @param content
* @returns string aip title
*/
export function validateAIPHeader(content: string) {
const fm = matter(content);
aipType.parse(fm.data);
return fm.data.title;
}
47 changes: 47 additions & 0 deletions src/ipfs/mocks/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Approval of BGD contribution to Aave
status: Proposed
author: BGD Labs (@bgdlabs)
shortDescription: Mandate to approve the BGD Labs onboarding on Aave
discussions: https://governance.aave.com/t/aave-bored-ghosts-developing-bgd/7527
created: 2022-05-02
---


## Summary

This proposal acts as a mandate from the Aave community to engage with BGD Labs for the technical work defined on the Aave governance forum [here](https://governance.aave.com/t/aave-bored-ghosts-developing-bgd/7527) and already pre-approved by the Aave community via Snapshot [here](https://snapshot.org/#/aave.eth/proposal/0x10e6378f193ec4a2953b3ca73b86947586676250191346a90ed4c83593f14883).

As a result of this proposal, the proposed budget will be approved to a BGD-controlled address: 40% of the total transferred upfront, and the rest only defined as a claimable stream during the following 15 months.
In order to enable this mandate, this proposal also includes what can be considered as the first development on the list of tasks proposed: the update of the ecosystem's reserve of Aave v2 Ethereum to enable stream of funds, to be used now by BGD, but also by anybody else in the future.

## Motivation

A full rationale of the proposal can be found on the Aave governance forum post [here](https://governance.aave.com/t/aave-bored-ghosts-developing-bgd/7527), but the main motivation is the onboarding of another technical provider to the decentralized Aave ecosystem.

## Specification

A full specification of the contracts update enabling this mandate can be found on the BGD Labs github [here](https://github.com/bgd-labs/aave-ecosystem-reserve-v2/tree/release/final-proposal), together will all the security procedures applied, and the implementation of the governance payload.
In summary, the proposal:
- Updates the Aave v2 Ethereum ecosystem reserve and the AAVE ecosystem reserve with a common implementation contract, with streaming capabilities.
- Sets as fundsAdmin of those ecosystem reserves a new AaveEcosystemReserveController contract, controlled directly by the Aave governance and the only entity able to transfer and approve funds, create streams, and cancel streams.
- Releases the 40% of the budget defined upfront: 1'200'000 USDC, 1'000'000 USDT, 1'000'000 DAI and 8'400 AAVE.
- Creates 15-months duration streams: 4'800'000 USDC and 12'600 AAVE.

## Security
As mandatory with any change on a running Aave smart contract, extensive security procedures have been applied to the proposal payload and the changes included. A full security report can be found [here](https://github.com/bgd-labs/aave-ecosystem-reserve-v2/tree/release/final-proposal#security), but as summary:
- Full test coverage via units tests in the Forge environment.
- Security review from Aave community members.
- Minimal changes from the Sablier's v1 logic used as base for the streaming capabilities. The original codebase is audited and battle tested (running in production with meaningful funds for a long time).
- Set of properties (formal verification) by Certora.
- Slither analysis of the codebase.

## Implementation

- [AaveEcosystemReserveController](https://etherscan.io/address/0x3d569673daa0575c936c7c67c4e6aeda69cc630c#code)
- [AaveEcosystemReserveV2](https://etherscan.io/address/0x1aa435ed226014407fa6b889e9d06c02b1a12af3#code)
- [PayloadAaveBGD](https://etherscan.io/address/0x1e12071bd95341aa92fcba1513c714f9f49282a4#code)

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
10 changes: 5 additions & 5 deletions src/diffReports.ts → src/reports/diff-reports.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import hash from "object-hash";
import { fetchRateStrategyImage } from "./fetchIRStrategy";
import { renderReserve, renderReserveDiff } from "./renderer/reserve";
import { renderStrategy, renderStrategyDiff } from "./renderer/strategy";
import { AaveV3Snapshot, AaveV3Reserve } from "./types";
import { diff } from "./utils/diff";
import { fetchRateStrategyImage } from "./fetch-IR-strategy";
import { renderReserve, renderReserveDiff } from "./reserve";
import { renderStrategy, renderStrategyDiff } from "./strategy";
import { AaveV3Snapshot, AaveV3Reserve } from "./snapshot-types";
import { diff } from "./diff";

export async function diffReports<
A extends AaveV3Snapshot,
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions src/renderer/reserve.ts → src/reports/reserve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { formatUnits } from "viem";
import { AaveV3Reserve, CHAIN_ID } from "../types";
import { AaveV3Reserve, CHAIN_ID } from "./snapshot-types";

export const getBlockExplorerLink: {
[key in CHAIN_ID]: (address: string) => string;
Expand All @@ -13,7 +13,7 @@ export const getBlockExplorerLink: {
[CHAIN_ID.FANTOM]: (address) =>
`[${address}](https://ftmscan.com/address/${address})`,
[CHAIN_ID.ARBITRUM]: (address) =>
`[${address}](https://https://arbiscan.io/address/${address})`,
`[${address}](https://arbiscan.io/address/${address})`,
[CHAIN_ID.AVALANCHE]: (address) =>
`[${address}](https://snowtrace.io/address/${address})`,
[CHAIN_ID.METIS]: (address) =>
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/renderer/strategy.ts → src/reports/strategy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AaveV3Strategy } from "../types";
import { AaveV3Strategy } from "./snapshot-types";
import { formatUnits } from "viem";

export function renderStrategyValue<T extends keyof AaveV3Strategy>(
Expand Down
12 changes: 10 additions & 2 deletions src/utils/json.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
/**
* While javascript supports bigint, JSON.parse doesn't
* Therefore this file contains utilities to parse JSON containing bigint
*/
import JSONbig from "json-bigint";
import fs from "fs";
import path from "path";

export function readJson(filePath: string) {
const content = fs.readFileSync(path.join(process.cwd(), filePath));
export function readJsonString(content: string) {
return JSON.parse(
JSON.stringify(JSONbig({ storeAsString: true }).parse(content))
);
}

export function readJsonFile(filePath: string) {
const content = fs.readFileSync(path.join(process.cwd(), filePath), "utf8");
return readJsonString(content);
}
Loading