diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 80e840577..c6749ef3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: if: matrix.package == 'solidity' uses: foundry-rs/foundry-toolchain@v1 - name: Install dependencies - run: yarn install --network-concurrency 1 + run: yarn install - name: Compile TypeScript run: yarn tsc working-directory: packages/core/${{matrix.package}} diff --git a/netlify.toml b/netlify.toml index eb5616b93..1dada69e3 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,9 +4,6 @@ publish = "packages/ui/public" edge_functions = "packages/ui/api" -[build.environment] - YARN_FLAGS = "--network-concurrency 1" - [[edge_functions]] path = "/ai" function = "ai" \ No newline at end of file diff --git a/packages/core/solidity/CHANGELOG.md b/packages/core/solidity/CHANGELOG.md index f6dd00f77..746dde13a 100644 --- a/packages/core/solidity/CHANGELOG.md +++ b/packages/core/solidity/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.3 (2025-03-13) + +- Add ERC20 Cross-Chain Bridging, SuperchainERC20. ([#436](https://github.com/OpenZeppelin/contracts-wizard/pull/436)) +**Note:** Cross-Chain Bridging is experimental and may be subject to change. + +- **Potentially breaking changes**: + - Change order of constructor argument `recipient` when using `premint`. + ## 0.5.2 (2025-02-21) - Fix modifiers order to follow Solidity style guides. ([#450](https://github.com/OpenZeppelin/contracts-wizard/pull/450)) @@ -8,7 +16,7 @@ ## 0.5.1 (2025-02-05) - **Potentially breaking changes**: - - Add constructor argument `recipient` when using `premint` in `erc20`, `stablecoin`, and `realWorldAsset`. + - Add constructor argument `recipient` when using `premint` in `erc20`, `stablecoin`, and `realWorldAsset`. ([#435](https://github.com/OpenZeppelin/contracts-wizard/pull/435)) ## 0.5.0 (2025-01-23) diff --git a/packages/core/solidity/package.json b/packages/core/solidity/package.json index 968fd114a..9b48718a3 100644 --- a/packages/core/solidity/package.json +++ b/packages/core/solidity/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard", - "version": "0.5.2", + "version": "0.5.3", "description": "A boilerplate generator to get started with OpenZeppelin Contracts", "license": "AGPL-3.0-only", "repository": "https://github.com/OpenZeppelin/contracts-wizard", diff --git a/packages/core/solidity/src/erc20.test.ts b/packages/core/solidity/src/erc20.test.ts index 84409b78d..14a2caa46 100644 --- a/packages/core/solidity/src/erc20.test.ts +++ b/packages/core/solidity/src/erc20.test.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { OptionsError } from '.'; import { erc20 } from '.'; import type { ERC20Options } from './erc20'; @@ -98,6 +99,126 @@ testERC20('erc20 flashmint', { flashmint: true, }); +testERC20('erc20 crossChainBridging custom', { + crossChainBridging: 'custom', +}); + +testERC20('erc20 crossChainBridging custom ownable', { + crossChainBridging: 'custom', + access: 'ownable', +}); + +testERC20('erc20 crossChainBridging custom ownable mintable burnable', { + crossChainBridging: 'custom', + access: 'ownable', + mintable: true, + burnable: true, +}); + +testERC20('erc20 crossChainBridging custom roles', { + crossChainBridging: 'custom', + access: 'roles', +}); + +testERC20('erc20 crossChainBridging custom managed', { + crossChainBridging: 'custom', + access: 'managed', +}); + +testERC20('erc20 crossChainBridging superchain', { + crossChainBridging: 'superchain', +}); + +testERC20('erc20 crossChainBridging superchain ownable', { + crossChainBridging: 'superchain', + access: 'ownable', +}); + +testERC20('erc20 crossChainBridging superchain roles', { + crossChainBridging: 'superchain', + access: 'roles', +}); + +testERC20('erc20 crossChainBridging superchain managed', { + crossChainBridging: 'superchain', + access: 'managed', +}); + +test('erc20 crossChainBridging custom, upgradeable not allowed', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + crossChainBridging: 'custom', + upgradeable: 'transparent', + }), + ); + t.is( + (error as OptionsError).messages.crossChainBridging, + 'Upgradeability is not currently supported with Cross-Chain Bridging', + ); +}); + +test('erc20 crossChainBridging superchain, upgradeable not allowed', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + crossChainBridging: 'superchain', + upgradeable: 'transparent', + }), + ); + t.is( + (error as OptionsError).messages.crossChainBridging, + 'Upgradeability is not currently supported with Cross-Chain Bridging', + ); +}); + +test('erc20 crossChainBridging superchain, premintChainId required', async t => { + const error = t.throws(() => + buildERC20({ + name: 'MyToken', + symbol: 'MTK', + crossChainBridging: 'superchain', + premint: '2000', + }), + ); + t.is( + (error as OptionsError).messages.premintChainId, + 'Chain ID is required when using Premint with Cross-Chain Bridging', + ); +}); + +testERC20('erc20 premint ignores chainId when not crossChainBridging', { + premint: '2000', + premintChainId: '10', +}); + +testERC20('erc20 premint chainId crossChainBridging custom', { + premint: '2000', + premintChainId: '10', + crossChainBridging: 'custom', +}); + +testERC20('erc20 premint chainId crossChainBridging superchain', { + premint: '2000', + premintChainId: '10', + crossChainBridging: 'superchain', +}); + +testERC20('erc20 full crossChainBridging custom non-upgradeable', { + premint: '2000', + access: 'roles', + burnable: true, + mintable: true, + pausable: true, + permit: true, + votes: true, + flashmint: true, + crossChainBridging: 'custom', + premintChainId: '10', +}); + testERC20('erc20 full upgradeable transparent', { premint: '2000', access: 'roles', diff --git a/packages/core/solidity/src/erc20.test.ts.md b/packages/core/solidity/src/erc20.test.ts.md index 4adca6d9e..81a604339 100644 --- a/packages/core/solidity/src/erc20.test.ts.md +++ b/packages/core/solidity/src/erc20.test.ts.md @@ -439,6 +439,492 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## erc20 crossChainBridging custom + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ + address public immutable TOKEN_BRIDGE;␊ + error Unauthorized();␊ + ␊ + constructor(address tokenBridge)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + {␊ + require(tokenBridge != address(0), "Invalid TOKEN_BRIDGE address");␊ + TOKEN_BRIDGE = tokenBridge;␊ + }␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + if (caller != TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging custom ownable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, Ownable {␊ + address public immutable TOKEN_BRIDGE;␊ + error Unauthorized();␊ + ␊ + constructor(address tokenBridge, address initialOwner)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + Ownable(initialOwner)␊ + {␊ + require(tokenBridge != address(0), "Invalid TOKEN_BRIDGE address");␊ + TOKEN_BRIDGE = tokenBridge;␊ + }␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + if (caller != TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging custom ownable mintable burnable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Burnable, Ownable, ERC20Permit {␊ + address public immutable TOKEN_BRIDGE;␊ + error Unauthorized();␊ + ␊ + constructor(address tokenBridge, address initialOwner)␊ + ERC20("MyToken", "MTK")␊ + Ownable(initialOwner)␊ + ERC20Permit("MyToken")␊ + {␊ + require(tokenBridge != address(0), "Invalid TOKEN_BRIDGE address");␊ + TOKEN_BRIDGE = tokenBridge;␊ + }␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + if (caller != TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyOwner {␊ + _mint(to, amount);␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging custom roles + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, AccessControl, ERC20Permit {␊ + bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE");␊ + error Unauthorized();␊ + ␊ + constructor(address defaultAdmin, address tokenBridge)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + {␊ + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ + _grantRole(TOKEN_BRIDGE_ROLE, tokenBridge);␊ + }␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + if (!hasRole(TOKEN_BRIDGE_ROLE, caller)) revert Unauthorized();␊ + }␊ + ␊ + // The following functions are overrides required by Solidity.␊ + ␊ + function supportsInterface(bytes4 interfaceId)␊ + public␊ + view␊ + override(ERC20Bridgeable, AccessControl)␊ + returns (bool)␊ + {␊ + return super.supportsInterface(interfaceId);␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging custom managed + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";␊ + import {AuthorityUtils} from "@openzeppelin/contracts/access/manager/AuthorityUtils.sol";␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, AccessManaged, ERC20Permit {␊ + error Unauthorized();␊ + ␊ + constructor(address initialAuthority)␊ + ERC20("MyToken", "MTK")␊ + AccessManaged(initialAuthority)␊ + ERC20Permit("MyToken")␊ + {}␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + (bool immediate,) = AuthorityUtils.canCallWithDelay(authority(), caller, address(this), bytes4(_msgData()[0:4]));␊ + if (!immediate) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging superchain + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ + address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + error Unauthorized();␊ + ␊ + constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {}␊ + ␊ + /**␊ + * @dev Checks if the caller is the predeployed SuperchainTokenBridge. Reverts otherwise.␊ + *␊ + * IMPORTANT: The predeployed SuperchainTokenBridge is only available on chains in the Superchain.␊ + */␊ + function _checkTokenBridge(address caller) internal pure override {␊ + if (caller != SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging superchain ownable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, Ownable {␊ + address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + error Unauthorized();␊ + ␊ + constructor(address initialOwner)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + Ownable(initialOwner)␊ + {}␊ + ␊ + /**␊ + * @dev Checks if the caller is the predeployed SuperchainTokenBridge. Reverts otherwise.␊ + *␊ + * IMPORTANT: The predeployed SuperchainTokenBridge is only available on chains in the Superchain.␊ + */␊ + function _checkTokenBridge(address caller) internal pure override {␊ + if (caller != SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging superchain roles + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, AccessControl {␊ + address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + error Unauthorized();␊ + ␊ + constructor(address defaultAdmin)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + {␊ + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ + }␊ + ␊ + /**␊ + * @dev Checks if the caller is the predeployed SuperchainTokenBridge. Reverts otherwise.␊ + *␊ + * IMPORTANT: The predeployed SuperchainTokenBridge is only available on chains in the Superchain.␊ + */␊ + function _checkTokenBridge(address caller) internal pure override {␊ + if (caller != SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + ␊ + // The following functions are overrides required by Solidity.␊ + ␊ + function supportsInterface(bytes4 interfaceId)␊ + public␊ + view␊ + override(ERC20Bridgeable, AccessControl)␊ + returns (bool)␊ + {␊ + return super.supportsInterface(interfaceId);␊ + }␊ + }␊ + ` + +## erc20 crossChainBridging superchain managed + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, AccessManaged {␊ + address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + error Unauthorized();␊ + ␊ + constructor(address initialAuthority)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + AccessManaged(initialAuthority)␊ + {}␊ + ␊ + /**␊ + * @dev Checks if the caller is the predeployed SuperchainTokenBridge. Reverts otherwise.␊ + *␊ + * IMPORTANT: The predeployed SuperchainTokenBridge is only available on chains in the Superchain.␊ + */␊ + function _checkTokenBridge(address caller) internal pure override {␊ + if (caller != SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 premint ignores chainId when not crossChainBridging + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Permit {␊ + constructor(address recipient)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ + }␊ + }␊ + ` + +## erc20 premint chainId crossChainBridging custom + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ + address public immutable TOKEN_BRIDGE;␊ + error Unauthorized();␊ + ␊ + constructor(address tokenBridge, address recipient)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + {␊ + require(tokenBridge != address(0), "Invalid TOKEN_BRIDGE address");␊ + TOKEN_BRIDGE = tokenBridge;␊ + if (block.chainid == 10) {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ + }␊ + }␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + if (caller != TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 premint chainId crossChainBridging superchain + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ + address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + error Unauthorized();␊ + ␊ + constructor(address recipient)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + {␊ + if (block.chainid == 10) {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ + }␊ + }␊ + ␊ + /**␊ + * @dev Checks if the caller is the predeployed SuperchainTokenBridge. Reverts otherwise.␊ + *␊ + * IMPORTANT: The predeployed SuperchainTokenBridge is only available on chains in the Superchain.␊ + */␊ + function _checkTokenBridge(address caller) internal pure override {␊ + if (caller != SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();␊ + }␊ + }␊ + ` + +## erc20 full crossChainBridging custom non-upgradeable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";␊ + import {ERC20FlashMint} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol";␊ + import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";␊ + import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Bridgeable, AccessControl, ERC20Burnable, ERC20Pausable, ERC20Permit, ERC20Votes, ERC20FlashMint {␊ + bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE");␊ + error Unauthorized();␊ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊ + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊ + ␊ + constructor(address defaultAdmin, address tokenBridge, address recipient, address pauser, address minter)␊ + ERC20("MyToken", "MTK")␊ + ERC20Permit("MyToken")␊ + {␊ + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ + _grantRole(TOKEN_BRIDGE_ROLE, tokenBridge);␊ + if (block.chainid == 10) {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ + }␊ + _grantRole(PAUSER_ROLE, pauser);␊ + _grantRole(MINTER_ROLE, minter);␊ + }␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + if (!hasRole(TOKEN_BRIDGE_ROLE, caller)) revert Unauthorized();␊ + }␊ + ␊ + function pause() public onlyRole(PAUSER_ROLE) {␊ + _pause();␊ + }␊ + ␊ + function unpause() public onlyRole(PAUSER_ROLE) {␊ + _unpause();␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {␊ + _mint(to, amount);␊ + }␊ + ␊ + // The following functions are overrides required by Solidity.␊ + ␊ + function _update(address from, address to, uint256 value)␊ + internal␊ + override(ERC20, ERC20Pausable, ERC20Votes)␊ + {␊ + super._update(from, to, value);␊ + }␊ + ␊ + function supportsInterface(bytes4 interfaceId)␊ + public␊ + view␊ + override(ERC20Bridgeable, AccessControl)␊ + returns (bool)␊ + {␊ + return super.supportsInterface(interfaceId);␊ + }␊ + ␊ + function nonces(address owner)␊ + public␊ + view␊ + override(ERC20Permit, Nonces)␊ + returns (uint256)␊ + {␊ + return super.nonces(owner);␊ + }␊ + }␊ + ` + ## erc20 full upgradeable transparent > Snapshot 1 @@ -466,7 +952,7 @@ Generated by [AVA](https://avajs.dev). _disableInitializers();␊ }␊ ␊ - function initialize(address defaultAdmin, address pauser, address recipient, address minter)␊ + function initialize(address recipient, address defaultAdmin, address pauser, address minter)␊ public initializer␊ {␊ __ERC20_init("MyToken", "MTK");␊ @@ -477,9 +963,9 @@ Generated by [AVA](https://avajs.dev). __ERC20Votes_init();␊ __ERC20FlashMint_init();␊ ␊ + _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ _grantRole(PAUSER_ROLE, pauser);␊ - _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(MINTER_ROLE, minter);␊ }␊ ␊ @@ -544,7 +1030,7 @@ Generated by [AVA](https://avajs.dev). _disableInitializers();␊ }␊ ␊ - function initialize(address defaultAdmin, address pauser, address recipient, address minter, address upgrader)␊ + function initialize(address recipient, address defaultAdmin, address pauser, address minter, address upgrader)␊ public initializer␊ {␊ __ERC20_init("MyToken", "MTK");␊ @@ -556,9 +1042,9 @@ Generated by [AVA](https://avajs.dev). __ERC20FlashMint_init();␊ __UUPSUpgradeable_init();␊ ␊ + _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ _grantRole(PAUSER_ROLE, pauser);␊ - _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(MINTER_ROLE, minter);␊ _grantRole(UPGRADER_ROLE, upgrader);␊ }␊ @@ -626,7 +1112,7 @@ Generated by [AVA](https://avajs.dev). _disableInitializers();␊ }␊ ␊ - function initialize(address initialAuthority, address recipient)␊ + function initialize(address recipient, address initialAuthority)␊ public initializer␊ {␊ __ERC20_init("MyToken", "MTK");␊ diff --git a/packages/core/solidity/src/erc20.test.ts.snap b/packages/core/solidity/src/erc20.test.ts.snap index 65a672dc8..c17d6fdaa 100644 Binary files a/packages/core/solidity/src/erc20.test.ts.snap and b/packages/core/solidity/src/erc20.test.ts.snap differ diff --git a/packages/core/solidity/src/erc20.ts b/packages/core/solidity/src/erc20.ts index 3b0f3419b..df7246a76 100644 --- a/packages/core/solidity/src/erc20.ts +++ b/packages/core/solidity/src/erc20.ts @@ -5,11 +5,17 @@ import { addPauseFunctions } from './add-pausable'; import { defineFunctions } from './utils/define-functions'; import type { CommonOptions } from './common-options'; import { withCommonDefaults, defaults as commonDefaults } from './common-options'; +import type { Upgradeable } from './set-upgradeable'; import { setUpgradeable } from './set-upgradeable'; import { setInfo } from './set-info'; import { printContract } from './print'; import type { ClockMode } from './set-clock-mode'; import { clockModeDefault, setClockMode } from './set-clock-mode'; +import { supportsInterface } from './common-functions'; +import { OptionsError } from './error'; + +export const crossChainBridgingOptions = [false, 'custom', 'superchain'] as const; +export type CrossChainBridging = (typeof crossChainBridgingOptions)[number]; export interface ERC20Options extends CommonOptions { name: string; @@ -17,6 +23,7 @@ export interface ERC20Options extends CommonOptions { burnable?: boolean; pausable?: boolean; premint?: string; + premintChainId?: string; mintable?: boolean; permit?: boolean; /** @@ -25,6 +32,7 @@ export interface ERC20Options extends CommonOptions { */ votes?: boolean | ClockMode; flashmint?: boolean; + crossChainBridging?: CrossChainBridging; } export const defaults: Required = { @@ -33,10 +41,12 @@ export const defaults: Required = { burnable: false, pausable: false, premint: '0', + premintChainId: '', mintable: false, permit: true, votes: false, flashmint: false, + crossChainBridging: false, access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, info: commonDefaults.info, @@ -49,10 +59,12 @@ export function withDefaults(opts: ERC20Options): Required { burnable: opts.burnable ?? defaults.burnable, pausable: opts.pausable ?? defaults.pausable, premint: opts.premint || defaults.premint, + premintChainId: opts.premintChainId || defaults.premintChainId, mintable: opts.mintable ?? defaults.mintable, permit: opts.permit ?? defaults.permit, votes: opts.votes ?? defaults.votes, flashmint: opts.flashmint ?? defaults.flashmint, + crossChainBridging: opts.crossChainBridging ?? defaults.crossChainBridging, }; } @@ -73,6 +85,14 @@ export function buildERC20(opts: ERC20Options): ContractBuilder { addBase(c, allOpts.name, allOpts.symbol); + if (allOpts.crossChainBridging) { + addCrossChainBridging(c, allOpts.crossChainBridging, allOpts.upgradeable, access); + } + + if (allOpts.premint) { + addPremint(c, allOpts.premint, allOpts.premintChainId, allOpts.crossChainBridging); + } + if (allOpts.burnable) { addBurnable(c); } @@ -81,10 +101,6 @@ export function buildERC20(opts: ERC20Options): ContractBuilder { addPausableExtension(c, access); } - if (allOpts.premint) { - addPremint(c, allOpts.premint); - } - if (allOpts.mintable) { addMintable(c, access); } @@ -141,7 +157,18 @@ function addBurnable(c: ContractBuilder) { export const premintPattern = /^(\d*)(?:\.(\d+))?(?:e(\d+))?$/; -function addPremint(c: ContractBuilder, amount: string) { +export const chainIdPattern = /^(?!$)[1-9]\d*$/; + +export function isValidChainId(str: string): boolean { + return chainIdPattern.test(str); +} + +function addPremint( + c: ContractBuilder, + amount: string, + premintChainId: string, + crossChainBridging: CrossChainBridging, +) { const m = amount.match(premintPattern); if (m) { const integer = m[1]?.replace(/^0+/, '') ?? ''; @@ -153,9 +180,35 @@ function addPremint(c: ContractBuilder, amount: string) { const zeroes = new Array(Math.max(0, -decimalPlace)).fill('0').join(''); const units = integer + decimals + zeroes; const exp = decimalPlace <= 0 ? 'decimals()' : `(decimals() - ${decimalPlace})`; + c.addConstructorArgument({ type: 'address', name: 'recipient' }); - c.addConstructorCode(`_mint(recipient, ${units} * 10 ** ${exp});`); + + const mintLine = `_mint(recipient, ${units} * 10 ** ${exp});`; + + if (crossChainBridging) { + if (premintChainId === '') { + throw new OptionsError({ + premintChainId: 'Chain ID is required when using Premint with Cross-Chain Bridging', + }); + } + + if (!isValidChainId(premintChainId)) { + throw new OptionsError({ + premintChainId: 'Not a valid chain ID', + }); + } + + c.addConstructorCode(`if (block.chainid == ${premintChainId}) {`); + c.addConstructorCode(` ${mintLine}`); + c.addConstructorCode(`}`); + } else { + c.addConstructorCode(mintLine); + } } + } else { + throw new OptionsError({ + premint: 'Not a valid number', + }); } } @@ -206,6 +259,113 @@ function addFlashMint(c: ContractBuilder) { }); } +function addCrossChainBridging( + c: ContractBuilder, + crossChainBridging: 'custom' | 'superchain', + upgradeable: Upgradeable, + access: Access, +) { + const ERC20Bridgeable = { + name: 'ERC20Bridgeable', + path: `@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol`, + }; + + c.addParent(ERC20Bridgeable); + c.addOverride(ERC20Bridgeable, supportsInterface); + + if (upgradeable) { + throw new OptionsError({ + crossChainBridging: 'Upgradeability is not currently supported with Cross-Chain Bridging', + }); + } + + c.addOverride(ERC20Bridgeable, functions._checkTokenBridge); + switch (crossChainBridging) { + case 'custom': + addCustomBridging(c, access); + break; + case 'superchain': + addSuperchainERC20(c); + break; + default: { + const _: never = crossChainBridging; + throw new Error('Unknown value for `crossChainBridging`'); + } + } + c.addVariable('error Unauthorized();'); +} + +function addCustomBridging(c: ContractBuilder, access: Access) { + switch (access) { + case false: + case 'ownable': { + const addedBridgeImmutable = c.addVariable(`address public immutable TOKEN_BRIDGE;`); + if (addedBridgeImmutable) { + c.addConstructorArgument({ type: 'address', name: 'tokenBridge' }); + c.addConstructorCode(`require(tokenBridge != address(0), "Invalid TOKEN_BRIDGE address");`); + c.addConstructorCode(`TOKEN_BRIDGE = tokenBridge;`); + } + c.setFunctionBody([`if (caller != TOKEN_BRIDGE) revert Unauthorized();`], functions._checkTokenBridge, 'view'); + break; + } + case 'roles': { + setAccessControl(c, access); + const roleOwner = 'tokenBridge'; + const roleId = 'TOKEN_BRIDGE_ROLE'; + const addedRoleConstant = c.addVariable(`bytes32 public constant ${roleId} = keccak256("${roleId}");`); + if (addedRoleConstant) { + c.addConstructorArgument({ type: 'address', name: roleOwner }); + c.addConstructorCode(`_grantRole(${roleId}, ${roleOwner});`); + } + c.setFunctionBody( + [`if (!hasRole(${roleId}, caller)) revert Unauthorized();`], + functions._checkTokenBridge, + 'view', + ); + break; + } + case 'managed': { + setAccessControl(c, access); + c.addImportOnly({ + name: 'AuthorityUtils', + path: `@openzeppelin/contracts/access/manager/AuthorityUtils.sol`, + }); + c.setFunctionBody( + [ + `(bool immediate,) = AuthorityUtils.canCallWithDelay(authority(), caller, address(this), bytes4(_msgData()[0:4]));`, + `if (!immediate) revert Unauthorized();`, + ], + functions._checkTokenBridge, + 'view', + ); + break; + } + default: { + const _: never = access; + throw new Error('Unknown value for `access`'); + } + } +} + +function addSuperchainERC20(c: ContractBuilder) { + c.addVariable('address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;'); + c.setFunctionBody( + ['if (caller != SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();'], + functions._checkTokenBridge, + 'pure', + ); + c.setFunctionComments( + [ + '/**', + ' * @dev Checks if the caller is the predeployed SuperchainTokenBridge. Reverts otherwise.', + ' *', + ' * IMPORTANT: The predeployed SuperchainTokenBridge is only available on chains in the Superchain.', + ' */', + ], + functions._checkTokenBridge, + ); +} + export const functions = defineFunctions({ _update: { kind: 'internal' as const, @@ -255,4 +415,9 @@ export const functions = defineFunctions({ returns: ['uint256'], mutability: 'view' as const, }, + + _checkTokenBridge: { + kind: 'internal' as const, + args: [{ name: 'caller', type: 'address' }], + }, }); diff --git a/packages/core/solidity/src/error.ts b/packages/core/solidity/src/error.ts index 96eb23489..a26433319 100644 --- a/packages/core/solidity/src/error.ts +++ b/packages/core/solidity/src/error.ts @@ -2,6 +2,6 @@ export type OptionsErrorMessages = { [prop in string]?: string }; export class OptionsError extends Error { constructor(readonly messages: OptionsErrorMessages) { - super('Invalid options for Governor'); + super('Invalid options'); } } diff --git a/packages/core/solidity/src/generate/erc20.ts b/packages/core/solidity/src/generate/erc20.ts index 3db65e95f..9f48e16d5 100644 --- a/packages/core/solidity/src/generate/erc20.ts +++ b/packages/core/solidity/src/generate/erc20.ts @@ -1,4 +1,4 @@ -import type { ERC20Options } from '../erc20'; +import { crossChainBridgingOptions, type ERC20Options } from '../erc20'; import { accessOptions } from '../set-access-control'; import { clockModeOptions } from '../set-clock-mode'; import { infoOptions } from '../set-info'; @@ -17,11 +17,18 @@ const blueprint = { votes: [...booleans, ...clockModeOptions] as const, flashmint: booleans, premint: ['1'], + premintChainId: ['10'], + crossChainBridging: crossChainBridgingOptions, access: accessOptions, upgradeable: upgradeableOptions, info: infoOptions, }; export function* generateERC20Options(): Generator> { - yield* generateAlternatives(blueprint); + for (const opts of generateAlternatives(blueprint)) { + // crossChainBridging does not currently support upgradeable + if (!(opts.crossChainBridging && opts.upgradeable)) { + yield opts; + } + } } diff --git a/packages/core/solidity/src/generate/stablecoin.ts b/packages/core/solidity/src/generate/stablecoin.ts index 8cbcd1adc..8eba148fd 100644 --- a/packages/core/solidity/src/generate/stablecoin.ts +++ b/packages/core/solidity/src/generate/stablecoin.ts @@ -1,31 +1,50 @@ -import type { StablecoinOptions } from '../stablecoin'; +import { crossChainBridgingOptions } from '../erc20'; import { accessOptions } from '../set-access-control'; -import { clockModeOptions } from '../set-clock-mode'; import { infoOptions } from '../set-info'; -import { upgradeableOptions } from '../set-upgradeable'; +import type { StablecoinOptions } from '../stablecoin'; import { generateAlternatives } from './alternatives'; const booleans = [true, false]; -const blueprint = { +const erc20Basic = { name: ['MyStablecoin'], symbol: ['MST'], - burnable: booleans, - pausable: booleans, - mintable: booleans, - permit: booleans, - limitations: [false, 'allowlist', 'blocklist'] as const, - votes: [...booleans, ...clockModeOptions] as const, - flashmint: booleans, + burnable: [false] as const, + pausable: [false] as const, + mintable: [false] as const, + permit: [false] as const, + votes: [false] as const, + flashmint: [false] as const, premint: ['1'], - custodian: booleans, + premintChainId: [''], + crossChainBridging: [false] as const, + access: [false] as const, + info: [{}] as const, +}; + +const erc20Full = { + name: ['MyStablecoin'], + symbol: ['MST'], + burnable: [true] as const, + pausable: [true] as const, + mintable: [true] as const, + permit: [true] as const, + votes: ['timestamp'] as const, + flashmint: [true] as const, + premint: ['1'], + premintChainId: ['10'], + crossChainBridging: crossChainBridgingOptions, access: accessOptions, - upgradeable: upgradeableOptions, info: infoOptions, }; +const stablecoinExtensions = { + limitations: [false, 'allowlist', 'blocklist'] as const, + custodian: booleans, + upgradeable: [false] as const, +}; + export function* generateStablecoinOptions(): Generator> { - for (const opts of generateAlternatives(blueprint)) { - yield { ...opts, upgradeable: false }; - } + yield* generateAlternatives({ ...erc20Basic, ...stablecoinExtensions }); + yield* generateAlternatives({ ...erc20Full, ...stablecoinExtensions }); } diff --git a/packages/core/solidity/src/index.ts b/packages/core/solidity/src/index.ts index 4b4bba141..189d5743a 100644 --- a/packages/core/solidity/src/index.ts +++ b/packages/core/solidity/src/index.ts @@ -10,7 +10,7 @@ export type { Access } from './set-access-control'; export type { Upgradeable } from './set-upgradeable'; export type { Info } from './set-info'; -export { premintPattern } from './erc20'; +export { premintPattern, chainIdPattern } from './erc20'; export { defaults as infoDefaults } from './set-info'; export type { OptionsErrorMessages } from './error'; diff --git a/packages/core/solidity/src/stablecoin.test.ts b/packages/core/solidity/src/stablecoin.test.ts index b5a837863..edcc1b735 100644 --- a/packages/core/solidity/src/stablecoin.test.ts +++ b/packages/core/solidity/src/stablecoin.test.ts @@ -110,6 +110,23 @@ testStablecoin('stablecoin flashmint', { flashmint: true, }); +testStablecoin('stablecoin full', { + name: 'MyStablecoin', + symbol: 'MST', + premint: '2000', + access: 'roles', + burnable: true, + mintable: true, + pausable: true, + permit: true, + votes: true, + flashmint: true, + crossChainBridging: 'custom', + premintChainId: '10', + limitations: 'allowlist', + custodian: true, +}); + testAPIEquivalence('stablecoin API default'); testAPIEquivalence('stablecoin API basic', { @@ -128,6 +145,8 @@ testAPIEquivalence('stablecoin API full', { permit: true, votes: true, flashmint: true, + crossChainBridging: 'custom', + premintChainId: '10', limitations: 'allowlist', custodian: true, }); diff --git a/packages/core/solidity/src/stablecoin.test.ts.md b/packages/core/solidity/src/stablecoin.test.ts.md index 2a7720ac0..5d6de2c37 100644 --- a/packages/core/solidity/src/stablecoin.test.ts.md +++ b/packages/core/solidity/src/stablecoin.test.ts.md @@ -565,3 +565,110 @@ Generated by [AVA](https://avajs.dev). constructor() ERC20("MyStablecoin", "MST") ERC20Permit("MyStablecoin") {}␊ }␊ ` + +## stablecoin full + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.22;␊ + ␊ + import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Allowlist} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Allowlist.sol";␊ + import {ERC20Bridgeable} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Bridgeable.sol";␊ + import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";␊ + import {ERC20Custodian} from "@openzeppelin/community-contracts/contracts/token/ERC20/extensions/ERC20Custodian.sol";␊ + import {ERC20FlashMint} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol";␊ + import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";␊ + import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";␊ + ␊ + contract MyStablecoin is ERC20, ERC20Bridgeable, AccessControl, ERC20Burnable, ERC20Pausable, ERC20Permit, ERC20Votes, ERC20FlashMint, ERC20Custodian, ERC20Allowlist {␊ + bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE");␊ + error Unauthorized();␊ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊ + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊ + bytes32 public constant CUSTODIAN_ROLE = keccak256("CUSTODIAN_ROLE");␊ + bytes32 public constant LIMITER_ROLE = keccak256("LIMITER_ROLE");␊ + ␊ + constructor(address defaultAdmin, address tokenBridge, address recipient, address pauser, address minter, address custodian, address limiter)␊ + ERC20("MyStablecoin", "MST")␊ + ERC20Permit("MyStablecoin")␊ + {␊ + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ + _grantRole(TOKEN_BRIDGE_ROLE, tokenBridge);␊ + if (block.chainid == 10) {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ + }␊ + _grantRole(PAUSER_ROLE, pauser);␊ + _grantRole(MINTER_ROLE, minter);␊ + _grantRole(CUSTODIAN_ROLE, custodian);␊ + _grantRole(LIMITER_ROLE, limiter);␊ + }␊ + ␊ + function _checkTokenBridge(address caller) internal view override {␊ + if (!hasRole(TOKEN_BRIDGE_ROLE, caller)) revert Unauthorized();␊ + }␊ + ␊ + function pause() public onlyRole(PAUSER_ROLE) {␊ + _pause();␊ + }␊ + ␊ + function unpause() public onlyRole(PAUSER_ROLE) {␊ + _unpause();␊ + }␊ + ␊ + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {␊ + _mint(to, amount);␊ + }␊ + ␊ + function _isCustodian(address user) internal view override returns (bool) {␊ + return hasRole(CUSTODIAN_ROLE, user);␊ + }␊ + ␊ + function allowUser(address user) public onlyRole(LIMITER_ROLE) {␊ + _allowUser(user);␊ + }␊ + ␊ + function disallowUser(address user) public onlyRole(LIMITER_ROLE) {␊ + _disallowUser(user);␊ + }␊ + ␊ + // The following functions are overrides required by Solidity.␊ + ␊ + function _update(address from, address to, uint256 value)␊ + internal␊ + override(ERC20, ERC20Pausable, ERC20Votes, ERC20Custodian, ERC20Allowlist)␊ + {␊ + super._update(from, to, value);␊ + }␊ + ␊ + function _approve(address owner, address spender, uint256 value, bool emitEvent)␊ + internal␊ + override(ERC20, ERC20Allowlist)␊ + {␊ + super._approve(owner, spender, value, emitEvent);␊ + }␊ + ␊ + function supportsInterface(bytes4 interfaceId)␊ + public␊ + view␊ + override(ERC20Bridgeable, AccessControl)␊ + returns (bool)␊ + {␊ + return super.supportsInterface(interfaceId);␊ + }␊ + ␊ + function nonces(address owner)␊ + public␊ + view␊ + override(ERC20Permit, Nonces)␊ + returns (uint256)␊ + {␊ + return super.nonces(owner);␊ + }␊ + }␊ + ` diff --git a/packages/core/solidity/src/stablecoin.test.ts.snap b/packages/core/solidity/src/stablecoin.test.ts.snap index ca8cf9053..c0503d7f1 100644 Binary files a/packages/core/solidity/src/stablecoin.test.ts.snap and b/packages/core/solidity/src/stablecoin.test.ts.snap differ diff --git a/packages/core/solidity/src/zip-foundry.test.ts.md b/packages/core/solidity/src/zip-foundry.test.ts.md index 42cb623e8..f14e98187 100644 --- a/packages/core/solidity/src/zip-foundry.test.ts.md +++ b/packages/core/solidity/src/zip-foundry.test.ts.md @@ -108,11 +108,11 @@ Generated by [AVA](https://avajs.dev). // TODO: Set addresses for the variables below, then uncomment the following section:␊ /*␊ vm.startBroadcast();␊ + address recipient = ;␊ address defaultAdmin = ;␊ address pauser = ;␊ - address recipient = ;␊ address minter = ;␊ - MyToken instance = new MyToken(defaultAdmin, pauser, recipient, minter);␊ + MyToken instance = new MyToken(recipient, defaultAdmin, pauser, minter);␊ console.log("Contract deployed to %s", address(instance));␊ vm.stopBroadcast();␊ */␊ @@ -136,13 +136,13 @@ Generated by [AVA](https://avajs.dev). bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊ ␊ - constructor(address defaultAdmin, address pauser, address recipient, address minter)␊ + constructor(address recipient, address defaultAdmin, address pauser, address minter)␊ ERC20("My Token", "MTK")␊ ERC20Permit("My Token")␊ {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ _grantRole(PAUSER_ROLE, pauser);␊ - _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(MINTER_ROLE, minter);␊ }␊ ␊ @@ -187,11 +187,11 @@ Generated by [AVA](https://avajs.dev). MyToken public instance;␊ ␊ function setUp() public {␊ - address defaultAdmin = vm.addr(1);␊ - address pauser = vm.addr(2);␊ - address recipient = vm.addr(3);␊ + address recipient = vm.addr(1);␊ + address defaultAdmin = vm.addr(2);␊ + address pauser = vm.addr(3);␊ address minter = vm.addr(4);␊ - instance = new MyToken(defaultAdmin, pauser, recipient, minter);␊ + instance = new MyToken(recipient, defaultAdmin, pauser, minter);␊ }␊ ␊ function testName() public view {␊ diff --git a/packages/core/solidity/src/zip-foundry.test.ts.snap b/packages/core/solidity/src/zip-foundry.test.ts.snap index 7ec62e30d..12d799427 100644 Binary files a/packages/core/solidity/src/zip-foundry.test.ts.snap and b/packages/core/solidity/src/zip-foundry.test.ts.snap differ diff --git a/packages/core/solidity/src/zip-hardhat.test.ts.md b/packages/core/solidity/src/zip-hardhat.test.ts.md index 17b7438f5..a7d55801f 100644 --- a/packages/core/solidity/src/zip-hardhat.test.ts.md +++ b/packages/core/solidity/src/zip-hardhat.test.ts.md @@ -26,13 +26,13 @@ Generated by [AVA](https://avajs.dev). bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊ ␊ - constructor(address defaultAdmin, address pauser, address recipient, address minter)␊ + constructor(address recipient, address defaultAdmin, address pauser, address minter)␊ ERC20("My Token", "MTK")␊ ERC20Permit("My Token")␊ {␊ + _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);␊ _grantRole(PAUSER_ROLE, pauser);␊ - _mint(recipient, 2000 * 10 ** decimals());␊ _grantRole(MINTER_ROLE, minter);␊ }␊ ␊ @@ -106,7 +106,7 @@ Generated by [AVA](https://avajs.dev). const ContractFactory = await ethers.getContractFactory("MyToken");␊ ␊ // TODO: Set addresses for the contract arguments below␊ - const instance = await ContractFactory.deploy(defaultAdmin, pauser, recipient, minter);␊ + const instance = await ContractFactory.deploy(recipient, defaultAdmin, pauser, minter);␊ await instance.waitForDeployment();␊ ␊ console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊ @@ -126,12 +126,12 @@ Generated by [AVA](https://avajs.dev). it("Test contract", async function () {␊ const ContractFactory = await ethers.getContractFactory("MyToken");␊ ␊ - const defaultAdmin = (await ethers.getSigners())[0].address;␊ - const pauser = (await ethers.getSigners())[1].address;␊ - const recipient = (await ethers.getSigners())[2].address;␊ + const recipient = (await ethers.getSigners())[0].address;␊ + const defaultAdmin = (await ethers.getSigners())[1].address;␊ + const pauser = (await ethers.getSigners())[2].address;␊ const minter = (await ethers.getSigners())[3].address;␊ ␊ - const instance = await ContractFactory.deploy(defaultAdmin, pauser, recipient, minter);␊ + const instance = await ContractFactory.deploy(recipient, defaultAdmin, pauser, minter);␊ await instance.waitForDeployment();␊ ␊ expect(await instance.name()).to.equal("My Token");␊ diff --git a/packages/core/solidity/src/zip-hardhat.test.ts.snap b/packages/core/solidity/src/zip-hardhat.test.ts.snap index 231d727b3..9169ca98d 100644 Binary files a/packages/core/solidity/src/zip-hardhat.test.ts.snap and b/packages/core/solidity/src/zip-hardhat.test.ts.snap differ diff --git a/packages/ui/src/cairo/App.svelte b/packages/ui/src/cairo/App.svelte index 6c5d57ac3..c49c87cc2 100644 --- a/packages/ui/src/cairo/App.svelte +++ b/packages/ui/src/cairo/App.svelte @@ -159,7 +159,7 @@
-
+
diff --git a/packages/ui/src/common/icons/OPIcon.svelte b/packages/ui/src/common/icons/OPIcon.svelte new file mode 100644 index 000000000..f90e200ee --- /dev/null +++ b/packages/ui/src/common/icons/OPIcon.svelte @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/packages/ui/src/common/post-message.ts b/packages/ui/src/common/post-message.ts index c5ddb81d9..9535c7b93 100644 --- a/packages/ui/src/common/post-message.ts +++ b/packages/ui/src/common/post-message.ts @@ -19,6 +19,8 @@ export interface UnsupportedVersionMessage { export interface DefenderDeployMessage { kind: 'oz-wizard-defender-deploy'; sources: SolcInputSources; + enforceDeterministicReason?: string; + groupNetworksBy?: 'superchain'; } export function postMessage(msg: Message) { diff --git a/packages/ui/src/common/styles/global.css b/packages/ui/src/common/styles/global.css index db2113924..1d4101988 100644 --- a/packages/ui/src/common/styles/global.css +++ b/packages/ui/src/common/styles/global.css @@ -149,4 +149,8 @@ input.input-inline { .hljs.-stylus{ background: linear-gradient(30deg, #181215, #360c1f); +} + +.light-link { + color: white; } \ No newline at end of file diff --git a/packages/ui/src/solidity/App.svelte b/packages/ui/src/solidity/App.svelte index edca97eea..1f3829f3d 100644 --- a/packages/ui/src/solidity/App.svelte +++ b/packages/ui/src/solidity/App.svelte @@ -93,10 +93,58 @@ $: code = printContract(contract); $: highlightedCode = injectHyperlinks(hljs.highlight('solidity', code).value); - $: if (showDeployModal) postMessageToIframe('defender-deploy', { - kind: 'oz-wizard-defender-deploy', - sources: getSolcSources(contract) - });; + $: hasErrors = errors[tab] !== undefined; + $: showDeployModal = !hasErrors && showDeployModal; + + $: if (showDeployModal) { + let enforceDeterministicReason: string | undefined; + let groupNetworksBy: 'superchain' | undefined; + + const isSuperchainERC20 = opts !== undefined && + (opts.kind === 'ERC20' || opts.kind === 'Stablecoin' || opts.kind === 'RealWorldAsset') && + opts.crossChainBridging === 'superchain'; + if (isSuperchainERC20) { + enforceDeterministicReason = 'SuperchainERC20 requires deploying your contract to the same address on every chain in the Superchain.'; + groupNetworksBy = 'superchain'; + } + + postMessageToIframe('defender-deploy', { + kind: 'oz-wizard-defender-deploy', + sources: getSolcSources(contract), + enforceDeterministicReason, + groupNetworksBy, + }); + } + + $: showButtons = getButtonVisiblities(opts); + + interface ButtonVisibilities { + openInRemix: boolean; + downloadHardhat: boolean; + downloadFoundry: boolean; + } + + const getButtonVisiblities = (opts?: KindedOptions[Kind]): ButtonVisibilities => { + if (opts?.kind === "Governor") { + return { + openInRemix: true, + downloadHardhat: false, + downloadFoundry: false, + } + } else if (opts?.kind === "Stablecoin" || opts?.kind === "RealWorldAsset" || (opts?.kind === "ERC20" && opts.crossChainBridging)) { + return { + openInRemix: false, + downloadHardhat: false, + downloadFoundry: false, + } + } else { + return { + openInRemix: true, + downloadHardhat: true, + downloadFoundry: true, + } + } + } const getSolcSources = (contract: Contract) => { const sources = getImports(contract); @@ -222,6 +270,49 @@
+ {#if hasErrors} +
+ + +
+

There are errors in the input options.

+

Fix them to continue.

+
+
+ + +
+

There are errors in the input options.

+

Fix them to continue.

+
+
+
+ {:else}
- {#if opts?.kind !== "Stablecoin" && opts?.kind !== "RealWorldAsset"} + {#if showButtons.openInRemix} - {#if opts?.kind !== "Governor" && opts?.kind !== "Stablecoin" && opts?.kind !== "RealWorldAsset"} + {#if showButtons.downloadHardhat} {/if} - {#if opts?.kind !== "Governor" && opts?.kind !== "Stablecoin" && opts?.kind !== "RealWorldAsset"} + {#if showButtons.downloadFoundry}
-
+
- +
@@ -324,6 +416,7 @@
+ {#if !hasErrors}
+ {/if}
-        {@html highlightedCode}
+        {@html highlightedCode}
       
@@ -418,6 +512,11 @@ --blur: 0px; } } + +.no-select { + user-select: none; +} + .button-bg{ animation: conic-effect 12s ease-in-out infinite; animation-delay: 4.2s; diff --git a/packages/ui/src/solidity/ERC20Controls.svelte b/packages/ui/src/solidity/ERC20Controls.svelte index 20cc69c58..4c153b156 100644 --- a/packages/ui/src/solidity/ERC20Controls.svelte +++ b/packages/ui/src/solidity/ERC20Controls.svelte @@ -1,13 +1,17 @@
@@ -39,8 +68,18 @@ Premint Create an initial amount of tokens for the deployer. - + + + {#if showChainId} +

+ + Chain ID of the network on which to premint tokens. +

+ {/if}
@@ -122,8 +161,44 @@
+
+

+ + +

+
+ * Experimental: These features are not audited and are subject to change +
+ +
+ + + +
+
+ - + diff --git a/packages/ui/src/solidity/RealWorldAssetControls.svelte b/packages/ui/src/solidity/RealWorldAssetControls.svelte index 62dfaaa25..4641387e7 100644 --- a/packages/ui/src/solidity/RealWorldAssetControls.svelte +++ b/packages/ui/src/solidity/RealWorldAssetControls.svelte @@ -7,6 +7,8 @@ import AccessControlSection from './AccessControlSection.svelte'; import InfoSection from './InfoSection.svelte'; import ToggleRadio from '../common/inputs/ToggleRadio.svelte'; + import OPIcon from '../common/icons/OPIcon.svelte'; + import { superchainTooltipProps } from './superchain-tooltip'; export let opts: Required = { kind: 'RealWorldAsset', @@ -18,11 +20,29 @@ }; $: requireAccessControl = realWorldAsset.isAccessControlRequired(opts); + + // Show notice when SuperchainERC20 is enabled + import tippy, { Instance as TippyInstance } from 'tippy.js'; + import { onMount } from 'svelte'; + + let superchainLabel: HTMLElement; + let superchainTooltip: TippyInstance; + onMount(() => { + superchainTooltip = tippy(superchainLabel, superchainTooltipProps); + }); + + let wasSuperchain = false; + $: { + if (!wasSuperchain && opts.crossChainBridging === 'superchain') { + superchainTooltip.show(); + } + wasSuperchain = opts.crossChainBridging === 'superchain'; + }
- * Experimental: Some of the following features are not audited and subject to change + * Experimental: Some of the following features are not audited and are subject to change
@@ -97,7 +117,7 @@