diff --git a/.github/workflows/ci-dependency-review.yaml b/.github/workflows/ci-dependency-review.yaml index 4c2c5950b7..a9f0b7e9d9 100644 --- a/.github/workflows/ci-dependency-review.yaml +++ b/.github/workflows/ci-dependency-review.yaml @@ -14,6 +14,6 @@ jobs: steps: - uses: actions/checkout@v5 - name: Dependency Review - uses: actions/dependency-review-action@v4.7.3 + uses: actions/dependency-review-action@v4.8.0 with: show-openssf-scorecard: false diff --git a/packages/apps/dashboard/client/package.json b/packages/apps/dashboard/client/package.json index 641156903c..288191b390 100644 --- a/packages/apps/dashboard/client/package.json +++ b/packages/apps/dashboard/client/package.json @@ -37,7 +37,7 @@ "react-number-format": "^5.4.3", "react-router-dom": "^6.23.1", "recharts": "^2.13.0-alpha.4", - "simplebar-react": "^3.2.5", + "simplebar-react": "^3.3.2", "styled-components": "^6.1.11", "swiper": "^11.1.3", "use-debounce": "^10.0.2", diff --git a/packages/apps/dashboard/server/src/modules/details/dto/validation/role-validation.ts b/packages/apps/dashboard/server/src/modules/details/dto/validation/role-validation.ts index 2079d8029a..8fbe4a59c4 100644 --- a/packages/apps/dashboard/server/src/modules/details/dto/validation/role-validation.ts +++ b/packages/apps/dashboard/server/src/modules/details/dto/validation/role-validation.ts @@ -12,7 +12,7 @@ export class IsValidRoleConstraint implements ValidatorConstraintInterface { } defaultMessage() { - return `Role must be one of the following values: ${Object.values( + return `role must be one of the following values: ${Object.values( Role, ).join(', ')}`; } diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/errors/index.ts b/packages/apps/fortune/exchange-oracle/server/src/common/errors/index.ts index 6747ce6861..8a765cd51e 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/errors/index.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/errors/index.ts @@ -11,8 +11,10 @@ export class BaseError extends Error { } export class ValidationError extends BaseError { - constructor(message: string, stack?: string) { + public errors?: string[]; + constructor(message: string, stack?: string, errors?: string[]) { super(message, stack); + this.errors = errors; } } diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts b/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts index cc6d435f03..96e7b88236 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts @@ -60,11 +60,15 @@ export class ExceptionFilter implements IExceptionFilter { response.removeHeader('Cache-Control'); - response.status(status).json({ + const payload: any = { status_code: status, timestamp: new Date().toISOString(), message: message, path: request.url, - }); + }; + if (exception instanceof ValidationError && exception.errors?.length) { + payload.validation_errors = exception.errors; + } + response.status(status).json(payload); } } diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/interceptors/snake-case.ts b/packages/apps/fortune/exchange-oracle/server/src/common/interceptors/snake-case.ts index 0d62e5f222..05b5b1ff1f 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/interceptors/snake-case.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/interceptors/snake-case.ts @@ -6,7 +6,10 @@ import { } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CaseConverter } from '../utils/case-converter'; +import { + transformKeysFromCamelToSnake, + transformKeysFromSnakeToCamel, +} from '../utils/case-converter'; @Injectable() export class SnakeCaseInterceptor implements NestInterceptor { @@ -14,15 +17,15 @@ export class SnakeCaseInterceptor implements NestInterceptor { const request = context.switchToHttp().getRequest(); if (request.body) { - request.body = CaseConverter.transformToCamelCase(request.body); + request.body = transformKeysFromSnakeToCamel(request.body); } if (request.query) { - request.query = CaseConverter.transformToCamelCase(request.query); + request.query = transformKeysFromSnakeToCamel(request.query); } return next .handle() - .pipe(map((data) => CaseConverter.transformToSnakeCase(data))); + .pipe(map((data) => transformKeysFromCamelToSnake(data))); } } diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/pipes/validation.ts b/packages/apps/fortune/exchange-oracle/server/src/common/pipes/validation.ts index fd42f291f5..de2a59296e 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/pipes/validation.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/pipes/validation.ts @@ -5,18 +5,29 @@ import { ValidationPipeOptions, } from '@nestjs/common'; import { ValidationError } from '../errors'; +import { camelToSnake } from '../utils/case-converter'; @Injectable() export class HttpValidationPipe extends ValidationPipe { constructor(options?: ValidationPipeOptions) { super({ exceptionFactory: (errors: ValidError[]): ValidationError => { - const flattenErrors = this.flattenValidationErrors(errors); - throw new ValidationError(flattenErrors.join(', ')); + const messages = this.formatErrorsSnakeCase(errors); + throw new ValidationError('Validation error', undefined, messages); }, transform: true, whitelist: true, ...options, }); } + + private formatErrorsSnakeCase(errors: ValidError[]): string[] { + return errors + .flatMap((error) => this.mapChildrenToValidationErrors(error)) + .flatMap((error) => + Object.values(error.constraints || {}).map((msg) => + msg.replace(error.property, camelToSnake(error.property)), + ), + ); + } } diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/utils/case-converter.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/common/utils/case-converter.spec.ts new file mode 100644 index 0000000000..0a74e428c1 --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/src/common/utils/case-converter.spec.ts @@ -0,0 +1,173 @@ +import { faker } from '@faker-js/faker'; + +import * as CaseConverter from './case-converter'; + +describe('Case converting utilities', () => { + describe('transformKeysFromSnakeToCamel', () => { + it.each([ + 'string', + 42, + BigInt(0), + new Date(), + Symbol('test'), + true, + null, + undefined, + ])('should not transform basic value [%#]', (value: unknown) => { + expect(CaseConverter.transformKeysFromSnakeToCamel(value)).toEqual(value); + }); + + it('should not transform simple array', () => { + const input = faker.helpers.multiple(() => faker.string.sample()); + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual(input); + }); + + it('should transform array of objects', () => { + const input = faker.helpers.multiple(() => ({ + test_case: faker.string.sample(), + })); + const expectedOutput = input.map((v) => ({ + testCase: v.test_case, + })); + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual(expectedOutput); + }); + + it('should transform plain object to camelCase', () => { + const input = { + random_string: faker.string.sample(), + random_number: faker.number.float(), + random_boolean: faker.datatype.boolean(), + always_null: null, + }; + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual({ + randomString: input.random_string, + randomNumber: input.random_number, + randomBoolean: input.random_boolean, + alwaysNull: null, + }); + }); + + it('should transform input with nested data', () => { + const randomString = faker.string.sample(); + + const input = { + nested_object: { + with_array: [ + { + of_objects: { + with_random_string: randomString, + }, + }, + ], + }, + }; + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual({ + nestedObject: { + withArray: [ + { + ofObjects: { + withRandomString: randomString, + }, + }, + ], + }, + }); + }); + }); + + describe('transformKeysFromCamelToSnake', () => { + it.each([ + 'string', + 42, + BigInt(0), + new Date(), + Symbol('test'), + true, + null, + undefined, + ])('should not transform primitive [%#]', (value: unknown) => { + expect(CaseConverter.transformKeysFromCamelToSnake(value)).toEqual(value); + }); + + it('should not transform simple array', () => { + const input = faker.helpers.multiple(() => faker.string.sample()); + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual(input); + }); + + it('should transform array of objects', () => { + const input = faker.helpers.multiple(() => ({ + testCase: faker.string.sample(), + })); + const expectedOutput = input.map((v) => ({ + test_case: v.testCase, + })); + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual(expectedOutput); + }); + + it('should transform plain object to camelCase', () => { + const input = { + randomString: faker.string.sample(), + randomNumber: faker.number.float(), + randomBoolean: faker.datatype.boolean(), + alwaysNull: null, + }; + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual({ + random_string: input.randomString, + random_number: input.randomNumber, + random_boolean: input.randomBoolean, + always_null: null, + }); + }); + + it('should transform input with nested data', () => { + const randomString = faker.string.sample(); + + const input = { + nestedObject: { + withArray: [ + { + ofObjects: { + withRandomString: randomString, + }, + }, + ], + }, + }; + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual({ + nested_object: { + with_array: [ + { + of_objects: { + with_random_string: randomString, + }, + }, + ], + }, + }); + }); + }); +}); diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/utils/case-converter.ts b/packages/apps/fortune/exchange-oracle/server/src/common/utils/case-converter.ts index 54bd31c904..cea876e48b 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/utils/case-converter.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/utils/case-converter.ts @@ -1,37 +1,48 @@ -export class CaseConverter { - static transformToCamelCase(obj: any): any { - if (Array.isArray(obj)) { - return obj.map((item) => CaseConverter.transformToCamelCase(item)); - } else if (typeof obj === 'object' && obj !== null) { - return Object.keys(obj).reduce( - (acc: Record, key: string) => { - const camelCaseKey = key.replace(/_([a-z])/g, (g) => - g[1].toUpperCase(), - ); - acc[camelCaseKey] = CaseConverter.transformToCamelCase(obj[key]); - return acc; - }, - {}, - ); - } else { - return obj; - } +type CaseTransformer = (input: string) => string; + +/** + * TODO: check if replacing it with lodash.camelCase + * won't break anything + */ +export const snakeToCamel: CaseTransformer = (input) => { + return input.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()); +}; + +/** + * TODO: check if replacing it with lodash.snakeCase + * won't break anything + */ +export const camelToSnake: CaseTransformer = (input) => { + return input.replace(/([A-Z])/g, '_$1').toLowerCase(); +}; + +function transformKeysCase( + input: unknown, + transformer: CaseTransformer, +): unknown { + /** + * Primitives and Date objects returned as is + * to keep their original value for later use + */ + if (input === null || typeof input !== 'object' || input instanceof Date) { + return input; + } + + if (Array.isArray(input)) { + return input.map((value) => transformKeysCase(value, transformer)); } - static transformToSnakeCase(obj: any): any { - if (Array.isArray(obj)) { - return obj.map((item) => CaseConverter.transformToSnakeCase(item)); - } else if (typeof obj === 'object' && obj !== null) { - return Object.keys(obj).reduce( - (acc: Record, key: string) => { - const snakeCaseKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); - acc[snakeCaseKey] = CaseConverter.transformToSnakeCase(obj[key]); - return acc; - }, - {}, - ); - } else { - return obj; - } + const transformedObject: Record = {}; + for (const [key, value] of Object.entries(input)) { + transformedObject[transformer(key)] = transformKeysCase(value, transformer); } + return transformedObject; +} + +export function transformKeysFromSnakeToCamel(input: unknown): unknown { + return transformKeysCase(input, snakeToCamel); +} + +export function transformKeysFromCamelToSnake(input: unknown): unknown { + return transformKeysCase(input, camelToSnake); } diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts index a8a6f8d9b0..fac0c3820a 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts @@ -4,16 +4,16 @@ import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; import { firstValueFrom } from 'rxjs'; -import logger from '../../logger'; import { ServerConfigService } from '../../common/config/server-config.service'; import { Web3ConfigService } from '../../common/config/web3-config.service'; import { HEADER_SIGNATURE_KEY } from '../../common/constant'; import { ErrorWebhook } from '../../common/constant/errors'; import { EventType, WebhookStatus } from '../../common/enums/webhook'; import { NotFoundError, ValidationError } from '../../common/errors'; -import { CaseConverter } from '../../common/utils/case-converter'; +import { transformKeysFromCamelToSnake } from '../../common/utils/case-converter'; import { formatAxiosError } from '../../common/utils/http'; import { signMessage } from '../../common/utils/signature'; +import logger from '../../logger'; import { JobService } from '../job/job.service'; import { StorageService } from '../storage/storage.service'; import { Web3Service } from '../web3/web3.service'; @@ -107,7 +107,7 @@ export class WebhookService { reason: webhook.failureDetail, }; } - const transformedWebhook = CaseConverter.transformToSnakeCase(webhookData); + const transformedWebhook: any = transformKeysFromCamelToSnake(webhookData); const signedBody = await signMessage( transformedWebhook, diff --git a/packages/apps/fortune/recording-oracle/src/common/errors/index.ts b/packages/apps/fortune/recording-oracle/src/common/errors/index.ts index 2eaeb72c4f..f267bf0e12 100644 --- a/packages/apps/fortune/recording-oracle/src/common/errors/index.ts +++ b/packages/apps/fortune/recording-oracle/src/common/errors/index.ts @@ -9,8 +9,10 @@ export class BaseError extends Error { } export class ValidationError extends BaseError { - constructor(message: string, stack?: string) { + public errors?: string[]; + constructor(message: string, stack?: string, errors?: string[]) { super(message, stack); + this.errors = errors; } } diff --git a/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts b/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts index 88bbbf7a50..558cff998a 100644 --- a/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts +++ b/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts @@ -57,11 +57,15 @@ export class ExceptionFilter implements IExceptionFilter { response.removeHeader('Cache-Control'); - response.status(status).json({ + const payload: any = { status_code: status, timestamp: new Date().toISOString(), message: message, path: request.url, - }); + }; + if (exception instanceof ValidationError && exception.errors?.length) { + payload.validation_errors = exception.errors; + } + response.status(status).json(payload); } } diff --git a/packages/apps/fortune/recording-oracle/src/common/interceptors/snake-case.ts b/packages/apps/fortune/recording-oracle/src/common/interceptors/snake-case.ts index 0d62e5f222..05b5b1ff1f 100644 --- a/packages/apps/fortune/recording-oracle/src/common/interceptors/snake-case.ts +++ b/packages/apps/fortune/recording-oracle/src/common/interceptors/snake-case.ts @@ -6,7 +6,10 @@ import { } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CaseConverter } from '../utils/case-converter'; +import { + transformKeysFromCamelToSnake, + transformKeysFromSnakeToCamel, +} from '../utils/case-converter'; @Injectable() export class SnakeCaseInterceptor implements NestInterceptor { @@ -14,15 +17,15 @@ export class SnakeCaseInterceptor implements NestInterceptor { const request = context.switchToHttp().getRequest(); if (request.body) { - request.body = CaseConverter.transformToCamelCase(request.body); + request.body = transformKeysFromSnakeToCamel(request.body); } if (request.query) { - request.query = CaseConverter.transformToCamelCase(request.query); + request.query = transformKeysFromSnakeToCamel(request.query); } return next .handle() - .pipe(map((data) => CaseConverter.transformToSnakeCase(data))); + .pipe(map((data) => transformKeysFromCamelToSnake(data))); } } diff --git a/packages/apps/fortune/recording-oracle/src/common/pipes/validation.ts b/packages/apps/fortune/recording-oracle/src/common/pipes/validation.ts index 9654e9fd60..a5147842f2 100644 --- a/packages/apps/fortune/recording-oracle/src/common/pipes/validation.ts +++ b/packages/apps/fortune/recording-oracle/src/common/pipes/validation.ts @@ -1,18 +1,19 @@ import { Injectable, - ValidationError as ValidError, ValidationPipe, ValidationPipeOptions, + ValidationError as ValidError, } from '@nestjs/common'; import { ValidationError } from '../errors'; +import { camelToSnake } from '../utils/case-converter'; @Injectable() export class HttpValidationPipe extends ValidationPipe { constructor(options?: ValidationPipeOptions) { super({ exceptionFactory: (errors: ValidError[]): ValidationError => { - const flattenErrors = this.flattenValidationErrors(errors); - throw new ValidationError(flattenErrors.join(', ')); + const messages = this.formatErrorsSnakeCase(errors); + throw new ValidationError('Validation error', undefined, messages); }, transform: true, whitelist: true, @@ -21,4 +22,14 @@ export class HttpValidationPipe extends ValidationPipe { ...options, }); } + + private formatErrorsSnakeCase(errors: ValidError[]): string[] { + return errors + .flatMap((error) => this.mapChildrenToValidationErrors(error)) + .flatMap((error) => + Object.values(error.constraints || {}).map((msg) => + msg.replace(error.property, camelToSnake(error.property)), + ), + ); + } } diff --git a/packages/apps/fortune/recording-oracle/src/common/utils/case-converter.spec.ts b/packages/apps/fortune/recording-oracle/src/common/utils/case-converter.spec.ts new file mode 100644 index 0000000000..0a74e428c1 --- /dev/null +++ b/packages/apps/fortune/recording-oracle/src/common/utils/case-converter.spec.ts @@ -0,0 +1,173 @@ +import { faker } from '@faker-js/faker'; + +import * as CaseConverter from './case-converter'; + +describe('Case converting utilities', () => { + describe('transformKeysFromSnakeToCamel', () => { + it.each([ + 'string', + 42, + BigInt(0), + new Date(), + Symbol('test'), + true, + null, + undefined, + ])('should not transform basic value [%#]', (value: unknown) => { + expect(CaseConverter.transformKeysFromSnakeToCamel(value)).toEqual(value); + }); + + it('should not transform simple array', () => { + const input = faker.helpers.multiple(() => faker.string.sample()); + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual(input); + }); + + it('should transform array of objects', () => { + const input = faker.helpers.multiple(() => ({ + test_case: faker.string.sample(), + })); + const expectedOutput = input.map((v) => ({ + testCase: v.test_case, + })); + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual(expectedOutput); + }); + + it('should transform plain object to camelCase', () => { + const input = { + random_string: faker.string.sample(), + random_number: faker.number.float(), + random_boolean: faker.datatype.boolean(), + always_null: null, + }; + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual({ + randomString: input.random_string, + randomNumber: input.random_number, + randomBoolean: input.random_boolean, + alwaysNull: null, + }); + }); + + it('should transform input with nested data', () => { + const randomString = faker.string.sample(); + + const input = { + nested_object: { + with_array: [ + { + of_objects: { + with_random_string: randomString, + }, + }, + ], + }, + }; + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual({ + nestedObject: { + withArray: [ + { + ofObjects: { + withRandomString: randomString, + }, + }, + ], + }, + }); + }); + }); + + describe('transformKeysFromCamelToSnake', () => { + it.each([ + 'string', + 42, + BigInt(0), + new Date(), + Symbol('test'), + true, + null, + undefined, + ])('should not transform primitive [%#]', (value: unknown) => { + expect(CaseConverter.transformKeysFromCamelToSnake(value)).toEqual(value); + }); + + it('should not transform simple array', () => { + const input = faker.helpers.multiple(() => faker.string.sample()); + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual(input); + }); + + it('should transform array of objects', () => { + const input = faker.helpers.multiple(() => ({ + testCase: faker.string.sample(), + })); + const expectedOutput = input.map((v) => ({ + test_case: v.testCase, + })); + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual(expectedOutput); + }); + + it('should transform plain object to camelCase', () => { + const input = { + randomString: faker.string.sample(), + randomNumber: faker.number.float(), + randomBoolean: faker.datatype.boolean(), + alwaysNull: null, + }; + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual({ + random_string: input.randomString, + random_number: input.randomNumber, + random_boolean: input.randomBoolean, + always_null: null, + }); + }); + + it('should transform input with nested data', () => { + const randomString = faker.string.sample(); + + const input = { + nestedObject: { + withArray: [ + { + ofObjects: { + withRandomString: randomString, + }, + }, + ], + }, + }; + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual({ + nested_object: { + with_array: [ + { + of_objects: { + with_random_string: randomString, + }, + }, + ], + }, + }); + }); + }); +}); diff --git a/packages/apps/fortune/recording-oracle/src/common/utils/case-converter.ts b/packages/apps/fortune/recording-oracle/src/common/utils/case-converter.ts index 54bd31c904..cea876e48b 100644 --- a/packages/apps/fortune/recording-oracle/src/common/utils/case-converter.ts +++ b/packages/apps/fortune/recording-oracle/src/common/utils/case-converter.ts @@ -1,37 +1,48 @@ -export class CaseConverter { - static transformToCamelCase(obj: any): any { - if (Array.isArray(obj)) { - return obj.map((item) => CaseConverter.transformToCamelCase(item)); - } else if (typeof obj === 'object' && obj !== null) { - return Object.keys(obj).reduce( - (acc: Record, key: string) => { - const camelCaseKey = key.replace(/_([a-z])/g, (g) => - g[1].toUpperCase(), - ); - acc[camelCaseKey] = CaseConverter.transformToCamelCase(obj[key]); - return acc; - }, - {}, - ); - } else { - return obj; - } +type CaseTransformer = (input: string) => string; + +/** + * TODO: check if replacing it with lodash.camelCase + * won't break anything + */ +export const snakeToCamel: CaseTransformer = (input) => { + return input.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()); +}; + +/** + * TODO: check if replacing it with lodash.snakeCase + * won't break anything + */ +export const camelToSnake: CaseTransformer = (input) => { + return input.replace(/([A-Z])/g, '_$1').toLowerCase(); +}; + +function transformKeysCase( + input: unknown, + transformer: CaseTransformer, +): unknown { + /** + * Primitives and Date objects returned as is + * to keep their original value for later use + */ + if (input === null || typeof input !== 'object' || input instanceof Date) { + return input; + } + + if (Array.isArray(input)) { + return input.map((value) => transformKeysCase(value, transformer)); } - static transformToSnakeCase(obj: any): any { - if (Array.isArray(obj)) { - return obj.map((item) => CaseConverter.transformToSnakeCase(item)); - } else if (typeof obj === 'object' && obj !== null) { - return Object.keys(obj).reduce( - (acc: Record, key: string) => { - const snakeCaseKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); - acc[snakeCaseKey] = CaseConverter.transformToSnakeCase(obj[key]); - return acc; - }, - {}, - ); - } else { - return obj; - } + const transformedObject: Record = {}; + for (const [key, value] of Object.entries(input)) { + transformedObject[transformer(key)] = transformKeysCase(value, transformer); } + return transformedObject; +} + +export function transformKeysFromSnakeToCamel(input: unknown): unknown { + return transformKeysCase(input, snakeToCamel); +} + +export function transformKeysFromCamelToSnake(input: unknown): unknown { + return transformKeysCase(input, camelToSnake); } diff --git a/packages/apps/fortune/recording-oracle/src/common/utils/webhook.ts b/packages/apps/fortune/recording-oracle/src/common/utils/webhook.ts index 744b310e32..b5dc0447f9 100644 --- a/packages/apps/fortune/recording-oracle/src/common/utils/webhook.ts +++ b/packages/apps/fortune/recording-oracle/src/common/utils/webhook.ts @@ -4,7 +4,7 @@ import { firstValueFrom } from 'rxjs'; import logger from '../../logger'; import { WebhookDto } from '../../modules/webhook/webhook.dto'; import { HEADER_SIGNATURE_KEY } from '../constants'; -import { CaseConverter } from './case-converter'; +import { transformKeysFromCamelToSnake } from './case-converter'; import { signMessage } from './signature'; import { formatAxiosError } from './http'; @@ -14,7 +14,7 @@ export async function sendWebhook( webhookBody: WebhookDto, privateKey: string, ): Promise { - const snake_case_body = CaseConverter.transformToSnakeCase(webhookBody); + const snake_case_body: any = transformKeysFromCamelToSnake(webhookBody); const signedBody = await signMessage(snake_case_body, privateKey); try { await firstValueFrom( diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts index b24a50550b..abfc928d02 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts @@ -34,7 +34,7 @@ export class JobsDiscoveryController { private readonly service: JobsDiscoveryService, private readonly environmentConfigService: EnvironmentConfigService, @InjectMapper() private readonly mapper: Mapper, - ) {} + ) { } @Get('/jobs') @ApiOperation({ @@ -51,12 +51,13 @@ export class JobsDiscoveryController { HttpStatus.FORBIDDEN, ); } - // TODO: temporar - THIRSTYFI + // TODO: temporal - THIRSTYFI if ( jobsDiscoveryParamsDto.oracle_address === process.env.THIRSTYFI_ORACLE_ADDRESS ) { let data: any; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (new Date(process.env.THIRSTYFI_TASK_EXPIRATION_DATE!) >= new Date()) { const response = await axios.get( `${process.env.THIRSTIFY_EXO}/participant`, @@ -66,17 +67,16 @@ export class JobsDiscoveryController { }, ); data = response.data; - } - return (data?.id ?? 0 > 0) - ? { + return (data?.id ?? 0 > 0) + ? { page: 0, page_size: 1, total_pages: 1, total_results: 0, results: [], } - : { + : { page: 0, page_size: 1, total_pages: 1, @@ -96,6 +96,15 @@ export class JobsDiscoveryController { }, ], }; + } + + return { + page: 0, + page_size: 1, + total_pages: 1, + total_results: 0, + results: [], + }; } const jobsDiscoveryParamsCommand: JobsDiscoveryParamsCommand = diff --git a/packages/apps/job-launcher/server/package.json b/packages/apps/job-launcher/server/package.json index 721b91ec87..7c55d39019 100644 --- a/packages/apps/job-launcher/server/package.json +++ b/packages/apps/job-launcher/server/package.json @@ -70,6 +70,7 @@ "stripe": "^17.7.0", "typeorm": "^0.3.25", "typeorm-naming-strategies": "^4.1.0", + "validator": "^13.12.0", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/packages/apps/job-launcher/server/src/common/errors/index.ts b/packages/apps/job-launcher/server/src/common/errors/index.ts index 6747ce6861..8a765cd51e 100644 --- a/packages/apps/job-launcher/server/src/common/errors/index.ts +++ b/packages/apps/job-launcher/server/src/common/errors/index.ts @@ -11,8 +11,10 @@ export class BaseError extends Error { } export class ValidationError extends BaseError { - constructor(message: string, stack?: string) { + public errors?: string[]; + constructor(message: string, stack?: string, errors?: string[]) { super(message, stack); + this.errors = errors; } } diff --git a/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts b/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts index fd52737363..c1b6f3362a 100644 --- a/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts +++ b/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts @@ -60,11 +60,17 @@ export class ExceptionFilter implements IExceptionFilter { response.removeHeader('Cache-Control'); - response.status(status).json({ + const payload: any = { statusCode: status, timestamp: new Date().toISOString(), message: message, path: request.url, - }); + }; + + if (exception instanceof ValidationError && exception.errors?.length) { + payload.validation_errors = exception.errors; + } + + response.status(status).json(payload); } } diff --git a/packages/apps/job-launcher/server/src/common/interceptors/snake-case.ts b/packages/apps/job-launcher/server/src/common/interceptors/snake-case.ts index e79777e804..e9a11f5ddd 100644 --- a/packages/apps/job-launcher/server/src/common/interceptors/snake-case.ts +++ b/packages/apps/job-launcher/server/src/common/interceptors/snake-case.ts @@ -7,7 +7,10 @@ import { } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CaseConverter } from '../utils/case-converter'; +import { + transformKeysFromCamelToSnake, + transformKeysFromSnakeToCamel, +} from '../utils/case-converter'; @Injectable() export class SnakeCaseInterceptor implements NestInterceptor { @@ -15,11 +18,11 @@ export class SnakeCaseInterceptor implements NestInterceptor { const request = context.switchToHttp().getRequest(); if (request.body) { - request.body = CaseConverter.transformToCamelCase(request.body); + request.body = transformKeysFromSnakeToCamel(request.body); } if (request.query) { - request.query = CaseConverter.transformToCamelCase(request.query); + request.query = transformKeysFromSnakeToCamel(request.query); } return next.handle().pipe( @@ -27,7 +30,7 @@ export class SnakeCaseInterceptor implements NestInterceptor { if (data instanceof StreamableFile) { return data; } - return CaseConverter.transformToSnakeCase(data); + return transformKeysFromCamelToSnake(data); }), ); } diff --git a/packages/apps/job-launcher/server/src/common/pipes/validation.ts b/packages/apps/job-launcher/server/src/common/pipes/validation.ts index 9654e9fd60..abc106d128 100644 --- a/packages/apps/job-launcher/server/src/common/pipes/validation.ts +++ b/packages/apps/job-launcher/server/src/common/pipes/validation.ts @@ -5,14 +5,15 @@ import { ValidationPipeOptions, } from '@nestjs/common'; import { ValidationError } from '../errors'; +import { camelToSnake } from '../utils/case-converter'; @Injectable() export class HttpValidationPipe extends ValidationPipe { constructor(options?: ValidationPipeOptions) { super({ exceptionFactory: (errors: ValidError[]): ValidationError => { - const flattenErrors = this.flattenValidationErrors(errors); - throw new ValidationError(flattenErrors.join(', ')); + const messages = this.formatErrorsSnakeCase(errors); + throw new ValidationError('Validation error', undefined, messages); }, transform: true, whitelist: true, @@ -21,4 +22,14 @@ export class HttpValidationPipe extends ValidationPipe { ...options, }); } + + private formatErrorsSnakeCase(errors: ValidError[]): string[] { + return errors + .flatMap((error) => this.mapChildrenToValidationErrors(error)) + .flatMap((error) => + Object.values(error.constraints || {}).map((msg) => + msg.replace(error.property, camelToSnake(error.property)), + ), + ); + } } diff --git a/packages/apps/job-launcher/server/src/common/utils/case-converter.spec.ts b/packages/apps/job-launcher/server/src/common/utils/case-converter.spec.ts new file mode 100644 index 0000000000..0a74e428c1 --- /dev/null +++ b/packages/apps/job-launcher/server/src/common/utils/case-converter.spec.ts @@ -0,0 +1,173 @@ +import { faker } from '@faker-js/faker'; + +import * as CaseConverter from './case-converter'; + +describe('Case converting utilities', () => { + describe('transformKeysFromSnakeToCamel', () => { + it.each([ + 'string', + 42, + BigInt(0), + new Date(), + Symbol('test'), + true, + null, + undefined, + ])('should not transform basic value [%#]', (value: unknown) => { + expect(CaseConverter.transformKeysFromSnakeToCamel(value)).toEqual(value); + }); + + it('should not transform simple array', () => { + const input = faker.helpers.multiple(() => faker.string.sample()); + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual(input); + }); + + it('should transform array of objects', () => { + const input = faker.helpers.multiple(() => ({ + test_case: faker.string.sample(), + })); + const expectedOutput = input.map((v) => ({ + testCase: v.test_case, + })); + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual(expectedOutput); + }); + + it('should transform plain object to camelCase', () => { + const input = { + random_string: faker.string.sample(), + random_number: faker.number.float(), + random_boolean: faker.datatype.boolean(), + always_null: null, + }; + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual({ + randomString: input.random_string, + randomNumber: input.random_number, + randomBoolean: input.random_boolean, + alwaysNull: null, + }); + }); + + it('should transform input with nested data', () => { + const randomString = faker.string.sample(); + + const input = { + nested_object: { + with_array: [ + { + of_objects: { + with_random_string: randomString, + }, + }, + ], + }, + }; + + const output = CaseConverter.transformKeysFromSnakeToCamel(input); + + expect(output).toEqual({ + nestedObject: { + withArray: [ + { + ofObjects: { + withRandomString: randomString, + }, + }, + ], + }, + }); + }); + }); + + describe('transformKeysFromCamelToSnake', () => { + it.each([ + 'string', + 42, + BigInt(0), + new Date(), + Symbol('test'), + true, + null, + undefined, + ])('should not transform primitive [%#]', (value: unknown) => { + expect(CaseConverter.transformKeysFromCamelToSnake(value)).toEqual(value); + }); + + it('should not transform simple array', () => { + const input = faker.helpers.multiple(() => faker.string.sample()); + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual(input); + }); + + it('should transform array of objects', () => { + const input = faker.helpers.multiple(() => ({ + testCase: faker.string.sample(), + })); + const expectedOutput = input.map((v) => ({ + test_case: v.testCase, + })); + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual(expectedOutput); + }); + + it('should transform plain object to camelCase', () => { + const input = { + randomString: faker.string.sample(), + randomNumber: faker.number.float(), + randomBoolean: faker.datatype.boolean(), + alwaysNull: null, + }; + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual({ + random_string: input.randomString, + random_number: input.randomNumber, + random_boolean: input.randomBoolean, + always_null: null, + }); + }); + + it('should transform input with nested data', () => { + const randomString = faker.string.sample(); + + const input = { + nestedObject: { + withArray: [ + { + ofObjects: { + withRandomString: randomString, + }, + }, + ], + }, + }; + + const output = CaseConverter.transformKeysFromCamelToSnake(input); + + expect(output).toEqual({ + nested_object: { + with_array: [ + { + of_objects: { + with_random_string: randomString, + }, + }, + ], + }, + }); + }); + }); +}); diff --git a/packages/apps/job-launcher/server/src/common/utils/case-converter.ts b/packages/apps/job-launcher/server/src/common/utils/case-converter.ts index 54bd31c904..cea876e48b 100644 --- a/packages/apps/job-launcher/server/src/common/utils/case-converter.ts +++ b/packages/apps/job-launcher/server/src/common/utils/case-converter.ts @@ -1,37 +1,48 @@ -export class CaseConverter { - static transformToCamelCase(obj: any): any { - if (Array.isArray(obj)) { - return obj.map((item) => CaseConverter.transformToCamelCase(item)); - } else if (typeof obj === 'object' && obj !== null) { - return Object.keys(obj).reduce( - (acc: Record, key: string) => { - const camelCaseKey = key.replace(/_([a-z])/g, (g) => - g[1].toUpperCase(), - ); - acc[camelCaseKey] = CaseConverter.transformToCamelCase(obj[key]); - return acc; - }, - {}, - ); - } else { - return obj; - } +type CaseTransformer = (input: string) => string; + +/** + * TODO: check if replacing it with lodash.camelCase + * won't break anything + */ +export const snakeToCamel: CaseTransformer = (input) => { + return input.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()); +}; + +/** + * TODO: check if replacing it with lodash.snakeCase + * won't break anything + */ +export const camelToSnake: CaseTransformer = (input) => { + return input.replace(/([A-Z])/g, '_$1').toLowerCase(); +}; + +function transformKeysCase( + input: unknown, + transformer: CaseTransformer, +): unknown { + /** + * Primitives and Date objects returned as is + * to keep their original value for later use + */ + if (input === null || typeof input !== 'object' || input instanceof Date) { + return input; + } + + if (Array.isArray(input)) { + return input.map((value) => transformKeysCase(value, transformer)); } - static transformToSnakeCase(obj: any): any { - if (Array.isArray(obj)) { - return obj.map((item) => CaseConverter.transformToSnakeCase(item)); - } else if (typeof obj === 'object' && obj !== null) { - return Object.keys(obj).reduce( - (acc: Record, key: string) => { - const snakeCaseKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); - acc[snakeCaseKey] = CaseConverter.transformToSnakeCase(obj[key]); - return acc; - }, - {}, - ); - } else { - return obj; - } + const transformedObject: Record = {}; + for (const [key, value] of Object.entries(input)) { + transformedObject[transformer(key)] = transformKeysCase(value, transformer); } + return transformedObject; +} + +export function transformKeysFromSnakeToCamel(input: unknown): unknown { + return transformKeysCase(input, snakeToCamel); +} + +export function transformKeysFromCamelToSnake(input: unknown): unknown { + return transformKeysCase(input, camelToSnake); } diff --git a/packages/apps/job-launcher/server/src/common/utils/gcstorage.ts b/packages/apps/job-launcher/server/src/common/utils/gcstorage.ts index c7cf098e36..87f57e9086 100644 --- a/packages/apps/job-launcher/server/src/common/utils/gcstorage.ts +++ b/packages/apps/job-launcher/server/src/common/utils/gcstorage.ts @@ -1,3 +1,4 @@ +import { isURL } from 'validator'; import { GS_PROTOCOL } from '../constants'; import { ErrorBucket } from '../constants/errors'; @@ -89,8 +90,15 @@ export function isGCSBucketUrl(url: string): boolean { export function isValidUrl(maybeUrl: string): boolean { try { const url = new URL(maybeUrl); - return ['http:', 'https:', 'gs:'].includes(url.protocol); - } catch (_error) { + if (url.protocol === 'gs:') { + return true; + } else { + return isURL(maybeUrl, { + require_protocol: true, + protocols: ['http', 'https'], + }); + } + } catch { return false; } } diff --git a/packages/apps/job-launcher/server/src/modules/auth/auth.dto.ts b/packages/apps/job-launcher/server/src/modules/auth/auth.dto.ts index 2105d23875..f76a49b3e9 100644 --- a/packages/apps/job-launcher/server/src/modules/auth/auth.dto.ts +++ b/packages/apps/job-launcher/server/src/modules/auth/auth.dto.ts @@ -43,9 +43,7 @@ export class RefreshDto { export class ValidatePasswordDto { @ApiProperty() - @MinLength(8, { - message: 'Password must be at least 8 characters long.', - }) + @MinLength(8) public password: string; } diff --git a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts index 9890a2505f..24580e408e 100644 --- a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts @@ -51,7 +51,12 @@ import { ErrorCronJob, } from '../../common/constants/errors'; import { CronJobType } from '../../common/enums/cron-job'; -import { CvatJobType, FortuneJobType, JobStatus } from '../../common/enums/job'; +import { + CvatJobType, + EscrowFundToken, + FortuneJobType, + JobStatus, +} from '../../common/enums/job'; import { WebhookStatus } from '../../common/enums/webhook'; import { ConflictError } from '../../common/errors'; import { ContentModerationRequestRepository } from '../content-moderation/content-moderation-request.repository'; @@ -700,6 +705,7 @@ describe('CronJobService', () => { jobEntityMock1 = { status: JobStatus.TO_CANCEL, fundAmount: 100, + token: EscrowFundToken.HMT, userId: 1, id: 1, manifestUrl: MOCK_FILE_URL, @@ -712,11 +718,12 @@ describe('CronJobService', () => { jobEntityMock2 = { status: JobStatus.TO_CANCEL, fundAmount: 100, + token: EscrowFundToken.USDC, userId: 1, id: 2, manifestUrl: MOCK_FILE_URL, escrowAddress: MOCK_ADDRESS, - chainId: ChainId.LOCALHOST, + chainId: ChainId.POLYGON, retriesCount: 0, requestType: CvatJobType.IMAGE_POINTS, }; diff --git a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts index d5ecc22ac4..83e532b8f3 100644 --- a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts @@ -4,14 +4,15 @@ import { ErrorCronJob, ErrorEscrow, ErrorJob, + ErrorPayment, } from '../../common/constants/errors'; import { CronJobType } from '../../common/enums/cron-job'; -import { EscrowStatus, EscrowUtils } from '@human-protocol/sdk'; +import { ChainId, EscrowStatus, EscrowUtils } from '@human-protocol/sdk'; import { Cron } from '@nestjs/schedule'; import { ethers } from 'ethers'; import { NetworkConfigService } from '../../common/config/network-config.service'; -import { JobStatus } from '../../common/enums/job'; +import { EscrowFundToken, JobStatus } from '../../common/enums/job'; import { EventType, OracleType, @@ -30,6 +31,7 @@ import { WebhookService } from '../webhook/webhook.service'; import { CronJobEntity } from './cron-job.entity'; import { CronJobRepository } from './cron-job.repository'; import logger from '../../logger'; +import { TOKEN_ADDRESSES } from '../../common/constants/tokens'; @Injectable() export class CronJobService { @@ -269,8 +271,18 @@ export class CronJobService { ) { const { amountRefunded } = await this.jobService.processEscrowCancellation(jobEntity); + const token = (TOKEN_ADDRESSES[jobEntity.chainId as ChainId] ?? {})[ + jobEntity.token as EscrowFundToken + ]; + + if (!token) { + throw new Error(ErrorPayment.UnsupportedToken); + } + await this.paymentService.createRefundPayment({ - refundAmount: Number(ethers.formatEther(amountRefunded)), + refundAmount: Number( + ethers.formatUnits(amountRefunded, token.decimals), + ), refundCurrency: jobEntity.token, userId: jobEntity.userId, jobId: jobEntity.id, diff --git a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts index 4533cbb615..02cfad81db 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts @@ -57,6 +57,7 @@ export class JobDto { @ApiPropertyOptional({ type: String, + name: 'reputation_oracle', description: 'Address of the reputation oracle', }) @IsEthereumAddress() @@ -65,6 +66,7 @@ export class JobDto { @ApiPropertyOptional({ type: String, + name: 'exchange_oracle', description: 'Address of the exchange oracle', }) @IsEthereumAddress() @@ -73,6 +75,7 @@ export class JobDto { @ApiPropertyOptional({ type: String, + name: 'recording_oracle', description: 'Address of the recording oracle', }) @IsEthereumAddress() @@ -154,16 +157,22 @@ export class StorageDataDto { export class CvatDataDto { @ApiProperty() @IsObject() + @ValidateNested() + @Type(() => StorageDataDto) public dataset: StorageDataDto; @ApiPropertyOptional() @IsObject() @IsOptional() + @ValidateNested() + @Type(() => StorageDataDto) public points?: StorageDataDto; @ApiPropertyOptional() @IsObject() @IsOptional() + @ValidateNested() + @Type(() => StorageDataDto) public boxes?: StorageDataDto; } @@ -175,11 +184,15 @@ export class JobCvatDto extends JobDto { @ApiProperty() @IsObject() + @ValidateNested() + @Type(() => CvatDataDto) public data: CvatDataDto; @ApiProperty({ type: [Label] }) @IsArray() @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => Label) public labels: Label[]; @ApiProperty({ name: 'min_quality' }) @@ -190,6 +203,8 @@ export class JobCvatDto extends JobDto { @ApiProperty({ name: 'ground_truth' }) @IsObject() + @ValidateNested() + @Type(() => StorageDataDto) public groundTruth: StorageDataDto; @ApiProperty({ name: 'user_guide' }) @@ -211,6 +226,8 @@ class AudinoLabel { class AudinoDataDto { @ApiProperty() @IsObject() + @ValidateNested() + @Type(() => StorageDataDto) public dataset: StorageDataDto; } @@ -222,6 +239,8 @@ export class JobAudinoDto extends JobDto { @ApiProperty() @IsObject() + @ValidateNested() + @Type(() => AudinoDataDto) public data: AudinoDataDto; @ApiProperty({ type: [AudinoLabel] }) @@ -328,10 +347,14 @@ class CommonDetails { export class JobDetailsDto { @ApiProperty({ description: 'Details of the job' }) @IsNotEmpty() + @ValidateNested() + @Type(() => CommonDetails) public details: CommonDetails; @ApiProperty({ description: 'Manifest details' }) @IsNotEmpty() + @ValidateNested() + @Type(() => ManifestDetails) public manifest: ManifestDetails; } @@ -475,6 +498,8 @@ class JobCaptchaAnnotationsDto { export class JobCaptchaDto extends JobDto { @ApiProperty() @IsObject() + @ValidateNested() + @Type(() => StorageDataDto) data: StorageDataDto; @ApiProperty({ name: 'accuracy_target' }) diff --git a/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts b/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts index d751cc26dd..f6a0bc9c73 100644 --- a/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts @@ -38,6 +38,7 @@ import { ContentType } from '../../common/enums/storage'; import { ServerError, ValidationError } from '../../common/errors'; import { hashString } from '../../common/utils'; import { StorageService } from './storage.service'; +import { faker } from '@faker-js/faker/.'; describe('StorageService', () => { let storageService: StorageService; @@ -81,6 +82,35 @@ describe('StorageService', () => { storageService = moduleRef.get(StorageService); }); + describe('isValidUrl', () => { + it.each([ + '', + faker.string.alphanumeric(), + faker.internet.domainName(), + faker.internet.protocol(), + faker.internet.ipv4(), + // invalid port + `${faker.internet.url({ appendSlash: false })}:${faker.lorem.word({ length: 4 })}`, + `http://[${faker.internet.domainName()}]/`, + 'https://white space.test/', + `ftp://${faker.internet.domainName()}`, + ])('should return false for invalid http url [%#]', (url) => { + expect(StorageService.isValidUrl(url)).toBe(false); + }); + + it.each([ + faker.internet.url({ protocol: 'http' }), + faker.internet.url({ protocol: 'https' }), + `http://${faker.internet.ipv4()}`, + `${faker.internet.url({ protocol: 'http' })}:${faker.internet.port()}`, + 'http://localhost:3000', + 'http://minio:9000', + 'http://job-launcher-server:5000', + ])('should return true for valid http url [%#]', (url) => { + expect(StorageService.isValidUrl(url)).toBe(true); + }); + }); + describe('downloadFileFromUrl', () => { it.each(['no-protocol.com', 'ftp://invalid-protocol.com'])( 'throws if invalid URL [%#]', diff --git a/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts b/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts index 972d094a34..c25e8c0733 100644 --- a/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts +++ b/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts @@ -3,6 +3,7 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import axios from 'axios'; import stringify from 'json-stable-stringify'; import * as Minio from 'minio'; +import { isURL } from 'validator'; import { S3ConfigService } from '../../common/config/s3-config.service'; import { ErrorBucket, ErrorStorage } from '../../common/constants/errors'; import { ContentType, Extension } from '../../common/enums/storage'; @@ -28,12 +29,11 @@ export class StorageService { } public static isValidUrl(maybeUrl: string): boolean { - try { - const url = new URL(maybeUrl); - return ['http:', 'https:'].includes(url.protocol); - } catch (_error) { - return false; - } + return isURL(maybeUrl, { + require_protocol: true, + protocols: ['http', 'https'], + require_tld: false, + }); } public static async downloadFileFromUrl(url: string): Promise { diff --git a/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts b/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts index dd76cd0a9b..146f6c87d0 100644 --- a/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts +++ b/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts @@ -14,7 +14,7 @@ import { HEADER_SIGNATURE_KEY } from '../../common/constants'; import { ErrorWebhook } from '../../common/constants/errors'; import { EventType, WebhookStatus } from '../../common/enums/webhook'; import { ServerError, ValidationError } from '../../common/errors'; -import { CaseConverter } from '../../common/utils/case-converter'; +import { transformKeysFromCamelToSnake } from '../../common/utils/case-converter'; import { formatAxiosError } from '../../common/utils/http'; import { signMessage } from '../../common/utils/signature'; import { JobRepository } from '../job/job.repository'; @@ -60,7 +60,7 @@ export class WebhookService { } // Build the webhook data object based on the oracle type. - const webhookData = CaseConverter.transformToSnakeCase({ + const webhookData: any = transformKeysFromCamelToSnake({ escrowAddress: webhook.escrowAddress, chainId: webhook.chainId, eventType: webhook.eventType, diff --git a/packages/apps/reputation-oracle/server/package.json b/packages/apps/reputation-oracle/server/package.json index b783e03d0e..ae76e501b1 100644 --- a/packages/apps/reputation-oracle/server/package.json +++ b/packages/apps/reputation-oracle/server/package.json @@ -68,6 +68,7 @@ "typeorm": "^0.3.25", "typeorm-naming-strategies": "^4.1.0", "uuid": "^11.1.0", + "validator": "^13.12.0", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/packages/apps/reputation-oracle/server/src/common/pipes/validation.ts b/packages/apps/reputation-oracle/server/src/common/pipes/validation.ts index 4c1585135a..9c6d4db05a 100644 --- a/packages/apps/reputation-oracle/server/src/common/pipes/validation.ts +++ b/packages/apps/reputation-oracle/server/src/common/pipes/validation.ts @@ -7,15 +7,16 @@ import { type ValidationPipeOptions, } from '@nestjs/common'; +import { camelToSnake } from '@/utils/case-converters'; + @Injectable() export class HttpValidationPipe extends ValidationPipe { constructor(options?: ValidationPipeOptions) { super({ exceptionFactory: (errors: ValidationError[]) => { - const flattenErrors = this.flattenValidationErrors(errors); - + const messages = this.formatErrorsSnakeCase(errors); return new HttpException( - { validationErrors: flattenErrors, message: 'Validation error' }, + { validationErrors: messages, message: 'Validation error' }, HttpStatus.BAD_REQUEST, ); }, @@ -24,4 +25,14 @@ export class HttpValidationPipe extends ValidationPipe { ...options, }); } + + private formatErrorsSnakeCase(errors: ValidationError[]): string[] { + return errors + .flatMap((error) => this.mapChildrenToValidationErrors(error)) + .flatMap((error) => + Object.values(error.constraints || {}).map((msg) => + msg.replace(error.property, camelToSnake(error.property)), + ), + ); + } } diff --git a/packages/apps/reputation-oracle/server/src/common/validators/index.ts b/packages/apps/reputation-oracle/server/src/common/validators/index.ts index 3b123b8843..e71aa847a1 100644 --- a/packages/apps/reputation-oracle/server/src/common/validators/index.ts +++ b/packages/apps/reputation-oracle/server/src/common/validators/index.ts @@ -4,8 +4,7 @@ import { IsEmail, IsEnum, IsString, - MaxLength, - MinLength, + Length, ValidationOptions, } from 'class-validator'; @@ -40,13 +39,5 @@ export function IsLowercasedEnum( } export function IsValidPassword() { - return applyDecorators( - IsString(), - MinLength(8, { - message: 'Password must be at least 8 characters long.', - }), - MaxLength(128, { - message: 'Password must be at most 128 characters', - }), - ); + return applyDecorators(IsString(), Length(8, 128)); } diff --git a/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.spec.ts b/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.spec.ts index 743f9d4537..cff8995e74 100644 --- a/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.spec.ts +++ b/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.spec.ts @@ -69,7 +69,7 @@ describe('HCaptchaGuard', () => { thrownError = error; } expect(thrownError).toBeInstanceOf(HttpException); - expect(thrownError.message).toBe('hCaptcha token not provided'); + expect(thrownError.message).toBe('h_captcha_token not provided'); expect(thrownError.status).toBe(HttpStatus.BAD_REQUEST); }); @@ -91,7 +91,7 @@ describe('HCaptchaGuard', () => { thrownError = error; } expect(thrownError).toBeInstanceOf(HttpException); - expect(thrownError.message).toBe('Invalid hCaptcha token'); + expect(thrownError.message).toBe('Invalid h_captcha_token'); expect(thrownError.status).toBe(HttpStatus.BAD_REQUEST); }); }); diff --git a/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.ts b/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.ts index 1b6c71e38a..ea58b532ed 100644 --- a/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.ts +++ b/packages/apps/reputation-oracle/server/src/integrations/hcaptcha/hcaptcha.guard.ts @@ -24,14 +24,17 @@ export class HCaptchaGuard implements CanActivate { const hCaptchaToken: string = body['h_captcha_token']; if (!hCaptchaToken) { throw new HttpException( - 'hCaptcha token not provided', + 'h_captcha_token not provided', HttpStatus.BAD_REQUEST, ); } const isTokenValid = await this.hCaptchaService.verifyToken(hCaptchaToken); if (!isTokenValid) { - throw new HttpException('Invalid hCaptcha token', HttpStatus.BAD_REQUEST); + throw new HttpException( + 'Invalid h_captcha_token', + HttpStatus.BAD_REQUEST, + ); } return true; diff --git a/packages/apps/reputation-oracle/server/src/modules/user/user.dto.ts b/packages/apps/reputation-oracle/server/src/modules/user/user.dto.ts index 0efc024c39..9309432360 100644 --- a/packages/apps/reputation-oracle/server/src/modules/user/user.dto.ts +++ b/packages/apps/reputation-oracle/server/src/modules/user/user.dto.ts @@ -62,9 +62,7 @@ export class RegistrationInExchangeOracleDto { name: 'oracle_address', description: 'Ethereum address of the oracle', }) - @IsEthereumAddress({ - message: 'oracle_address must be an Ethereum address', - }) + @IsEthereumAddress() oracleAddress: string; @ApiProperty({ name: 'h_captcha_token' }) diff --git a/packages/apps/reputation-oracle/server/src/utils/case-converters.ts b/packages/apps/reputation-oracle/server/src/utils/case-converters.ts index ce3d3f09a6..7a1eeed4d4 100644 --- a/packages/apps/reputation-oracle/server/src/utils/case-converters.ts +++ b/packages/apps/reputation-oracle/server/src/utils/case-converters.ts @@ -6,7 +6,7 @@ type CaseTransformer = (input: string) => string; * TODO: check if replacing it with lodash.camelCase * won't break anything */ -const snakeToCamel: CaseTransformer = (input) => { +export const snakeToCamel: CaseTransformer = (input) => { return input.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()); }; @@ -14,7 +14,7 @@ const snakeToCamel: CaseTransformer = (input) => { * TODO: check if replacing it with lodash.snakeCase * won't break anything */ -const camelToSnake: CaseTransformer = (input) => { +export const camelToSnake: CaseTransformer = (input) => { return input.replace(/([A-Z])/g, '_$1').toLowerCase(); }; diff --git a/packages/apps/reputation-oracle/server/src/utils/http.spec.ts b/packages/apps/reputation-oracle/server/src/utils/http.spec.ts index 0819a25e2f..c4e7a07546 100644 --- a/packages/apps/reputation-oracle/server/src/utils/http.spec.ts +++ b/packages/apps/reputation-oracle/server/src/utils/http.spec.ts @@ -60,6 +60,9 @@ describe('HTTP utilities', () => { faker.internet.url({ protocol: 'https' }), `http://${faker.internet.ipv4()}`, `${faker.internet.url({ protocol: 'http' })}:${faker.internet.port()}`, + 'http://localhost:3000', + 'http://minio:9000', + 'http://reputation-oracle:5000', ])('should return true for valid http url [%#]', (url) => { expect(httpUtils.isValidHttpUrl(url)).toBe(true); }); diff --git a/packages/apps/reputation-oracle/server/src/utils/http.ts b/packages/apps/reputation-oracle/server/src/utils/http.ts index 1d3dd2eb3c..63ff1a6dfa 100644 --- a/packages/apps/reputation-oracle/server/src/utils/http.ts +++ b/packages/apps/reputation-oracle/server/src/utils/http.ts @@ -1,6 +1,7 @@ import { Readable } from 'stream'; import axios, { AxiosError } from 'axios'; +import { isURL } from 'validator'; import { BaseError } from '@/common/errors/base'; @@ -22,22 +23,12 @@ class FileDownloadError extends BaseError { } } -function isValidUrl(maybeUrl: string, protocols?: string[]): boolean { - try { - const url = new URL(maybeUrl); - - if (protocols?.length) { - return protocols.includes(url.protocol.replace(':', '')); - } - - return true; - } catch { - return false; - } -} - export function isValidHttpUrl(maybeUrl: string): boolean { - return isValidUrl(maybeUrl, ['http', 'https']); + return isURL(maybeUrl, { + require_protocol: true, + protocols: ['http', 'https'], + require_tld: false, + }); } type DownloadFileOptions = { diff --git a/packages/core/.openzeppelin/polygon.json b/packages/core/.openzeppelin/polygon.json index 9dd6f83030..3e7e80a6bc 100644 --- a/packages/core/.openzeppelin/polygon.json +++ b/packages/core/.openzeppelin/polygon.json @@ -12,6 +12,11 @@ { "address": "0x1371057BAec59944B924A7963F2EeCF43ff94CE4", "kind": "uups" + }, + { + "address": "0x98ca1cbCf337e500c7557F28b3B0770602f4Bb81", + "txHash": "0x568b30c00c08f20af291b8d517c5c51c775d1b18a9fb6b9d8ed78869a77e8747", + "kind": "transparent" } ], "impls": { @@ -718,6 +723,695 @@ }, "namespaces": {} } + }, + "4ce58128162dd4657e8191cb81f01bcd8d412ac24cacecafa126c9609d51b344": { + "address": "0x3280F7005271F583312275cf17961b0CAa97D8Ca", + "txHash": "0x53965d6153e226a4b96dcbbbbf9b6177fce7f15d42cb008a9707c30e079ca5c0", + "layout": { + "solcVersion": "0.8.23", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_hashedName", + "offset": 0, + "slot": "101", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:40", + "renamedFrom": "_HASHED_NAME" + }, + { + "label": "_hashedVersion", + "offset": 0, + "slot": "102", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:42", + "renamedFrom": "_HASHED_VERSION" + }, + { + "label": "_name", + "offset": 0, + "slot": "103", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:44" + }, + { + "label": "_version", + "offset": 0, + "slot": "104", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:45" + }, + { + "label": "__gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)48_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:204" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol:325" + }, + { + "label": "__gap", + "offset": 0, + "slot": "203", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorTimelockUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/IGovernorTimelockUpgradeable.sol:38" + }, + { + "label": "_name", + "offset": 0, + "slot": "253", + "type": "t_string_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:51" + }, + { + "label": "_proposals", + "offset": 0, + "slot": "254", + "type": "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:54", + "retypedFrom": "mapping(uint256 => Governor.ProposalCore)" + }, + { + "label": "_governanceCall", + "offset": 0, + "slot": "255", + "type": "t_struct(Bytes32Deque)10879_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:60" + }, + { + "label": "__gap", + "offset": 0, + "slot": "257", + "type": "t_array(t_uint256)46_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:735" + }, + { + "label": "_votingDelay", + "offset": 0, + "slot": "303", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:15" + }, + { + "label": "_votingPeriod", + "offset": 0, + "slot": "304", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:16" + }, + { + "label": "_proposalThreshold", + "offset": 0, + "slot": "305", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "306", + "type": "t_array(t_uint256)47_storage", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:121" + }, + { + "label": "_owner", + "offset": 0, + "slot": "353", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "354", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "spokeContractsMapping", + "offset": 0, + "slot": "403", + "type": "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:18" + }, + { + "label": "spokeContracts", + "offset": 0, + "slot": "404", + "type": "t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:19" + }, + { + "label": "spokeContractsMappingSnapshots", + "offset": 0, + "slot": "405", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:20" + }, + { + "label": "spokeContractsSnapshots", + "offset": 0, + "slot": "406", + "type": "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:22" + }, + { + "label": "spokeVotes", + "offset": 0, + "slot": "407", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:23" + }, + { + "label": "_proposalVotes", + "offset": 0, + "slot": "408", + "type": "t_mapping(t_uint256,t_struct(ProposalVote)23565_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:25" + }, + { + "label": "token", + "offset": 0, + "slot": "409", + "type": "t_contract(IERC5805Upgradeable)4653", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "410", + "type": "t_array(t_uint256)50_storage", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:68" + }, + { + "label": "_quorumNumerator", + "offset": 0, + "slot": "460", + "type": "t_uint256", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:20" + }, + { + "label": "_quorumNumeratorHistory", + "offset": 0, + "slot": "461", + "type": "t_struct(Trace224)6249_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:23", + "retypedFrom": "Checkpoints.History" + }, + { + "label": "__gap", + "offset": 0, + "slot": "462", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:132" + }, + { + "label": "_timelock", + "offset": 0, + "slot": "510", + "type": "t_contract(TimelockControllerUpgradeable)3488", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:28" + }, + { + "label": "_timelockIds", + "offset": 0, + "slot": "511", + "type": "t_mapping(t_uint256,t_bytes32)", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "512", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:177" + }, + { + "label": "_magistrate", + "offset": 0, + "slot": "560", + "type": "t_address", + "contract": "Magistrate", + "src": "contracts/governance/magistrate/Magistrate.sol:13" + }, + { + "label": "wormholeRelayer", + "offset": 0, + "slot": "561", + "type": "t_contract(IWormholeRelayer)26843", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:46" + }, + { + "label": "secondsPerBlock", + "offset": 0, + "slot": "562", + "type": "t_uint256", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:48" + }, + { + "label": "chainId", + "offset": 0, + "slot": "563", + "type": "t_uint16", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:49" + }, + { + "label": "processedMessages", + "offset": 0, + "slot": "564", + "type": "t_mapping(t_bytes32,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:51" + }, + { + "label": "collectionStarted", + "offset": 0, + "slot": "565", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:52" + }, + { + "label": "collectionFinished", + "offset": 0, + "slot": "566", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:53" + }, + { + "label": "__gap", + "offset": 0, + "slot": "567", + "type": "t_array(t_uint256)50_storage", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:55" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(Checkpoint224)6254_storage)dyn_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)47_storage": { + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes24": { + "label": "bytes24", + "numberOfBytes": "24" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IERC5805Upgradeable)4653": { + "label": "contract IERC5805Upgradeable", + "numberOfBytes": "20" + }, + "t_contract(IWormholeRelayer)26843": { + "label": "contract IWormholeRelayer", + "numberOfBytes": "20" + }, + "t_contract(TimelockControllerUpgradeable)3488": { + "label": "contract TimelockControllerUpgradeable", + "numberOfBytes": "20" + }, + "t_int128": { + "label": "int128", + "numberOfBytes": "16" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))": { + "label": "mapping(bytes32 => mapping(uint16 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage))": { + "label": "mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote))", + "numberOfBytes": "32" + }, + "t_mapping(t_int128,t_bytes32)": { + "label": "mapping(int128 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_bool)": { + "label": "mapping(uint16 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage)": { + "label": "mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.CrossChainAddress[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bytes32)": { + "label": "mapping(uint256 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => bool)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)": { + "label": "mapping(uint256 => struct GovernorUpgradeable.ProposalCore)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalVote)23565_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.ProposalVote)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Bytes32Deque)10879_storage": { + "label": "struct DoubleEndedQueueUpgradeable.Bytes32Deque", + "members": [ + { + "label": "_begin", + "type": "t_int128", + "offset": 0, + "slot": "0" + }, + { + "label": "_end", + "type": "t_int128", + "offset": 16, + "slot": "0" + }, + { + "label": "_data", + "type": "t_mapping(t_int128,t_bytes32)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Checkpoint224)6254_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224", + "members": [ + { + "label": "_key", + "type": "t_uint32", + "offset": 0, + "slot": "0" + }, + { + "label": "_value", + "type": "t_uint224", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(CrossChainAddress)23540_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress", + "members": [ + { + "label": "contractAddress", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "chainId", + "type": "t_uint16", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(ProposalCore)600_storage": { + "label": "struct GovernorUpgradeable.ProposalCore", + "members": [ + { + "label": "voteStart", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 8, + "slot": "0" + }, + { + "label": "__gap_unused0", + "type": "t_bytes4", + "offset": 28, + "slot": "0" + }, + { + "label": "voteEnd", + "type": "t_uint64", + "offset": 0, + "slot": "1" + }, + { + "label": "__gap_unused1", + "type": "t_bytes24", + "offset": 8, + "slot": "1" + }, + { + "label": "executed", + "type": "t_bool", + "offset": 0, + "slot": "2" + }, + { + "label": "canceled", + "type": "t_bool", + "offset": 1, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ProposalVote)23565_storage": { + "label": "struct CrossChainGovernorCountingSimple.ProposalVote", + "members": [ + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "hasVoted", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(SpokeProposalVote)23549_storage": { + "label": "struct CrossChainGovernorCountingSimple.SpokeProposalVote", + "members": [ + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "initialized", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(Trace224)6249_storage": { + "label": "struct CheckpointsUpgradeable.Trace224", + "members": [ + { + "label": "_checkpoints", + "type": "t_array(t_struct(Checkpoint224)6254_storage)dyn_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint224": { + "label": "uint224", + "numberOfBytes": "28" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/packages/core/.openzeppelin/sepolia.json b/packages/core/.openzeppelin/sepolia.json index 345bff1701..9db9f24c42 100644 --- a/packages/core/.openzeppelin/sepolia.json +++ b/packages/core/.openzeppelin/sepolia.json @@ -15,6 +15,11 @@ "address": "0xaEf023AdF6D48239E520F69080bC14eCE4fCFBdd", "txHash": "0x6fe2bccee60cd77036ad31d957b1862ee1d9d6726967202a47e3a34facbf5913", "kind": "uups" + }, + { + "address": "0xF95541CC8548B9329e6122aeBD006603eB9F3d26", + "txHash": "0x026b9c522a535f4da3359265d2d268e038b1f20f0a45715d2ea1fe9a49270e85", + "kind": "transparent" } ], "impls": { @@ -1025,6 +1030,2057 @@ }, "namespaces": {} } + }, + "997c4276ebd1068cf2fca3ea27c2437e7b4653172694493e47a1b4e2d25bae8e": { + "address": "0x4be4c98F0AB61D0248a09398827aD9Bb693Ee609", + "txHash": "0xa793af2a4e929cb34ab18cc12987d817a206ea7e99417664bf54aa367e5cac8b", + "layout": { + "solcVersion": "0.8.23", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_hashedName", + "offset": 0, + "slot": "101", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:40", + "renamedFrom": "_HASHED_NAME" + }, + { + "label": "_hashedVersion", + "offset": 0, + "slot": "102", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:42", + "renamedFrom": "_HASHED_VERSION" + }, + { + "label": "_name", + "offset": 0, + "slot": "103", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:44" + }, + { + "label": "_version", + "offset": 0, + "slot": "104", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:45" + }, + { + "label": "__gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)48_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:204" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol:325" + }, + { + "label": "__gap", + "offset": 0, + "slot": "203", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorTimelockUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/IGovernorTimelockUpgradeable.sol:38" + }, + { + "label": "_name", + "offset": 0, + "slot": "253", + "type": "t_string_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:51" + }, + { + "label": "_proposals", + "offset": 0, + "slot": "254", + "type": "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:54", + "retypedFrom": "mapping(uint256 => Governor.ProposalCore)" + }, + { + "label": "_governanceCall", + "offset": 0, + "slot": "255", + "type": "t_struct(Bytes32Deque)10266_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:60" + }, + { + "label": "__gap", + "offset": 0, + "slot": "257", + "type": "t_array(t_uint256)46_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:735" + }, + { + "label": "_votingDelay", + "offset": 0, + "slot": "303", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:15" + }, + { + "label": "_votingPeriod", + "offset": 0, + "slot": "304", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:16" + }, + { + "label": "_proposalThreshold", + "offset": 0, + "slot": "305", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "306", + "type": "t_array(t_uint256)47_storage", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:121" + }, + { + "label": "_owner", + "offset": 0, + "slot": "353", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "354", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "spokeContractsMapping", + "offset": 0, + "slot": "403", + "type": "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:18" + }, + { + "label": "spokeContracts", + "offset": 0, + "slot": "404", + "type": "t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:19" + }, + { + "label": "spokeContractsMappingSnapshots", + "offset": 0, + "slot": "405", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:20" + }, + { + "label": "spokeContractsSnapshots", + "offset": 0, + "slot": "406", + "type": "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:22" + }, + { + "label": "spokeVotes", + "offset": 0, + "slot": "407", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:23" + }, + { + "label": "_proposalVotes", + "offset": 0, + "slot": "408", + "type": "t_mapping(t_uint256,t_struct(ProposalVote)13630_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:25" + }, + { + "label": "token", + "offset": 0, + "slot": "409", + "type": "t_contract(IERC5805Upgradeable)4632", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "410", + "type": "t_array(t_uint256)50_storage", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:68" + }, + { + "label": "_quorumNumerator", + "offset": 0, + "slot": "460", + "type": "t_uint256", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:20" + }, + { + "label": "_quorumNumeratorHistory", + "offset": 0, + "slot": "461", + "type": "t_struct(Trace224)5746_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:23", + "retypedFrom": "Checkpoints.History" + }, + { + "label": "__gap", + "offset": 0, + "slot": "462", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:132" + }, + { + "label": "_timelock", + "offset": 0, + "slot": "510", + "type": "t_contract(TimelockControllerUpgradeable)3488", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:28" + }, + { + "label": "_timelockIds", + "offset": 0, + "slot": "511", + "type": "t_mapping(t_uint256,t_bytes32)", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "512", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:177" + }, + { + "label": "_magistrate", + "offset": 0, + "slot": "560", + "type": "t_address", + "contract": "Magistrate", + "src": "contracts/governance/magistrate/Magistrate.sol:13" + }, + { + "label": "wormholeRelayer", + "offset": 0, + "slot": "561", + "type": "t_contract(IWormholeRelayer)16471", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:48" + }, + { + "label": "secondsPerBlock", + "offset": 0, + "slot": "562", + "type": "t_uint256", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:50" + }, + { + "label": "chainId", + "offset": 0, + "slot": "563", + "type": "t_uint16", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:51" + }, + { + "label": "processedMessages", + "offset": 0, + "slot": "564", + "type": "t_mapping(t_bytes32,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:53" + }, + { + "label": "collectionStarted", + "offset": 0, + "slot": "565", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:54" + }, + { + "label": "collectionFinished", + "offset": 0, + "slot": "566", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:55" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(Checkpoint224)5751_storage)dyn_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)47_storage": { + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes24": { + "label": "bytes24", + "numberOfBytes": "24" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IERC5805Upgradeable)4632": { + "label": "contract IERC5805Upgradeable", + "numberOfBytes": "20" + }, + "t_contract(IWormholeRelayer)16471": { + "label": "contract IWormholeRelayer", + "numberOfBytes": "20" + }, + "t_contract(TimelockControllerUpgradeable)3488": { + "label": "contract TimelockControllerUpgradeable", + "numberOfBytes": "20" + }, + "t_int128": { + "label": "int128", + "numberOfBytes": "16" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))": { + "label": "mapping(bytes32 => mapping(uint16 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage))": { + "label": "mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote))", + "numberOfBytes": "32" + }, + "t_mapping(t_int128,t_bytes32)": { + "label": "mapping(int128 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_bool)": { + "label": "mapping(uint16 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage)": { + "label": "mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.CrossChainAddress[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bytes32)": { + "label": "mapping(uint256 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => bool)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)": { + "label": "mapping(uint256 => struct GovernorUpgradeable.ProposalCore)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalVote)13630_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.ProposalVote)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Bytes32Deque)10266_storage": { + "label": "struct DoubleEndedQueueUpgradeable.Bytes32Deque", + "members": [ + { + "label": "_begin", + "type": "t_int128", + "offset": 0, + "slot": "0" + }, + { + "label": "_end", + "type": "t_int128", + "offset": 16, + "slot": "0" + }, + { + "label": "_data", + "type": "t_mapping(t_int128,t_bytes32)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Checkpoint224)5751_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224", + "members": [ + { + "label": "_key", + "type": "t_uint32", + "offset": 0, + "slot": "0" + }, + { + "label": "_value", + "type": "t_uint224", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(CrossChainAddress)13605_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress", + "members": [ + { + "label": "contractAddress", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "chainId", + "type": "t_uint16", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(ProposalCore)600_storage": { + "label": "struct GovernorUpgradeable.ProposalCore", + "members": [ + { + "label": "voteStart", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 8, + "slot": "0" + }, + { + "label": "__gap_unused0", + "type": "t_bytes4", + "offset": 28, + "slot": "0" + }, + { + "label": "voteEnd", + "type": "t_uint64", + "offset": 0, + "slot": "1" + }, + { + "label": "__gap_unused1", + "type": "t_bytes24", + "offset": 8, + "slot": "1" + }, + { + "label": "executed", + "type": "t_bool", + "offset": 0, + "slot": "2" + }, + { + "label": "canceled", + "type": "t_bool", + "offset": 1, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ProposalVote)13630_storage": { + "label": "struct CrossChainGovernorCountingSimple.ProposalVote", + "members": [ + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "hasVoted", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(SpokeProposalVote)13614_storage": { + "label": "struct CrossChainGovernorCountingSimple.SpokeProposalVote", + "members": [ + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "initialized", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(Trace224)5746_storage": { + "label": "struct CheckpointsUpgradeable.Trace224", + "members": [ + { + "label": "_checkpoints", + "type": "t_array(t_struct(Checkpoint224)5751_storage)dyn_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint224": { + "label": "uint224", + "numberOfBytes": "28" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "fd661d18c38477f9c943739b80bab1bbfa30389ba9656eee838e7a47a12c63db": { + "address": "0x6325FaaB4E829236843373Bb7Ced43ed8F5Ce6ed", + "txHash": "0x19362ac1e0dfff1066092ef5ee7268d8196b1c8df6bde72f97bcf9e59552f8c6", + "layout": { + "solcVersion": "0.8.23", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_hashedName", + "offset": 0, + "slot": "101", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:40", + "renamedFrom": "_HASHED_NAME" + }, + { + "label": "_hashedVersion", + "offset": 0, + "slot": "102", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:42", + "renamedFrom": "_HASHED_VERSION" + }, + { + "label": "_name", + "offset": 0, + "slot": "103", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:44" + }, + { + "label": "_version", + "offset": 0, + "slot": "104", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:45" + }, + { + "label": "__gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)48_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:204" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol:325" + }, + { + "label": "__gap", + "offset": 0, + "slot": "203", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorTimelockUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/IGovernorTimelockUpgradeable.sol:38" + }, + { + "label": "_name", + "offset": 0, + "slot": "253", + "type": "t_string_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:51" + }, + { + "label": "_proposals", + "offset": 0, + "slot": "254", + "type": "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:54", + "retypedFrom": "mapping(uint256 => Governor.ProposalCore)" + }, + { + "label": "_governanceCall", + "offset": 0, + "slot": "255", + "type": "t_struct(Bytes32Deque)10266_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:60" + }, + { + "label": "__gap", + "offset": 0, + "slot": "257", + "type": "t_array(t_uint256)46_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:735" + }, + { + "label": "_votingDelay", + "offset": 0, + "slot": "303", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:15" + }, + { + "label": "_votingPeriod", + "offset": 0, + "slot": "304", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:16" + }, + { + "label": "_proposalThreshold", + "offset": 0, + "slot": "305", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "306", + "type": "t_array(t_uint256)47_storage", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:121" + }, + { + "label": "_owner", + "offset": 0, + "slot": "353", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "354", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "spokeContractsMapping", + "offset": 0, + "slot": "403", + "type": "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:18" + }, + { + "label": "spokeContracts", + "offset": 0, + "slot": "404", + "type": "t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:19" + }, + { + "label": "spokeContractsMappingSnapshots", + "offset": 0, + "slot": "405", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:20" + }, + { + "label": "spokeContractsSnapshots", + "offset": 0, + "slot": "406", + "type": "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:22" + }, + { + "label": "spokeVotes", + "offset": 0, + "slot": "407", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:23" + }, + { + "label": "_proposalVotes", + "offset": 0, + "slot": "408", + "type": "t_mapping(t_uint256,t_struct(ProposalVote)13630_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:25" + }, + { + "label": "token", + "offset": 0, + "slot": "409", + "type": "t_contract(IERC5805Upgradeable)4632", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "410", + "type": "t_array(t_uint256)50_storage", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:68" + }, + { + "label": "_quorumNumerator", + "offset": 0, + "slot": "460", + "type": "t_uint256", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:20" + }, + { + "label": "_quorumNumeratorHistory", + "offset": 0, + "slot": "461", + "type": "t_struct(Trace224)5746_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:23", + "retypedFrom": "Checkpoints.History" + }, + { + "label": "__gap", + "offset": 0, + "slot": "462", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:132" + }, + { + "label": "_timelock", + "offset": 0, + "slot": "510", + "type": "t_contract(TimelockControllerUpgradeable)3488", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:28" + }, + { + "label": "_timelockIds", + "offset": 0, + "slot": "511", + "type": "t_mapping(t_uint256,t_bytes32)", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "512", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:177" + }, + { + "label": "_magistrate", + "offset": 0, + "slot": "560", + "type": "t_address", + "contract": "Magistrate", + "src": "contracts/governance/magistrate/Magistrate.sol:13" + }, + { + "label": "wormholeRelayer", + "offset": 0, + "slot": "561", + "type": "t_contract(IWormholeRelayer)16479", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:48" + }, + { + "label": "secondsPerBlock", + "offset": 0, + "slot": "562", + "type": "t_uint256", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:50" + }, + { + "label": "chainId", + "offset": 0, + "slot": "563", + "type": "t_uint16", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:51" + }, + { + "label": "processedMessages", + "offset": 0, + "slot": "564", + "type": "t_mapping(t_bytes32,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:53" + }, + { + "label": "collectionStarted", + "offset": 0, + "slot": "565", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:54" + }, + { + "label": "collectionFinished", + "offset": 0, + "slot": "566", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:55" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(Checkpoint224)5751_storage)dyn_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)47_storage": { + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes24": { + "label": "bytes24", + "numberOfBytes": "24" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IERC5805Upgradeable)4632": { + "label": "contract IERC5805Upgradeable", + "numberOfBytes": "20" + }, + "t_contract(IWormholeRelayer)16479": { + "label": "contract IWormholeRelayer", + "numberOfBytes": "20" + }, + "t_contract(TimelockControllerUpgradeable)3488": { + "label": "contract TimelockControllerUpgradeable", + "numberOfBytes": "20" + }, + "t_int128": { + "label": "int128", + "numberOfBytes": "16" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))": { + "label": "mapping(bytes32 => mapping(uint16 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage))": { + "label": "mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote))", + "numberOfBytes": "32" + }, + "t_mapping(t_int128,t_bytes32)": { + "label": "mapping(int128 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_bool)": { + "label": "mapping(uint16 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage)": { + "label": "mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)13605_storage)dyn_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.CrossChainAddress[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bytes32)": { + "label": "mapping(uint256 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => bool)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)13614_storage)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)": { + "label": "mapping(uint256 => struct GovernorUpgradeable.ProposalCore)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalVote)13630_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.ProposalVote)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Bytes32Deque)10266_storage": { + "label": "struct DoubleEndedQueueUpgradeable.Bytes32Deque", + "members": [ + { + "label": "_begin", + "type": "t_int128", + "offset": 0, + "slot": "0" + }, + { + "label": "_end", + "type": "t_int128", + "offset": 16, + "slot": "0" + }, + { + "label": "_data", + "type": "t_mapping(t_int128,t_bytes32)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Checkpoint224)5751_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224", + "members": [ + { + "label": "_key", + "type": "t_uint32", + "offset": 0, + "slot": "0" + }, + { + "label": "_value", + "type": "t_uint224", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(CrossChainAddress)13605_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress", + "members": [ + { + "label": "contractAddress", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "chainId", + "type": "t_uint16", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(ProposalCore)600_storage": { + "label": "struct GovernorUpgradeable.ProposalCore", + "members": [ + { + "label": "voteStart", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 8, + "slot": "0" + }, + { + "label": "__gap_unused0", + "type": "t_bytes4", + "offset": 28, + "slot": "0" + }, + { + "label": "voteEnd", + "type": "t_uint64", + "offset": 0, + "slot": "1" + }, + { + "label": "__gap_unused1", + "type": "t_bytes24", + "offset": 8, + "slot": "1" + }, + { + "label": "executed", + "type": "t_bool", + "offset": 0, + "slot": "2" + }, + { + "label": "canceled", + "type": "t_bool", + "offset": 1, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ProposalVote)13630_storage": { + "label": "struct CrossChainGovernorCountingSimple.ProposalVote", + "members": [ + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "hasVoted", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(SpokeProposalVote)13614_storage": { + "label": "struct CrossChainGovernorCountingSimple.SpokeProposalVote", + "members": [ + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "initialized", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(Trace224)5746_storage": { + "label": "struct CheckpointsUpgradeable.Trace224", + "members": [ + { + "label": "_checkpoints", + "type": "t_array(t_struct(Checkpoint224)5751_storage)dyn_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint224": { + "label": "uint224", + "numberOfBytes": "28" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "4ce58128162dd4657e8191cb81f01bcd8d412ac24cacecafa126c9609d51b344": { + "address": "0x855eed71B17052d0DF1786cc47EA35BDFF5Cad43", + "txHash": "0x0389b877ad9087d81bd9604eee14833e2f311c9c24bf2d37f22dd5d1d59a99d1", + "layout": { + "solcVersion": "0.8.23", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_hashedName", + "offset": 0, + "slot": "101", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:40", + "renamedFrom": "_HASHED_NAME" + }, + { + "label": "_hashedVersion", + "offset": 0, + "slot": "102", + "type": "t_bytes32", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:42", + "renamedFrom": "_HASHED_VERSION" + }, + { + "label": "_name", + "offset": 0, + "slot": "103", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:44" + }, + { + "label": "_version", + "offset": 0, + "slot": "104", + "type": "t_string_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:45" + }, + { + "label": "__gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)48_storage", + "contract": "EIP712Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol:204" + }, + { + "label": "__gap", + "offset": 0, + "slot": "153", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol:325" + }, + { + "label": "__gap", + "offset": 0, + "slot": "203", + "type": "t_array(t_uint256)50_storage", + "contract": "IGovernorTimelockUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/IGovernorTimelockUpgradeable.sol:38" + }, + { + "label": "_name", + "offset": 0, + "slot": "253", + "type": "t_string_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:51" + }, + { + "label": "_proposals", + "offset": 0, + "slot": "254", + "type": "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:54", + "retypedFrom": "mapping(uint256 => Governor.ProposalCore)" + }, + { + "label": "_governanceCall", + "offset": 0, + "slot": "255", + "type": "t_struct(Bytes32Deque)10879_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:60" + }, + { + "label": "__gap", + "offset": 0, + "slot": "257", + "type": "t_array(t_uint256)46_storage", + "contract": "GovernorUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol:735" + }, + { + "label": "_votingDelay", + "offset": 0, + "slot": "303", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:15" + }, + { + "label": "_votingPeriod", + "offset": 0, + "slot": "304", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:16" + }, + { + "label": "_proposalThreshold", + "offset": 0, + "slot": "305", + "type": "t_uint256", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:17" + }, + { + "label": "__gap", + "offset": 0, + "slot": "306", + "type": "t_array(t_uint256)47_storage", + "contract": "GovernorSettingsUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol:121" + }, + { + "label": "_owner", + "offset": 0, + "slot": "353", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "354", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "spokeContractsMapping", + "offset": 0, + "slot": "403", + "type": "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:18" + }, + { + "label": "spokeContracts", + "offset": 0, + "slot": "404", + "type": "t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:19" + }, + { + "label": "spokeContractsMappingSnapshots", + "offset": 0, + "slot": "405", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:20" + }, + { + "label": "spokeContractsSnapshots", + "offset": 0, + "slot": "406", + "type": "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:22" + }, + { + "label": "spokeVotes", + "offset": 0, + "slot": "407", + "type": "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage)))", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:23" + }, + { + "label": "_proposalVotes", + "offset": 0, + "slot": "408", + "type": "t_mapping(t_uint256,t_struct(ProposalVote)23565_storage)", + "contract": "CrossChainGovernorCountingSimple", + "src": "contracts/governance/CrossChainGovernorCountingSimple.sol:25" + }, + { + "label": "token", + "offset": 0, + "slot": "409", + "type": "t_contract(IERC5805Upgradeable)4653", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "410", + "type": "t_array(t_uint256)50_storage", + "contract": "GovernorVotesUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol:68" + }, + { + "label": "_quorumNumerator", + "offset": 0, + "slot": "460", + "type": "t_uint256", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:20" + }, + { + "label": "_quorumNumeratorHistory", + "offset": 0, + "slot": "461", + "type": "t_struct(Trace224)6249_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:23", + "retypedFrom": "Checkpoints.History" + }, + { + "label": "__gap", + "offset": 0, + "slot": "462", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorVotesQuorumFractionUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol:132" + }, + { + "label": "_timelock", + "offset": 0, + "slot": "510", + "type": "t_contract(TimelockControllerUpgradeable)3488", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:28" + }, + { + "label": "_timelockIds", + "offset": 0, + "slot": "511", + "type": "t_mapping(t_uint256,t_bytes32)", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "512", + "type": "t_array(t_uint256)48_storage", + "contract": "GovernorTimelockControlUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol:177" + }, + { + "label": "_magistrate", + "offset": 0, + "slot": "560", + "type": "t_address", + "contract": "Magistrate", + "src": "contracts/governance/magistrate/Magistrate.sol:13" + }, + { + "label": "wormholeRelayer", + "offset": 0, + "slot": "561", + "type": "t_contract(IWormholeRelayer)26845", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:48" + }, + { + "label": "secondsPerBlock", + "offset": 0, + "slot": "562", + "type": "t_uint256", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:50" + }, + { + "label": "chainId", + "offset": 0, + "slot": "563", + "type": "t_uint16", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:51" + }, + { + "label": "processedMessages", + "offset": 0, + "slot": "564", + "type": "t_mapping(t_bytes32,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:53" + }, + { + "label": "collectionStarted", + "offset": 0, + "slot": "565", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:54" + }, + { + "label": "collectionFinished", + "offset": 0, + "slot": "566", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:55" + }, + { + "label": "__gap", + "offset": 0, + "slot": "567", + "type": "t_array(t_uint256)50_storage", + "contract": "MetaHumanGovernor", + "src": "contracts/governance/MetaHumanGovernor.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(Checkpoint224)6254_storage)dyn_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)46_storage": { + "label": "uint256[46]", + "numberOfBytes": "1472" + }, + "t_array(t_uint256)47_storage": { + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes24": { + "label": "bytes24", + "numberOfBytes": "24" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IERC5805Upgradeable)4653": { + "label": "contract IERC5805Upgradeable", + "numberOfBytes": "20" + }, + "t_contract(IWormholeRelayer)26845": { + "label": "contract IWormholeRelayer", + "numberOfBytes": "20" + }, + "t_contract(TimelockControllerUpgradeable)3488": { + "label": "contract TimelockControllerUpgradeable", + "numberOfBytes": "20" + }, + "t_int128": { + "label": "int128", + "numberOfBytes": "16" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_bool))": { + "label": "mapping(bytes32 => mapping(uint16 => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage))": { + "label": "mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote))", + "numberOfBytes": "32" + }, + "t_mapping(t_int128,t_bytes32)": { + "label": "mapping(int128 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_bool)": { + "label": "mapping(uint16 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage)": { + "label": "mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_struct(CrossChainAddress)23540_storage)dyn_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.CrossChainAddress[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bytes32)": { + "label": "mapping(uint256 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_bool)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => bool)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_bytes32,t_mapping(t_uint16,t_struct(SpokeProposalVote)23549_storage)))": { + "label": "mapping(uint256 => mapping(bytes32 => mapping(uint16 => struct CrossChainGovernorCountingSimple.SpokeProposalVote)))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalCore)600_storage)": { + "label": "mapping(uint256 => struct GovernorUpgradeable.ProposalCore)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(ProposalVote)23565_storage)": { + "label": "mapping(uint256 => struct CrossChainGovernorCountingSimple.ProposalVote)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Bytes32Deque)10879_storage": { + "label": "struct DoubleEndedQueueUpgradeable.Bytes32Deque", + "members": [ + { + "label": "_begin", + "type": "t_int128", + "offset": 0, + "slot": "0" + }, + { + "label": "_end", + "type": "t_int128", + "offset": 16, + "slot": "0" + }, + { + "label": "_data", + "type": "t_mapping(t_int128,t_bytes32)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Checkpoint224)6254_storage": { + "label": "struct CheckpointsUpgradeable.Checkpoint224", + "members": [ + { + "label": "_key", + "type": "t_uint32", + "offset": 0, + "slot": "0" + }, + { + "label": "_value", + "type": "t_uint224", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(CrossChainAddress)23540_storage": { + "label": "struct CrossChainGovernorCountingSimple.CrossChainAddress", + "members": [ + { + "label": "contractAddress", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "chainId", + "type": "t_uint16", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(ProposalCore)600_storage": { + "label": "struct GovernorUpgradeable.ProposalCore", + "members": [ + { + "label": "voteStart", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 8, + "slot": "0" + }, + { + "label": "__gap_unused0", + "type": "t_bytes4", + "offset": 28, + "slot": "0" + }, + { + "label": "voteEnd", + "type": "t_uint64", + "offset": 0, + "slot": "1" + }, + { + "label": "__gap_unused1", + "type": "t_bytes24", + "offset": 8, + "slot": "1" + }, + { + "label": "executed", + "type": "t_bool", + "offset": 0, + "slot": "2" + }, + { + "label": "canceled", + "type": "t_bool", + "offset": 1, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ProposalVote)23565_storage": { + "label": "struct CrossChainGovernorCountingSimple.ProposalVote", + "members": [ + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "hasVoted", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(SpokeProposalVote)23549_storage": { + "label": "struct CrossChainGovernorCountingSimple.SpokeProposalVote", + "members": [ + { + "label": "forVotes", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "againstVotes", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "abstainVotes", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "initialized", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(Trace224)6249_storage": { + "label": "struct CheckpointsUpgradeable.Trace224", + "members": [ + { + "label": "_checkpoints", + "type": "t_array(t_struct(Checkpoint224)6254_storage)dyn_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint224": { + "label": "uint224", + "numberOfBytes": "28" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/packages/core/contracts/governance/CrossChainGovernorCountingSimple.sol b/packages/core/contracts/governance/CrossChainGovernorCountingSimple.sol index d251222f05..214a7de3ed 100644 --- a/packages/core/contracts/governance/CrossChainGovernorCountingSimple.sol +++ b/packages/core/contracts/governance/CrossChainGovernorCountingSimple.sol @@ -1,15 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import '@openzeppelin/contracts/governance/Governor.sol'; -import '@openzeppelin/contracts/access/Ownable.sol'; +import '@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; /** * @title CrossChainGovernorCountingSimple * @dev CrossChainGovernorCountingSimple is an abstract contract that provides counting and vote functionality for a cross-chain governor. * It extends the Governor and Ownable contracts. */ -abstract contract CrossChainGovernorCountingSimple is Governor, Ownable { +abstract contract CrossChainGovernorCountingSimple is + Initializable, + GovernorUpgradeable, + OwnableUpgradeable +{ mapping(bytes32 => mapping(uint16 => bool)) public spokeContractsMapping; CrossChainAddress[] public spokeContracts; mapping(uint256 => mapping(bytes32 => mapping(uint16 => bool))) @@ -49,9 +54,16 @@ abstract contract CrossChainGovernorCountingSimple is Governor, Ownable { event SpokesUpdated(CrossChainAddress[] indexed spokes); - constructor( + function __CrossChainGovernorCountingSimple_init( CrossChainAddress[] memory _spokeContracts - ) Ownable(msg.sender) { + ) internal onlyInitializing { + __Ownable_init(); + __CrossChainGovernorCountingSimple_init_unchained(_spokeContracts); + } + + function __CrossChainGovernorCountingSimple_init_unchained( + CrossChainAddress[] memory _spokeContracts + ) internal onlyInitializing { updateSpokeContracts(_spokeContracts); } diff --git a/packages/core/contracts/governance/DAOSpokeContract.sol b/packages/core/contracts/governance/DAOSpokeContract.sol index de9f6601cb..54d33ecd45 100644 --- a/packages/core/contracts/governance/DAOSpokeContract.sol +++ b/packages/core/contracts/governance/DAOSpokeContract.sol @@ -17,6 +17,20 @@ import './magistrate/Magistrate.sol'; */ contract DAOSpokeContract is IWormholeReceiver, Magistrate { using Address for address payable; + error NotStartedVote(); + error VoteFinished(); + error VoteNotActive(); + error VoteAlreadyCast(); + error InvalidVoteType(uint8 support); + error OnlyRelayerAllowed(); + error OnlyMessagesFromHub(); + error MessageAlreadyProcessed(); + error InvalidIntendedRecipient(); + error ProposalIdMustBeUnique(); + error VoteNotFinished(); + error RelaySendFailed(); + error ZeroBalance(); + bytes32 public immutable hubContractAddress; uint16 public immutable hubContractChainId; IVotes public immutable token; @@ -67,6 +81,7 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { * @param _targetSecondsPerBlock The target number of seconds per block for block estimation. * @param _chainId The chain ID of the current contract. * @param _wormholeRelayerAddress The address of the wormhole automatic relayer contract used for cross-chain communication. + * @param _magistrateAddress The initial magistrate address. */ constructor( bytes32 _hubContractAddress, @@ -76,7 +91,8 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { uint16 _chainId, address _wormholeRelayerAddress, address _magistrateAddress - ) Magistrate(_magistrateAddress) { + ) { + _transferMagistrate(_magistrateAddress); token = _token; targetSecondsPerBlock = _targetSecondsPerBlock; chainId = _chainId; @@ -112,13 +128,12 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { uint8 support ) public virtual returns (uint256) { RemoteProposal storage proposal = proposals[proposalId]; - require(isProposal(proposalId), 'DAOSpokeContract: not a started vote'); - require(!proposal.voteFinished, 'DAOSpokeContract: vote finished'); - require( - block.timestamp >= proposal.localVoteStart && - block.timestamp < proposal.localVoteEnd, - 'DAOSpokeContract: vote not currently active' - ); + if (!isProposal(proposalId)) revert NotStartedVote(); + if (proposal.voteFinished) revert VoteFinished(); + if ( + !(block.timestamp >= proposal.localVoteStart && + block.timestamp < proposal.localVoteEnd) + ) revert VoteNotActive(); uint256 weight = token.getPastVotes( msg.sender, @@ -146,10 +161,7 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { ) internal virtual { ProposalVote storage proposalVote = proposalVotes[proposalId]; - require( - !proposalVote.hasVoted[account], - 'DAOSpokeContract: vote already cast' - ); + if (proposalVote.hasVoted[account]) revert VoteAlreadyCast(); proposalVote.hasVoted[account] = true; if (support == uint8(VoteType.Against)) { @@ -159,7 +171,7 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { } else if (support == uint8(VoteType.Abstain)) { proposalVote.abstainVotes += weight; } else { - revert('DAOSpokeContract: invalid value for enum VoteType'); + revert InvalidVoteType(support); } } @@ -188,6 +200,45 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { return estimatedBlock; } + /** + * @dev Sends the vote result of a proposal to the hub contract. + * @param proposalId The ID of the proposal. + * @return A boolean indicating whether the message was sent successfully. + */ + function _sendVoteResultToHub(uint256 proposalId) internal returns (bool) { + ProposalVote storage votes = proposalVotes[proposalId]; + bytes memory messageToSend = abi.encode( + 0, + proposalId, + votes.forVotes, + votes.againstVotes, + votes.abstainVotes + ); + bytes memory payloadToSend = abi.encode( + hubContractAddress, + hubContractChainId, + msg.sender, + messageToSend + ); + + uint256 cost = quoteCrossChainMessage(hubContractChainId); + try + wormholeRelayer.sendPayloadToEvm{value: cost}( + hubContractChainId, + address(uint160(uint256(hubContractAddress))), + payloadToSend, + 0, + GAS_LIMIT, + hubContractChainId, + magistrate() + ) + { + return true; + } catch { + return false; + } + } + /** * @dev Receives messages from the Wormhole protocol's relay mechanism and processes them accordingly. * This function is intended to be called only by the designated Wormhole relayer. @@ -199,19 +250,18 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { function receiveWormholeMessages( bytes memory payload, bytes[] memory, // additionalVaas - bytes32 sourceAddress, // address that called 'sendPayloadToEvm' (Hub contract address) + bytes32 sourceAddress, uint16 sourceChain, - bytes32 deliveryHash // this can be stored in a mapping deliveryHash => bool to prevent duplicate deliveries + bytes32 deliveryHash ) public payable override { - require(msg.sender == address(wormholeRelayer), 'Only relayer allowed'); + if (msg.sender != address(wormholeRelayer)) revert OnlyRelayerAllowed(); - require( - hubContractAddress == sourceAddress && - hubContractChainId == sourceChain, - 'Only messages from the hub contract can be received!' - ); + if ( + !(hubContractAddress == sourceAddress && + hubContractChainId == sourceChain) + ) revert OnlyMessagesFromHub(); - require(!processedMessages[deliveryHash], 'Message already processed'); + if (processedMessages[deliveryHash]) revert MessageAlreadyProcessed(); ( address intendedRecipient, //chainId //sender @@ -220,10 +270,8 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { bytes memory decodedMessage ) = abi.decode(payload, (address, uint16, address, bytes)); - require( - intendedRecipient == address(this), - 'Message is not addressed for this contract' - ); + if (intendedRecipient != address(this)) + revert InvalidIntendedRecipient(); processedMessages[deliveryHash] = true; @@ -245,7 +293,7 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { decodedMessage, (uint16, uint256, uint256, uint256, uint256) ); - require(!isProposal(proposalId), 'Proposal ID must be unique.'); + if (isProposal(proposalId)) revert ProposalIdMustBeUnique(); proposals[proposalId] = RemoteProposal( proposalCreationTimestamp, @@ -255,82 +303,34 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { false ); } else if (option == 1) { - // Send vote results back to the hub chain (, uint256 proposalId) = abi.decode( decodedMessage, (uint16, uint256) ); - ProposalVote storage votes = proposalVotes[proposalId]; - bytes memory messageToSend = abi.encode( - 0, - proposalId, - votes.forVotes, - votes.againstVotes, - votes.abstainVotes - ); - bytes memory payloadToSend = abi.encode( - hubContractAddress, - hubContractChainId, - msg.sender, - messageToSend - ); - - // Send a message to other contracts - // Cost of requesting a message to be sent to - // chain 'targetChain' with a gasLimit of 'GAS_LIMIT' - uint256 cost = quoteCrossChainMessage(hubContractChainId); proposals[proposalId].voteFinished = true; - wormholeRelayer.sendPayloadToEvm{value: cost}( - hubContractChainId, - address(uint160(uint256(hubContractAddress))), - payloadToSend, - 0, // no receiver value needed - GAS_LIMIT, - hubContractChainId, - magistrate() - ); + _sendVoteResultToHub(proposalId); } } function sendVoteResultToHub( uint256 proposalId ) public payable onlyMagistrate { - require( - proposals[proposalId].voteFinished, - 'DAOSpokeContract: vote is not finished' - ); - - ProposalVote storage votes = proposalVotes[proposalId]; - bytes memory messageToSend = abi.encode( - 0, - proposalId, - votes.forVotes, - votes.againstVotes, - votes.abstainVotes - ); - bytes memory payloadToSend = abi.encode( - hubContractAddress, - hubContractChainId, - msg.sender, - messageToSend - ); + if (!proposals[proposalId].voteFinished) revert VoteNotFinished(); - // Send a message to other contracts - // Cost of requesting a message to be sent to - // chain 'targetChain' with a gasLimit of 'GAS_LIMIT' - uint256 cost = quoteCrossChainMessage(hubContractChainId); + bool ok = _sendVoteResultToHub(proposalId); + if (!ok) revert RelaySendFailed(); + } - wormholeRelayer.sendPayloadToEvm{value: cost}( - hubContractChainId, - address(uint160(uint256(hubContractAddress))), - payloadToSend, - 0, // no receiver value needed - GAS_LIMIT, - hubContractChainId, - magistrate() - ); + /** + * @dev Withdraws the contract's balance to the magistrate address. + * Can only be called by the magistrate. + */ + function withdraw() external onlyMagistrate { + uint256 balance = address(this).balance; + if (balance == 0) revert ZeroBalance(); + payable(msg.sender).sendValue(balance); } /** @@ -346,4 +346,6 @@ contract DAOSpokeContract is IWormholeReceiver, Magistrate { GAS_LIMIT ); } + + receive() external payable {} } diff --git a/packages/core/contracts/governance/MetaHumanGovernor.sol b/packages/core/contracts/governance/MetaHumanGovernor.sol index 43fd1ee495..35dd58de45 100644 --- a/packages/core/contracts/governance/MetaHumanGovernor.sol +++ b/packages/core/contracts/governance/MetaHumanGovernor.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import '@openzeppelin/contracts/governance/Governor.sol'; -import '@openzeppelin/contracts/governance/extensions/GovernorSettings.sol'; -import '@openzeppelin/contracts/governance/extensions/GovernorVotes.sol'; -import '@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol'; -import '@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol'; -import '@openzeppelin/contracts/utils/Address.sol'; +import '@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/governance/extensions/GovernorTimelockControlUpgradeable.sol'; import './CrossChainGovernorCountingSimple.sol'; -import './DAOSpokeContract.sol'; import './wormhole/IWormholeRelayer.sol'; import './wormhole/IWormholeReceiver.sol'; import './magistrate/Magistrate.sol'; @@ -22,17 +21,15 @@ import './magistrate/Magistrate.sol'; * For more details check out [OpenZeppelin's documentation](https://docs.openzeppelin.com/contracts/4.x/api/governance#governor). */ contract MetaHumanGovernor is - Governor, - GovernorSettings, + GovernorUpgradeable, + GovernorSettingsUpgradeable, CrossChainGovernorCountingSimple, - GovernorVotes, - GovernorVotesQuorumFraction, - GovernorTimelockControl, + GovernorVotesUpgradeable, + GovernorVotesQuorumFractionUpgradeable, + GovernorTimelockControlUpgradeable, Magistrate, IWormholeReceiver { - using Address for address payable; - error MessageAlreadyProcessed(); error OnlyRelayerAllowed(); error InvalidIntendedRecipient(); @@ -41,16 +38,22 @@ contract MetaHumanGovernor is error RequestAfterVotePeriodOver(); error CollectionPhaseAlreadyStarted(); error OnlyMessagesFromSpokeReceived(); + error UseCrossChainCancel(); + error UseCrossChainPropose(); + error ProposalNotSuccessful(); + error ZeroBalance(); - IWormholeRelayer public immutable wormholeRelayer; + IWormholeRelayer public wormholeRelayer; uint256 internal constant GAS_LIMIT = 500_000; - uint256 public immutable secondsPerBlock; - uint16 public immutable chainId; + uint256 public secondsPerBlock; + uint16 public chainId; mapping(bytes32 => bool) public processedMessages; mapping(uint256 => bool) public collectionStarted; mapping(uint256 => bool) public collectionFinished; + uint256[50] private __gap; + /** * @dev Contract constructor. * @param _token The address of the token contract used for voting. @@ -59,9 +62,9 @@ contract MetaHumanGovernor is * @param _chainId The chain ID of the current contract. * @param _wormholeRelayerAddress The address of the wormhole automatic relayer contract used for cross-chain communication */ - constructor( - IVotes _token, - TimelockController _timelock, + function initialize( + IVotesUpgradeable _token, + TimelockControllerUpgradeable _timelock, CrossChainAddress[] memory _spokeContracts, uint16 _chainId, address _wormholeRelayerAddress, @@ -71,19 +74,19 @@ contract MetaHumanGovernor is uint32 _votingPeriodInSeconds, uint256 _proposalThreshold, uint256 _quorumFraction - ) - Governor('MetaHumanGovernor') - GovernorSettings( + ) public initializer { + __Governor_init('MetaHumanGovernor'); + __GovernorSettings_init( _votingDelayInSeconds, _votingPeriodInSeconds, _proposalThreshold - ) - GovernorVotes(_token) - GovernorVotesQuorumFraction(_quorumFraction) - GovernorTimelockControl(_timelock) - CrossChainGovernorCountingSimple(_spokeContracts) - Magistrate(_magistrateAddress) - { + ); + __CrossChainGovernorCountingSimple_init(_spokeContracts); + __GovernorVotes_init(_token); + __GovernorVotesQuorumFraction_init(_quorumFraction); + __GovernorTimelockControl_init(_timelock); + __Magistrate_init(_magistrateAddress); + chainId = _chainId; wormholeRelayer = IWormholeRelayer(_wormholeRelayerAddress); secondsPerBlock = _secondsPerBlock; @@ -94,7 +97,11 @@ contract MetaHumanGovernor is uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash - ) public override(Governor) returns (uint256) { + ) + public + override(GovernorUpgradeable, IGovernorUpgradeable) + returns (uint256) + { uint256 proposalId = hashProposal( targets, values, @@ -103,7 +110,7 @@ contract MetaHumanGovernor is ); if (spokeContractsSnapshots[proposalId].length > 0) { - revert('Please use crossChainCancel for proposals with spokes.'); + revert UseCrossChainCancel(); } return super.cancel(targets, values, calldatas, descriptionHash); @@ -122,7 +129,7 @@ contract MetaHumanGovernor is descriptionHash ); bytes memory message = abi.encode(uint16(2), proposalId); // selector 2 = cancel - _sendCrossChainMessageToSpokes(proposalId, message, address(this), 2); + _sendCrossChainMessageToSpokes(proposalId, message, address(this)); return proposalId; } @@ -136,7 +143,7 @@ contract MetaHumanGovernor is */ function receiveWormholeMessages( bytes calldata payload, - bytes[] calldata additionalVaas, // additionalVaas + bytes[] calldata, // additionalVaas bytes32 sourceAddress, // address that called 'sendPayloadToEvm' (HelloWormhole contract address) uint16 sourceChain, bytes32 deliveryHash // this can be stored in a mapping deliveryHash => bool to prevent duplicate deliveries @@ -204,13 +211,6 @@ contract MetaHumanGovernor is revert OnlyMessagesFromSpokeReceived(); } - require( - spokeContractsMappingSnapshots[_proposalId][emitterAddress][ - emitterChainId - ], - 'Only messages from the spoke contracts can be received!' - ); - // As long as the received data isn't already initialized... if ( spokeVotes[_proposalId][emitterAddress][emitterChainId].initialized @@ -234,13 +234,12 @@ contract MetaHumanGovernor is bool phaseFinished = true; uint256 spokeContractsLength = spokeContractsSnapshots[proposalId] .length; - for (uint16 i = 1; i <= spokeContractsLength && phaseFinished; ++i) { + for (uint16 i = 0; i < spokeContractsLength && phaseFinished; ++i) { phaseFinished = phaseFinished && spokeVotes[proposalId][ - spokeContractsSnapshots[proposalId][i - 1].contractAddress - ][spokeContractsSnapshots[proposalId][i - 1].chainId] - .initialized; + spokeContractsSnapshots[proposalId][i].contractAddress + ][spokeContractsSnapshots[proposalId][i].chainId].initialized; } collectionFinished[proposalId] = phaseFinished; @@ -250,7 +249,7 @@ contract MetaHumanGovernor is * @dev Requests the voting data from all of the spoke chains. * @param proposalId The ID of the proposal. */ - function requestCollections(uint256 proposalId) public payable { + function requestCollections(uint256 proposalId) external payable { if (block.timestamp < proposalDeadline(proposalId)) { revert RequestAfterVotePeriodOver(); } @@ -270,7 +269,7 @@ contract MetaHumanGovernor is } bytes memory message = abi.encode(uint16(1), proposalId); // selector 1 = requestCollections - _sendCrossChainMessageToSpokes(proposalId, message, msg.sender, 1); + _sendCrossChainMessageToSpokes(proposalId, message, msg.sender); } /** @@ -308,7 +307,7 @@ contract MetaHumanGovernor is voteStartTimestamp, voteEndTimestamp ); - _sendCrossChainMessageToSpokes(proposalId, message, address(this), 0); + _sendCrossChainMessageToSpokes(proposalId, message, address(this)); return proposalId; } @@ -317,26 +316,21 @@ contract MetaHumanGovernor is * @param proposalId The ID of the proposal. * @param message The encoded message to send. * @param sender The address to set as msg.sender in the payload (for requestCollections, otherwise address(this)). - * @param selector The function selector (0: propose, 1: requestCollections, 2: cancel). */ function _sendCrossChainMessageToSpokes( uint256 proposalId, bytes memory message, - address sender, - uint16 selector + address sender ) internal { uint256 spokeContractsLength = spokeContractsSnapshots[proposalId] .length; - uint256 sendMessageToHubCost = _quoteCrossChainMessage(chainId, 0); - bool isRequestCollectionsMessage = (selector == 1); - for (uint16 i = 1; i <= spokeContractsLength; ++i) { - uint16 spokeChainId = spokeContractsSnapshots[proposalId][i - 1] + for (uint16 i = 0; i < spokeContractsLength; ++i) { + uint16 spokeChainId = spokeContractsSnapshots[proposalId][i] .chainId; address spokeAddress = address( uint160( uint256( - spokeContractsSnapshots[proposalId][i - 1] - .contractAddress + spokeContractsSnapshots[proposalId][i].contractAddress ) ) ); @@ -346,15 +340,12 @@ contract MetaHumanGovernor is sender, message ); - uint256 cost = _quoteCrossChainMessage( - spokeChainId, - isRequestCollectionsMessage ? sendMessageToHubCost : 0 - ); + uint256 cost = _quoteCrossChainMessage(spokeChainId, 0); wormholeRelayer.sendPayloadToEvm{value: cost}( spokeChainId, spokeAddress, payload, - isRequestCollectionsMessage ? sendMessageToHubCost : 0, + 0, GAS_LIMIT, spokeChainId, magistrate() @@ -386,7 +377,7 @@ contract MetaHumanGovernor is function votingDelay() public view - override(Governor, GovernorSettings) + override(IGovernorUpgradeable, GovernorSettingsUpgradeable) returns (uint256) { return super.votingDelay(); // Ensure this returns time in seconds @@ -399,7 +390,7 @@ contract MetaHumanGovernor is function votingPeriod() public view - override(Governor, GovernorSettings) + override(IGovernorUpgradeable, GovernorSettingsUpgradeable) returns (uint256) { return super.votingPeriod(); // Ensure this returns time in seconds @@ -415,7 +406,7 @@ contract MetaHumanGovernor is ) public view - override(Governor, GovernorVotesQuorumFraction) + override(IGovernorUpgradeable, GovernorVotesQuorumFractionUpgradeable) returns (uint256) { return super.quorum(snapshotTime); @@ -432,7 +423,7 @@ contract MetaHumanGovernor is ) public view - override(Governor, GovernorTimelockControl) + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) returns (ProposalState) { return super.state(proposalId); @@ -446,8 +437,13 @@ contract MetaHumanGovernor is uint256[] memory, bytes[] memory, string memory - ) public pure override(Governor) returns (uint256) { - revert('Please use crossChainPropose instead.'); + ) + public + pure + override(GovernorUpgradeable, IGovernorUpgradeable) + returns (uint256) + { + revert UseCrossChainPropose(); } /** @@ -457,36 +453,12 @@ contract MetaHumanGovernor is function proposalThreshold() public view - override(Governor, GovernorSettings) + override(GovernorUpgradeable, GovernorSettingsUpgradeable) returns (uint256) { return super.proposalThreshold(); } - /** - * @dev Function to queue a proposal to the timelock. - */ - function queue( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) public virtual override returns (uint256) { - uint256 proposalId = hashProposal( - targets, - values, - calldatas, - descriptionHash - ); - - require( - state(proposalId) == ProposalState.Succeeded, - 'Governor: proposal not successful' - ); - - return super.queue(targets, values, calldatas, descriptionHash); - } - /** * @dev Cancels a proposal. * @param targets The array of target addresses. @@ -500,7 +472,11 @@ contract MetaHumanGovernor is uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) returns (uint256) { + ) + internal + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) + returns (uint256) + { return super._cancel(targets, values, calldatas, descriptionHash); } @@ -511,49 +487,29 @@ contract MetaHumanGovernor is function _executor() internal view - override(Governor, GovernorTimelockControl) + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) returns (address) { return super._executor(); } - function _queueOperations( + function _execute( uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) returns (uint48) { - return - super._queueOperations( - proposalId, - targets, - values, - calldatas, - descriptionHash - ); - } - - function _executeOperations( - uint256 proposalId, - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) { + ) + internal + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) + { _finishCollectionPhase(proposalId); if (!collectionFinished[proposalId]) { revert CollectionPhaseUnfinished(); } - super._executeOperations( - proposalId, - targets, - values, - calldatas, - descriptionHash - ); + super._execute(proposalId, targets, values, calldatas, descriptionHash); } /** @@ -563,19 +519,26 @@ contract MetaHumanGovernor is */ function supportsInterface( bytes4 interfaceId - ) public view override(Governor) returns (bool) { - return super.supportsInterface(interfaceId); - } - - function proposalNeedsQueuing( - uint256 proposalId ) public view - virtual - override(Governor, GovernorTimelockControl) + override(GovernorUpgradeable, GovernorTimelockControlUpgradeable) returns (bool) { - return super.proposalNeedsQueuing(proposalId); + return super.supportsInterface(interfaceId); + } + + /** + * @dev Withdraws the contract's balance to the magistrate address. + * Can only be called by the magistrate. + */ + function withdraw() external onlyMagistrate { + uint256 balance = address(this).balance; + if (balance == 0) { + revert ZeroBalance(); + } + payable(msg.sender).transfer(balance); } + + receive() external payable override(GovernorUpgradeable) {} } diff --git a/packages/core/contracts/governance/magistrate/Magistrate.sol b/packages/core/contracts/governance/magistrate/Magistrate.sol index 06f21dd9f2..7f25bb3d29 100644 --- a/packages/core/contracts/governance/magistrate/Magistrate.sol +++ b/packages/core/contracts/governance/magistrate/Magistrate.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.0; -import '@openzeppelin/contracts/utils/Context.sol'; +import '@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol'; +import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; /** * @dev This contract is based on OpenZeppelin's access/Ownable.sol contract. * The only thing changed is the removal of `renounceOwnership()` function and name of the variables. */ -abstract contract Magistrate is Context { +abstract contract Magistrate is Initializable, ContextUpgradeable { address private _magistrate; event MagistrateChanged( @@ -17,9 +18,16 @@ abstract contract Magistrate is Context { ); /** - * @dev Initializes the contract setting the provided address as the initial magistrate. + * @dev Initializer to set the initial magistrate. Must be called during proxy initialization. */ - constructor(address magistrate_) { + function __Magistrate_init(address magistrate_) internal onlyInitializing { + __Context_init(); + __Magistrate_init_unchained(magistrate_); + } + + function __Magistrate_init_unchained( + address magistrate_ + ) internal onlyInitializing { _transferMagistrate(magistrate_); } diff --git a/packages/core/contracts/vendor/TimelockController.import.sol b/packages/core/contracts/vendor/TimelockController.import.sol new file mode 100644 index 0000000000..145615c5eb --- /dev/null +++ b/packages/core/contracts/vendor/TimelockController.import.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +// This file intentionally imports the non-upgradeable OZ Timelock so Hardhat compiles it +// and TypeChain generates typings + artifacts for tests. +import '@openzeppelin/contracts/governance/TimelockController.sol'; diff --git a/packages/core/hardhat.config.ts b/packages/core/hardhat.config.ts index 045a329124..8110bea27c 100644 --- a/packages/core/hardhat.config.ts +++ b/packages/core/hardhat.config.ts @@ -52,8 +52,12 @@ const config: HardhatUserConfig = { viaIR: true, optimizer: { enabled: true, - runs: 10, + runs: 1, }, + metadata: { + bytecodeHash: 'none', + }, + evmVersion: 'paris', }, }, ], @@ -130,7 +134,7 @@ const config: HardhatUserConfig = { alphaSort: true, runOnCompile: true, disambiguatePaths: false, - strict: true, + strict: false, only: [], except: [], }, @@ -164,15 +168,7 @@ const config: HardhatUserConfig = { }, ], etherscan: { - apiKey: { - mainnet: process.env.ETHERSCAN_API_KEY || '', - sepolia: process.env.ETHERSCAN_API_KEY || '', - polygon: process.env.POLYGONSCAN_API_KEY || '', - polygonAmoy: process.env.POLYGONSCAN_API_KEY || '', - bsc: process.env.BSC_API_KEY || '', - bscTestnet: process.env.BSC_API_KEY || '', - auroraTestnet: 'empty', - }, + apiKey: process.env.ETHERSCAN_API_KEY || '', customChains: [ { network: 'auroraTestnet', diff --git a/packages/core/package.json b/packages/core/package.json index 05bf21cc2a..486b7df2b8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,6 +24,7 @@ "deploy:local": "yarn deploy --network localhost", "deploy:proxy": "hardhat run scripts/deploy-proxies.ts", "upgrade:proxy": "hardhat run scripts/upgrade-proxies.ts", + "upgrade:governor": "hardhat run scripts/upgrade-governor.ts", "deploy:hub": "hardhat run scripts/deploy-hub.ts", "deploy:spokes": "hardhat run scripts/deploy-spokes.ts", "update:spokes": "hardhat run scripts/update-spokes.ts", diff --git a/packages/core/scripts/create-proposal.ts b/packages/core/scripts/create-proposal.ts index 64aa57721b..932e772c76 100644 --- a/packages/core/scripts/create-proposal.ts +++ b/packages/core/scripts/create-proposal.ts @@ -26,7 +26,7 @@ async function main() { proposal.values, proposal.calldatas, proposal.description, - { value: ethers.parseEther('0.025') } + { value: ethers.parseEther('0.01') } ); await transactionResponse.wait(); diff --git a/packages/core/scripts/deploy-hub.ts b/packages/core/scripts/deploy-hub.ts index 68020a2563..acfd1e30d4 100644 --- a/packages/core/scripts/deploy-hub.ts +++ b/packages/core/scripts/deploy-hub.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import dotenv from 'dotenv'; dotenv.config(); @@ -55,41 +55,51 @@ async function main() { const quorumFraction = process.env.QUORUM_FRACTION ? parseInt(process.env.QUORUM_FRACTION) : 0; - const TimelockController = - await ethers.getContractFactory('TimelockController'); - const TimelockControllerContract = await TimelockController.deploy( - 1, - [], - [], - await deployer.getAddress() - ); - await TimelockControllerContract.waitForDeployment(); - console.log( - 'TimelockController Address:', - await TimelockControllerContract.getAddress() - ); + + const existingTimelockAddress = + process.env.HUB_TIMELOCK_CONTROLLER_ADDRESS || ''; + let timelockAddress = existingTimelockAddress; + if (timelockAddress) { + console.log('Using existing TimelockController Address:', timelockAddress); + } else { + const TimelockController = + await ethers.getContractFactory('TimelockController'); + const TimelockControllerContract = await TimelockController.deploy( + 1, + [], + [], + await deployer.getAddress() + ); + await TimelockControllerContract.waitForDeployment(); + timelockAddress = await TimelockControllerContract.getAddress(); + console.log('TimelockController Address:', timelockAddress); + } const MetaHumanGovernor = await ethers.getContractFactory( 'contracts/governance/MetaHumanGovernor.sol:MetaHumanGovernor' ); - const metaHumanGovernorContract = await MetaHumanGovernor.deploy( - vhmTokenAddress, - TimelockControllerContract.getAddress(), - [], - chainId, - hubAutomaticRelayerAddress, - magistrateAddress, - hubSecondsPerBlock, - votingDelay, - votingPeriod, - proposalThreshold, - quorumFraction + const metaHumanGovernorContract = await upgrades.deployProxy( + MetaHumanGovernor, + [ + vhmTokenAddress, + timelockAddress, + [], + chainId, + hubAutomaticRelayerAddress, + magistrateAddress, + hubSecondsPerBlock, + votingDelay, + votingPeriod, + proposalThreshold, + quorumFraction, + ], + { initializer: 'initialize' } ); - await metaHumanGovernorContract.waitForDeployment(); - console.log( - 'Governor deployed to:', - await metaHumanGovernorContract.getAddress() - ); + const proxyAddress = await metaHumanGovernorContract.getAddress(); + const implementationAddress = + await upgrades.erc1967.getImplementationAddress(proxyAddress); + console.log('Governor Proxy deployed at:', proxyAddress); + console.log('Governor Implementation at:', implementationAddress); } main().catch((error) => { diff --git a/packages/core/scripts/upgrade-governor.ts b/packages/core/scripts/upgrade-governor.ts new file mode 100644 index 0000000000..82ae82b7d0 --- /dev/null +++ b/packages/core/scripts/upgrade-governor.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-console */ +import 'dotenv/config'; +import { ethers, upgrades } from 'hardhat'; + +async function main() { + const governorProxy = process.env.GOVERNOR_ADDRESS; + const fqName = + process.env.GOVERNOR_FQN || + 'contracts/governance/MetaHumanGovernor.sol:MetaHumanGovernor'; + + if (!governorProxy) { + console.error('Missing env: GOVERNOR_ADDRESS'); + process.exitCode = 1; + return; + } + + console.log('Governor Proxy Address: ', governorProxy); + console.log('Using implementation FQN: ', fqName); + + const beforeImpl = + await upgrades.erc1967.getImplementationAddress(governorProxy); + console.log('Current Governor Implementation: ', beforeImpl); + + const GovernorFactory = await ethers.getContractFactory(fqName); + + console.log('Preparing new Governor implementation...'); + const newImpl = await upgrades.prepareUpgrade(governorProxy, GovernorFactory); + console.log('Prepared Governor Implementation: ', newImpl); + + console.log('Upgrading Governor proxy...'); + const upgraded = await upgrades.upgradeProxy(governorProxy, GovernorFactory); + const contract = await upgraded.waitForDeployment(); + const txHash = contract.deploymentTransaction()?.hash; + if (txHash) { + await ethers.provider.getTransactionReceipt(txHash); + } + console.log('Governor proxy upgraded successfully!'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/core/test/DAOSpokeContract.ts b/packages/core/test/DAOSpokeContract.ts index 663e3e2c97..baff980948 100644 --- a/packages/core/test/DAOSpokeContract.ts +++ b/packages/core/test/DAOSpokeContract.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import { Signer } from 'ethers'; import { MetaHumanGovernor, @@ -64,12 +64,12 @@ describe('DAOSpokeContract', function () { proposers = [await owner.getAddress()]; const TimelockController = await ethers.getContractFactory('TimelockController'); - timelockController = await TimelockController.deploy( + timelockController = (await TimelockController.deploy( 1, proposers, executors, - owner.getAddress() - ); + await owner.getAddress() + )) as TimelockController; await timelockController.waitForDeployment(); const WormholeMock = await ethers.getContractFactory('WormholeMock'); @@ -87,18 +87,22 @@ describe('DAOSpokeContract', function () { const MetaHumanContract = await ethers.getContractFactory( 'contracts/governance/MetaHumanGovernor.sol:MetaHumanGovernor' ); - governor = (await MetaHumanContract.deploy( - voteToken.getAddress(), - timelockController.getAddress(), - [], - 0, - await wormholeMockForGovernor.getAddress(), - owner.getAddress(), - SECONDS_PER_BLOCK, - SECONDS_PER_BLOCK * 1, - SECONDS_PER_BLOCK * 20 * 15, - 0, - 4 + governor = (await upgrades.deployProxy( + MetaHumanContract, + [ + await voteToken.getAddress(), + await timelockController.getAddress(), + [], + 0, + await wormholeMockForGovernor.getAddress(), + await owner.getAddress(), + SECONDS_PER_BLOCK, + SECONDS_PER_BLOCK * 1, + SECONDS_PER_BLOCK * 20 * 15, + 0, + 4, + ], + { initializer: 'initialize' } )) as MetaHumanGovernor; // Set Governor on worm hole mock @@ -120,11 +124,11 @@ describe('DAOSpokeContract', function () { daoSpoke = (await DAOSpokeContract.deploy( ethers.zeroPadBytes(await governor.getAddress(), 32), 5, // hubChainId - voteToken.getAddress(), + await voteToken.getAddress(), SECONDS_PER_BLOCK, // voting period 6, // spokeChainId await wormholeMockForDaoSpoke.getAddress(), - owner.getAddress() // admin address + await owner.getAddress() // admin address )) as DAOSpokeContract; // Set DAOSpokeContract on worm hole mock @@ -270,9 +274,9 @@ describe('DAOSpokeContract', function () { await mineNBlocks(3); - await expect( - daoSpoke.connect(user1).castVote(proposalId, 3) - ).to.be.revertedWith('DAOSpokeContract: invalid value for enum VoteType'); + await expect(daoSpoke.connect(user1).castVote(proposalId, 3)) + .to.be.revertedWithCustomError(daoSpoke, 'InvalidVoteType') + .withArgs(3); }); it('should revert when vote is finished', async () => { @@ -294,15 +298,15 @@ describe('DAOSpokeContract', function () { await expect( daoSpoke.connect(user1).castVote(proposalId, 0) - ).to.be.revertedWith('DAOSpokeContract: vote finished'); + ).to.be.revertedWithCustomError(daoSpoke, 'VoteFinished'); }); it('should revert when proposal does not exist', async () => { await createMockUserWithVotingPower(voteToken, user1); - await expect(daoSpoke.connect(user1).castVote(1, 0)).to.be.revertedWith( - 'DAOSpokeContract: not a started vote' - ); + await expect( + daoSpoke.connect(user1).castVote(1, 0) + ).to.be.revertedWithCustomError(daoSpoke, 'NotStartedVote'); }); it('should revert when the vote was already cast', async () => { @@ -321,7 +325,7 @@ describe('DAOSpokeContract', function () { await expect( daoSpoke.connect(user1).castVote(proposalId, 0) - ).to.be.revertedWith('DAOSpokeContract: vote already cast'); + ).to.be.revertedWithCustomError(daoSpoke, 'VoteAlreadyCast'); }); }); @@ -337,9 +341,7 @@ describe('DAOSpokeContract', function () { await expect( callReceiveMessageWithWormholeMock(wormholeMockForDaoSpoke, mockPayload) - ).to.be.revertedWith( - 'Only messages from the hub contract can be received!' - ); + ).to.be.revertedWithCustomError(daoSpoke, 'OnlyMessagesFromHub'); }); it('should revert when contract is not intended recipient', async () => { @@ -351,7 +353,7 @@ describe('DAOSpokeContract', function () { await expect( callReceiveMessageWithWormholeMock(wormholeMockForDaoSpoke, mockPayload) - ).to.be.revertedWith('Message is not addressed for this contract'); + ).to.be.revertedWithCustomError(daoSpoke, 'InvalidIntendedRecipient'); }); it('should revert when proposal id is not unique', async () => { @@ -367,7 +369,7 @@ describe('DAOSpokeContract', function () { ); await expect( callReceiveMessageWithWormholeMock(wormholeMockForDaoSpoke, mockPayload) - ).to.be.revertedWith('Message already processed'); + ).to.be.revertedWithCustomError(daoSpoke, 'MessageAlreadyProcessed'); }); it('should process message when proposal start before block timestamp', async () => { @@ -535,9 +537,9 @@ describe('DAOSpokeContract', function () { await governor.getAddress() ); - await expect(daoSpoke.sendVoteResultToHub(proposalId)).to.be.revertedWith( - 'DAOSpokeContract: vote is not finished' - ); + await expect( + daoSpoke.sendVoteResultToHub(proposalId) + ).to.be.revertedWithCustomError(daoSpoke, 'VoteNotFinished'); }); it('should revert when the caller is not the magistrate', async () => { diff --git a/packages/core/test/GovernanceUtils.ts b/packages/core/test/GovernanceUtils.ts index ade3a39739..ee813dc226 100644 --- a/packages/core/test/GovernanceUtils.ts +++ b/packages/core/test/GovernanceUtils.ts @@ -257,8 +257,6 @@ export async function signProposalWithReasonAndParams( proposalId: string, governor: MetaHumanGovernor, support: number, - voter: string, - nonce: number, reason: string, params: string, signer: Signer @@ -271,7 +269,7 @@ export async function signProposalWithReasonAndParams( verifyingContract: await governor.getAddress(), }, { - Ballot: [ + ExtendedBallot: [ { name: 'proposalId', type: 'uint256', @@ -280,29 +278,19 @@ export async function signProposalWithReasonAndParams( name: 'support', type: 'uint8', }, - { - name: 'voter', - type: 'address', - }, - { - name: 'nonce', - type: 'uint256', - }, { name: 'reason', - type: 'bytes32', + type: 'string', }, { name: 'params', - type: 'bytes32', + type: 'bytes', }, ], }, { proposalId, support, - voter, - nonce, reason, params, } @@ -313,8 +301,6 @@ export async function signProposal( proposalId: string, governor: MetaHumanGovernor, support: number, - voter: string, - nonce: number, signer: Signer ): Promise { return await signer.signTypedData( @@ -334,21 +320,11 @@ export async function signProposal( name: 'support', type: 'uint8', }, - { - name: 'voter', - type: 'address', - }, - { - name: 'nonce', - type: 'uint256', - }, ], }, { proposalId, support, - voter, - nonce, } ); } diff --git a/packages/core/test/MetaHumanGovernor.ts b/packages/core/test/MetaHumanGovernor.ts index 3aa35ccaca..a5f4e6dd7d 100644 --- a/packages/core/test/MetaHumanGovernor.ts +++ b/packages/core/test/MetaHumanGovernor.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import { Signer, BigNumberish } from 'ethers'; import { MetaHumanGovernor, @@ -89,20 +89,23 @@ describe('MetaHumanGovernor', function () { const MetaHumanContract = await ethers.getContractFactory( 'contracts/governance/MetaHumanGovernor.sol:MetaHumanGovernor' ); - governor = (await MetaHumanContract.deploy( - voteToken.getAddress(), - timelockController.getAddress(), - [], - 0, - await wormholeMockForGovernor.getAddress(), - owner.getAddress(), - SECONDS_PER_BLOCK, - SECONDS_PER_BLOCK * 1, - SECONDS_PER_BLOCK * 300, - 0, - 4 + governor = (await upgrades.deployProxy( + MetaHumanContract, + [ + await voteToken.getAddress(), + await timelockController.getAddress(), + [], + 0, + await wormholeMockForGovernor.getAddress(), + await owner.getAddress(), + SECONDS_PER_BLOCK, + SECONDS_PER_BLOCK * 1, + SECONDS_PER_BLOCK * 300, + 0, + 4, + ], + { initializer: 'initialize' } )) as MetaHumanGovernor; - // Set Governor on worm hole mock await wormholeMockForGovernor.setReceiver(await governor.getAddress()); @@ -121,14 +124,13 @@ describe('MetaHumanGovernor', function () { ); daoSpoke = (await DAOSpokeContract.deploy( ethers.zeroPadBytes(await governor.getAddress(), 32), - hubChainId, // hubChainId - voteToken.getAddress(), - SECONDS_PER_BLOCK, // voting period - spokeChainId, // spokeChainId + hubChainId, + await voteToken.getAddress(), + SECONDS_PER_BLOCK, + spokeChainId, await wormholeMockForDaoSpoke.getAddress(), - owner.getAddress() // admin address + await owner.getAddress() )) as DAOSpokeContract; - // Set DAOSpokeContract on worm hole mock await wormholeMockForDaoSpoke.setReceiver(await daoSpoke.getAddress()); @@ -166,7 +168,7 @@ describe('MetaHumanGovernor', function () { 12, 6, '0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0', - await owner.getAddress() // admin address + await owner.getAddress() )) as DAOSpokeContract; const spokeContractsX = [ @@ -305,7 +307,7 @@ describe('MetaHumanGovernor', function () { await expect( governor.connect(owner).updateSpokeContracts(spokeContracts) - ).to.be.revertedWithCustomError(governor, 'OwnableUnauthorizedAccount'); + ).to.be.revertedWith('Ownable: caller is not the owner'); }); it('Should create a grant proposal', async function () { @@ -431,10 +433,7 @@ describe('MetaHumanGovernor', function () { await expect( governor.connect(user1).castVote(proposalId, 1) - ).to.be.revertedWithCustomError( - governor, - 'GovernorUnexpectedProposalState' - ); + ).to.be.revertedWith('Governor: vote not currently active'); }); it('Should allow voting against', async function () { @@ -1246,10 +1245,7 @@ describe('MetaHumanGovernor', function () { governor .connect(user1) .castVoteWithReasonAndParams(proposalId, 1, 'test reason', params) - ).to.be.revertedWithCustomError( - governor, - 'GovernorUnexpectedProposalState' - ); + ).to.be.revertedWith('Governor: vote not currently active'); }); it('Should vote on proposal by signature', async function () { @@ -1265,22 +1261,15 @@ describe('MetaHumanGovernor', function () { // create signature const support = 1; - const user1Address = await user1.getAddress(); - const signature = await signProposal( - proposalId, - governor, - support, - user1Address, - 0, - user1 - ); + const signature = await signProposal(proposalId, governor, support, user1); // wait for next block await mineNBlocks(2); // cast vote with sig + const sig1 = ethers.Signature.from(signature); await governor .connect(user1) - .castVoteBySig(proposalId, support, user1Address, signature); + .castVoteBySig(proposalId, support, sig1.v, sig1.r, sig1.s); //assert votes const { againstVotes, forVotes, abstainVotes } = @@ -1306,25 +1295,15 @@ describe('MetaHumanGovernor', function () { // create signature const support = 1; - const user1Address = await user1.getAddress(); - const signature = await signProposal( - proposalId, - governor, - support, - user1Address, - 0, - user1 - ); + const signature = await signProposal(proposalId, governor, support, user1); // cast vote with sig + const sig2 = ethers.Signature.from(signature); await expect( governor .connect(user1) - .castVoteBySig(proposalId, support, await user1.getAddress(), signature) - ).to.be.revertedWithCustomError( - governor, - 'GovernorUnexpectedProposalState' - ); + .castVoteBySig(proposalId, support, sig2.v, sig2.r, sig2.s) + ).to.be.revertedWith('Governor: vote not currently active'); }); it('Should revert when creating proposal with propose', async function () { @@ -1339,7 +1318,7 @@ describe('MetaHumanGovernor', function () { await expect( governor.propose(targets, values, calldatas, 'test') - ).to.be.revertedWith('Please use crossChainPropose instead.'); + ).to.be.revertedWithCustomError(governor, 'UseCrossChainPropose'); }); it('Should return magistrate', async function () { @@ -1359,4 +1338,53 @@ describe('MetaHumanGovernor', function () { governor.transferMagistrate(ethers.ZeroAddress) ).to.be.revertedWith('Magistrate: new magistrate is the zero address'); }); + + it('Should revert withdraw with ZeroBalance when empty', async function () { + await expect(governor.withdraw()).to.be.revertedWithCustomError( + governor, + 'ZeroBalance' + ); + }); + + it('Should revert withdraw when caller is not magistrate', async function () { + await owner.sendTransaction({ + to: await governor.getAddress(), + value: ethers.parseEther('0.1'), + }); + await expect(governor.connect(user1).withdraw()).to.be.revertedWith( + 'Magistrate: caller is not the magistrate' + ); + }); + + it('Should allow magistrate to withdraw full balance', async function () { + const amount = ethers.parseEther('0.2'); + await owner.sendTransaction({ + to: await governor.getAddress(), + value: amount, + }); + + const balanceBefore = await ethers.provider.getBalance( + await owner.getAddress() + ); + const contractBefore = await ethers.provider.getBalance( + await governor.getAddress() + ); + expect(contractBefore).to.equal(amount); + + const tx = await governor.withdraw(); + const receipt = await tx.wait(); + expect(receipt?.status).to.equal(1); + + const contractAfter = await ethers.provider.getBalance( + await governor.getAddress() + ); + expect(contractAfter).to.equal(0n); + + const balanceAfter = await ethers.provider.getBalance( + await owner.getAddress() + ); + expect(balanceAfter).to.be.greaterThan( + balanceBefore - ethers.parseEther('0.001') + ); + }); }); diff --git a/packages/core/test/MetaHumanGovernorHubOnly.ts b/packages/core/test/MetaHumanGovernorHubOnly.ts index 09e0353316..97bef56ad0 100644 --- a/packages/core/test/MetaHumanGovernorHubOnly.ts +++ b/packages/core/test/MetaHumanGovernorHubOnly.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import { Signer, BigNumberish } from 'ethers'; import { MetaHumanGovernor, @@ -75,22 +75,26 @@ describe('MetaHumanGovernorHubOnly', function () { value: ethers.parseEther('1'), }); - // Deploy MetaHumanGovernor + // Deploy MetaHumanGovernor (proxy) const MetaHumanContract = await ethers.getContractFactory( 'contracts/governance/MetaHumanGovernor.sol:MetaHumanGovernor' ); - governor = (await MetaHumanContract.deploy( - voteToken.getAddress(), - timelockController.getAddress(), - [], - 0, - await wormholeMockForGovernor.getAddress(), - owner.getAddress(), - SECONDS_PER_BLOCK, - SECONDS_PER_BLOCK * 1, - SECONDS_PER_BLOCK * 300, - 0, - 4 + governor = (await upgrades.deployProxy( + MetaHumanContract, + [ + await voteToken.getAddress(), + await timelockController.getAddress(), + [], + 0, + await wormholeMockForGovernor.getAddress(), + await owner.getAddress(), + SECONDS_PER_BLOCK, + SECONDS_PER_BLOCK * 1, + SECONDS_PER_BLOCK * 300, + 0, + 4, + ], + { initializer: 'initialize' } )) as MetaHumanGovernor; // Grant proposer role on timelock controller @@ -171,10 +175,7 @@ describe('MetaHumanGovernorHubOnly', function () { await expect( governor.connect(user1).castVote(proposalId, 1) - ).to.be.revertedWithCustomError( - governor, - 'GovernorUnexpectedProposalState' - ); + ).to.be.revertedWith('Governor: vote not currently active'); }); it('Should allow voting against', async function () { @@ -700,10 +701,7 @@ describe('MetaHumanGovernorHubOnly', function () { governor .connect(user1) .castVoteWithReasonAndParams(proposalId, 1, 'test reason', params) - ).to.be.revertedWithCustomError( - governor, - 'GovernorUnexpectedProposalState' - ); + ).to.be.revertedWith('Governor: vote not currently active'); }); it('Should vote on proposal by signature', async function () { @@ -717,22 +715,15 @@ describe('MetaHumanGovernorHubOnly', function () { // create signature const support = 1; - const user1Address = await user1.getAddress(); - const signature = await signProposal( - proposalId, - governor, - support, - user1Address, - 0, - user1 - ); + const signature = await signProposal(proposalId, governor, support, user1); // wait for next block await mineNBlocks(2); // cast vote with sig + const sig1 = ethers.Signature.from(signature); await governor .connect(user1) - .castVoteBySig(proposalId, support, user1Address, signature); + .castVoteBySig(proposalId, support, sig1.v, sig1.r, sig1.s); //assert votes const { againstVotes, forVotes, abstainVotes } = @@ -754,25 +745,15 @@ describe('MetaHumanGovernorHubOnly', function () { // create signature const support = 1; - const user1Address = await user1.getAddress(); - const signature = await signProposal( - proposalId, - governor, - support, - user1Address, - 0, - user1 - ); + const signature = await signProposal(proposalId, governor, support, user1); // cast vote with sig + const sig2 = ethers.Signature.from(signature); await expect( governor .connect(user1) - .castVoteBySig(proposalId, support, await user1.getAddress(), signature) - ).to.be.revertedWithCustomError( - governor, - 'GovernorUnexpectedProposalState' - ); + .castVoteBySig(proposalId, support, sig2.v, sig2.r, sig2.s) + ).to.be.revertedWith('Governor: vote not currently active'); }); it('Should revert when creating proposal with propose', async function () { @@ -787,7 +768,7 @@ describe('MetaHumanGovernorHubOnly', function () { await expect( governor.propose(targets, values, calldatas, 'test') - ).to.be.revertedWith('Please use crossChainPropose instead.'); + ).to.be.revertedWithCustomError(governor, 'UseCrossChainPropose'); }); it('Should return magistrate', async function () { @@ -807,4 +788,43 @@ describe('MetaHumanGovernorHubOnly', function () { governor.transferMagistrate(ethers.ZeroAddress) ).to.be.revertedWith('Magistrate: new magistrate is the zero address'); }); + + it('Should revert withdraw with ZeroBalance when empty', async function () { + await expect(governor.withdraw()).to.be.revertedWithCustomError( + governor, + 'ZeroBalance' + ); + }); + + it('Should revert withdraw when caller is not magistrate', async function () { + await owner.sendTransaction({ + to: await governor.getAddress(), + value: ethers.parseEther('0.05'), + }); + await expect(governor.connect(user1).withdraw()).to.be.revertedWith( + 'Magistrate: caller is not the magistrate' + ); + }); + + it('Should allow magistrate to withdraw full balance', async function () { + const amount = ethers.parseEther('0.05'); + await owner.sendTransaction({ + to: await governor.getAddress(), + value: amount, + }); + + const contractBefore = await ethers.provider.getBalance( + await governor.getAddress() + ); + expect(contractBefore).to.equal(amount); + + const tx = await governor.withdraw(); + const receipt = await tx.wait(); + expect(receipt?.status).to.equal(1); + + const contractAfter = await ethers.provider.getBalance( + await governor.getAddress() + ); + expect(contractAfter).to.equal(0n); + }); }); diff --git a/packages/sdk/typescript/human-protocol-sdk/package.json b/packages/sdk/typescript/human-protocol-sdk/package.json index 32013b10be..13113c1136 100644 --- a/packages/sdk/typescript/human-protocol-sdk/package.json +++ b/packages/sdk/typescript/human-protocol-sdk/package.json @@ -47,6 +47,7 @@ "minio": "7.1.3", "openpgp": "^5.11.2", "secp256k1": "^5.0.1", + "validator": "^13.12.0", "vitest": "^3.0.9" }, "devDependencies": { diff --git a/packages/sdk/typescript/human-protocol-sdk/src/utils.ts b/packages/sdk/typescript/human-protocol-sdk/src/utils.ts index acb9e67592..975215da05 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/utils.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/utils.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ethers } from 'ethers'; +import { isURL } from 'validator'; +import { SUBGRAPH_API_KEY_PLACEHOLDER } from './constants'; import { ChainId } from './enums'; import { ContractExecutionError, @@ -13,7 +15,6 @@ import { WarnSubgraphApiKeyNotProvided, } from './error'; import { NetworkData } from './types'; -import { SUBGRAPH_API_KEY_PLACEHOLDER } from './constants'; /** * **Handle and throw the error.* @@ -45,13 +46,12 @@ export const throwError = (e: any) => { * @param {string} url * @returns */ -export const isValidUrl = (url: string) => { - try { - new URL(url); - return true; - } catch (err) { - return false; - } +export const isValidUrl = (url: string): boolean => { + return isURL(url, { + require_protocol: true, + protocols: ['http', 'https'], + require_tld: false, + }); }; /** diff --git a/packages/sdk/typescript/human-protocol-sdk/test/storage.test.ts b/packages/sdk/typescript/human-protocol-sdk/test/storage.test.ts index 76cc41e4b1..6c1988c700 100644 --- a/packages/sdk/typescript/human-protocol-sdk/test/storage.test.ts +++ b/packages/sdk/typescript/human-protocol-sdk/test/storage.test.ts @@ -203,7 +203,7 @@ describe('Storage tests', () => { .createHash('sha1') .update(JSON.stringify(file)) .digest('hex'); - const url = `http://${DEFAULT_PUBLIC_BUCKET}/${hash}.json`; + const url = `http://${DEFAULT_ENDPOINT}:${DEFAULT_PORT}/${DEFAULT_PUBLIC_BUCKET}/${hash}.json`; const result = await StorageClient.downloadFileFromUrl(url); expect(result).toEqual(file); @@ -216,7 +216,7 @@ describe('Storage tests', () => { .createHash('sha1') .update(JSON.stringify(file)) .digest('hex'); - const url = `http://${DEFAULT_PUBLIC_BUCKET}/${hash}.json`; + const url = `http://${DEFAULT_ENDPOINT}:${DEFAULT_PORT}/${DEFAULT_PUBLIC_BUCKET}/${hash}.json`; vi.spyOn(StorageClient, 'downloadFileFromUrl').mockImplementation(() => { throw ErrorStorageFileNotFound; diff --git a/packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts b/packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts new file mode 100644 index 0000000000..745119098c --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts @@ -0,0 +1,144 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { describe, expect, test, vi } from 'vitest'; +import { ChainId } from '../src'; +import { NETWORKS } from '../src/constants'; +import { + ContractExecutionError, + EthereumError, + InvalidArgumentError, + NonceExpired, + NumericFault, + ReplacementUnderpriced, + TransactionReplaced, + WarnSubgraphApiKeyNotProvided, +} from '../src/error'; +import { + getSubgraphUrl, + getUnixTimestamp, + isValidJson, + isValidUrl, + throwError, +} from '../src/utils'; + +describe('isValidUrl', () => { + test.each([ + 'http://localhost:3000', + 'http://minio:9000', + 'http://reputation-oracle:5000', + 'http://example.com', + 'https://example.com/path', + 'http://service:8080/path', + 'http://127.0.0.1:8080', + 'https://sub.domain.local', + ])('returns true for valid url: %s', (url) => { + expect(isValidUrl(url)).toBe(true); + }); + + test.each([ + '', + 'example.com', + 'ftp://example.com', + 'http://', + 'http://:8080', + 'http://white space', + 'minio:9000', + 'localhost:3000', + ])('returns false for invalid url: %s', (url) => { + expect(isValidUrl(url)).toBe(false); + }); +}); + +describe('isValidJson', () => { + test('returns true for valid JSON', () => { + expect(isValidJson('{"a":1}')).toBe(true); + expect(isValidJson('[1,2,3]')).toBe(true); + expect(isValidJson('"string"')).toBe(true); + }); + test('returns false for invalid JSON', () => { + expect(isValidJson('{a:1}')).toBe(false); + expect(isValidJson('not json')).toBe(false); + expect(isValidJson('')).toBe(false); + }); +}); + +describe('getUnixTimestamp', () => { + test('returns correct unix timestamp for a date', () => { + const date = new Date(); + expect(getUnixTimestamp(date)).toBe(Math.floor(date.getTime() / 1000)); + }); +}); + +describe('getSubgraphUrl', () => { + const networkData = NETWORKS[ChainId.LOCALHOST]!; + + test('returns subgraphUrl if no API key', () => { + delete process.env.SUBGRAPH_API_KEY; + expect(getSubgraphUrl(networkData)).toBe(networkData.subgraphUrl); + }); + + test('returns subgraphUrlApiKey with replaced key if API key present', () => { + process.env.SUBGRAPH_API_KEY = 'real-key'; + const url = getSubgraphUrl({ + ...networkData, + subgraphUrlApiKey: + 'http://localhost:8000/subgraphs/name/humanprotocol/localhost?key=real-key', + }); + expect(url).toContain('real-key'); + delete process.env.SUBGRAPH_API_KEY; + }); + + test('warns if no API key and not localhost', () => { + const spy = vi.spyOn(console, 'warn').mockReturnValueOnce(undefined); + getSubgraphUrl({ ...networkData, chainId: 1 }); + expect(spy).toHaveBeenCalledWith(WarnSubgraphApiKeyNotProvided); + }); +}); + +describe('throwError', () => { + test.each([ + [ + { + code: 'INVALID_ARGUMENT', + }, + InvalidArgumentError, + ], + [ + { + code: 'CALL_EXCEPTION', + }, + ContractExecutionError, + ], + [ + { + code: 'TRANSACTION_REPLACED', + }, + TransactionReplaced, + ], + [ + { + code: 'REPLACEMENT_UNDERPRICED', + }, + ReplacementUnderpriced, + ], + [ + { + code: 'NUMERIC_FAULT', + }, + NumericFault, + ], + [ + { + code: 'NONCE_EXPIRED', + }, + NonceExpired, + ], + [ + { + code: 'UNKNOWN', + }, + EthereumError, + ], + ])('throws %p as %p', (errorObj, expectedError) => { + expect(() => throwError(errorObj)).toThrow(expectedError); + }); +}); diff --git a/yarn.lock b/yarn.lock index da531fe724..72674dd140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4251,7 +4251,7 @@ __metadata: react-router-dom: "npm:^6.23.1" recharts: "npm:^2.13.0-alpha.4" sass: "npm:^1.89.2" - simplebar-react: "npm:^3.2.5" + simplebar-react: "npm:^3.3.2" styled-components: "npm:^6.1.11" swiper: "npm:^11.1.3" typescript: "npm:^5.6.3" @@ -4745,6 +4745,7 @@ __metadata: typeorm: "npm:^0.3.25" typeorm-naming-strategies: "npm:^4.1.0" typescript: "npm:^5.6.3" + validator: "npm:^13.12.0" zxcvbn: "npm:^4.4.2" languageName: unknown linkType: soft @@ -4842,6 +4843,7 @@ __metadata: typescript: "npm:^5.9.2" typescript-eslint: "npm:^8.39.1" uuid: "npm:^11.1.0" + validator: "npm:^13.12.0" zxcvbn: "npm:^4.4.2" languageName: unknown linkType: soft @@ -4865,6 +4867,7 @@ __metadata: typedoc: "npm:^0.28.7" typedoc-plugin-markdown: "npm:^4.2.3" typescript: "npm:^5.8.3" + validator: "npm:^13.12.0" vitest: "npm:^3.0.9" languageName: unknown linkType: soft @@ -28010,23 +28013,24 @@ __metadata: languageName: node linkType: hard -"simplebar-core@npm:^1.3.0": - version: 1.3.0 - resolution: "simplebar-core@npm:1.3.0" +"simplebar-core@npm:^1.3.2": + version: 1.3.2 + resolution: "simplebar-core@npm:1.3.2" dependencies: lodash: "npm:^4.17.21" - checksum: 10c0/c8aa86a402e9a13ffb7a65d75851d400345f109f092f8c23c546bd3e451a7c63d84784fc3bb1c15283cd05ece2ccddd28025f42db0b50aa00db385d8afa7f5e6 + lodash-es: "npm:^4.17.21" + checksum: 10c0/a4844edac50099b30abb60e85f0852ebbcbfbe47e4b67e682f57f21449b08d06a3ac2432db7db5a6bcf571c3ce74d2a418da22f467909587ab295a33b6b551e3 languageName: node linkType: hard -"simplebar-react@npm:^3.2.5": - version: 3.3.0 - resolution: "simplebar-react@npm:3.3.0" +"simplebar-react@npm:^3.3.2": + version: 3.3.2 + resolution: "simplebar-react@npm:3.3.2" dependencies: - simplebar-core: "npm:^1.3.0" + simplebar-core: "npm:^1.3.2" peerDependencies: react: ">=16.8.0" - checksum: 10c0/ad07ac2b239f7372af48f92ad01b82a7e6465fcd1c43da9f5d12edb26cba81a55c87badd2cffac4aa8aa769da56c05b4e2eb5de19fec52588ca57f87d1f6202b + checksum: 10c0/1ccc2fbfef3659c6ebd6ee9eaa9cffa3c5179e5cf825351335928673ff5f715042f71e05fa3f5813ab1b6fe947536a52050667e149023975c2dcf4287d705292 languageName: node linkType: hard @@ -30683,6 +30687,13 @@ __metadata: languageName: node linkType: hard +"validator@npm:^13.12.0": + version: 13.15.15 + resolution: "validator@npm:13.15.15" + checksum: 10c0/f5349d1fbb9cc36f9f6c5dab1880764ddad1d0d2b084e2a71e5964f7de1635d20e406611559df9a3db24828ce775cbee5e3b6dd52f0d555a61939ed7ea5990bd + languageName: node + linkType: hard + "validator@npm:^13.9.0": version: 13.15.0 resolution: "validator@npm:13.15.0"