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
8 changes: 7 additions & 1 deletion the-guild-smart-contracts/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ BASE_SEPOLIA_URL=https://base-sepolia.therpc.io
ETHERSCAN_API_KEY=

# For deployment
PRIVATE_KEY=
PRIVATE_KEY=

# Batch attestation script
JSON_PATH=./attestations.json
SCHEMA_ID=0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a47de
DRY_RUN=false
RPC_URL=
75 changes: 75 additions & 0 deletions the-guild-smart-contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,81 @@ forge script script/TheGuildBadgeRanking.s.sol:TheGuildBadgeRankingScript \
--broadcast
```

### Batch Attestations

The `EmitAttestationsCsv.s.sol` script allows batch creation of attestations from JSON data using EAS's `multiAttest()` function for gas efficiency.

#### JSON Format

Prepare your attestations data in JSON format:
```json
{
"attestations": [
{
"recipient": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"badgeName": "Rust",
"justification": "Outstanding Rust contributions to the project"
},
{
"recipient": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"badgeName": "Solidity",
"justification": "Excellent Solidity smart contract development"
}
]
}
```

- `recipient`: Ethereum address of the recipient
- `badgeName`: Name of the badge to attest (will be converted to bytes32)
- `justification`: Text explanation or link justifying the attestation

#### Usage

```shell
# Using the helper script (recommended)
export PRIVATE_KEY=your_private_key
export RPC_URL=https://polygon-amoy.drpc.org

# Dry run first
./run_batch_attestations.sh attestations.json true

# Production run
./run_batch_attestations.sh attestations.json false

# Manual approach
# Set environment variables
export PRIVATE_KEY=your_private_key
export JSON_PATH=./attestations.json
export SCHEMA_ID=0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a47de

# Dry run (recommended first)
export DRY_RUN=true
forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \
--rpc-url <your_rpc_url>

# Production run
unset DRY_RUN
forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \
--rpc-url <your_rpc_url> \
--broadcast
```

#### Environment Variables

- `PRIVATE_KEY`: Private key for transaction signing
- `JSON_PATH`: Path to the JSON file (default: `./attestations.json`)
- `SCHEMA_ID`: EAS schema ID to use (default: Amoy production schema)
- `DRY_RUN`: Set to `true` for validation without broadcasting (default: `false`)
- `EAS_ADDRESS`: EAS contract address (auto-detected for Amoy/Base Sepolia, required for other networks)
- `RPC_URL`: RPC endpoint URL (for helper script)

The script will:
1. Read the JSON file directly using `vm.readFile()`
2. Parse and validate all attestations
3. Create `MultiAttestationRequest` batches (max 50 per batch)
4. Call EAS `multiAttest()` for gas-efficient batch processing
5. Log all attestation UIDs upon completion

### Cast

```shell
Expand Down
14 changes: 14 additions & 0 deletions the-guild-smart-contracts/attestations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"attestations": [
{
"recipient": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"badgeName": "Rust",
"justification": "Awarded for outstanding Rust contributions"
},
{
"recipient": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
"badgeName": "Solidity",
"justification": "Solidity development excellence"
}
]
}
2 changes: 2 additions & 0 deletions the-guild-smart-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ libs = ["lib"]
coverage = true
# Exclude scripts, libs, and tests from coverage
no_match_coverage = "^(script/|lib/|test/)"
# Allow file reading for scripts
fs_permissions = [{ access = "read", path = "./" }]

# Deterministic deployments settings (see: https://getfoundry.sh/guides/deterministic-deployments-using-create2)
solc = "0.8.28"
Expand Down
59 changes: 59 additions & 0 deletions the-guild-smart-contracts/run_batch_attestations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash

# Helper script for running TheGuild batch attestation script
# Usage: ./run_batch_attestations.sh <json_file> [dry_run]

set -e

# Source .env file if it exists
if [ -f .env ]; then
source .env
fi

