Skip to content
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
24 changes: 12 additions & 12 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ jobs:
echo "Head SHA: $HEAD_SHA"

# Get changed files, filtering out deleted files and files in ignored directories
CHANGED_TS_JS=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.(js|ts|jsx|tsx|cjs|mjs)$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true)
CHANGED_SOL=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.sol$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts|/test/)' || true)
CHANGED_MD=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.md$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true)
CHANGED_JSON=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.json$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true)
CHANGED_YAML=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.(yml|yaml)$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true)
CHANGED_TS_JS=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.(js|ts|jsx|tsx|cjs|mjs)$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' | grep -v '^packages/billing/' || true)
CHANGED_SOL=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.sol$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts|/test/)' | grep -v '^packages/billing/' || true)
CHANGED_MD=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.md$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' | grep -v '^packages/billing/' || true)
CHANGED_JSON=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.json$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' | grep -v '^packages/billing/' || true)
CHANGED_YAML=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.(yml|yaml)$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' | grep -v '^packages/billing/' || true)

# Save to files for later use
echo "$CHANGED_TS_JS" > changed_ts_js.txt
Expand Down Expand Up @@ -104,10 +104,10 @@ jobs:
run: |
if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then
echo "Linting all Solidity files in each package..."
# Run solhint in each workspace package that has contracts (check mode)
pnpm -r exec bash -c 'if [ -d "contracts" ]; then npx solhint --max-warnings=0 --noPrompt --noPoster "contracts/**/*.sol" 2>/dev/null || exit 1; fi'
# Run solhint in each workspace package that has contracts (check mode), skipping imported billing-contracts
pnpm -r --filter '!./packages/billing' exec bash -c 'if [ -d "contracts" ]; then npx solhint --max-warnings=0 --noPrompt --noPoster "contracts/**/*.sol" 2>/dev/null || exit 1; fi'
# Check formatting with prettier
SOL_FILES=$(git ls-files '*.sol')
SOL_FILES=$(git ls-files '*.sol' ':!packages/billing/**')
if [ -n "$SOL_FILES" ]; then
echo "$SOL_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn
else
Expand Down Expand Up @@ -148,7 +148,7 @@ jobs:
run: |
if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then
echo "Linting all TypeScript/JavaScript files..."
TS_FILES=$(git ls-files '*.js' '*.ts' '*.cjs' '*.mjs' '*.jsx' '*.tsx')
TS_FILES=$(git ls-files '*.js' '*.ts' '*.cjs' '*.mjs' '*.jsx' '*.tsx' ':!packages/billing/**')
if [ -n "$TS_FILES" ]; then
echo "$TS_FILES" | tr '\n' '\0' | xargs -0 npx eslint --max-warnings=0 --no-warn-ignored
echo "$TS_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern
Expand All @@ -169,7 +169,7 @@ jobs:
run: |
if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then
echo "Linting all Markdown files..."
MD_FILES=$(git ls-files '*.md')
MD_FILES=$(git ls-files '*.md' ':!packages/billing/**')
if [ -n "$MD_FILES" ]; then
echo "$MD_FILES" | tr '\n' '\0' | xargs -0 npx markdownlint --ignore-path .gitignore --ignore-path .markdownlintignore
echo "$MD_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern
Expand All @@ -191,7 +191,7 @@ jobs:
if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then
echo "Linting all JSON files..."
# Exclude Ignition deployment artifacts and other build artifacts
JSON_FILES=$(git ls-files '*.json' | { grep -v -E '(ignition/deployments/.*/artifacts/|ignition/deployments/.*/build-info/|/\.openzeppelin/|deployments/.*/solcInputs/)' || true; })
JSON_FILES=$(git ls-files '*.json' ':!packages/billing/**' | { grep -v -E '(ignition/deployments/.*/artifacts/|ignition/deployments/.*/build-info/|/\.openzeppelin/|deployments/.*/solcInputs/)' || true; })
if [ -n "$JSON_FILES" ]; then
echo "$JSON_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern
else
Expand All @@ -215,7 +215,7 @@ jobs:
run: |
if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then
echo "Linting all YAML files..."
YAML_FILES=$(git ls-files '*.yml' '*.yaml')
YAML_FILES=$(git ls-files '*.yml' '*.yaml' ':!packages/billing/**')
if [ -n "$YAML_FILES" ]; then
echo "$YAML_FILES" | tr '\n' '\0' | xargs -0 npx yaml-lint
echo "$YAML_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern
Expand Down
3 changes: 3 additions & 0 deletions .markdownlintignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ deployments/hardhat/
**/*-localhost/
**/*-hardhat/
.changeset/

