diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..54d3ac0 --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,38 @@ +IdentityTokenTest:test_DeleteAttribute() (gas: 104049) +IdentityTokenTest:test_DeleteAttribute_EmitsEvent() (gas: 103782) +IdentityTokenTest:test_DeleteAttribute_NeverSet_DoesNotRevert() (gas: 96220) +IdentityTokenTest:test_DeleteAttribute_ThenReSet() (gas: 133002) +IdentityTokenTest:test_DeleteAttribute_Twice_DoesNotRevert() (gas: 106037) +IdentityTokenTest:test_Endorse() (gas: 280823) +IdentityTokenTest:test_GetAttribute() (gas: 121241) +IdentityTokenTest:test_GetAttribute_MatchesRawMapping() (gas: 123833) +IdentityTokenTest:test_GetIdentityByOwner_ReturnsEmptyIfNoToken() (gas: 10630) +IdentityTokenTest:test_GetIdentityByOwner_ReturnsTokenId() (gas: 87786) +IdentityTokenTest:test_GetIdentity_EndorsementCountUpdates() (gas: 269741) +IdentityTokenTest:test_GetIdentity_ReturnsCorrectFields() (gas: 98926) +IdentityTokenTest:test_GetIdentity_RevertsForNonexistentToken() (gas: 12336) +IdentityTokenTest:test_HasIdentity_False() (gas: 10198) +IdentityTokenTest:test_HasIdentity_True() (gas: 86907) +IdentityTokenTest:test_IsExpired_FalseBeforeExpiry() (gas: 261330) +IdentityTokenTest:test_IsExpired_FalseWhenNoValidUntil() (gas: 88627) +IdentityTokenTest:test_IsExpired_TrueAfterExpiry() (gas: 261752) +IdentityTokenTest:test_IsVerified_FalseWithExpiredEndorsement() (gas: 281042) +IdentityTokenTest:test_IsVerified_FalseWithNoEndorsements() (gas: 88627) +IdentityTokenTest:test_IsVerified_TrueWithActiveEndorsement() (gas: 260278) +IdentityTokenTest:test_Mint() (gas: 88284) +IdentityTokenTest:test_OverwriteAttribute() (gas: 129040) +IdentityTokenTest:test_RevertIf_BatchLengthMismatch() (gas: 92319) +IdentityTokenTest:test_RevertIf_CompromisedIdentityDeletesAttribute() (gas: 325685) +IdentityTokenTest:test_RevertIf_NotOwnerBatchSetsAttribute() (gas: 91542) +IdentityTokenTest:test_RevertIf_NotOwnerDeletesAttribute() (gas: 121538) +IdentityTokenTest:test_RevertIf_NotOwnerSetsAttribute() (gas: 90472) +IdentityTokenTest:test_RevertIf_NotOwnerUsesSetName() (gas: 89907) +IdentityTokenTest:test_SchemaConstants() (gas: 3090) +IdentityTokenTest:test_SetAttribute() (gas: 120877) +IdentityTokenTest:test_SetAttribute_EmptyValue() (gas: 99664) +IdentityTokenTest:test_SetAttribute_LongURL() (gas: 189521) +IdentityTokenTest:test_SetAttribute_SocialLinks() (gas: 188857) +IdentityTokenTest:test_SetAttributesBatch() (gas: 220621) +IdentityTokenTest:test_SetAttributesBatch_SingleEntry() (gas: 122819) +IdentityTokenTest:test_SetGithub() (gas: 120296) +IdentityTokenTest:test_SetName() (gas: 120329) diff --git a/.github/workflows/4naly3er.yml b/.github/workflows/4naly3er.yml new file mode 100644 index 0000000..eec8089 --- /dev/null +++ b/.github/workflows/4naly3er.yml @@ -0,0 +1,35 @@ +name: 4naly3er Report +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +jobs: + analyzer_4naly3er: + name: 4naly3er Gas Optimization Report + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Install 4naly3er + run: | + git clone https://github.com/Picodes/4naly3er + cd 4naly3er + rm -f src/issues/NC/uselessOverride.ts || true + corepack enable + yarn install + - name: Run 4naly3er on src/ + run: | + cd 4naly3er + yarn analyze ../src ../4naly3er-report.md + - name: Upload 4naly3er report + uses: actions/upload-artifact@v4 + with: + name: 4naly3er-report + path: 4naly3er-report.md diff --git a/.github/workflows/abi-diff.yml b/.github/workflows/abi-diff.yml new file mode 100644 index 0000000..98c11db --- /dev/null +++ b/.github/workflows/abi-diff.yml @@ -0,0 +1,65 @@ +name: ABI Diff Check + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + FOUNDRY_PROFILE: ci + +jobs: + abi-diff: + name: ABI Diff Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build contracts + run: forge build + + - name: Generate ABIs and diff against baseline + run: | + mkdir -p .abi-current + CHANGED=0 + + if [ ! -d .abi-baselines ]; then + echo "No .abi-baselines directory. Skipping check." + exit 0 + fi + + # Derive contract names from existing baselines to avoid placeholder drift. + mapfile -t CONTRACTS < <(find .abi-baselines -type f -name '*.json' -exec basename {} .json \;) + if [ "${#CONTRACTS[@]}" -eq 0 ]; then + echo "No ABI baselines found in .abi-baselines/. Skipping ABI diff." + exit 0 + fi + + for contract in "${CONTRACTS[@]}"; do + if ! forge inspect "$contract" abi > ".abi-current/${contract}.json" 2>/dev/null; then + echo "::error::Failed to generate ABI for $contract" + CHANGED=1 + continue + fi + + baseline=".abi-baselines/${contract}.json" + if [ -f "$baseline" ]; then + if ! diff -u "$baseline" ".abi-current/${contract}.json"; then + echo "❌ ABI changed for $contract — this may be a breaking change!" + CHANGED=1 + fi + else + echo "::error::No ABI baseline for $contract. Add .abi-baselines/${contract}.json" + CHANGED=1 + fi + done + + if [ "$CHANGED" -eq 1 ]; then + exit 1 + fi diff --git a/.github/workflows/contract-size.yml b/.github/workflows/contract-size.yml new file mode 100644 index 0000000..c20ea63 --- /dev/null +++ b/.github/workflows/contract-size.yml @@ -0,0 +1,37 @@ +name: Contract Size Check +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +env: + FOUNDRY_PROFILE: ci +jobs: + contract-size: + name: Contract Size Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Build and check contract sizes + run: | + forge build --sizes 2>&1 | tee sizes.txt + # Fail if any contract is >= 23616 bytes (warn zone before 24KB EIP-170 limit) + if awk -F '[|]' ' + BEGIN { found = 0 } + /^\s*[|]/ && $0 !~ /Contract/ { + deployed = $3 + gsub(/[,[:space:]]/, "", deployed) + if (deployed ~ /^[0-9]+$/ && deployed + 0 >= 23616) { + print + found = 1 + } + } + END { exit(found ? 0 : 1) } + ' sizes.txt; then + echo "❌ One or more contracts are dangerously close to or over the 24KB limit." + exit 1 + fi diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..c52df8b --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,37 @@ +name: Coverage Report + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + FOUNDRY_PROFILE: ci + +jobs: + coverage: + name: Coverage Report + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install lcov + run: | + sudo apt-get update + sudo apt-get install -y lcov + + - name: Generate coverage report + run: forge coverage --report lcov + + - name: Upload to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./lcov.info + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/gas-report.yml b/.github/workflows/gas-report.yml new file mode 100644 index 0000000..0c74556 --- /dev/null +++ b/.github/workflows/gas-report.yml @@ -0,0 +1,25 @@ +name: Gas Report + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + FOUNDRY_PROFILE: ci + +jobs: + gas-report: + name: Gas Report on Test Run + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run tests with gas report + run: forge test --gas-report diff --git a/.github/workflows/gas-snapshot.yml b/.github/workflows/gas-snapshot.yml new file mode 100644 index 0000000..373a2ec --- /dev/null +++ b/.github/workflows/gas-snapshot.yml @@ -0,0 +1,33 @@ +name: Gas Snapshot Diff + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + FOUNDRY_PROFILE: ci + +jobs: + gas-snapshot: + name: Gas Snapshot Diff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Compare gas snapshot diff + run: | + if [ ! -f .gas-snapshot ]; then + echo "::error::.gas-snapshot is missing. Commit a baseline snapshot to enable gas regression checks." + exit 1 + fi + if ! forge snapshot --diff .gas-snapshot; then + echo "❌ Gas usage increased. Review the diff above." + exit 1 + fi diff --git a/.github/workflows/mythril.yml b/.github/workflows/mythril.yml new file mode 100644 index 0000000..8127b0c --- /dev/null +++ b/.github/workflows/mythril.yml @@ -0,0 +1,35 @@ +name: Mythril Security Scan +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +env: + FOUNDRY_PROFILE: ci +jobs: + mythril: + name: Mythril Security Scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Build contracts + run: forge build + - name: Run Mythril on all contracts + run: | + sudo apt-get update && sudo apt-get install -y jq + find out -name "*.json" | while read artifact; do + CONTRACT=$(jq -r '.contractName // empty' "$artifact") + BYTECODE=$(jq -r '.deployedBytecode.object // empty' "$artifact") + if [ -n "$CONTRACT" ] && [ -n "$BYTECODE" ] && [ "$BYTECODE" != "0x" ]; then + echo "🔍 Scanning $CONTRACT from $artifact ..." + docker run --rm \ + mythril/myth analyze \ + -c "$BYTECODE" \ + --bin-runtime \ + --execution-timeout 60 + fi + done diff --git a/.github/workflows/storage-layout-diff.yml b/.github/workflows/storage-layout-diff.yml new file mode 100644 index 0000000..915ed03 --- /dev/null +++ b/.github/workflows/storage-layout-diff.yml @@ -0,0 +1,77 @@ +name: Storage Layout Diff + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + FOUNDRY_PROFILE: ci + +jobs: + storage-layout-diff: + name: Storage Layout Diff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build contracts + run: forge build + + - name: Generate storage layouts + run: | + set -euo pipefail + if [ ! -d .storage-baselines ]; then + echo "No .storage-baselines directory. Skipping generation." + exit 0 + fi + + mkdir -p .storage-layouts + mapfile -t CONTRACTS < <(find .storage-baselines -type f -name '*.json' -exec basename {} .json \;) + + if [ "${#CONTRACTS[@]}" -eq 0 ]; then + echo "No baselines found in .storage-baselines. Skipping generation." + exit 0 + fi + + for contract in "${CONTRACTS[@]}"; do + forge inspect "$contract" storage-layout > ".storage-layouts/${contract}.json" + done + + - name: Diff against baseline + run: | + shopt -s nullglob + CHANGED=0 + if [ ! -d .storage-layouts ]; then + echo "No .storage-layouts generated. Skipping diff." + exit 0 + fi + files=(.storage-layouts/*.json) + if [ ${#files[@]} -eq 0 ]; then + echo "No storage layouts were generated. Skipping diff." + exit 0 + fi + + for file in "${files[@]}"; do + name=$(basename "$file") + baseline=".storage-baselines/$name" + if [ -f "$baseline" ]; then + if ! diff -u "$baseline" "$file"; then + echo "❌ Storage layout changed for $name" + CHANGED=1 + fi + else + echo "❌ No baseline found for $name — add it to .storage-baselines/" + CHANGED=1 + fi + done + + if [ "$CHANGED" -eq 1 ]; then + exit 1 + fi