diff --git a/.changeset/silent-bags-repair.md b/.changeset/silent-bags-repair.md new file mode 100644 index 000000000..af87a58cb --- /dev/null +++ b/.changeset/silent-bags-repair.md @@ -0,0 +1,5 @@ +--- +'@openzeppelin/wizard-stellar': patch +--- + +Set security contact as contract metadata diff --git a/packages/core/stellar/src/contract.test.ts b/packages/core/stellar/src/contract.test.ts index c66010496..502eae9ca 100644 --- a/packages/core/stellar/src/contract.test.ts +++ b/packages/core/stellar/src/contract.test.ts @@ -3,6 +3,7 @@ import test from 'ava'; import type { BaseFunction, BaseTraitImplBlock } from './contract'; import { ContractBuilder } from './contract'; import { printContract } from './print'; +import { setInfo } from './set-info'; test('contract basics', t => { const Foo = new ContractBuilder('Foo'); @@ -89,15 +90,41 @@ test('contract with documentation', t => { t.snapshot(printContract(Foo)); }); -test('contract with security info', t => { +test('contract with security contact metadata', t => { const Foo = new ContractBuilder('Foo'); - Foo.addSecurityTag('security@example.com'); + Foo.addContractMetadata({ key: 'contact', value: 'security@example.com' }); t.snapshot(printContract(Foo)); }); -test('contract with security info and documentation', t => { +test('setting metadata with same key throws', t => { const Foo = new ContractBuilder('Foo'); - Foo.addSecurityTag('security@example.com'); + Foo.addContractMetadata({ key: 'contact', value: 'security@example.com' }); + + t.throws(() => Foo.addContractMetadata({ key: 'contact', value: 'security@example.com' })); +}); + +test('contract with multiple metadata', t => { + const Foo = new ContractBuilder('Foo'); + Foo.addContractMetadata([ + { key: 'contact', value: 'security@example.com' }, + { key: 'meta', value: 'data' }, + ]); + t.snapshot(printContract(Foo)); +}); + +test('contract with multiple metadata and documentation', t => { + const Foo = new ContractBuilder('Foo'); + Foo.addContractMetadata([ + { key: 'contact', value: 'security@example.com' }, + { key: 'meta', value: 'data' }, + ]); Foo.addDocumentation('Some documentation'); t.snapshot(printContract(Foo)); }); + +test('contract with setInfo', t => { + const Foo = new ContractBuilder('Foo'); + setInfo(Foo, { securityContact: 'security@example.com', license: 'MIT' }); + + t.snapshot(printContract(Foo)); +}); diff --git a/packages/core/stellar/src/contract.test.ts.md b/packages/core/stellar/src/contract.test.ts.md index 01113f975..40c6b6b58 100644 --- a/packages/core/stellar/src/contract.test.ts.md +++ b/packages/core/stellar/src/contract.test.ts.md @@ -155,23 +155,40 @@ Generated by [AVA](https://avajs.dev). pub struct Foo;␊ ` -## contract with security info +## contract with security contact metadata > Snapshot 1 `// SPDX-License-Identifier: MIT␊ // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ + #![no_std]␊ + ␊ + use soroban_sdk::contractmeta;␊ ␊ - //! # Security␊ - //!␊ - //! For security issues, please contact: security@example.com␊ + contractmeta!(key="contact", val="security@example.com");␊ + ␊ + #[contract]␊ + pub struct Foo;␊ + ` + +## contract with multiple metadata + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ #![no_std]␊ ␊ + use soroban_sdk::contractmeta;␊ + ␊ + contractmeta!(key="contact", val="security@example.com");␊ + contractmeta!(key="meta", val="data");␊ + ␊ #[contract]␊ pub struct Foo;␊ ` -## contract with security info and documentation +## contract with multiple metadata and documentation > Snapshot 1 @@ -179,12 +196,29 @@ Generated by [AVA](https://avajs.dev). // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ ␊ //! Some documentation␊ + #![no_std]␊ + ␊ + use soroban_sdk::contractmeta;␊ ␊ - //! # Security␊ - //!␊ - //! For security issues, please contact: security@example.com␊ + contractmeta!(key="contact", val="security@example.com");␊ + contractmeta!(key="meta", val="data");␊ + ␊ + #[contract]␊ + pub struct Foo;␊ + ` + +## contract with setInfo + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Stellar Soroban Contracts ^0.4.1␊ #![no_std]␊ ␊ + use soroban_sdk::contractmeta;␊ + ␊ + contractmeta!(key="security_contact", val="security@example.com");␊ + ␊ #[contract]␊ pub struct Foo;␊ ` diff --git a/packages/core/stellar/src/contract.test.ts.snap b/packages/core/stellar/src/contract.test.ts.snap index de5945c23..bd748d120 100644 Binary files a/packages/core/stellar/src/contract.test.ts.snap and b/packages/core/stellar/src/contract.test.ts.snap differ diff --git a/packages/core/stellar/src/contract.ts b/packages/core/stellar/src/contract.ts index 05731075b..0d2def057 100644 --- a/packages/core/stellar/src/contract.ts +++ b/packages/core/stellar/src/contract.ts @@ -1,8 +1,8 @@ import { toIdentifier } from './utils/convert-strings'; export interface Contract { + metadata: Map; license: string; - securityContact: string; documentations: string[]; name: string; useClauses: UseClause[]; @@ -74,7 +74,7 @@ export interface Argument { export class ContractBuilder implements Contract { readonly name: string; license = 'MIT'; - securityContact = ''; + metadata = new Map(); ownable = false; readonly documentations: string[] = []; @@ -267,7 +267,13 @@ export class ContractBuilder implements Contract { this.documentations.push(description); } - addSecurityTag(securityContact: string) { - this.securityContact = securityContact; + addContractMetadata(metadata: { key: string; value: string }[] | { key: string; value: string }) { + (Array.isArray(metadata) ? metadata : [metadata]).forEach(({ key, value }) => { + if (this.metadata.has(key)) throw new Error(`Setting metadata failed: ${key} is already set`); + + this.metadata.set(key, value); + }); + + if (this.metadata.size > 0) this.addUseClause('soroban_sdk', 'contractmeta'); } } diff --git a/packages/core/stellar/src/fungible.compile.test.ts b/packages/core/stellar/src/fungible.compile.test.ts index c77bf5607..40e030500 100644 --- a/packages/core/stellar/src/fungible.compile.test.ts +++ b/packages/core/stellar/src/fungible.compile.test.ts @@ -290,3 +290,24 @@ test.serial( { snapshotResult: false }, ), ); + +test.serial( + 'compilation fungible upgradable mintable pausable with security contact metadata', + runRustCompilationTest( + buildFungible, + { + kind: 'Fungible', + name: 'MyToken', + symbol: 'MTK', + premint: '2000', + burnable: false, + mintable: true, + pausable: true, + upgradeable: true, + info: { + securityContact: 'contact@security.lol', + }, + }, + { snapshotResult: false }, + ), +); diff --git a/packages/core/stellar/src/print.ts b/packages/core/stellar/src/print.ts index ce0058440..78adcabbb 100644 --- a/packages/core/stellar/src/print.ts +++ b/packages/core/stellar/src/print.ts @@ -4,6 +4,7 @@ import type { Lines } from './utils/format-lines'; import { formatLines, spaceBetween } from './utils/format-lines'; import { getSelfArg } from './common-options'; import { compatibleContractsSemver } from './utils/version'; +import { toByteArray } from './utils/convert-strings'; const DEFAULT_SECTION = '1. with no section'; const STANDALONE_IMPORTS_GROUP = 'Standalone Imports'; @@ -24,11 +25,11 @@ export function printContract(contract: Contract): string { `// SPDX-License-Identifier: ${contract.license}`, `// Compatible with OpenZeppelin Stellar Soroban Contracts ${compatibleContractsSemver}`, ...(contract.documentations.length ? ['', ...printDocumentations(contract.documentations)] : []), - ...(contract.securityContact ? ['', ...printSecurityTag(contract.securityContact)] : []), ...createLevelAttributes, ], spaceBetween( printUseClauses(contract), + printMetadata(contract), printVariables(contract), printContractStruct(contract), printContractErrors(contract), @@ -367,6 +368,8 @@ function printDocumentations(documentations: string[]): string[] { return documentations.map(documentation => `//! ${documentation}`); } -function printSecurityTag(securityContact: string) { - return ['//! # Security', '//!', `//! For security issues, please contact: ${securityContact}`]; +function printMetadata(contract: Contract) { + return Array.from(contract.metadata.entries()).map( + ([key, value]) => `contractmeta!(key="${key}", val="${toByteArray(value)}");`, + ); } diff --git a/packages/core/stellar/src/set-info.ts b/packages/core/stellar/src/set-info.ts index c22f9e6ac..9a71f7def 100644 --- a/packages/core/stellar/src/set-info.ts +++ b/packages/core/stellar/src/set-info.ts @@ -1,7 +1,5 @@ import type { ContractBuilder } from './contract'; -export const TAG_SECURITY_CONTACT = `@custom:security-contact`; - export const infoOptions = [{}, { license: 'WTFPL' }] as const; export const defaults: Info = { license: 'MIT' }; @@ -11,14 +9,8 @@ export type Info = { securityContact?: string; }; -export function setInfo(c: ContractBuilder, info: Info): void { - const { securityContact, license } = info; - - if (securityContact) { - c.addSecurityTag(securityContact); - } +export function setInfo(c: ContractBuilder, { securityContact, license }: Info): void { + if (securityContact) c.addContractMetadata({ key: 'security_contact', value: securityContact }); - if (license) { - c.license = license; - } + if (license) c.license = license; }