# Imported billing-contracts, must stay byte-identical
packages/billing/
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@ deployments/hardhat/
# Verified mainnet source, must stay byte-identical
packages/legacy/verified/

# Imported billing-contracts, must stay byte-identical
packages/billing/

# Pre-port artifacts, must stay byte-identical
packages/*/test/fixtures/pre-port/
1 change: 1 addition & 0 deletions .solhintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
packages/legacy/verified/
packages/billing/
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ pnpm lint:yaml # YAML files
- Test fixtures
- Hardhat extensions

6. **billing** - Graph Studio billing contracts (Billing, BillingConnector, BanxaWrapper), private, Hardhat 2 with ethers v5

### Key Architectural Patterns

- **Proxy Upgradeable Pattern**: Most contracts use OpenZeppelin's upgradeable proxy pattern
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ This repository is a pnpm workspaces monorepo containing the following packages:

| Package | Latest version | Description |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| [billing](./packages/billing) | Private, not published | Graph Studio billing contracts: Billing, BillingConnector and BanxaWrapper. |
| [data-edge](./packages/data-edge) | [![npm version](https://badge.fury.io/js/@graphprotocol%2Fdata-edge.svg)](https://badge.fury.io/js/@graphprotocol%2Fdata-edge) | Data edge testing and utilities for The Graph protocol. |
| [hardhat-graph-protocol](./packages/hardhat-graph-protocol) | [![npm version](https://badge.fury.io/js/hardhat-graph-protocol.svg)](https://badge.fury.io/js/hardhat-graph-protocol) | A Hardhat plugin that extends the runtime environment with functionality for The Graph protocol. |
| [horizon](./packages/horizon) | [![npm version](https://badge.fury.io/js/@graphprotocol%2Fhorizon.svg)](https://badge.fury.io/js/@graphprotocol%2Fhorizon) | Contracts for Graph Horizon, the next iteration of The Graph protocol. |
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ const eslintConfig = [
// Autogenerated GraphClient files (committed but should not be linted)
'**/.graphclient-extracted/**',
'**/.graphclient/**',
// Imported billing-contracts, must stay byte-identical
'packages/billing/**',
],
},

Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"lint:ts": "eslint --fix --cache 'packages/**/*.{js,ts,cjs,mjs,jsx,tsx}' 'scripts/**/*.{js,ts,cjs,mjs}' '*.{js,ts,cjs,mjs}'; prettier -w --cache --log-level warn 'packages/**/*.{js,ts,cjs,mjs,jsx,tsx}' 'scripts/**/*.{js,ts,cjs,mjs}' '*.{js,ts,cjs,mjs}'",
"lint:sol": "pnpm -r run lint:sol; prettier -w --cache --log-level warn 'packages/**/*.sol'; pnpm todo",
"lint:forge": "pnpm -r run lint:forge",
"lint:md": "markdownlint --fix --ignore-path .gitignore 'packages/**/*.md' 'docs/**/*.md' '*.md'; prettier -w --cache --log-level warn 'packages/**/*.md' 'docs/**/*.md' '*.md'",
"lint:md": "markdownlint --fix --ignore-path .gitignore --ignore 'packages/billing/**' 'packages/**/*.md' 'docs/**/*.md' '*.md'; prettier -w --cache --log-level warn 'packages/**/*.md' 'docs/**/*.md' '*.md'",
"lint:json": "prettier -w --cache --log-level warn 'packages/**/*.{json,json5}' '.changeset/**/*.json' '*.{json,json5}'",
"lint:yaml": "npx yaml-lint .github/**/*.{yml,yaml}; prettier -w --cache --log-level warn 'packages/**/*.{yml,yaml}' '.github/**/*.{yml,yaml}'",
"test": "pnpm build && pnpm -r run test:self",
Expand Down Expand Up @@ -82,13 +82,13 @@
"scripts/lint-staged-run.sh 'prettier -w --cache --log-level warn'"
],
"*.md": [
"markdownlint --fix",
"prettier -w --cache --log-level warn"
"scripts/lint-staged-run.sh 'markdownlint --fix'",
"scripts/lint-staged-run.sh 'prettier -w --cache --log-level warn'"
],
"*.{json,json5}": "prettier -w --cache --log-level warn",
"*.{json,json5}": "scripts/lint-staged-run.sh 'prettier -w --cache --log-level warn'",
"*.{yml,yaml}": [
"npx yamllint",
"prettier -w --cache --log-level warn"
"scripts/lint-staged-run.sh 'npx yamllint'",
"scripts/lint-staged-run.sh 'prettier -w --cache --log-level warn'"
]
}
}
12 changes: 12 additions & 0 deletions packages/billing/.example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
INFURA_KEY=
MNEMONIC=""
L1_MNEMONIC=""
L2_MNEMONIC=""
L1_CHAINID=5
L2_CHAINID=421613
PRIVATE_KEY=""
ETHERSCAN_API_KEY=
ARBISCAN_API_KEY=
POLYGON_RPC_URL=
ARBITRUM_RPC_URL=
ADDRESS_BOOK="addresses.json"
1 change: 1 addition & 0 deletions packages/billing/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sol linguist-language=Solidity
26 changes: 26 additions & 0 deletions packages/billing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Ignore node stuff
node_modules/
yarn-error.log