if [ $# -lt 1 ]; then
echo "Usage: $0 <json_file> [dry_run]"
echo " json_file: Path to JSON file with attestations"
echo " dry_run: Set to 'true' for dry run (default: false)"
exit 1
fi

JSON_FILE="$1"
DRY_RUN="${2:-false}"

if [ ! -f "$JSON_FILE" ]; then
echo "Error: JSON file '$JSON_FILE' not found"
exit 1
fi

# Set JSON file path
export JSON_PATH="$JSON_FILE"

# Set dry run mode
if [ "$DRY_RUN" = "true" ]; then
export DRY_RUN=true
echo "Running in DRY RUN mode..."
else
unset DRY_RUN
echo "Running in PRODUCTION mode..."
fi

# Check for required environment variables
if [ -z "$PRIVATE_KEY" ]; then
echo "Error: PRIVATE_KEY environment variable not set"
exit 1
fi

if [ -z "$RPC_URL" ]; then
echo "Error: RPC_URL environment variable not set"
exit 1
fi

# Run the script
if [ "$DRY_RUN" = "true" ]; then
forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \
--rpc-url "$RPC_URL"
else
forge script script/EmitAttestationsCsv.s.sol:EmitAttestationsCsv \
--rpc-url "$RPC_URL" \
--broadcast
fi
202 changes: 202 additions & 0 deletions the-guild-smart-contracts/script/EmitAttestationsCsv.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Script, stdJson} from "forge-std/Script.sol";
import {EAS} from "eas-contracts/EAS.sol";
import {AttestationRequestData, MultiAttestationRequest} from "eas-contracts/IEAS.sol";
import {EASUtils} from "./utils/EASUtils.s.sol";
import {console} from "forge-std/console.sol";

