diff --git a/.changeset/deep-sites-bake.md b/.changeset/deep-sites-bake.md new file mode 100644 index 00000000..eaafff58 --- /dev/null +++ b/.changeset/deep-sites-bake.md @@ -0,0 +1,5 @@ +--- +"@cartesi/devnet": patch +--- + +Add a deployment script for UsdWithdrawalOutputBuilder to support emergency withdrawal tests. Remove solidity code and deployment script for test-tokens, instead it will package only the ones provided by rollups-contracts artifacts (devnet only). diff --git a/packages/devnet/build.ts b/packages/devnet/build.ts index 0b5da822..ae14a3c0 100644 --- a/packages/devnet/build.ts +++ b/packages/devnet/build.ts @@ -1,5 +1,5 @@ import { semver } from "bun"; -import { existsSync, readdirSync } from "fs-extra"; +import { cpSync, existsSync, readdirSync } from "fs-extra"; import { Listr, type ListrTask } from "listr2"; import * as path from "node:path"; import { @@ -135,15 +135,18 @@ const perChainDeploymentTasks: ListrTask[] = Object.entries( * @param options - The options for deploying contracts * @param options.privateKey - The private key to use for the deployment * @param options.rpcUrl - The RPC URL to use for the deployment - * @returns + * @returns - the exit code of the deployment script, 0 if successful + * @throws {Error} - if the deployment script exits with a non-zero code, the error message will contain the stderr output of the script */ -const deploy = async (options: { privateKey: string; rpcUrl: string }) => { - // execute forge script +const runDeploymentScript = async ( + scriptName: string, + options: { privateKey: string; rpcUrl: string }, +) => { const proc = Bun.spawn( [ "forge", "script", - "Deploy", + scriptName, "--broadcast", "--non-interactive", "--private-key", @@ -155,14 +158,66 @@ const deploy = async (options: { privateKey: string; rpcUrl: string }) => { { stdio: ["ignore", "pipe", "pipe"] }, ); const exitCode = await proc.exited; + if (exitCode !== 0) { throw new Error(`Forge script exited with code ${exitCode}`, { cause: await new Response(proc.stderr).text(), }); } + + return exitCode; +}; + +const deploy = async (options: { privateKey: string; rpcUrl: string }) => { + const contractName = "UsdWithdrawalOutputBuilder"; + + // Deploys an UsdWithdrawalOutputBuilder but saves the deployment information as TestUsdWithdrawalOutputBuilder. + const exitCode = await runDeploymentScript( + `DeployTest${contractName}`, + options, + ); + + if (exitCode === 0) { + // To continue the build process, we make sure there is a copy of the expected .sol folder + JSON content with added prefix + // in the name (i.e. Test[.sol, .json]) in the out folder. Therefore, the rest of the process can find the expected information + // and copy it to the right place. + const targetPath = path.join( + "out", + `${contractName}.sol`, + `${contractName}.json`, + ); + const destinationPath = path.join( + "out", + `Test${contractName}.sol`, + `Test${contractName}.json`, + ); + createCopy(targetPath, destinationPath); + } + return exitCode; }; +/** + * Copy file or directory from source to destination, if source is a directory it will be copied recursively, + * if destination already exist it will be overwritten. + * @param source - path to file or directory to be copied + * @param destination - path to destination where file or directory should be copied + * @returns true if copy was successful, otherwise throws an error + * @throws {Error} if copy operation fails, the error message will contain the original error message + */ +const createCopy = (source: string, destination: string) => { + try { + cpSync(source, destination, { recursive: true, force: true }); + console.info(`Source copied from ${source} to ${destination}`); + } catch (error) { + throw new Error(`Failed to copy from ${source} to ${destination}`, { + cause: (error as Error).message, + }); + } + + return true; +}; + const build = async () => { type Ctx = { anvilProc?: Bun.Subprocess; diff --git a/packages/devnet/foundry.toml b/packages/devnet/foundry.toml index 251fa960..6f091e00 100644 --- a/packages/devnet/foundry.toml +++ b/packages/devnet/foundry.toml @@ -6,5 +6,6 @@ optimizer = true fs_permissions = [{ access = "read-write", path = "build" }] [dependencies] -"@openzeppelin-contracts" = "5.5.0" +"@openzeppelin-contracts" = "5.2.0" forge-std = "1.9.6" +cartesi-rollups-contracts = "3.0.0-alpha.6" diff --git a/packages/devnet/script/BaseDeploymentScript.sol b/packages/devnet/script/BaseDeploymentScript.sol index 025d46d0..d30f8ead 100644 --- a/packages/devnet/script/BaseDeploymentScript.sol +++ b/packages/devnet/script/BaseDeploymentScript.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.8; -import {Create2} from "@openzeppelin-contracts-5.5.0/utils/Create2.sol"; +import {Create2} from "@openzeppelin-contracts-5.2.0/utils/Create2.sol"; import {Script} from "forge-std-1.9.6/src/Script.sol"; diff --git a/packages/devnet/script/Deploy.s.sol b/packages/devnet/script/Deploy.s.sol deleted file mode 100644 index 1fc37e8a..00000000 --- a/packages/devnet/script/Deploy.s.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8; - -import {BaseDeploymentScript} from "./BaseDeploymentScript.sol"; -import {TestMultiToken} from "../src/TestMultiToken.sol"; -import {TestNFT} from "../src/TestNFT.sol"; -import {TestToken} from "../src/TestToken.sol"; - -contract Deploy is BaseDeploymentScript { - function run() public { - address deployer = msg.sender; - - vm.startBroadcast(); - - // Deploy TestMultiToken - _storeDeployment( - type(TestMultiToken).name, - _create2(type(TestMultiToken).creationCode, abi.encode(deployer)) - ); - - // Deploy TestNFT - _storeDeployment( - type(TestNFT).name, - _create2(type(TestNFT).creationCode, abi.encode(deployer)) - ); - - // Deploy TestToken - _storeDeployment( - type(TestToken).name, - _create2(type(TestToken).creationCode, abi.encode(deployer)) - ); - - vm.stopBroadcast(); - } -} diff --git a/packages/devnet/script/DeployTestUsdWithdrawalOutputBuilder.s.sol b/packages/devnet/script/DeployTestUsdWithdrawalOutputBuilder.s.sol new file mode 100644 index 00000000..ce66f50c --- /dev/null +++ b/packages/devnet/script/DeployTestUsdWithdrawalOutputBuilder.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8; + +import {BaseDeploymentScript} from "./BaseDeploymentScript.sol"; +import {IERC20} from "@openzeppelin-contracts-5.2.0/token/ERC20/IERC20.sol"; +import {IUsdWithdrawalOutputBuilderFactory} from "cartesi-rollups-contracts-3.0.0-alpha.6/src/withdrawal/IUsdWithdrawalOutputBuilderFactory.sol"; + +contract DeployTestUsdWithdrawalOutputBuilder is BaseDeploymentScript { + bytes32 private constant SALT = bytes32(uint256(0)); + + function run() public { + address factoryAddress = _loadDeployment( + ".", + "UsdWithdrawalOutputBuilderFactory" + ); + IERC20 token = IERC20(_loadDeployment(".", "TestFungibleToken")); + + IUsdWithdrawalOutputBuilderFactory factory = + IUsdWithdrawalOutputBuilderFactory(factoryAddress); + + vm.startBroadcast(); + + address builder = factory.calculateUsdWithdrawalOutputBuilderAddress( + token, + SALT + ); + + // Keep this script idempotent when running against a state dump that + // may already contain this token+salt deployment. + if (builder.code.length == 0) { + address deployed = address(factory.newUsdWithdrawalOutputBuilder(token, SALT)); + vm.assertEq(deployed, builder); + builder = deployed; + } + + _storeDeployment("TestUsdWithdrawalOutputBuilder", builder); + + vm.stopBroadcast(); + } +} diff --git a/packages/devnet/soldeer.lock b/packages/devnet/soldeer.lock index 39167063..3c549c7a 100644 --- a/packages/devnet/soldeer.lock +++ b/packages/devnet/soldeer.lock @@ -1,9 +1,16 @@ [[dependencies]] name = "@openzeppelin-contracts" -version = "5.5.0" -url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_5_0_01-11-2025_09:56:40_contracts.zip" -checksum = "2ee78837130cf06b456d9ef7ab70753685d9c1c9c2c5b70f24bd4c16695d0337" -integrity = "da8336cf949f0e0667ae8360af849681e3a3e76d7e61e7a86b1a3414a158aeea" +version = "5.2.0" +url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_2_0_11-01-2025_09:30:20_contracts.zip" +checksum = "6dbd0440446b2ed16ca25e9f1af08fc0c5c1e73e71fee86ae8a00daa774e3817" +integrity = "4cb7f3777f67fdf4b7d0e2f94d2f93f198b2e5dce718b7062ac7c2c83e1183bd" + +[[dependencies]] +name = "cartesi-rollups-contracts" +version = "3.0.0-alpha.6" +url = "https://soldeer-revisions.s3.amazonaws.com/cartesi-rollups-contracts/3_0_0-alpha_6_21-05-2026_12:17:44_rollups-contracts.zip" +checksum = "cdfd86f90895ba37e188103e8fb62d6ca8e45a53846ebc63cb7b7af66f18d034" +integrity = "664e731ff29c1f9c27d32391cbe45529ab812a71eed188cb235a1f37f84ee012" [[dependencies]] name = "forge-std" diff --git a/packages/devnet/src/TestMultiToken.sol b/packages/devnet/src/TestMultiToken.sol deleted file mode 100644 index 0f30089d..00000000 --- a/packages/devnet/src/TestMultiToken.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Compatible with OpenZeppelin Contracts ^5.0 -pragma solidity ^0.8; - -import {ERC1155} from "@openzeppelin-contracts-5.5.0/token/ERC1155/ERC1155.sol"; -import {Ownable} from "@openzeppelin-contracts-5.5.0/access/Ownable.sol"; - -contract TestMultiToken is ERC1155, Ownable { - constructor(address initialOwner) ERC1155("") Ownable(initialOwner) {} - - function setURI(string memory newuri) public onlyOwner { - _setURI(newuri); - } - - function mint( - address account, - uint256 id, - uint256 amount, - bytes memory data - ) public onlyOwner { - _mint(account, id, amount, data); - } - - function mintBatch( - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) public onlyOwner { - _mintBatch(to, ids, amounts, data); - } -} diff --git a/packages/devnet/src/TestNFT.sol b/packages/devnet/src/TestNFT.sol deleted file mode 100644 index 3c453a01..00000000 --- a/packages/devnet/src/TestNFT.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Compatible with OpenZeppelin Contracts ^5.0 -pragma solidity ^0.8; - -import {ERC721} from "@openzeppelin-contracts-5.5.0/token/ERC721/ERC721.sol"; -import { - ERC721URIStorage -} from "@openzeppelin-contracts-5.5.0/token/ERC721/extensions/ERC721URIStorage.sol"; -import {Ownable} from "@openzeppelin-contracts-5.5.0/access/Ownable.sol"; - -contract TestNFT is ERC721, ERC721URIStorage, Ownable { - constructor( - address initialOwner - ) ERC721("TestNFT", "SUNN") Ownable(initialOwner) {} - - function safeMint( - address to, - uint256 tokenId, - string memory uri - ) public onlyOwner { - _safeMint(to, tokenId); - _setTokenURI(tokenId, uri); - } - - // The following functions are overrides required by Solidity. - - function tokenURI( - uint256 tokenId - ) public view override(ERC721, ERC721URIStorage) returns (string memory) { - return super.tokenURI(tokenId); - } - - function supportsInterface( - bytes4 interfaceId - ) public view override(ERC721, ERC721URIStorage) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/packages/devnet/src/TestToken.sol b/packages/devnet/src/TestToken.sol deleted file mode 100644 index c71f5a11..00000000 --- a/packages/devnet/src/TestToken.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Compatible with OpenZeppelin Contracts ^5.0 -pragma solidity ^0.8; - -import {ERC20} from "@openzeppelin-contracts-5.5.0/token/ERC20/ERC20.sol"; -import { - ERC20Burnable -} from "@openzeppelin-contracts-5.5.0/token/ERC20/extensions/ERC20Burnable.sol"; -import { - ERC20Pausable -} from "@openzeppelin-contracts-5.5.0/token/ERC20/extensions/ERC20Pausable.sol"; -import { - AccessManaged -} from "@openzeppelin-contracts-5.5.0/access/manager/AccessManaged.sol"; -import { - ERC20Permit -} from "@openzeppelin-contracts-5.5.0/token/ERC20/extensions/ERC20Permit.sol"; - -contract TestToken is - ERC20, - ERC20Burnable, - ERC20Pausable, - AccessManaged, - ERC20Permit -{ - constructor( - address initialAuthority - ) - ERC20("TestToken", "TEST") - AccessManaged(initialAuthority) - ERC20Permit("TestToken") - { - _mint(initialAuthority, 1000000000 * 10 ** decimals()); - } - - function pause() public restricted { - _pause(); - } - - function unpause() public restricted { - _unpause(); - } - - // The following functions are overrides required by Solidity. - - function _update( - address from, - address to, - uint256 value - ) internal override(ERC20, ERC20Pausable) { - super._update(from, to, value); - } -}