# Ignore build stuff
cache/
build/
dist/

# Hardhat cache
cached/

# Ignore solc bin output
bin/

# Others
.env
.DS_Store
.vscode

# Coverage and other reports
/reports
coverage.json

tasks/ops/depositors.json
tasks/ops/depositors.json.bak
10 changes: 10 additions & 0 deletions packages/billing/.solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const skipFiles = ['tests/', 'arbitrum/']

module.exports = {
providerOptions: {
mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect',
network_id: 1337,
},
skipFiles,
istanbulFolder: './reports/coverage',
}
7 changes: 7 additions & 0 deletions packages/billing/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2020 Edge & Node

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
127 changes: 127 additions & 0 deletions packages/billing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Billing Contract

This repository contains a Billing contract. It allows users to add tokens (GRT) into the contract for an agent to pull when a billing period is up.

## Contract Design

The contract is designed with the following requirements:

- The contract is owned by the `governor`.
- There is a privileged role named `collector` that has a special permission to pull user funds (this is, for now, the Gateway that acts as intermediary between Consumers and Indexers in The Graph Network).
- Users add tokens into the contract by calling `add()`. It is important to note that the contract
is designed so that the users are trusting the Collector. Once the user adds tokens, the Collector can
pull the funds whenever it wants.
- The trust risk for the User is that the Collector would pull funds before the User spent their funds, which are query fees in The Graph Network.
- The trust risk for the Collector is that the user adds funds, spends them on queries, and then
removes their tokens before the Collector pulled the tokens.
- These combined trust risks make for a situation that we expect both parties to act responsibly.
It will always be recommended to keep the amount of GRT in the contract low for each user, and for
the Collector to pull regularly. This way, if one side does not play nice, the funds lost won't be
that large.

## L1-L2 communication

The Billing contract is designed to be deployed to an L2 (Arbitrum). A BillingConnector contract can be deployed to L1 (mainnet), and allows users to add funds to the Billing contract using a token gateway that allows callhooks as described in [GIP-0031](https://forum.thegraph.com/t/gip-0031-arbitrum-grt-bridge/3305)

To add tokens from L1 to L2, there are two options:

a) If the user's address is an EOA, they can use [BillingConnector.addToL2WithPermit](./contracts/IBillingConnector.sol#L59-L81). They first need to sign a permit for the BillingConnector contract to pull the GRT, and then run the tx including that signature, which will send those GRT to the billing balance for that same address in L2.