contract EmitAttestationsCsv is Script {
using stdJson for string;

// Configuration constants
uint256 constant MAX_BATCH_SIZE = 50; // Limit batch size to prevent gas issues
uint256 constant MAX_ATTESTATIONS = 1000; // Reasonable limit for processing

struct AttestationData {
address recipient;
bytes32 badgeName;
bytes justification;
}

function run() public {
bool isDryRun = vm.envOr("DRY_RUN", false);

console.log("=== TheGuild Batch Attestation Script ===");
console.log("Dry run mode:", isDryRun ? "ENABLED" : "DISABLED");

// Get EAS address for current network
EAS eas = EAS(EASUtils.getEASAddress(vm));
console.log("EAS Address:", address(eas));

// Read and validate JSON file
string memory jsonPath = vm.envOr("JSON_PATH", string("attestations.json"));
console.log("Reading JSON from:", jsonPath);

string memory jsonData = vm.readFile(jsonPath);
AttestationData[] memory attestations = parseAndValidateJson(jsonData);

console.log(string(abi.encodePacked("Parsed ", vm.toString(attestations.length), " attestations from JSON")));

// Get schema ID
bytes32 schemaId = vm.envOr("SCHEMA_ID", bytes32(0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a47de));
console.log("Using Schema ID:", vm.toString(schemaId));

// Create batch requests
MultiAttestationRequest[] memory requests = createBatchRequests(attestations, schemaId);

if (isDryRun) {
console.log(string(abi.encodePacked("DRY RUN: Would process ", vm.toString(requests.length), " batches")));
for (uint256 i = 0; i < requests.length; i++) {
console.log(string(abi.encodePacked("Batch ", vm.toString(i), ": ", vm.toString(requests[i].data.length), " attestations")));
}
console.log("Dry run completed successfully!");
return;
}

// Execute attestations
executeAttestations(eas, requests);
}

function parseAndValidateJson(string memory jsonData) internal view returns (AttestationData[] memory) {
// JSON parseJson returns data with types inferred from JSON - addresses as address, strings as string
console.log("Parsing JSON attestations array...");

// Count attestations - parse the full array to get length
// We'll use a temporary storage to collect attestations
AttestationData[] memory tempAttestations = new AttestationData[](MAX_ATTESTATIONS);
uint256 count = 0;

// Parse attestations until we run out
for (uint256 i = 0; i < MAX_ATTESTATIONS; i++) {
string memory basePath = string(abi.encodePacked(".attestations[", vm.toString(i), "]"));

// Try to parse this attestation - if it doesn't exist, parseJson will revert
bytes memory testPath = vm.parseJson(jsonData, string(abi.encodePacked(basePath, ".recipient")));
if (testPath.length == 0) break; // No more attestations

// Parse badgeName with proper bytes32 conversion
string memory badgeNameStr = abi.decode(
vm.parseJson(jsonData, string(abi.encodePacked(basePath, ".badgeName"))),
(string)
);
bytes32 badgeName;
bytes memory badgeNameBytes = bytes(badgeNameStr);
require(badgeNameBytes.length <= 32, "badgeName too long");
assembly {
badgeName := mload(add(badgeNameBytes, 32))
}

bytes memory justification = abi.decode(
vm.parseJson(jsonData, string(abi.encodePacked(basePath, ".justification"))),
(bytes)
);

tempAttestations[count] = AttestationData({
recipient: abi.decode(testPath, (address)),
badgeName: badgeName,
justification: justification
});

count++;
}

// Copy to properly sized array
AttestationData[] memory attestations = new AttestationData[](count);
for (uint256 i = 0; i < count; i++) {
attestations[i] = tempAttestations[i];
}

console.log("Found", attestations.length, "attestations in JSON");

require(attestations.length > 0, "JSON must contain at least 1 attestation");
require(attestations.length <= MAX_ATTESTATIONS, "Too many attestations, max 1000 allowed");

// Validate each attestation
for (uint256 i = 0; i < attestations.length; i++) {
require(attestations[i].recipient != address(0),
string(abi.encodePacked("Attestation ", vm.toString(i), ": invalid recipient address")));
require(attestations[i].badgeName != bytes32(0),
string(abi.encodePacked("Attestation ", vm.toString(i), ": badgeName cannot be empty")));
}

return attestations;
}

function createBatchRequests(
AttestationData[] memory attestations,
bytes32 schemaId
) internal pure returns (MultiAttestationRequest[] memory) {
uint256 totalBatches = (attestations.length + MAX_BATCH_SIZE - 1) / MAX_BATCH_SIZE;
MultiAttestationRequest[] memory requests = new MultiAttestationRequest[](totalBatches);

for (uint256 batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
uint256 startIdx = batchIndex * MAX_BATCH_SIZE;
uint256 endIdx = startIdx + MAX_BATCH_SIZE;
if (endIdx > attestations.length) endIdx = attestations.length;

uint256 batchSize = endIdx - startIdx;
requests[batchIndex].schema = schemaId;
requests[batchIndex].data = new AttestationRequestData[](batchSize);

for (uint256 i = 0; i < batchSize; i++) {
AttestationData memory att = attestations[startIdx + i];
requests[batchIndex].data[i] = AttestationRequestData({
recipient: att.recipient,
expirationTime: 0, // No expiration
revocable: true,
refUID: bytes32(0), // No reference
data: abi.encode(att.badgeName, att.justification),
value: 0 // No ETH value
});
}
}

return requests;
}

function executeAttestations(EAS eas, MultiAttestationRequest[] memory requests) internal {
uint256 pk = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(pk);

console.log("Starting batch attestation execution...");

uint256 totalAttestations = 0;
for (uint256 batchIndex = 0; batchIndex < requests.length; batchIndex++) {
console.log(string(abi.encodePacked("Processing batch ", vm.toString(batchIndex + 1), "/", vm.toString(requests.length), " - ", vm.toString(requests[batchIndex].data.length), " attestations")));

// Create single-element array for this batch
MultiAttestationRequest[] memory singleBatch = new MultiAttestationRequest[](1);
singleBatch[0] = requests[batchIndex];

try eas.multiAttest(singleBatch) returns (bytes32[] memory uids) {
console.log(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " completed with ", vm.toString(uids.length), " attestations")));
totalAttestations += uids.length;

// Log first few UIDs
for (uint256 i = 0; i < uids.length && i < 3; i++) {
console.log(string(abi.encodePacked(" UID ", vm.toString(i), ": ", vm.toString(uids[i]))));
}
if (uids.length > 3) {
console.log(string(abi.encodePacked(" ... and ", vm.toString(uids.length - 3), " more")));
}
} catch Error(string memory reason) {
console.log(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed: ", reason)));
revert(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed: ", reason)));
} catch {
console.log(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed with unknown error")));
revert(string(abi.encodePacked("Batch ", vm.toString(batchIndex + 1), " failed with unknown error")));
}
}

vm.stopBroadcast();

console.log("=== Attestation Summary ===");
console.log("Total batches processed:", requests.length);
console.log("Total attestations created:", totalAttestations);
console.log("Execution completed successfully!");
}


}
Loading
Loading