b) If the user's address is a contract (e.g. a multisig), or they don't want to sign a permit, then they can use [BillingConnector.addToL2](./contracts/IBillingConnector.sol#L24-L39), but they need to run `GraphToken.approve(billingConnector.address, amount)` beforehand.

To estimate the L2 submission and gas parameters (`_maxGas`, `_gasPriceBid`, and `_maxSubmissionCost`), we recommend using the [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk). An example for this is available in the [graphprotocol/contracts](https://github.com/graphprotocol/contracts/blob/pcv/l2-bridge/cli/commands/bridge/to-l2.ts#L63-L94) repo - just replace the calldata with the [calldata produced by BillingConnector](./test/billingConnector.test.ts#L277-L284)

To remove tokens on L2, users that have an EOA, or whose account exists in L2 with the same address as L1, can call `Billing.remove` directly on L2 to remove funds to an L2 address. Users whose address doesn't exist in L2 (e.g. when using a multisig), can instead use [BillingConnector.removeOnL2](./contracts/IBillingConnector.sol#L41-L57) to send a message from L1 that will tell the Billing contract to move the tokens to the desired address. There is no validation of the amount actually existing on the L2 balance, and guarantee that the message will arrive before funds are pulled by the Collector, so the message might revert on L2 if the balance is insufficient. Note that this call will also require estimating L2 gas parameters. In this case, the retryable ticket is created directly by the BillingConnector (instead of by the L1GraphTokenGateway), so keep this in mind when estimating gas with the Arbitrum SDK.

## Testing

Run the test suite with:

```bash
yarn test
```

## Using the console

Here is how to use hardhat to quickly do some transactions:

```bash
hh console --network polygon

accounts = await ethers.getSigners()
token = await hre.contracts.Token.connect(accounts[0])
await token.approve('0x5DE9A13C486f5aA12F4D8e5E77246F6E24dac274', '1000000000000000000000')

billing = await hre.contracts.Billing.connect(accounts[0])
await billing.add('1000000000000000000')
```

## Deploy instructions

To deploy, see these instructions:

### Billing
Use arbitrum-one (or arbitrum-goerli) to deploy Billing:

```
hh deploy-billing --network arbitrum-one \
--collector <COLLECTOR_ADDRESS> \
--token <GRT_ADDRESS> \
--governor <GOVERNOR_ADDRESS> \
--tokengateway <L2GRAPHTOKENGATEWAY_ADDRESS>
```

Then run this to verify on Etherscan:

```
hh verify --network arbitrum-one \
<NEW_DEPLOYED_ADDRESS> \
<COLLECTOR_ADDRESS> \
<GRT_ADDRESS> \
<GOVERNOR_ADDRESS> \
<L2GRAPHTOKENGATEWAY_ADDRESS>
```

### BillingConnector
Use mainnet (or goerli) to deploy BillingConnector:

```
hh deploy-billing-connector --network mainnet \
--tokengateway <L1GRAPHTOKENGATEWAY_ADDRESS> \
--billing <L2_BILLING_ADDRESS> \
--token <GRT_ADDRESS> \
--governor <GOVERNOR_ADDRESS> \
--inbox <ARBITRUM_INBOX_ADDRESS>
```

Then run this to verify on Etherscan:

```
hh verify --network mainnet \
<NEW_DEPLOYED_ADDRESS> \
<L1GRAPHTOKENGATEWAY_ADDRESS> \
<L2_BILLING_ADDRESS> \
<GRT_ADDRESS> \
<GOVERNOR_ADDRESS> \
<ARBITRUM_INBOX_ADDRESS>
```

### BanxaWrapper
Use arbitrum-one (or arbitrum-goerli) to deploy BanxaConnector:

```
hh deploy-banxa --network arbitrum-one \
--token <GRT_ADDRESS> \
--billing <BILLING_ADDRESS> \
--governor <GOVERNOR_ADDRESS>
```

Then run this to verify on Etherscan:

```
hh verify --network arbitrum-one \
<NEW_DEPLOYED_ADDRESS> \
<GRT_ADDRESS> \
<BILLING_ADDRESS> \
<GOVERNOR_ADDRESS>
```
16 changes: 16 additions & 0 deletions packages/billing/addresses-staging.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"421614": {
"L2GraphToken": "0xf8c05dCF59E8B28BFD5eed176C562bEbcfc7Ac04",
"L2GraphTokenGateway": "0xB24Ce0f8c18c4DdDa584A7EeC132F49C966813bb",
"collector": "0x66FFdb62688deA21A0031Db7CD4C219C82e9B315",
"governor": "0x97B47D976b9ddD2608D61E77Ab10DAf05d17cbF9",
"Billing": "0x1AbcE187d2401687520dBf720210a0ec84C0690D"
},
"11155111": {
"ArbitrumInbox": "0xaAe29B0366299461418F5324a79Afc425BE5ae21",
"GraphToken": "0xCA59cCeb39bE1808d7aA607153f4A5062daF3a83",
"L1GraphTokenGateway": "0x549DCB0b510Ee7C8d6E8e346ab9aC75E22DD78B3",
"governor": "0x20cFA0e7Db2ECd14032784075A4c686befc76758",
"BillingConnector": "0xf9B9FCf0d603e6bead3335953EB5Bcf876990043"
}
}
Loading
Loading