From eb4c90572f60f1484f0259d034188725ff1ab4e9 Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Tue, 5 May 2026 06:51:45 -0300 Subject: [PATCH 1/3] Deposit refunds --- script/CodeGeneration.s.sol | 25 +- script/utils/ContractDeployers.sol | 66 +- script/utils/CoreContracts.sol | 17 +- src/common/DelegateCallVoucher.sol | 12 + src/common/Erc1155BatchDeposit.sol | 18 + src/common/Erc1155SingleDeposit.sol | 18 + src/common/Erc20Deposit.sol | 16 + src/common/Erc721Deposit.sol | 16 + src/common/EtherDeposit.sol | 12 + src/common/InputEncoding.sol | 86 ++ src/common/Voucher.sol | 14 + src/consensus/AbstractConsensus.sol | 9 + src/consensus/IOutputsMerkleRootValidator.sol | 15 + src/dapp/Application.sol | 167 +++- src/dapp/ApplicationFactory.sol | 12 + src/dapp/IApplication.sol | 76 ++ src/devnet/TestMultiToken.sol | 20 + src/devnet/TestNonFungibleToken.sol | 7 + src/library/LibDelegateCallVoucher.sol | 20 + src/library/LibErc1155BatchDeposit.sol | 30 + src/library/LibErc1155SingleDeposit.sol | 30 + src/library/LibErc20Deposit.sol | 24 + src/library/LibErc721Deposit.sol | 32 + src/library/LibError.sol | 2 +- src/library/LibEtherDeposit.sol | 21 + src/library/LibKeccak256.sol | 12 +- src/library/LibVoucher.sol | 16 + src/refund/IRefundOutputBuilder.sol | 28 + src/refund/IRefundOutputBuilderErrors.sol | 12 + src/refund/RefundOutputBuilder.sol | 82 ++ test/common/InputEncoding.t.sol | 74 ++ .../authority/AuthorityFactory.t.sol | 44 +- test/consensus/quorum/QuorumFactory.t.sol | 44 +- test/dapp/Application.t.sol | 884 ++++++++++++++++-- test/dapp/ApplicationFactory.t.sol | 12 + test/library/LibDelegateCallVoucher.t.sol | 31 + test/library/LibErc1155BatchDeposit.t.sol | 43 + test/library/LibErc1155SingleDeposit.t.sol | 42 + test/library/LibErc20Deposit.t.sol | 40 + test/library/LibErc721Deposit.t.sol | 38 + test/library/LibEtherDeposit.t.sol | 21 + test/library/LibVoucher.t.sol | 33 + test/refund/RefundOutputBuilder.t.sol | 287 ++++++ test/util/LibDepositDecoder.sol | 53 ++ test/util/LibDepositEncoder.sol | 86 ++ test/util/LibUint256Array.sol | 10 + test/util/LibUint256Array.t.sol | 8 + 47 files changed, 2564 insertions(+), 101 deletions(-) create mode 100644 src/common/DelegateCallVoucher.sol create mode 100644 src/common/Erc1155BatchDeposit.sol create mode 100644 src/common/Erc1155SingleDeposit.sol create mode 100644 src/common/Erc20Deposit.sol create mode 100644 src/common/Erc721Deposit.sol create mode 100644 src/common/EtherDeposit.sol create mode 100644 src/common/Voucher.sol create mode 100644 src/library/LibDelegateCallVoucher.sol create mode 100644 src/library/LibErc1155BatchDeposit.sol create mode 100644 src/library/LibErc1155SingleDeposit.sol create mode 100644 src/library/LibErc20Deposit.sol create mode 100644 src/library/LibErc721Deposit.sol create mode 100644 src/library/LibEtherDeposit.sol create mode 100644 src/library/LibVoucher.sol create mode 100644 src/refund/IRefundOutputBuilder.sol create mode 100644 src/refund/IRefundOutputBuilderErrors.sol create mode 100644 src/refund/RefundOutputBuilder.sol create mode 100644 test/common/InputEncoding.t.sol create mode 100644 test/library/LibDelegateCallVoucher.t.sol create mode 100644 test/library/LibErc1155BatchDeposit.t.sol create mode 100644 test/library/LibErc1155SingleDeposit.t.sol create mode 100644 test/library/LibErc20Deposit.t.sol create mode 100644 test/library/LibErc721Deposit.t.sol create mode 100644 test/library/LibEtherDeposit.t.sol create mode 100644 test/library/LibVoucher.t.sol create mode 100644 test/refund/RefundOutputBuilder.t.sol create mode 100644 test/util/LibDepositDecoder.sol create mode 100644 test/util/LibDepositEncoder.sol diff --git a/script/CodeGeneration.s.sol b/script/CodeGeneration.s.sol index 016053ed..eee7a83d 100644 --- a/script/CodeGeneration.s.sol +++ b/script/CodeGeneration.s.sol @@ -96,6 +96,13 @@ contract DeployersCodeGenerationScript is CodeGenerationScript { _addImport("src/portals", "ERC20Portal"); _addImport("src/portals", "ERC721Portal"); _addImport("src/portals", "EtherPortal"); + _addImport("src/portals", "IERC1155BatchPortal"); + _addImport("src/portals", "IERC1155SinglePortal"); + _addImport("src/portals", "IERC20Portal"); + _addImport("src/portals", "IERC721Portal"); + _addImport("src/portals", "IEtherPortal"); + _addImport("src/refund", "IRefundOutputBuilder"); + _addImport("src/refund", "RefundOutputBuilder"); _addImport("src/withdrawal", "IUsdWithdrawalOutputBuilder"); _addImport("src/withdrawal", "IUsdWithdrawalOutputBuilderFactory"); _addImport("src/withdrawal", "UsdWithdrawalOutputBuilderFactory"); @@ -112,7 +119,6 @@ contract DeployersCodeGenerationScript is CodeGenerationScript { { string[] memory paramTypes = new string[](0); - _addDeployer("ApplicationFactory", paramTypes); _addDeployer("AuthorityFactory", paramTypes); _addDeployer("InputBox", paramTypes); _addDeployer("QuorumFactory", paramTypes); @@ -132,6 +138,23 @@ contract DeployersCodeGenerationScript is CodeGenerationScript { _addDeployer("EtherPortal", paramTypes); } + { + string[] memory paramTypes = new string[](6); + paramTypes[0] = "IEtherPortal"; + paramTypes[1] = "IERC20Portal"; + paramTypes[2] = "IERC721Portal"; + paramTypes[3] = "IERC1155SinglePortal"; + paramTypes[4] = "IERC1155BatchPortal"; + paramTypes[5] = "ISafeERC20Transfer"; + _addDeployer("RefundOutputBuilder", paramTypes); + } + + { + string[] memory paramTypes = new string[](1); + paramTypes[0] = "IRefundOutputBuilder"; + _addDeployer("ApplicationFactory", paramTypes); + } + { string[] memory paramTypes = new string[](1); paramTypes[0] = "ISafeERC20Transfer"; diff --git a/script/utils/ContractDeployers.sol b/script/utils/ContractDeployers.sol index 03622614..c5307e00 100644 --- a/script/utils/ContractDeployers.sol +++ b/script/utils/ContractDeployers.sol @@ -23,6 +23,13 @@ import {ERC1155SinglePortal} from "src/portals/ERC1155SinglePortal.sol"; import {ERC20Portal} from "src/portals/ERC20Portal.sol"; import {ERC721Portal} from "src/portals/ERC721Portal.sol"; import {EtherPortal} from "src/portals/EtherPortal.sol"; +import {IERC1155BatchPortal} from "src/portals/IERC1155BatchPortal.sol"; +import {IERC1155SinglePortal} from "src/portals/IERC1155SinglePortal.sol"; +import {IERC20Portal} from "src/portals/IERC20Portal.sol"; +import {IERC721Portal} from "src/portals/IERC721Portal.sol"; +import {IEtherPortal} from "src/portals/IEtherPortal.sol"; +import {IRefundOutputBuilder} from "src/refund/IRefundOutputBuilder.sol"; +import {RefundOutputBuilder} from "src/refund/RefundOutputBuilder.sol"; import {IUsdWithdrawalOutputBuilder} from "src/withdrawal/IUsdWithdrawalOutputBuilder.sol"; import {IUsdWithdrawalOutputBuilderFactory} from "src/withdrawal/IUsdWithdrawalOutputBuilderFactory.sol"; import {UsdWithdrawalOutputBuilderFactory} from "src/withdrawal/UsdWithdrawalOutputBuilderFactory.sol"; @@ -44,22 +51,6 @@ function computeAddress(bytes32 salt, bytes32 initCodeHash) pure returns (addres ); } -function deployApplicationFactory() returns (ApplicationFactory deployment) { - bytes32 salt; - bytes memory creationCode = type(ApplicationFactory).creationCode; - bytes memory encodedArgs = abi.encode(); - bytes memory initCode = abi.encodePacked(creationCode, encodedArgs); - bytes32 initCodeHash = keccak256(initCode); - address precomputedAddress = computeAddress(salt, initCodeHash); - if (precomputedAddress.code.length == 0) { - deployment = new ApplicationFactory{salt: salt}(); - assert(address(deployment) == precomputedAddress); - assert(address(deployment).code.length > 0); - } else { - deployment = ApplicationFactory(precomputedAddress); - } -} - function deployAuthorityFactory() returns (AuthorityFactory deployment) { bytes32 salt; bytes memory creationCode = type(AuthorityFactory).creationCode; @@ -256,6 +247,49 @@ function deployEtherPortal(IInputBox param1) returns (EtherPortal deployment) { } } +function deployRefundOutputBuilder( + IEtherPortal param1, + IERC20Portal param2, + IERC721Portal param3, + IERC1155SinglePortal param4, + IERC1155BatchPortal param5, + ISafeERC20Transfer param6 +) returns (RefundOutputBuilder deployment) { + bytes32 salt; + bytes memory creationCode = type(RefundOutputBuilder).creationCode; + bytes memory encodedArgs = abi.encode(param1, param2, param3, param4, param5, param6); + bytes memory initCode = abi.encodePacked(creationCode, encodedArgs); + bytes32 initCodeHash = keccak256(initCode); + address precomputedAddress = computeAddress(salt, initCodeHash); + if (precomputedAddress.code.length == 0) { + deployment = new RefundOutputBuilder{salt: salt}( + param1, param2, param3, param4, param5, param6 + ); + assert(address(deployment) == precomputedAddress); + assert(address(deployment).code.length > 0); + } else { + deployment = RefundOutputBuilder(precomputedAddress); + } +} + +function deployApplicationFactory(IRefundOutputBuilder param1) + returns (ApplicationFactory deployment) +{ + bytes32 salt; + bytes memory creationCode = type(ApplicationFactory).creationCode; + bytes memory encodedArgs = abi.encode(param1); + bytes memory initCode = abi.encodePacked(creationCode, encodedArgs); + bytes32 initCodeHash = keccak256(initCode); + address precomputedAddress = computeAddress(salt, initCodeHash); + if (precomputedAddress.code.length == 0) { + deployment = new ApplicationFactory{salt: salt}(param1); + assert(address(deployment) == precomputedAddress); + assert(address(deployment).code.length > 0); + } else { + deployment = ApplicationFactory(precomputedAddress); + } +} + function deployUsdWithdrawalOutputBuilderFactory(ISafeERC20Transfer param1) returns (UsdWithdrawalOutputBuilderFactory deployment) { diff --git a/script/utils/CoreContracts.sol b/script/utils/CoreContracts.sol index 8bf8a33d..c2eda607 100644 --- a/script/utils/CoreContracts.sol +++ b/script/utils/CoreContracts.sol @@ -16,6 +16,7 @@ import {ERC1155SinglePortal} from "src/portals/ERC1155SinglePortal.sol"; import {ERC20Portal} from "src/portals/ERC20Portal.sol"; import {ERC721Portal} from "src/portals/ERC721Portal.sol"; import {EtherPortal} from "src/portals/EtherPortal.sol"; +import {RefundOutputBuilder} from "src/refund/RefundOutputBuilder.sol"; import {UsdWithdrawalOutputBuilderFactory} from "src/withdrawal/UsdWithdrawalOutputBuilderFactory.sol"; import "./ContractDeployers.sol" as G; @@ -31,6 +32,7 @@ struct Suite { EtherPortal etherPortal; InputBox inputBox; QuorumFactory quorumFactory; + RefundOutputBuilder refundOutputBuilder; SafeERC20Transfer safeErc20Transfer; SelfHostedApplicationFactory selfHostedApplicationFactory; UsdWithdrawalOutputBuilderFactory usdWithdrawalOutputBuilderFactory; @@ -45,7 +47,16 @@ function deploy() returns (Suite memory) { ERC1155BatchPortal erc1155BatchPortal = G.deployERC1155BatchPortal(inputBox); SafeERC20Transfer safeErc20Transfer = G.deploySafeERC20Transfer(); AuthorityFactory authorityFactory = G.deployAuthorityFactory(); - ApplicationFactory applicationFactory = G.deployApplicationFactory(); + RefundOutputBuilder refundOutputBuilder = G.deployRefundOutputBuilder( + etherPortal, + erc20Portal, + erc721Portal, + erc1155SinglePortal, + erc1155BatchPortal, + safeErc20Transfer + ); + ApplicationFactory applicationFactory = + G.deployApplicationFactory(refundOutputBuilder); QuorumFactory quorumFactory = G.deployQuorumFactory(); UsdWithdrawalOutputBuilderFactory usdWithdrawalOutputBuilderFactory = G.deployUsdWithdrawalOutputBuilderFactory(safeErc20Transfer); @@ -62,6 +73,7 @@ function deploy() returns (Suite memory) { etherPortal: etherPortal, inputBox: inputBox, quorumFactory: quorumFactory, + refundOutputBuilder: refundOutputBuilder, safeErc20Transfer: safeErc20Transfer, selfHostedApplicationFactory: selfHostedApplicationFactory, usdWithdrawalOutputBuilderFactory: usdWithdrawalOutputBuilderFactory @@ -81,6 +93,9 @@ function store(VmSafe vmSafe, Suite memory s) { storeDeployment(vmSafe, type(EtherPortal).name, address(s.etherPortal)); storeDeployment(vmSafe, type(QuorumFactory).name, address(s.quorumFactory)); storeDeployment(vmSafe, type(SafeERC20Transfer).name, address(s.safeErc20Transfer)); + storeDeployment( + vmSafe, type(RefundOutputBuilder).name, address(s.refundOutputBuilder) + ); storeDeployment( vmSafe, type(SelfHostedApplicationFactory).name, diff --git a/src/common/DelegateCallVoucher.sol b/src/common/DelegateCallVoucher.sol new file mode 100644 index 00000000..4c6836ad --- /dev/null +++ b/src/common/DelegateCallVoucher.sol @@ -0,0 +1,12 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +/// @notice A delegate-call voucher +/// @param destination The destination address +/// @param payload The delegate-call payload +struct DelegateCallVoucher { + address destination; + bytes payload; +} diff --git a/src/common/Erc1155BatchDeposit.sol b/src/common/Erc1155BatchDeposit.sol new file mode 100644 index 00000000..8b94c0b0 --- /dev/null +++ b/src/common/Erc1155BatchDeposit.sol @@ -0,0 +1,18 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {IERC1155} from "@openzeppelin-contracts-5.2.0/token/ERC1155/IERC1155.sol"; + +/// @notice An ERC-1155 single token deposit +/// @param token The token contract +/// @param sender The token sender +/// @param tokenIds The token identifiers +/// @param value The token amounts per token type +struct Erc1155BatchDeposit { + IERC1155 token; + address sender; + uint256[] tokenIds; + uint256[] values; +} diff --git a/src/common/Erc1155SingleDeposit.sol b/src/common/Erc1155SingleDeposit.sol new file mode 100644 index 00000000..696f85c3 --- /dev/null +++ b/src/common/Erc1155SingleDeposit.sol @@ -0,0 +1,18 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {IERC1155} from "@openzeppelin-contracts-5.2.0/token/ERC1155/IERC1155.sol"; + +/// @notice An ERC-1155 single token deposit +/// @param token The token contract +/// @param sender The token sender +/// @param tokenId The token identifier +/// @param value The token amount +struct Erc1155SingleDeposit { + IERC1155 token; + address sender; + uint256 tokenId; + uint256 value; +} diff --git a/src/common/Erc20Deposit.sol b/src/common/Erc20Deposit.sol new file mode 100644 index 00000000..545ed07a --- /dev/null +++ b/src/common/Erc20Deposit.sol @@ -0,0 +1,16 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {IERC20} from "@openzeppelin-contracts-5.2.0/token/ERC20/IERC20.sol"; + +/// @notice An ERC-20 token deposit +/// @param token The token contract +/// @param sender The token sender +/// @param value The token amount +struct Erc20Deposit { + IERC20 token; + address sender; + uint256 value; +} diff --git a/src/common/Erc721Deposit.sol b/src/common/Erc721Deposit.sol new file mode 100644 index 00000000..ad4cd111 --- /dev/null +++ b/src/common/Erc721Deposit.sol @@ -0,0 +1,16 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {IERC721} from "@openzeppelin-contracts-5.2.0/token/ERC721/IERC721.sol"; + +/// @notice An ERC-721 token deposit +/// @param token The token contract +/// @param sender The token sender +/// @param tokenId The token identifier +struct Erc721Deposit { + IERC721 token; + address sender; + uint256 tokenId; +} diff --git a/src/common/EtherDeposit.sol b/src/common/EtherDeposit.sol new file mode 100644 index 00000000..5844d9df --- /dev/null +++ b/src/common/EtherDeposit.sol @@ -0,0 +1,12 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +/// @notice An Ether token deposit +/// @param sender The Ether sender +/// @param value The Ether amount (in Wei) +struct EtherDeposit { + address sender; + uint256 value; +} diff --git a/src/common/InputEncoding.sol b/src/common/InputEncoding.sol index 03a13731..426859eb 100644 --- a/src/common/InputEncoding.sol +++ b/src/common/InputEncoding.sol @@ -7,6 +7,12 @@ import {IERC1155} from "@openzeppelin-contracts-5.2.0/token/ERC1155/IERC1155.sol import {IERC20} from "@openzeppelin-contracts-5.2.0/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin-contracts-5.2.0/token/ERC721/IERC721.sol"; +import {Erc1155BatchDeposit} from "./Erc1155BatchDeposit.sol"; +import {Erc1155SingleDeposit} from "./Erc1155SingleDeposit.sol"; +import {Erc20Deposit} from "./Erc20Deposit.sol"; +import {Erc721Deposit} from "./Erc721Deposit.sol"; +import {EtherDeposit} from "./EtherDeposit.sol"; + /// @title Input Encoding Library /// @notice Defines the encoding of inputs added by core trustless and @@ -29,6 +35,20 @@ library InputEncoding { ); } + /// @notice Decode an Ether deposit. + /// @param payload The encoded input payload + /// @return deposit The decoded Ether deposit + function decodeEtherDeposit(bytes calldata payload) + internal + pure + returns (EtherDeposit memory deposit) + { + return EtherDeposit({ + sender: address(uint160(bytes20(payload[:20]))), + value: uint256(bytes32(payload[20:52])) + }); + } + /// @notice Encode an ERC-20 token deposit. /// @param token The token contract /// @param sender The token sender @@ -49,6 +69,21 @@ library InputEncoding { ); } + /// @notice Decode an ERC-20 token deposit. + /// @param payload The encoded input payload + /// @return deposit The decoded ERC-20 token deposit + function decodeErc20Deposit(bytes calldata payload) + internal + pure + returns (Erc20Deposit memory deposit) + { + return Erc20Deposit({ + token: IERC20(address(uint160(bytes20(payload[:20])))), + sender: address(uint160(bytes20(payload[20:40]))), + value: uint256(bytes32(payload[40:72])) + }); + } + /// @notice Encode an ERC-721 token deposit. /// @param token The token contract /// @param sender The token sender @@ -73,6 +108,21 @@ library InputEncoding { ); } + /// @notice Decode an ERC-721 token deposit. + /// @param payload The encoded input payload + /// @return deposit The decoded ERC-721 token deposit + function decodeErc721Deposit(bytes calldata payload) + internal + pure + returns (Erc721Deposit memory deposit) + { + return Erc721Deposit({ + token: IERC721(address(uint160(bytes20(payload[:20])))), + sender: address(uint160(bytes20(payload[20:40]))), + tokenId: uint256(bytes32(payload[40:72])) + }); + } + /// @notice Encode an ERC-1155 single token deposit. /// @param token The ERC-1155 token contract /// @param sender The token sender @@ -100,6 +150,22 @@ library InputEncoding { ); } + /// @notice Decode an ERC-1155 single token deposit. + /// @param payload The encoded input payload + /// @return deposit The decoded ERC-1155 single token deposit + function decodeErc1155SingleDeposit(bytes calldata payload) + internal + pure + returns (Erc1155SingleDeposit memory deposit) + { + return Erc1155SingleDeposit({ + token: IERC1155(address(uint160(bytes20(payload[:20])))), + sender: address(uint160(bytes20(payload[20:40]))), + tokenId: uint256(bytes32(payload[40:72])), + value: uint256(bytes32(payload[72:104])) + }); + } + /// @notice Encode an ERC-1155 batch token deposit. /// @param token The ERC-1155 token contract /// @param sender The token sender @@ -124,4 +190,24 @@ library InputEncoding { data // arbitrary size ); } + + /// @notice Decode an ERC-1155 batch token deposit. + /// @param payload The encoded input payload + /// @return deposit The decoded ERC-1155 batch token deposit + function decodeErc1155BatchDeposit(bytes calldata payload) + internal + pure + returns (Erc1155BatchDeposit memory deposit) + { + bytes calldata data = payload[40:]; + uint256[] memory tokenIds; + uint256[] memory values; + (tokenIds, values,,) = abi.decode(data, (uint256[], uint256[], bytes, bytes)); + return Erc1155BatchDeposit({ + token: IERC1155(address(uint160(bytes20(payload[:20])))), + sender: address(uint160(bytes20(payload[20:40]))), + tokenIds: tokenIds, + values: values + }); + } } diff --git a/src/common/Voucher.sol b/src/common/Voucher.sol new file mode 100644 index 00000000..64ad2bf4 --- /dev/null +++ b/src/common/Voucher.sol @@ -0,0 +1,14 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +/// @notice A voucher +/// @param destination The destination address +/// @param value The Ether amount (in Wei) +/// @param payload The call payload +struct Voucher { + address destination; + uint256 value; + bytes payload; +} diff --git a/src/consensus/AbstractConsensus.sol b/src/consensus/AbstractConsensus.sol index 0161b19f..e492d4dd 100644 --- a/src/consensus/AbstractConsensus.sol +++ b/src/consensus/AbstractConsensus.sol @@ -85,6 +85,15 @@ abstract contract AbstractConsensus is return _lastFinalizedMachineMerkleRoots[appContract]; } + function wasInputFinalized(address appContract, uint256, uint256 blockNumber) + public + view + override + returns (bool) + { + return blockNumber < _firstUnprocessedBlockNumbers[appContract]; + } + /// @inheritdoc IConsensus function getEpochLength() public view override returns (uint256) { return EPOCH_LENGTH; diff --git a/src/consensus/IOutputsMerkleRootValidator.sol b/src/consensus/IOutputsMerkleRootValidator.sol index 047f6cc9..acfb5f54 100644 --- a/src/consensus/IOutputsMerkleRootValidator.sol +++ b/src/consensus/IOutputsMerkleRootValidator.sol @@ -26,4 +26,19 @@ interface IOutputsMerkleRootValidator is IERC165 { external view returns (bytes32); + + /// @notice Check whether an input was finalized. + /// @param appContract The application contract address + /// @param inputIndex The index of the input in the application's input box + /// @param blockNumber The number of the base-layer block in which the input was added + /// @return Whether the input was finalized + /// @dev This function assumes that an input with such an index exists in the input + /// box of the application, and that it was added in such a base-layer block. + /// Foreclosed applications can use this function to issue refunds for deposit inputs + /// that were not finalized. + function wasInputFinalized( + address appContract, + uint256 inputIndex, + uint256 blockNumber + ) external view returns (bool); } diff --git a/src/dapp/Application.sol b/src/dapp/Application.sol index b98556e4..91847565 100644 --- a/src/dapp/Application.sol +++ b/src/dapp/Application.sol @@ -6,11 +6,14 @@ pragma solidity ^0.8.30; import {IOwnable} from "../access/IOwnable.sol"; import {AccountValidityProof} from "../common/AccountValidityProof.sol"; import {CanonicalMachine} from "../common/CanonicalMachine.sol"; +import {DataAvailability} from "../common/DataAvailability.sol"; +import {Inputs} from "../common/Inputs.sol"; import {OutputValidityProof} from "../common/OutputValidityProof.sol"; import {Outputs} from "../common/Outputs.sol"; import {RollupsContract} from "../common/RollupsContract.sol"; import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IOutputsMerkleRootValidator} from "../consensus/IOutputsMerkleRootValidator.sol"; +import {IInputBox} from "../inputs/IInputBox.sol"; import {LibAccountValidityProof} from "../library/LibAccountValidityProof.sol"; import {LibAddress} from "../library/LibAddress.sol"; import {LibBinaryMerkleTree} from "../library/LibBinaryMerkleTree.sol"; @@ -18,6 +21,7 @@ import {LibBytes} from "../library/LibBytes.sol"; import {LibKeccak256} from "../library/LibKeccak256.sol"; import {LibOutputValidityProof} from "../library/LibOutputValidityProof.sol"; import {LibWithdrawalConfig} from "../library/LibWithdrawalConfig.sol"; +import {IRefundOutputBuilder} from "../refund/IRefundOutputBuilder.sol"; import {IWithdrawalOutputBuilder} from "../withdrawal/IWithdrawalOutputBuilder.sol"; import {IApplication} from "./IApplication.sol"; import {IApplicationFactoryErrors} from "./IApplicationFactoryErrors.sol"; @@ -68,6 +72,10 @@ contract Application is /// @dev See the `getAccountsDriveStartIndex` function. uint64 immutable ACCOUNTS_DRIVE_START_INDEX; + /// @notice The refund output builder contract. + /// @dev See the `getRefundOutputBuilder` function. + IRefundOutputBuilder immutable REFUND_OUTPUT_BUILDER; + /// @notice The withdrawal output builder contract. /// @dev See the `getWithdrawalOutputBuilder` function. IWithdrawalOutputBuilder immutable WITHDRAWAL_OUTPUT_BUILDER; @@ -76,6 +84,10 @@ contract Application is /// @dev See the `wasOutputExecuted` function. BitMaps.BitMap internal _executed; + /// @notice Keeps track of which inputs have been refunded. + /// @dev See the `wasRefundForInputIssued` function. + BitMaps.BitMap internal _refunded; + /// @notice Keeps track of which accounts have been withdrawn. /// @dev See the `wereAccountFundsWithdrawn` function. BitMaps.BitMap internal _withdrawn; @@ -106,6 +118,10 @@ contract Application is /// @dev See the `getNumberOfExecutedOutputs` function. uint256 _numOfExecutedOutputs; + /// @notice The number of refunds issued by the application. + /// @dev See the `getNumberOfIssuedRefunds` function. + uint256 _numOfIssuedRefunds; + /// @notice The number of withdrawals from the application. /// @dev See the `getNumberOfWithdrawals` function. uint256 _numOfWithdrawals; @@ -115,6 +131,7 @@ contract Application is /// @param initialOwner The initial application owner /// @param templateHash The initial machine state hash /// @param dataAvailability The data availability solution + /// @param refundOutputBuilder The refund output builder /// @param withdrawalConfig The withdrawal configuration /// @dev Reverts if the initial application owner address is zero. constructor( @@ -122,6 +139,7 @@ contract Application is address initialOwner, bytes32 templateHash, bytes memory dataAvailability, + IRefundOutputBuilder refundOutputBuilder, WithdrawalConfig memory withdrawalConfig ) Ownable(initialOwner) { require( @@ -133,6 +151,7 @@ contract Application is LOG2_LEAVES_PER_ACCOUNT = withdrawalConfig.log2LeavesPerAccount; LOG2_MAX_NUM_OF_ACCOUNTS = withdrawalConfig.log2MaxNumOfAccounts; ACCOUNTS_DRIVE_START_INDEX = withdrawalConfig.accountsDriveStartIndex; + REFUND_OUTPUT_BUILDER = refundOutputBuilder; WITHDRAWAL_OUTPUT_BUILDER = withdrawalConfig.withdrawalOutputBuilder; _outputsMerkleRootValidator = outputsMerkleRootValidator; _dataAvailability = dataAvailability; @@ -165,6 +184,33 @@ contract Application is emit OutputExecuted(outputIndex, output); } + function issueRefund(uint256 inputIndex, bytes calldata input) + external + override + nonReentrant + onlyForeclosed + { + (uint256 blockNumber, address sender, bytes memory payload) = + validateInput(inputIndex, input); + + if (_wasInputFinalized(inputIndex, blockNumber)) { + revert CannotRefundFinalizedInput(inputIndex); + } + + bytes memory output = _buildRefundOutput(sender, payload); + + if (_refunded.get(inputIndex)) { + revert RefundAlreadyIssued(inputIndex); + } + + _executeOutput(output); + + _refunded.set(inputIndex); + + ++_numOfIssuedRefunds; + emit RefundIssued(inputIndex, input, output); + } + function proveAccountsDriveMerkleRoot( bytes32 accountsDriveMerkleRoot, bytes32[] calldata proof @@ -255,6 +301,15 @@ contract Application is return _executed.get(outputIndex); } + function wasRefundForInputIssued(uint256 inputIndex) + external + view + override + returns (bool) + { + return _refunded.get(inputIndex); + } + function wereAccountFundsWithdrawn(uint256 accountIndex) external view @@ -289,6 +344,58 @@ contract Application is } } + function validateInput(uint256 inputIndex, bytes calldata input) + public + view + override + returns (uint256 blockNumber, address inputSender, bytes memory inputPayload) + { + validateInputHash(inputIndex, keccak256(input)); + + require( + (input.length >= 4) && (bytes4(input[:4]) == Inputs.EvmAdvance.selector), + IllFormedInput() + ); + + uint256 chainId; + address appContract; + uint256 blockTimestamp; + uint256 index; + + ( + chainId, + appContract, + inputSender, + blockNumber, + blockTimestamp,/* prevRandao */, + index, + inputPayload + ) = + abi.decode( + input[4:], + (uint256, address, address, uint256, uint256, uint256, uint256, bytes) + ); + + require( + (chainId == block.chainid) && (appContract == address(this)) + && (blockNumber <= block.number) && (blockTimestamp <= block.timestamp) + && (index == inputIndex), + IllFormedInput() + ); + } + + function validateInputHash(uint256 inputIndex, bytes32 inputHash) + public + view + override + { + IInputBox inputBox = _getInputBox(); + uint256 numOfInputs = inputBox.getNumberOfInputs(address(this)); + require(inputIndex < numOfInputs, InvalidInputIndex(inputIndex, numOfInputs)); + bytes32 stInputHash = inputBox.getInputHash(address(this), inputIndex); + require(stInputHash == inputHash, InvalidInputHash(stInputHash, inputHash)); + } + function validateAccount(bytes calldata account, AccountValidityProof calldata proof) public view @@ -340,7 +447,7 @@ contract Application is } /// @inheritdoc IApplication - function getDataAvailability() external view override returns (bytes memory) { + function getDataAvailability() public view override returns (bytes memory) { return _dataAvailability; } @@ -354,6 +461,10 @@ contract Application is return _numOfExecutedOutputs; } + function getNumberOfIssuedRefunds() external view override returns (uint256) { + return _numOfIssuedRefunds; + } + function getNumberOfWithdrawals() external view override returns (uint256) { return _numOfWithdrawals; } @@ -374,6 +485,15 @@ contract Application is return GUARDIAN; } + function getRefundOutputBuilder() + public + view + override + returns (IRefundOutputBuilder) + { + return REFUND_OUTPUT_BUILDER; + } + function getWithdrawalOutputBuilder() public view @@ -442,6 +562,25 @@ contract Application is _; } + /// @notice Get the input box contract used as data availability. + function _getInputBox() internal view returns (IInputBox inputBox) { + bool hasSelector; + bytes32 selector; + bytes memory arguments; + + (hasSelector, selector, arguments) = getDataAvailability().consumeBytes4(); + + require(hasSelector, UnknownDataAvailability()); + + if (selector == DataAvailability.InputBox.selector) { + inputBox = abi.decode(arguments, (IInputBox)); + } else if (selector == DataAvailability.InputBoxAndEspresso.selector) { + (inputBox,,) = abi.decode(arguments, (IInputBox, uint256, uint32)); + } else { + revert UnknownDataAvailability(); + } + } + /// @notice Get the log (base 2) of the number of bytes in the machine memory that are /// reserved for the accounts drive. function _getLog2AccountsDriveSize() internal view returns (uint8) { @@ -480,6 +619,32 @@ contract Application is } } + /// @notice Check if an input was finalized, + /// according to the current outputs Merkle root validator. + /// @param inputIndex The index of the input in the application's input box + /// @param blockNumber The number of the base-layer block in which the input was added + function _wasInputFinalized(uint256 inputIndex, uint256 blockNumber) + internal + view + returns (bool) + { + return getOutputsMerkleRootValidator() + .wasInputFinalized(address(this), inputIndex, blockNumber); + } + + /// @notice Build a refund output from an input, + /// using the refund output builder contract. + /// @param sender The input sender + /// @param payload The input payload + /// @return output The refund output + function _buildRefundOutput(address sender, bytes memory payload) + internal + view + returns (bytes memory output) + { + return getRefundOutputBuilder().buildRefundOutput(address(this), sender, payload); + } + /// @notice Build a withdrawal output from an account, /// using the withdrawal output builder contract. /// @param account The account diff --git a/src/dapp/ApplicationFactory.sol b/src/dapp/ApplicationFactory.sol index 2b92e705..a8478206 100644 --- a/src/dapp/ApplicationFactory.sol +++ b/src/dapp/ApplicationFactory.sol @@ -8,6 +8,7 @@ import {Create2} from "@openzeppelin-contracts-5.2.0/utils/Create2.sol"; import {RollupsContract} from "../common/RollupsContract.sol"; import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IOutputsMerkleRootValidator} from "../consensus/IOutputsMerkleRootValidator.sol"; +import {IRefundOutputBuilder} from "../refund/IRefundOutputBuilder.sol"; import {Application} from "./Application.sol"; import {IApplication} from "./IApplication.sol"; import {IApplicationFactory} from "./IApplicationFactory.sol"; @@ -15,6 +16,14 @@ import {IApplicationFactory} from "./IApplicationFactory.sol"; /// @title Application Factory /// @notice Allows anyone to reliably deploy a new `IApplication` contract. contract ApplicationFactory is IApplicationFactory, RollupsContract { + IRefundOutputBuilder immutable REFUND_OUTPUT_BUILDER; + + /// @notice Creates an `ApplicationFactory` contract. + /// @param refundOutputBuilder The refund output builder + constructor(IRefundOutputBuilder refundOutputBuilder) { + REFUND_OUTPUT_BUILDER = refundOutputBuilder; + } + function newApplication( IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, @@ -27,6 +36,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { appOwner, templateHash, dataAvailability, + REFUND_OUTPUT_BUILDER, withdrawalConfig ); @@ -53,6 +63,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { appOwner, templateHash, dataAvailability, + REFUND_OUTPUT_BUILDER, withdrawalConfig ); @@ -84,6 +95,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { appOwner, templateHash, dataAvailability, + REFUND_OUTPUT_BUILDER, withdrawalConfig ) ) diff --git a/src/dapp/IApplication.sol b/src/dapp/IApplication.sol index 934d01d6..76889ea3 100644 --- a/src/dapp/IApplication.sol +++ b/src/dapp/IApplication.sol @@ -10,6 +10,8 @@ import {IVersionGetter} from "../common/IVersionGetter.sol"; import {OutputValidityProof} from "../common/OutputValidityProof.sol"; import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IOutputsMerkleRootValidator} from "../consensus/IOutputsMerkleRootValidator.sol"; +import {IRefundOutputBuilder} from "../refund/IRefundOutputBuilder.sol"; +import {IRefundOutputBuilderErrors} from "../refund/IRefundOutputBuilderErrors.sol"; import {IWithdrawalOutputBuilder} from "../withdrawal/IWithdrawalOutputBuilder.sol"; import {IWithdrawalOutputBuilderErrors} from "../withdrawal/IWithdrawalOutputBuilderErrors.sol"; @@ -33,6 +35,7 @@ import {IWithdrawalOutputBuilderErrors} from "../withdrawal/IWithdrawalOutputBui interface IApplication is IOwnable, BinaryMerkleTreeErrors, + IRefundOutputBuilderErrors, IWithdrawalOutputBuilderErrors, IVersionGetter { @@ -50,6 +53,12 @@ interface IApplication is /// @notice MUST trigger when the application is foreclosed. event Foreclosure(); + /// @notice MUST trigger when a refund for an input is issued. + /// @param inputIndex The index of the input + /// @param input The input + /// @param output The refund output + event RefundIssued(uint256 inputIndex, bytes input, bytes output); + /// @notice MUST trigger when the accounts drive Merkle root is proved. /// @param accountsDriveMerkleRoot The accounts drive Merkle root event AccountsDriveMerkleRootProved(bytes32 accountsDriveMerkleRoot); @@ -94,6 +103,35 @@ interface IApplication is /// and therefore some actions cannot be performed anymore. error Foreclosed(); + /// @notice Raised when trying to decode the data availability byte array, + /// but either it is ill-formed or encodes an unknown data availability solution. + error UnknownDataAvailability(); + + /// @notice Raised when trying to validate an input with an invalid index. + /// @param invalidInputIndex The invalid input index provided for validation + /// @param numOfInputs The actual number of inputs to the application + /// @dev This error is raised when invalidInputIndex >= numOfInputs. + error InvalidInputIndex(uint256 invalidInputIndex, uint256 numOfInputs); + + /// @notice Raised when trying to validate an input with an invalid hash. + /// @param storedInputHash The hash of the input stored in the input box + /// @param invalidInputHash The invalid input hash provided for validation + /// @dev This error is raised when storedInputHash != invalidInputHash. + error InvalidInputHash(bytes32 storedInputHash, bytes32 invalidInputHash); + + /// @notice Raised when decoding an ill-formed input. + /// @dev This error should never be raised if the application uses + /// the canonical input box contract as on-chain data availability. + error IllFormedInput(); + + /// @notice Raised when trying to issue a refund for a finalized input. + /// @param inputIndex The input index + error CannotRefundFinalizedInput(uint256 inputIndex); + + /// @notice Raised when trying to re-issue a refund for the same input. + /// @param inputIndex The input index + error RefundAlreadyIssued(uint256 inputIndex); + /// @notice Raised when the accounts drive Merkle root proof size is invalid. /// @dev The array length should be log2 of the machine memory size - log2 of the /// accounts drive size. See the `CanonicalMachine` library and the @@ -160,6 +198,14 @@ interface IApplication is function executeOutput(bytes calldata output, OutputValidityProof calldata proof) external; + /// @notice Issue a refund for an unprocessed input. + /// @param inputIndex The index of the input in the application's input box. + /// @param input The input that was sent to the application + /// @dev May raise `CannotRefundFinalizedInput`, `RefundAlreadyIssued`, + /// `UnknownInputSender`, as well as any of the errors raised by `validateInput`. + /// On success, marks the input as refunded, and emits a `RefundIssued` event. + function issueRefund(uint256 inputIndex, bytes calldata input) external; + /// @notice Prove the accounts drive Merkle root in the last-finalized machine state /// provided by the application's outputs Merkle root validator or in the initial /// machine Merkle root (template hash) if no machine Merkle root has been finalized @@ -248,6 +294,14 @@ interface IApplication is view returns (WithdrawalConfig memory withdrawalConfig); + /// @notice Get the number of issued refunds. + /// Useful for fast-syncing `RefundIssued` events. + function getNumberOfIssuedRefunds() external view returns (uint256); + + /// @notice Check whether a refund had been issued for an input. + /// @param inputIndex The index of the input in the application's input box + function wasRefundForInputIssued(uint256 inputIndex) external view returns (bool); + /// @notice Check whether the accounts drive Merkle root was proved and its value. /// @return wasAccountsDriveMerkleRootProved Whether the accounts drive Merkle root was proved /// @return accountsDriveMerkleRoot The accounts drive Merkle root (if proved) @@ -282,6 +336,28 @@ interface IApplication is /// at memory address `c*2^{a+b+5}` and has `2^{a+b+5}` bytes in size. function getAccountsDriveStartIndex() external view returns (uint64); + /// @notice Get the refund output builder, which gets static-called + /// whenever a deposit is to be refunded to the original depositor. + function getRefundOutputBuilder() external view returns (IRefundOutputBuilder); + + /// @notice Validates an input that was sent to the application. + /// @param inputIndex The index of the input in the application's input box. + /// @param input The input that was sent to the application + /// @return blockNumber The number of the base-layer block in which the input was added + /// @return inputSender The input sender + /// @return inputPayload The input payload + /// @dev May raise `IllFormedInput` as well as any of the errors raised by `validateInputHash`. + function validateInput(uint256 inputIndex, bytes calldata input) + external + view + returns (uint256 blockNumber, address inputSender, bytes memory inputPayload); + + /// @notice Validates an input that was sent to the application. + /// @param inputIndex The index of the input in the application's input box. + /// @param inputHash The hash of the input that was sent to the application + /// @dev May raise `UnknownDataAvailability`, `InvalidInputIndex` or `InvalidInputHash`. + function validateInputHash(uint256 inputIndex, bytes32 inputHash) external view; + /// @notice Get the withdrawal output builder, which gets static-called /// whenever the funds of an account are to be withdrawn. function getWithdrawalOutputBuilder() external view returns (IWithdrawalOutputBuilder); diff --git a/src/devnet/TestMultiToken.sol b/src/devnet/TestMultiToken.sol index 6892d5f8..f9299396 100644 --- a/src/devnet/TestMultiToken.sol +++ b/src/devnet/TestMultiToken.sol @@ -23,4 +23,24 @@ contract TestMultiToken is ERC1155 { bytes memory data; _mintBatch(msg.sender, tokenIds, values, data); } + + /// @notice Mint multi-tokens. + /// @param to The account that will receive the tokens + /// @param tokenId The multi-token ID + /// @param value The amount of fungible tokens to mint + function mint(address to, uint256 tokenId, uint256 value) external { + bytes memory data; + _mint(to, tokenId, value, data); + } + + /// @notice Mint a batch of multi-tokens. + /// @param to The account that will receive the tokens + /// @param tokenIds The multi-token IDs + /// @param values The amounts of fungible tokens to mint + function mintBatch(address to, uint256[] calldata tokenIds, uint256[] calldata values) + external + { + bytes memory data; + _mintBatch(to, tokenIds, values, data); + } } diff --git a/src/devnet/TestNonFungibleToken.sol b/src/devnet/TestNonFungibleToken.sol index a6fff88f..d9791eae 100644 --- a/src/devnet/TestNonFungibleToken.sol +++ b/src/devnet/TestNonFungibleToken.sol @@ -13,4 +13,11 @@ contract TestNonFungibleToken is ERC721 { function mint(uint256 tokenId) external { _mint(msg.sender, tokenId); } + + /// @notice Mint a non-fungible token. + /// @param to The account that will receive the token + /// @param tokenId The non-fungible token ID + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } } diff --git a/src/library/LibDelegateCallVoucher.sol b/src/library/LibDelegateCallVoucher.sol new file mode 100644 index 00000000..3d8d3f55 --- /dev/null +++ b/src/library/LibDelegateCallVoucher.sol @@ -0,0 +1,20 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {DelegateCallVoucher} from "../common/DelegateCallVoucher.sol"; +import {Outputs} from "../common/Outputs.sol"; + +library LibDelegateCallVoucher { + /// @notice Encode a delegate-call voucher as an output. + /// @param v The delegate-call voucher + /// @return output The encoded delegate-call voucher + function encode(DelegateCallVoucher memory v) + internal + pure + returns (bytes memory output) + { + return abi.encodeCall(Outputs.DelegateCallVoucher, (v.destination, v.payload)); + } +} diff --git a/src/library/LibErc1155BatchDeposit.sol b/src/library/LibErc1155BatchDeposit.sol new file mode 100644 index 00000000..8c0de3e2 --- /dev/null +++ b/src/library/LibErc1155BatchDeposit.sol @@ -0,0 +1,30 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Erc1155BatchDeposit} from "../common/Erc1155BatchDeposit.sol"; +import {Voucher} from "../common/Voucher.sol"; + +library LibErc1155BatchDeposit { + function buildRefund(Erc1155BatchDeposit memory deposit, address appContract) + internal + pure + returns (Voucher memory voucher) + { + return Voucher({ + destination: address(deposit.token), + value: 0, + payload: abi.encodeCall( + deposit.token.safeBatchTransferFrom, + ( + appContract, + deposit.sender, + deposit.tokenIds, + deposit.values, + new bytes(0) // no ERC-1155 transfer data + ) + ) + }); + } +} diff --git a/src/library/LibErc1155SingleDeposit.sol b/src/library/LibErc1155SingleDeposit.sol new file mode 100644 index 00000000..c4293d89 --- /dev/null +++ b/src/library/LibErc1155SingleDeposit.sol @@ -0,0 +1,30 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Erc1155SingleDeposit} from "../common/Erc1155SingleDeposit.sol"; +import {Voucher} from "../common/Voucher.sol"; + +library LibErc1155SingleDeposit { + function buildRefund(Erc1155SingleDeposit memory deposit, address appContract) + internal + pure + returns (Voucher memory voucher) + { + return Voucher({ + destination: address(deposit.token), + value: 0, + payload: abi.encodeCall( + deposit.token.safeTransferFrom, + ( + appContract, + deposit.sender, + deposit.tokenId, + deposit.value, + new bytes(0) // no ERC-1155 transfer data + ) + ) + }); + } +} diff --git a/src/library/LibErc20Deposit.sol b/src/library/LibErc20Deposit.sol new file mode 100644 index 00000000..9819adf7 --- /dev/null +++ b/src/library/LibErc20Deposit.sol @@ -0,0 +1,24 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {DelegateCallVoucher} from "../common/DelegateCallVoucher.sol"; +import {Erc20Deposit} from "../common/Erc20Deposit.sol"; +import {ISafeERC20Transfer} from "../delegatecall/ISafeERC20Transfer.sol"; + +library LibErc20Deposit { + function buildRefund(Erc20Deposit memory deposit, ISafeERC20Transfer safeTransfer) + internal + pure + returns (DelegateCallVoucher memory delegateCallVoucher) + { + return DelegateCallVoucher({ + destination: address(safeTransfer), + payload: abi.encodeCall( + ISafeERC20Transfer.safeTransfer, + (deposit.token, deposit.sender, deposit.value) + ) + }); + } +} diff --git a/src/library/LibErc721Deposit.sol b/src/library/LibErc721Deposit.sol new file mode 100644 index 00000000..cffbb271 --- /dev/null +++ b/src/library/LibErc721Deposit.sol @@ -0,0 +1,32 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Erc721Deposit} from "../common/Erc721Deposit.sol"; +import {Voucher} from "../common/Voucher.sol"; + +/// @title Auxiliary interface for encoding calls to ERC-721 safeTransferFrom +/// with abi.encodeCall (which leverages Solidity type checker) instead of +/// abi.encodeWithSignature (which does not type-check call arguments). +/// @dev See https://github.com/argotorg/solidity/issues/3556 +interface IERC721SafeTransferFromWithoutData { + function safeTransferFrom(address, address, uint256) external; +} + +library LibErc721Deposit { + function buildRefund(Erc721Deposit memory deposit, address appContract) + internal + pure + returns (Voucher memory voucher) + { + return Voucher({ + destination: address(deposit.token), + value: 0, + payload: abi.encodeCall( + IERC721SafeTransferFromWithoutData.safeTransferFrom, + (appContract, deposit.sender, deposit.tokenId) + ) + }); + } +} diff --git a/src/library/LibError.sol b/src/library/LibError.sol index 6279a95e..d45c22b4 100644 --- a/src/library/LibError.sol +++ b/src/library/LibError.sol @@ -10,7 +10,7 @@ library LibError { if (errordata.length == 0) { revert(); } else { - assembly { + assembly ("memory-safe") { revert(add(32, errordata), mload(errordata)) } } diff --git a/src/library/LibEtherDeposit.sol b/src/library/LibEtherDeposit.sol new file mode 100644 index 00000000..300637fa --- /dev/null +++ b/src/library/LibEtherDeposit.sol @@ -0,0 +1,21 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {EtherDeposit} from "../common/EtherDeposit.sol"; +import {Voucher} from "../common/Voucher.sol"; + +library LibEtherDeposit { + function buildRefund(EtherDeposit memory deposit) + internal + pure + returns (Voucher memory voucher) + { + return Voucher({ + destination: deposit.sender, + value: deposit.value, + payload: new bytes(0) // triggers receive() on Solidity (>= 0.6.0) contracts + }); + } +} diff --git a/src/library/LibKeccak256.sol b/src/library/LibKeccak256.sol index f7abf9f6..288659ac 100644 --- a/src/library/LibKeccak256.sol +++ b/src/library/LibKeccak256.sol @@ -7,8 +7,7 @@ library LibKeccak256 { /// @notice Hash a variable-length byte array. /// @param b The byte array function hashBytes(bytes memory b) internal pure returns (bytes32 result) { - /// @solidity memory-safe-assembly - assembly { + assembly ("memory-safe") { result := keccak256(add(b, 0x20), mload(b)) } } @@ -29,7 +28,7 @@ library LibKeccak256 { uint256 dataLength = data.length; if (end <= dataLength) { // Block is completely within data and can be hashed in-place, without memory allocation - assembly { + assembly ("memory-safe") { result := keccak256(add(add(data, 0x20), start), dataBlockSize) } } else { @@ -37,7 +36,7 @@ library LibKeccak256 { bytes memory dataBlock = new bytes(dataBlockSize); if (start < dataLength) { // Block is partially within data and requires a memory-copy operation - assembly { + assembly ("memory-safe") { mcopy( add(dataBlock, 0x20), add(add(data, 0x20), start), @@ -46,7 +45,7 @@ library LibKeccak256 { } } // Block is then hashed with a known size - assembly { + assembly ("memory-safe") { result := keccak256(add(dataBlock, 0x20), dataBlockSize) } } @@ -56,8 +55,7 @@ library LibKeccak256 { /// @dev Equivalent to keccak256(abi.encode(a, b)). /// @dev Uses assembly to avoid memory allocation or expansion. function hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32 result) { - /// @solidity memory-safe-assembly - assembly { + assembly ("memory-safe") { mstore(0x00, a) mstore(0x20, b) result := keccak256(0x00, 0x40) diff --git a/src/library/LibVoucher.sol b/src/library/LibVoucher.sol new file mode 100644 index 00000000..6109a7ca --- /dev/null +++ b/src/library/LibVoucher.sol @@ -0,0 +1,16 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Outputs} from "../common/Outputs.sol"; +import {Voucher} from "../common/Voucher.sol"; + +library LibVoucher { + /// @notice Encode a voucher as an output. + /// @param v The voucher + /// @return output The encoded voucher + function encode(Voucher memory v) internal pure returns (bytes memory output) { + return abi.encodeCall(Outputs.Voucher, (v.destination, v.value, v.payload)); + } +} diff --git a/src/refund/IRefundOutputBuilder.sol b/src/refund/IRefundOutputBuilder.sol new file mode 100644 index 00000000..64f211ef --- /dev/null +++ b/src/refund/IRefundOutputBuilder.sol @@ -0,0 +1,28 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {IVersionGetter} from "../common/IVersionGetter.sol"; +import {IRefundOutputBuilderErrors} from "./IRefundOutputBuilderErrors.sol"; + +interface IRefundOutputBuilder is IRefundOutputBuilderErrors, IVersionGetter { + /// @notice Build an output that, when executed by the application contract, reverts + /// an unprocessed deposit by transferring the asset(s) back to the original sender + /// account. This function will be called via the `STATICCALL` opcode, so any state + /// changes such as contract creations, log emissions, storage writes, Ether transfers + /// and self-destructions will revert the call and abort the execution of the refund + /// output. These state-changing constraints are already checked by the Solidity + /// compiler when implementing this function as either view or pure. + /// @param appContract The application contract address + /// @param inputSender The input sender + /// @param inputPayload The input payload + /// @return output The refund output + /// @dev This function assumes the input box of the application indeed contains an + /// input with such a sender and payload. May raise `UnknownInputSender`. + function buildRefundOutput( + address appContract, + address inputSender, + bytes calldata inputPayload + ) external view returns (bytes memory output); +} diff --git a/src/refund/IRefundOutputBuilderErrors.sol b/src/refund/IRefundOutputBuilderErrors.sol new file mode 100644 index 00000000..6f4a3058 --- /dev/null +++ b/src/refund/IRefundOutputBuilderErrors.sol @@ -0,0 +1,12 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +interface IRefundOutputBuilderErrors { + /// @notice This error is raised whenever a user provides an input whose sender is + /// unknown to the refund output builder contract. Usually, this happens when the user + /// provides a non-deposit input or an input sent by a non-canonical portal contract. + /// @param inputSender The input sender + error UnknownInputSender(address inputSender); +} diff --git a/src/refund/RefundOutputBuilder.sol b/src/refund/RefundOutputBuilder.sol new file mode 100644 index 00000000..cdb86b42 --- /dev/null +++ b/src/refund/RefundOutputBuilder.sol @@ -0,0 +1,82 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {DelegateCallVoucher} from "../common/DelegateCallVoucher.sol"; +import {Erc1155BatchDeposit} from "../common/Erc1155BatchDeposit.sol"; +import {Erc1155SingleDeposit} from "../common/Erc1155SingleDeposit.sol"; +import {Erc20Deposit} from "../common/Erc20Deposit.sol"; +import {Erc721Deposit} from "../common/Erc721Deposit.sol"; +import {EtherDeposit} from "../common/EtherDeposit.sol"; +import {InputEncoding} from "../common/InputEncoding.sol"; +import {RollupsContract} from "../common/RollupsContract.sol"; +import {Voucher} from "../common/Voucher.sol"; +import {ISafeERC20Transfer} from "../delegatecall/ISafeERC20Transfer.sol"; +import {LibDelegateCallVoucher} from "../library/LibDelegateCallVoucher.sol"; +import {LibErc1155BatchDeposit} from "../library/LibErc1155BatchDeposit.sol"; +import {LibErc1155SingleDeposit} from "../library/LibErc1155SingleDeposit.sol"; +import {LibErc20Deposit} from "../library/LibErc20Deposit.sol"; +import {LibErc721Deposit} from "../library/LibErc721Deposit.sol"; +import {LibEtherDeposit} from "../library/LibEtherDeposit.sol"; +import {LibVoucher} from "../library/LibVoucher.sol"; +import {IERC1155BatchPortal} from "../portals/IERC1155BatchPortal.sol"; +import {IERC1155SinglePortal} from "../portals/IERC1155SinglePortal.sol"; +import {IERC20Portal} from "../portals/IERC20Portal.sol"; +import {IERC721Portal} from "../portals/IERC721Portal.sol"; +import {IEtherPortal} from "../portals/IEtherPortal.sol"; +import {IRefundOutputBuilder} from "./IRefundOutputBuilder.sol"; + +contract RefundOutputBuilder is IRefundOutputBuilder, RollupsContract { + using InputEncoding for bytes; + using LibEtherDeposit for EtherDeposit; + using LibErc20Deposit for Erc20Deposit; + using LibErc721Deposit for Erc721Deposit; + using LibErc1155BatchDeposit for Erc1155BatchDeposit; + using LibErc1155SingleDeposit for Erc1155SingleDeposit; + using LibVoucher for Voucher; + using LibDelegateCallVoucher for DelegateCallVoucher; + + IEtherPortal immutable ETHER_PORTAL; + IERC20Portal immutable ERC20_PORTAL; + IERC721Portal immutable ERC721_PORTAL; + IERC1155SinglePortal immutable ERC1155_SINGLE_PORTAL; + IERC1155BatchPortal immutable ERC1155_BATCH_PORTAL; + ISafeERC20Transfer immutable SAFE_TRANSFER; + + constructor( + IEtherPortal etherPortal, + IERC20Portal erc20Portal, + IERC721Portal erc721Portal, + IERC1155SinglePortal erc1155SinglePortal, + IERC1155BatchPortal erc1155BatchPortal, + ISafeERC20Transfer safeTransfer + ) { + ETHER_PORTAL = etherPortal; + ERC20_PORTAL = erc20Portal; + ERC721_PORTAL = erc721Portal; + ERC1155_SINGLE_PORTAL = erc1155SinglePortal; + ERC1155_BATCH_PORTAL = erc1155BatchPortal; + SAFE_TRANSFER = safeTransfer; + } + + function buildRefundOutput( + address appContract, + address inputSender, + bytes calldata payload + ) external view override returns (bytes memory output) { + if (inputSender == address(ETHER_PORTAL)) { + return payload.decodeEtherDeposit().buildRefund().encode(); + } else if (inputSender == address(ERC20_PORTAL)) { + return payload.decodeErc20Deposit().buildRefund(SAFE_TRANSFER).encode(); + } else if (inputSender == address(ERC721_PORTAL)) { + return payload.decodeErc721Deposit().buildRefund(appContract).encode(); + } else if (inputSender == address(ERC1155_SINGLE_PORTAL)) { + return payload.decodeErc1155SingleDeposit().buildRefund(appContract).encode(); + } else if (inputSender == address(ERC1155_BATCH_PORTAL)) { + return payload.decodeErc1155BatchDeposit().buildRefund(appContract).encode(); + } else { + revert UnknownInputSender(inputSender); + } + } +} diff --git a/test/common/InputEncoding.t.sol b/test/common/InputEncoding.t.sol new file mode 100644 index 00000000..09f38045 --- /dev/null +++ b/test/common/InputEncoding.t.sol @@ -0,0 +1,74 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {Erc1155BatchDeposit} from "src/common/Erc1155BatchDeposit.sol"; +import {Erc1155SingleDeposit} from "src/common/Erc1155SingleDeposit.sol"; +import {Erc20Deposit} from "src/common/Erc20Deposit.sol"; +import {Erc721Deposit} from "src/common/Erc721Deposit.sol"; +import {EtherDeposit} from "src/common/EtherDeposit.sol"; + +import {LibDepositDecoder} from "../util/LibDepositDecoder.sol"; +import {LibDepositEncoder} from "../util/LibDepositEncoder.sol"; + +contract InputEncodingTest is Test { + using LibDepositEncoder for EtherDeposit; + using LibDepositEncoder for Erc20Deposit; + using LibDepositEncoder for Erc721Deposit; + using LibDepositEncoder for Erc1155SingleDeposit; + using LibDepositEncoder for Erc1155BatchDeposit; + using LibDepositDecoder for bytes; + + function testEtherDeposit( + EtherDeposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external pure { + assertEq( + abi.encode(deposit), + abi.encode(deposit.encode(extraData).decodeEtherDeposit()) + ); + } + + function testErc20Deposit( + Erc20Deposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external pure { + assertEq( + abi.encode(deposit), + abi.encode(deposit.encode(extraData).decodeErc20Deposit()) + ); + } + + function testErc721Deposit( + Erc721Deposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external pure { + assertEq( + abi.encode(deposit), + abi.encode(deposit.encode(extraData).decodeErc721Deposit()) + ); + } + + function testErc1155SingleDeposit( + Erc1155SingleDeposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external pure { + assertEq( + abi.encode(deposit), + abi.encode(deposit.encode(extraData).decodeErc1155SingleDeposit()) + ); + } + + function testErc1155BatchDeposit( + Erc1155BatchDeposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external pure { + assertEq( + abi.encode(deposit), + abi.encode(deposit.encode(extraData).decodeErc1155BatchDeposit()) + ); + } +} diff --git a/test/consensus/authority/AuthorityFactory.t.sol b/test/consensus/authority/AuthorityFactory.t.sol index 02c418d6..353d7ee7 100644 --- a/test/consensus/authority/AuthorityFactory.t.sol +++ b/test/consensus/authority/AuthorityFactory.t.sol @@ -439,6 +439,7 @@ contract AuthorityFactoryTest is } bytes32 lastFinalizedMachineMerkleRoot; + uint256 firstUnprocessedBlockNumber; for (uint256 claimIndex; claimIndex < blockNumbers.length; ++claimIndex) { claim.lastProcessedBlockNumber = blockNumbers[claimIndex]; @@ -607,10 +608,11 @@ contract AuthorityFactoryTest is { (bool isEmpty, uint256 max) = blockNumbers.maxBefore(claimIndex); - // If the claim was successful submitted, then its last processed + // If the claim was successfully accepted, then its last processed // block number cannot be equal to any past successful claim. if (isEmpty || claim.lastProcessedBlockNumber > max) { lastFinalizedMachineMerkleRoot = machineMerkleRoot; + firstUnprocessedBlockNumber = claim.lastProcessedBlockNumber + 1; } } @@ -753,6 +755,35 @@ contract AuthorityFactoryTest is "Check last finalized machine Merkle root" ); + if (firstUnprocessedBlockNumber >= 1) { + assertTrue( + authority.wasInputFinalized( + claim.appContract, + vm.randomUint(), // inputIndex + vm.randomUint(0, firstUnprocessedBlockNumber - 1) + ), + "Check all inputs added before the first unprocessed block were finalized" + ); + } + + assertFalse( + authority.wasInputFinalized( + claim.appContract, + vm.randomUint(), // inputIndex + vm.randomUint(firstUnprocessedBlockNumber, type(uint256).max) + ), + "Check all inputs added on the first unproccessed block or after were not finalized" + ); + + assertFalse( + authority.wasInputFinalized( + notAppContract, + vm.randomUint(), // inputIndex + vm.randomUint() // blockNumber + ), + "Check all inputs from other apps were not finalized" + ); + assertEq( authority.getLastFinalizedMachineMerkleRoot(notAppContract), bytes32(0), @@ -1028,6 +1059,17 @@ contract AuthorityFactoryTest is "initially, getLastFinalizedMachineMerkleRoot(...) == bytes32(0)" ); + // We check that initially no input was finalized. + assertEq( + authority.wasInputFinalized( + vm.randomAddress(), // appContract + vm.randomUint(), // inputIndex + vm.randomUint() // blockNumber + ), + false, + "initially, wasInputFinalized(...) == false" + ); + // We check that initially no claim is staged. assertEq( uint256( diff --git a/test/consensus/quorum/QuorumFactory.t.sol b/test/consensus/quorum/QuorumFactory.t.sol index 9dd9d5ae..a2e0ae90 100644 --- a/test/consensus/quorum/QuorumFactory.t.sol +++ b/test/consensus/quorum/QuorumFactory.t.sol @@ -370,6 +370,7 @@ contract QuorumFactoryTest is uint256[] memory blockNumbers = _randomEpochFinalBlockNumbers(epochLength); bytes32 lastFinalizedMachineMerkleRoot; + uint256 firstUnprocessedBlockNumber; for (uint256 claimIndex; claimIndex < blockNumbers.length; ++claimIndex) { uint256 lastProcessedBlockNumber = blockNumbers[claimIndex]; @@ -730,6 +731,26 @@ contract QuorumFactoryTest is "Check last finalized machine Merkle root" ); + if (firstUnprocessedBlockNumber >= 1) { + assertTrue( + quorum.wasInputFinalized( + claim.appContract, + vm.randomUint(), // inputIndex + vm.randomUint(0, firstUnprocessedBlockNumber - 1) + ), + "Check all inputs added before the first unprocessed block were finalized" + ); + } + + assertFalse( + quorum.wasInputFinalized( + claim.appContract, + vm.randomUint(), // inputIndex + vm.randomUint(firstUnprocessedBlockNumber, type(uint256).max) + ), + "Check all inputs added on the first unproccessed block or after were not finalized" + ); + assertEq( quorum.getNumberOfSubmittedClaims(appContract), totalNumOfSubmittedClaimsBefore + numOfClaimSubmittedEvents, @@ -750,6 +771,15 @@ contract QuorumFactoryTest is address notAppContract = vm.randomAddressNotIn(appContractSingleton); + assertFalse( + quorum.wasInputFinalized( + notAppContract, + vm.randomUint(), // inputIndex + vm.randomUint() // blockNumber + ), + "Check all inputs from other apps were not finalized" + ); + assertEq( quorum.getNumberOfSubmittedClaims(notAppContract), 0, @@ -1031,10 +1061,11 @@ contract QuorumFactoryTest is (bool isEmpty, uint256 max) = blockNumbers.maxBefore(claimIndex); - // If the claim was successful submitted, then its last processed + // If the claim was successful accepted, then its last processed // block number cannot be equal to any past successful claim. if (isEmpty || lastProcessedBlockNumber > max) { lastFinalizedMachineMerkleRoot = winningMachineMerkleRoot; + firstUnprocessedBlockNumber = lastProcessedBlockNumber + 1; } } } @@ -1332,6 +1363,17 @@ contract QuorumFactoryTest is "initially, getLastFinalizedMachineMerkleRoot(...) == bytes32(0)" ); + // We check that initially no input was finalized. + assertEq( + quorum.wasInputFinalized( + vm.randomAddress(), // appContract + vm.randomUint(), // inputIndex + vm.randomUint() // blockNumber + ), + false, + "initially, wasInputFinalized(...) == false" + ); + // We check that initially no validator is in favor of any claim in an epoch. assertEq( quorum.numOfValidatorsInFavorOfAnyClaimInEpoch( diff --git a/test/dapp/Application.t.sol b/test/dapp/Application.t.sol index db8eb5bb..dde79239 100644 --- a/test/dapp/Application.t.sol +++ b/test/dapp/Application.t.sol @@ -15,6 +15,7 @@ import {IAuthority} from "src/consensus/authority/IAuthority.sol"; import {IApplication} from "src/dapp/IApplication.sol"; import {ISafeERC20Transfer} from "src/delegatecall/ISafeERC20Transfer.sol"; import {LibUsdAccount} from "src/library/LibUsdAccount.sol"; +import {IRefundOutputBuilderErrors} from "src/refund/IRefundOutputBuilderErrors.sol"; import {IWithdrawalOutputBuilder} from "src/withdrawal/IWithdrawalOutputBuilder.sol"; import {IWithdrawalOutputBuilderErrors} from "src/withdrawal/IWithdrawalOutputBuilderErrors.sol"; @@ -34,10 +35,13 @@ import {ExternalLibUsdAccount} from "../library/LibUsdAccount.t.sol"; import {AddressGenerator} from "../util/AddressGenerator.sol"; import {ConsensusTestUtils} from "../util/ConsensusTestUtils.sol"; import {EtherReceiver, IEtherReceiver} from "../util/EtherReceiver.sol"; +import {InputBoxTestUtils} from "../util/InputBoxTestUtils.sol"; +import {LibAddressArray} from "../util/LibAddressArray.sol"; import {LibBytes} from "../util/LibBytes.sol"; import {LibBytes32Array} from "../util/LibBytes32Array.sol"; import {LibEmulator} from "../util/LibEmulator.sol"; import {LibTopic} from "../util/LibTopic.sol"; +import {LibUint256Array} from "../util/LibUint256Array.sol"; import {OwnableTest} from "../util/OwnableTest.sol"; import {RollupsTest} from "../util/RollupsTest.sol"; @@ -45,16 +49,28 @@ contract ApplicationTest is RollupsTest, OwnableTest, AddressGenerator, + InputBoxTestUtils, ConsensusTestUtils { using LibBytes for bytes; using LibTopic for address; using SafeCast for uint256; + using LibUint256Array for Vm; + using LibUint256Array for uint256[]; using LibBytes32Array for bytes32[]; + using LibAddressArray for address; using LibEmulator for LibEmulator.State; using LibEmulator for LibEmulator.ProofComponents; using ExternalLibBinaryMerkleTree for bytes32[]; + enum DepositType { + ETHER, + ERC20, + ERC721, + ERC1155_SINGLE, + ERC1155_BATCH + } + IApplication _appContract; IEtherReceiver _etherReceiver; IAuthority _authority; @@ -71,7 +87,6 @@ contract ApplicationTest is address _appOwner; address _authorityOwner; address _recipient; - address _tokenOwner; bytes _dataAvailability; string[] _outputNames; string[] _accountNames; @@ -93,7 +108,6 @@ contract ApplicationTest is _initVariables(); _computeTemplateHash(); _deployContracts(); - _mintTokens(); _addOutputs(); _addAccounts(); _submitClaim(); @@ -628,10 +642,8 @@ contract ApplicationTest is (, uint256 amount) = ExternalLibUsdAccount.decode(account); AccountValidityProof memory proof = _getAccountValidityProof(name); - uint256 balance = vm.randomUint(amount, _erc20Token.balanceOf(_tokenOwner)); - - vm.prank(_tokenOwner); - assertTrue(_erc20Token.transfer(address(_appContract), balance)); + uint256 balance = vm.randomUint(amount, type(uint256).max); + _contracts.dev.testFungibleToken.mint(address(_appContract), balance); vm.expectRevert(IApplication.NotForeclosed.selector); @@ -646,9 +658,7 @@ contract ApplicationTest is AccountValidityProof memory proof = _getAccountValidityProof(name); uint256 balance = vm.randomUint(0, amount - 1); - - vm.prank(_tokenOwner); - assertTrue(_erc20Token.transfer(address(_appContract), balance)); + _contracts.dev.testFungibleToken.mint(address(_appContract), balance); vm.prank(_appContract.getGuardian()); _appContract.foreclose(); @@ -666,10 +676,8 @@ contract ApplicationTest is (address user, uint256 amount) = ExternalLibUsdAccount.decode(account); AccountValidityProof memory proof = _getAccountValidityProof(name); - uint256 balance = vm.randomUint(amount, _erc20Token.balanceOf(_tokenOwner)); - - vm.prank(_tokenOwner); - assertTrue(_erc20Token.transfer(address(_appContract), balance)); + uint256 balance = vm.randomUint(amount, type(uint256).max); + _contracts.dev.testFungibleToken.mint(address(_appContract), balance); vm.mockCallRevert( address(_erc20Token), abi.encodeCall(IERC20.transfer, (user, amount)), error @@ -693,10 +701,8 @@ contract ApplicationTest is (address user, uint256 amount) = ExternalLibUsdAccount.decode(account); AccountValidityProof memory proof = _getAccountValidityProof(name); - uint256 balance = vm.randomUint(amount, _erc20Token.balanceOf(_tokenOwner)); - - vm.prank(_tokenOwner); - assertTrue(_erc20Token.transfer(address(_appContract), balance)); + uint256 balance = vm.randomUint(amount, type(uint256).max); + _contracts.dev.testFungibleToken.mint(address(_appContract), balance); vm.mockCall( address(_erc20Token), @@ -738,9 +744,7 @@ contract ApplicationTest is AccountValidityProof memory proof = _getAccountValidityProof(name); // Give the app a random ERC-20 token balance - uint256 appBalance = vm.randomUint(0, _erc20Token.balanceOf(_tokenOwner)); - vm.prank(_tokenOwner); - assertTrue(_erc20Token.transfer(address(_appContract), appBalance)); + _contracts.dev.testFungibleToken.mint(address(_appContract), vm.randomUint()); vm.prank(_appContract.getGuardian()); _appContract.foreclose(); @@ -767,13 +771,11 @@ contract ApplicationTest is (address user, uint256 amount) = ExternalLibUsdAccount.decode(account); AccountValidityProof memory proof = _getAccountValidityProof(name); - uint256 appBalance = vm.randomUint(amount, _erc20Token.balanceOf(_tokenOwner)); - vm.prank(_tokenOwner); - assertTrue(_erc20Token.transfer(address(_appContract), appBalance)); + uint256 appBalance = vm.randomUint(amount, type(uint256).max); + _contracts.dev.testFungibleToken.mint(address(_appContract), appBalance); - uint256 userBalance = vm.randomUint(0, _erc20Token.balanceOf(_tokenOwner)); - vm.prank(_tokenOwner); - assertTrue(_erc20Token.transfer(user, userBalance)); + uint256 userBalance = vm.randomUint(0, type(uint256).max - appBalance); + _contracts.dev.testFungibleToken.mint(user, userBalance); uint256 numOfWithdrawalsBefore = _appContract.getNumberOfWithdrawals(); @@ -800,6 +802,7 @@ contract ApplicationTest is uint256 numOfWithdrawalEventsInTx; uint256 numOfTransferEventsInTx; + bytes memory withdrawalOutput; for (uint256 i; i < logs.length; ++i) { Vm.Log memory log = logs[i]; @@ -815,21 +818,7 @@ contract ApplicationTest is assertEq(arg1, proof.accountIndex); assertEq(arg2, account); - // decode output - (bytes4 funcsel1, bytes memory callargs1) = arg3.consumeBytes4(); - assertEq(funcsel1, Outputs.DelegateCallVoucher.selector); - (address destination, bytes memory payload) = - abi.decode(callargs1, (address, bytes)); - assertEq(destination, address(_safeErc20Transfer)); - - // decode delegatecall payload - (bytes4 funcsel2, bytes memory callargs2) = payload.consumeBytes4(); - assertEq(funcsel2, ISafeERC20Transfer.safeTransfer.selector); - (address token, address to, uint256 value) = - abi.decode(callargs2, (address, address, uint256)); - assertEq(token, address(_erc20Token)); - assertEq(to, user); - assertEq(value, amount); + withdrawalOutput = arg3; } else { revert("unexpected event from app contract"); } @@ -852,6 +841,25 @@ contract ApplicationTest is assertEq(numOfWithdrawalEventsInTx, 1); assertEq(numOfTransferEventsInTx, 1); + + { + // decode output + (bytes4 funcsel1, bytes memory callargs1) = withdrawalOutput.consumeBytes4(); + assertEq(funcsel1, Outputs.DelegateCallVoucher.selector); + (address destination, bytes memory payload) = + abi.decode(callargs1, (address, bytes)); + assertEq(destination, address(_safeErc20Transfer)); + + // decode delegatecall payload + (bytes4 funcsel2, bytes memory callargs2) = payload.consumeBytes4(); + assertEq(funcsel2, ISafeERC20Transfer.safeTransfer.selector); + (address token, address to, uint256 value) = + abi.decode(callargs2, (address, address, uint256)); + assertEq(token, address(_erc20Token)); + assertEq(to, user); + assertEq(value, amount); + } + assertEq(_appContract.getNumberOfWithdrawals(), numOfWithdrawalsBefore + 1); assertTrue(_appContract.wereAccountFundsWithdrawn(proof.accountIndex)); assertEq(_erc20Token.balanceOf(address(_appContract)), appBalance - amount); @@ -883,6 +891,692 @@ contract ApplicationTest is vm.stopPrank(); } + // ------------------------------------ + // input validation and deposit refunds + // ------------------------------------ + + function testValidateInputAndAttemptRefund( + bytes[] calldata payloads, + bytes calldata randomBytes + ) external { + // 0. Randomize chain ID + vm.chainId(vm.randomUint(64)); + + bytes[] memory inputs = new bytes[](payloads.length); + uint256[] memory blockNumbers = new uint256[](payloads.length); + address[] memory inputSenders = new address[](payloads.length); + + // 1. Send all inputs to the application's input box from random EOA senders, + // at random (but cronologically consistent) block numbers and timestamps, + // and with random block prevrandao values. + for (uint256 i; i < payloads.length; ++i) { + bytes memory payload = payloads[i]; + address appContract = address(_appContract); + uint256 blockNumber = vm.randomUint(vm.getBlockNumber(), type(uint256).max); + blockNumbers[i] = blockNumber; + vm.roll(blockNumber); + vm.warp(vm.randomUint(vm.getBlockTimestamp(), type(uint256).max)); + vm.prevrandao(vm.randomUint()); + address inputSender = vm.addr(boundPrivateKey(vm.randomUint())); + vm.assume(inputSender.code.length == 0); + inputSenders[i] = inputSender; + vm.recordLogs(); + vm.prank(inputSender); + bytes32 inputHash = _contracts.core.inputBox.addInput(appContract, payload); + Vm.Log[] memory logs = vm.getRecordedLogs(); + uint256 numOfInputAdded; + for (uint256 j; j < logs.length; ++j) { + Vm.Log memory log = logs[j]; + if (log.emitter == address(_contracts.core.inputBox)) { + (bytes memory decodedInput, bytes memory decodedPayload) = + _decodeInputAdded(log, appContract, inputSender, i); + assertEq(decodedPayload, payload); + assertEq(keccak256(decodedInput), inputHash); + inputs[i] = decodedInput; + ++numOfInputAdded; + } else { + revert("unexpected log emitter"); + } + } + assertEq(numOfInputAdded, 1); + assertEq(_contracts.core.inputBox.getInputHash(appContract, i), inputHash); + assertEq(_contracts.core.inputBox.getNumberOfInputs(appContract), i + 1); + } + + // 2. Validate each input that was sent + for (uint256 i; i < inputs.length; ++i) { + _appContract.validateInputHash(i, keccak256(inputs[i])); + + (uint256 blockNumber, address inputSender, bytes memory inputPayload) = + _appContract.validateInput(i, inputs[i]); + + assertEq(blockNumber, blockNumbers[i]); + assertEq(inputSender, inputSenders[i]); + assertEq(inputPayload, payloads[i]); + } + + // 3. Attempt to validate an input with an invalid index and random bytes + uint256 invalidInputIndex = vm.randomUint(inputs.length, type(uint256).max); + vm.expectRevert(_encodeInvalidInputIndex(invalidInputIndex, inputs.length)); + _appContract.validateInput(invalidInputIndex, randomBytes); + vm.expectRevert(_encodeInvalidInputIndex(invalidInputIndex, inputs.length)); + _appContract.validateInputHash(invalidInputIndex, bytes32(vm.randomUint())); + + // 4. Attempt to validate an input with a different hash (if an input was sent) + if (inputs.length >= 1) { + uint256 inputIndex = vm.randomUint(0, inputs.length - 1); + bytes32 inputHash = keccak256(inputs[inputIndex]); + bytes memory invalidInput; + bytes32 invalidInputHash; + while (true) { + invalidInput = vm.randomBytes(vm.randomUint(0, (1 << 10))); + invalidInputHash = keccak256(invalidInput); + if (inputHash != invalidInputHash) { + break; // Found input with different hash + } + } + vm.expectRevert(_encodeInvalidInputHash(inputHash, invalidInputHash)); + _appContract.validateInput(inputIndex, invalidInput); + vm.expectRevert(_encodeInvalidInputHash(inputHash, invalidInputHash)); + _appContract.validateInputHash(inputIndex, invalidInputHash); + } + + // 5. Make guardian foreclose the application + vm.prank(_appContract.getGuardian()); + _appContract.foreclose(); + + // 6. Attempt to issue refunds for non-deposit inputs + for (uint256 i; i < inputs.length; ++i) { + vm.expectRevert( + abi.encodeWithSelector( + IRefundOutputBuilderErrors.UnknownInputSender.selector, + inputSenders[i] + ) + ); + vm.prank(vm.randomAddress()); + _appContract.issueRefund(i, inputs[i]); + } + } + + function testIssueRefund( + bytes[] calldata payloads, + uint256 tokenId, + uint256 value, + uint256[] calldata values, + bytes calldata baseLayerData, + bytes calldata execLayerData + ) external { + // Assume the depositor is an EOA (to avoid transfer failures) + address depositor = vm.addr(boundPrivateKey(vm.randomUint())); + vm.assume(depositor.code.length == 0); + + bytes memory input; + bytes memory payload; + address appContract = address(_appContract); + uint256 balance = vm.randomUint(value, type(uint256).max); + uint256 blockNumber = vm.randomUint(vm.getBlockNumber(), type(uint256).max); + + // 0. Randomize environment + vm.chainId(vm.randomUint(64)); + + // 1. Add some prior inputs + _addInputs(_contracts.core.inputBox, appContract, payloads); + uint256 inputIndex = _contracts.core.inputBox.getNumberOfInputs(appContract); + + // 2. Randomize deposit environment + vm.roll(blockNumber); + vm.warp(vm.randomUint(vm.getBlockTimestamp(), type(uint256).max)); + vm.prevrandao(vm.randomUint()); + + DepositType depositType = + DepositType(vm.randomUint(0, uint256(type(DepositType).max))); + + // 3. Deposit funds + address portalAddress; + uint256[] memory tokenIds; + uint256[] memory balances; + if (depositType == DepositType.ETHER) { + portalAddress = address(_contracts.core.etherPortal); + vm.deal(depositor, balance); + vm.recordLogs(); + vm.prank(depositor); + _contracts.core.etherPortal.depositEther{value: value}( + appContract, execLayerData + ); + } else if (depositType == DepositType.ERC20) { + portalAddress = address(_contracts.core.erc20Portal); + vm.startPrank(depositor); + _contracts.dev.testFungibleToken.mint(balance); + _contracts.dev.testFungibleToken + .approve(portalAddress, vm.randomUint(value, balance)); + vm.recordLogs(); + _contracts.core.erc20Portal + .depositERC20Tokens( + _contracts.dev.testFungibleToken, appContract, value, execLayerData + ); + vm.stopPrank(); + } else if (depositType == DepositType.ERC721) { + portalAddress = address(_contracts.core.erc721Portal); + vm.startPrank(depositor); + _contracts.dev.testNonFungibleToken.mint(tokenId); + _contracts.dev.testNonFungibleToken.approve(portalAddress, tokenId); + vm.recordLogs(); + _contracts.core.erc721Portal + .depositERC721Token( + _contracts.dev.testNonFungibleToken, + appContract, + tokenId, + baseLayerData, + execLayerData + ); + vm.stopPrank(); + } else if (depositType == DepositType.ERC1155_SINGLE) { + portalAddress = address(_contracts.core.erc1155SinglePortal); + vm.startPrank(depositor); + _contracts.dev.testMultiToken.mint(tokenId, balance); + _contracts.dev.testMultiToken.setApprovalForAll(portalAddress, true); + vm.recordLogs(); + _contracts.core.erc1155SinglePortal + .depositSingleERC1155Token( + _contracts.dev.testMultiToken, + appContract, + tokenId, + value, + baseLayerData, + execLayerData + ); + vm.stopPrank(); + } else if (depositType == DepositType.ERC1155_BATCH) { + portalAddress = address(_contracts.core.erc1155BatchPortal); + tokenIds = vm.randomUniqueUint256Array(values.length); + balances = vm.randomUintGe(values); + vm.startPrank(depositor); + _contracts.dev.testMultiToken.mintBatch(tokenIds, balances); + _contracts.dev.testMultiToken.setApprovalForAll(portalAddress, true); + vm.recordLogs(); + _contracts.core.erc1155BatchPortal + .depositBatchERC1155Token( + _contracts.dev.testMultiToken, + appContract, + tokenIds, + values, + baseLayerData, + execLayerData + ); + vm.stopPrank(); + } else { + revert("unexpected deposit type"); + } + + // 3.1. Parse deposit tx logs + { + Vm.Log[] memory logs = vm.getRecordedLogs(); + + uint256 numOfInputAdded; + uint256 numOfErc20Transfers; + uint256 numOfErc721Transfers; + uint256 numOfErc1155SingleTransfers; + uint256 numOfErc1155BatchTransfers; + + for (uint256 i; i < logs.length; ++i) { + Vm.Log memory log = logs[i]; + if (log.emitter == address(_contracts.core.inputBox)) { + (input, payload) = + _decodeInputAdded(log, appContract, portalAddress, inputIndex); + ++numOfInputAdded; + } else if (log.emitter == address(_contracts.dev.testFungibleToken)) { + assertGe(log.topics.length, 1); + if (log.topics[0] == IERC20.Transfer.selector) { + assertEq(log.topics[1], depositor.asTopic()); + assertEq(log.topics[2], address(_appContract).asTopic()); + assertEq(abi.decode(log.data, (uint256)), value); + ++numOfErc20Transfers; + } else { + revert("unexpected event from ERC-20 token contract"); + } + } else if (log.emitter == address(_contracts.dev.testNonFungibleToken)) { + assertGe(log.topics.length, 1); + if (log.topics[0] == IERC721.Transfer.selector) { + assertEq(log.topics[1], depositor.asTopic()); + assertEq(log.topics[2], address(_appContract).asTopic()); + assertEq(log.topics[3], bytes32(tokenId)); + ++numOfErc721Transfers; + } else { + revert("unexpected event from ERC-721 token contract"); + } + } else if (log.emitter == address(_contracts.dev.testMultiToken)) { + assertGe(log.topics.length, 1); + if (log.topics[0] == IERC1155.TransferSingle.selector) { + assertEq(log.topics[2], depositor.asTopic()); + assertEq(log.topics[3], address(_appContract).asTopic()); + + (uint256 arg1, uint256 arg2) = + abi.decode(log.data, (uint256, uint256)); + + if (depositType == DepositType.ERC1155_SINGLE) { + assertEq( + log.topics[1], + address(_contracts.core.erc1155SinglePortal).asTopic() + ); + assertEq(arg1, tokenId); + assertEq(arg2, value); + } else if (depositType == DepositType.ERC1155_BATCH) { + assertEq( + log.topics[1], + address(_contracts.core.erc1155BatchPortal).asTopic() + ); + assertEq(tokenIds.length, 1); + assertEq(arg1, tokenIds[0]); + assertEq(arg2, values[0]); + } else { + revert("unexpected deposit type"); + } + + ++numOfErc1155SingleTransfers; + } else if (log.topics[0] == IERC1155.TransferBatch.selector) { + assertEq( + log.topics[1], + address(_contracts.core.erc1155BatchPortal).asTopic() + ); + assertEq(log.topics[2], depositor.asTopic()); + assertEq(log.topics[3], address(_appContract).asTopic()); + + (uint256[] memory arg1, uint256[] memory arg2) = + abi.decode(log.data, (uint256[], uint256[])); + + assertEq(arg1, tokenIds); + assertEq(arg2, values); + + ++numOfErc1155BatchTransfers; + } else { + revert("unexpected event from ERC-1155 token contract"); + } + } else { + revert("unexpected log emitter"); + } + } + + assertEq(numOfInputAdded, 1); + assertEq(numOfErc20Transfers, (depositType == DepositType.ERC20) ? 1 : 0); + assertEq(numOfErc721Transfers, (depositType == DepositType.ERC721) ? 1 : 0); + assertEq( + numOfErc1155SingleTransfers, + ((depositType == DepositType.ERC1155_SINGLE) + || ((depositType == DepositType.ERC1155_BATCH) + && (tokenIds.length == 1))) + ? 1 + : 0 + ); + assertEq( + numOfErc1155BatchTransfers, + ((depositType == DepositType.ERC1155_BATCH) && (tokenIds.length != 1)) + ? 1 + : 0 + ); + } + + // 3.2. Check deposit effects + if (depositType == DepositType.ETHER) { + assertEq(depositor.balance, balance - value); + } else if (depositType == DepositType.ERC20) { + assertEq( + _contracts.dev.testFungibleToken.balanceOf(depositor), balance - value + ); + } else if (depositType == DepositType.ERC721) { + assertEq( + _contracts.dev.testNonFungibleToken.ownerOf(tokenId), + address(_appContract) + ); + } else if (depositType == DepositType.ERC1155_SINGLE) { + assertEq( + _contracts.dev.testMultiToken.balanceOf(depositor, tokenId), + balance - value + ); + } else if (depositType == DepositType.ERC1155_BATCH) { + assertEq( + _contracts.dev.testMultiToken + .balanceOfBatch(depositor.repeat(tokenIds.length), tokenIds), + balances.sub(values) + ); + } else { + revert("unexpected deposit type"); + } + + // 4. Randomize validation environment + vm.roll(vm.randomUint(vm.getBlockNumber(), type(uint256).max)); + vm.warp(vm.randomUint(vm.getBlockTimestamp(), type(uint256).max)); + + // 5. Validate deposit input on input box + vm.prank(vm.randomAddress()); + assertEq(_contracts.core.inputBox.getNumberOfInputs(appContract), 1 + inputIndex); + vm.prank(vm.randomAddress()); + assertEq( + _contracts.core.inputBox.getInputHash(appContract, inputIndex), + keccak256(input) + ); + + // 6. Validate deposit input hash on application + vm.prank(vm.randomAddress()); + _appContract.validateInputHash(inputIndex, keccak256(input)); + + // 7. Validate deposit input on application + { + uint256 decodedBlockNumber; + address decodedInputSender; + bytes memory decodedInputPayload; + + vm.prank(vm.randomAddress()); + (decodedBlockNumber, decodedInputSender, decodedInputPayload) = + _appContract.validateInput(inputIndex, input); + + assertEq(decodedBlockNumber, blockNumber); + assertEq(decodedInputSender, portalAddress); + assertEq(decodedInputPayload, payload); + } + + // 8. Try issuing refund before foreclosure + vm.expectRevert(IApplication.NotForeclosed.selector); + vm.prank(vm.randomAddress()); + _appContract.issueRefund(inputIndex, input); + + // 8.1 Try issuing refund of finalized input after foreclosure + vm.expectRevert(_encodeCannotRefundFinalizedInput(inputIndex)); + this.simulateClaimSubmissionAcceptanceForeclosureAndRefund(inputIndex, input); + + // 9. Make guardian foreclose the application + vm.prank(_appContract.getGuardian()); + _appContract.foreclose(); + + // 10. Issue refund for deposit + vm.prank(vm.randomAddress()); + vm.recordLogs(); + _appContract.issueRefund(inputIndex, input); + + { + Vm.Log[] memory logs = vm.getRecordedLogs(); + + uint256 numOfRefundsIssued; + bytes4 refundOutputSelector; + bytes memory refundOutputArgs; + + uint256 numOfErc20Transfers; + uint256 numOfErc721Transfers; + uint256 numOfErc1155SingleTransfers; + uint256 numOfErc1155BatchTransfers; + + for (uint256 i; i < logs.length; ++i) { + Vm.Log memory log = logs[i]; + if (log.emitter == address(_appContract)) { + assertEq(log.topics[0], IApplication.RefundIssued.selector); + ++numOfRefundsIssued; + + (uint256 arg1, bytes memory arg2, bytes memory arg3) = + abi.decode(log.data, (uint256, bytes, bytes)); + + assertEq(arg1, inputIndex); + assertEq(arg2, input); + + (refundOutputSelector, refundOutputArgs) = arg3.consumeBytes4(); + } else if (log.emitter == address(_contracts.dev.testFungibleToken)) { + assertGe(log.topics.length, 1); + if (log.topics[0] == IERC20.Transfer.selector) { + assertEq(log.topics[1], address(_appContract).asTopic()); + assertEq(log.topics[2], depositor.asTopic()); + assertEq(abi.decode(log.data, (uint256)), value); + ++numOfErc20Transfers; + } else { + revert("unexpected event from ERC-20 token contract"); + } + } else if (log.emitter == address(_contracts.dev.testNonFungibleToken)) { + assertGe(log.topics.length, 1); + if (log.topics[0] == IERC721.Transfer.selector) { + assertEq(log.topics[1], address(_appContract).asTopic()); + assertEq(log.topics[2], depositor.asTopic()); + assertEq(log.topics[3], bytes32(tokenId)); + ++numOfErc721Transfers; + } else { + revert("unexpected event from ERC-721 token contract"); + } + } else if (log.emitter == address(_contracts.dev.testMultiToken)) { + assertGe(log.topics.length, 1); + if (log.topics[0] == IERC1155.TransferSingle.selector) { + assertEq(log.topics[1], address(_appContract).asTopic()); + assertEq(log.topics[2], address(_appContract).asTopic()); + assertEq(log.topics[3], depositor.asTopic()); + + (uint256 arg1, uint256 arg2) = + abi.decode(log.data, (uint256, uint256)); + + if (depositType == DepositType.ERC1155_SINGLE) { + assertEq(arg1, tokenId); + assertEq(arg2, value); + } else if (depositType == DepositType.ERC1155_BATCH) { + assertEq(tokenIds.length, 1); + assertEq(arg1, tokenIds[0]); + assertEq(arg2, values[0]); + } else { + revert("unexpected deposit type"); + } + + ++numOfErc1155SingleTransfers; + } else if (log.topics[0] == IERC1155.TransferBatch.selector) { + assertEq(log.topics[1], address(_appContract).asTopic()); + assertEq(log.topics[2], address(_appContract).asTopic()); + assertEq(log.topics[3], depositor.asTopic()); + + (uint256[] memory arg1, uint256[] memory arg2) = + abi.decode(log.data, (uint256[], uint256[])); + + assertEq(arg1, tokenIds); + assertEq(arg2, values); + + ++numOfErc1155BatchTransfers; + } else { + revert("unexpected event from ERC-1155 token contract"); + } + } else { + revert("unexpected log emitter"); + } + } + + assertEq(numOfRefundsIssued, 1); + assertEq(numOfErc20Transfers, (depositType == DepositType.ERC20) ? 1 : 0); + assertEq(numOfErc721Transfers, (depositType == DepositType.ERC721) ? 1 : 0); + assertEq( + numOfErc1155SingleTransfers, + ((depositType == DepositType.ERC1155_SINGLE) + || ((depositType == DepositType.ERC1155_BATCH) + && (tokenIds.length == 1))) + ? 1 + : 0 + ); + assertEq( + numOfErc1155BatchTransfers, + ((depositType == DepositType.ERC1155_BATCH) && (tokenIds.length != 1)) + ? 1 + : 0 + ); + + if (depositType == DepositType.ETHER) { + assertEq(refundOutputSelector, Outputs.Voucher.selector); + + address voucherDestination; + uint256 voucherValue; + bytes memory voucherPayload; + + (voucherDestination, voucherValue, voucherPayload) = + abi.decode(refundOutputArgs, (address, uint256, bytes)); + + assertEq(voucherDestination, depositor); + assertEq(voucherValue, value); + assertEq(voucherPayload, new bytes(0)); + + assertEq(depositor.balance, balance); + } else if (depositType == DepositType.ERC20) { + assertEq(refundOutputSelector, Outputs.DelegateCallVoucher.selector); + + address voucherDestination; + bytes memory voucherPayload; + + (voucherDestination, voucherPayload) = + abi.decode(refundOutputArgs, (address, bytes)); + + assertEq(voucherDestination, address(_safeErc20Transfer)); + + bytes4 selector; + bytes memory arguments; + + (selector, arguments) = voucherPayload.consumeBytes4(); + assertEq(selector, ISafeERC20Transfer.safeTransfer.selector); + + (address refundToken, address refundRecipient, uint256 refundAmount) = + abi.decode(arguments, (address, address, uint256)); + + assertEq(refundToken, address(_contracts.dev.testFungibleToken)); + assertEq(refundRecipient, depositor); + assertEq(refundAmount, value); + + assertEq(_contracts.dev.testFungibleToken.balanceOf(depositor), balance); + } else if (depositType == DepositType.ERC721) { + assertEq(refundOutputSelector, Outputs.Voucher.selector); + + address voucherDestination; + uint256 voucherValue; + bytes memory voucherPayload; + + (voucherDestination, voucherValue, voucherPayload) = + abi.decode(refundOutputArgs, (address, uint256, bytes)); + + assertEq(voucherDestination, address(_contracts.dev.testNonFungibleToken)); + assertEq(voucherValue, 0); + + bytes4 selector; + bytes memory arguments; + + (selector, arguments) = voucherPayload.consumeBytes4(); + assertEq( + selector, + bytes4(keccak256("safeTransferFrom(address,address,uint256)")) + ); + + address refundFrom; + address refundTo; + uint256 refundTokenId; + + (refundFrom, refundTo, refundTokenId) = + abi.decode(arguments, (address, address, uint256)); + + assertEq(refundFrom, address(_appContract)); + assertEq(refundTo, depositor); + assertEq(refundTokenId, tokenId); + + assertEq(_contracts.dev.testNonFungibleToken.ownerOf(tokenId), depositor); + } else if (depositType == DepositType.ERC1155_SINGLE) { + assertEq(refundOutputSelector, Outputs.Voucher.selector); + + address voucherDestination; + uint256 voucherValue; + bytes memory voucherPayload; + + (voucherDestination, voucherValue, voucherPayload) = + abi.decode(refundOutputArgs, (address, uint256, bytes)); + + assertEq(voucherDestination, address(_contracts.dev.testMultiToken)); + assertEq(voucherValue, 0); + + bytes4 selector; + bytes memory arguments; + + (selector, arguments) = voucherPayload.consumeBytes4(); + assertEq(selector, IERC1155.safeTransferFrom.selector); + + address refundFrom; + address refundTo; + uint256 refundTokenId; + uint256 refundValue; + + (refundFrom, refundTo, refundTokenId, refundValue,) = + abi.decode(arguments, (address, address, uint256, uint256, bytes)); + + assertEq(refundFrom, address(_appContract)); + assertEq(refundTo, depositor); + assertEq(refundTokenId, tokenId); + assertEq(refundValue, value); + + assertEq( + _contracts.dev.testMultiToken.balanceOf(depositor, tokenId), balance + ); + } else if (depositType == DepositType.ERC1155_BATCH) { + assertEq(refundOutputSelector, Outputs.Voucher.selector); + + address voucherDestination; + uint256 voucherValue; + bytes memory voucherPayload; + + (voucherDestination, voucherValue, voucherPayload) = + abi.decode(refundOutputArgs, (address, uint256, bytes)); + + assertEq(voucherDestination, address(_contracts.dev.testMultiToken)); + assertEq(voucherValue, 0); + + bytes4 selector; + bytes memory arguments; + + (selector, arguments) = voucherPayload.consumeBytes4(); + assertEq(selector, IERC1155.safeBatchTransferFrom.selector); + + address refundFrom; + address refundTo; + uint256[] memory refundTokenIds; + uint256[] memory refundValues; + + (refundFrom, refundTo, refundTokenIds, refundValues,) = abi.decode( + arguments, (address, address, uint256[], uint256[], bytes) + ); + + assertEq(refundFrom, address(_appContract)); + assertEq(refundTo, depositor); + assertEq(refundTokenIds, tokenIds); + assertEq(refundValues, values); + + assertEq( + _contracts.dev.testMultiToken + .balanceOfBatch(depositor.repeat(tokenIds.length), tokenIds), + balances + ); + } else { + revert("unexpected deposit type"); + } + } + + assertEq(_appContract.getNumberOfIssuedRefunds(), 1); + assertTrue(_appContract.wasRefundForInputIssued(inputIndex)); + + // 11. Re-validate deposit input hash on application + vm.prank(vm.randomAddress()); + _appContract.validateInputHash(inputIndex, keccak256(input)); + + // 12. Re-validate deposit input on application + { + uint256 decodedBlockNumber; + address decodedInputSender; + bytes memory decodedInputPayload; + + vm.prank(vm.randomAddress()); + (decodedBlockNumber, decodedInputSender, decodedInputPayload) = + _appContract.validateInput(inputIndex, input); + + assertEq(decodedBlockNumber, blockNumber); + assertEq(decodedInputSender, portalAddress); + assertEq(decodedInputPayload, payload); + } + + // 13. Try re-issuing refund for the same deposit + vm.prank(vm.randomAddress()); + vm.expectRevert(_encodeRefundAlreadyIssued(inputIndex)); + _appContract.issueRefund(inputIndex, input); + } + // ------------------ // internal functions // ------------------ @@ -891,7 +1585,6 @@ contract ApplicationTest is _authorityOwner = _nextAddress(); _appOwner = _nextAddress(); _recipient = _nextAddress(); - _tokenOwner = _nextAddress(); _withdrawalConfig = WithdrawalConfig({ guardian: _nextAddress(), log2LeavesPerAccount: LibEmulator.LOG2_LEAVES_PER_ACCOUNT, @@ -934,15 +1627,6 @@ contract ApplicationTest is ); } - function _mintTokens() internal { - vm.startPrank(_tokenOwner); - _contracts.dev.testFungibleToken.mint(INITIAL_SUPPLY); - _contracts.dev.testNonFungibleToken.mint(TOKEN_ID); - _contracts.dev.testMultiToken.mint(TOKEN_ID, INITIAL_SUPPLY); - _contracts.dev.testMultiToken.mintBatch(_tokenIds, _initialSupplies); - vm.stopPrank(); - } - function _addOutputs() internal { _nameOutput("EmptyOutput", _addOutput(abi.encode())); _nameOutput("HelloWorldNotice", _addOutput(_encodeNotice("Hello, world!"))); @@ -1194,6 +1878,37 @@ contract ApplicationTest is revert("Successful proof"); } + /// @notice This function is used to simulate a claim acceptance, a foreclosure and + /// a refund issuance. If the proof succeeds, then the function reverts with + /// error message "Successful refund". If the proof fails, then the function propagates + /// the error from the app contract. + function simulateClaimSubmissionAcceptanceForeclosureAndRefund( + uint256 inputIndex, + bytes calldata input + ) external { + assertEq(msg.sender, address(this), "called by external account"); + uint256 lastProcessedBlockNumber = vm.getBlockNumber(); + vm.roll(vm.randomUint(lastProcessedBlockNumber + 1, type(uint256).max)); + vm.prank(_authorityOwner); + _authority.submitClaim( + address(_appContract), + lastProcessedBlockNumber, + _proofComponents.outputsMerkleRoot, + _proofComponents.getOutputsMerkleRootProof() + ); + vm.prank(vm.randomAddress()); + _authority.acceptClaim( + address(_appContract), + lastProcessedBlockNumber, + _proofComponents.getMachineMerkleRoot() + ); + vm.prank(_appContract.getGuardian()); + _appContract.foreclose(); + vm.prank(vm.randomAddress()); + _appContract.issueRefund(inputIndex, input); + revert("Successful proof"); + } + function _submitClaim() internal { _proofComponents = _emulator.buildProofComponents(); bytes32 outputsMerkleRoot = _proofComponents.outputsMerkleRoot; @@ -1288,6 +2003,45 @@ contract ApplicationTest is return abi.encodeWithSelector(IApplication.OutputNotReexecutable.selector, output); } + function _encodeInvalidInputIndex(uint256 invalidInputIndex, uint256 numOfInputs) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + IApplication.InvalidInputIndex.selector, invalidInputIndex, numOfInputs + ); + } + + function _encodeInvalidInputHash(bytes32 storedInputHash, bytes32 invalidInputHash) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + IApplication.InvalidInputHash.selector, storedInputHash, invalidInputHash + ); + } + + function _encodeRefundAlreadyIssued(uint256 inputIndex) + internal + pure + returns (bytes memory) + { + return + abi.encodeWithSelector(IApplication.RefundAlreadyIssued.selector, inputIndex); + } + + function _encodeCannotRefundFinalizedInput(uint256 inputIndex) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + IApplication.CannotRefundFinalizedInput.selector, inputIndex + ); + } + function _expectIncrementInNumberOfExecutedOutputs(uint256 before) internal view { assertEq( _appContract.getNumberOfExecutedOutputs(), @@ -1547,24 +2301,16 @@ contract ApplicationTest is internal { uint256 numberOfExecutedOutputsBefore = _appContract.getNumberOfExecutedOutputs(); - assertEq( - _erc721Token.ownerOf(TOKEN_ID), - _tokenOwner, - "The NFT is initially owned by `_tokenOwner`" - ); vm.expectRevert( abi.encodeWithSelector( - IERC721Errors.ERC721InsufficientApproval.selector, - address(_appContract), - TOKEN_ID + IERC721Errors.ERC721NonexistentToken.selector, TOKEN_ID ) ); _appContract.executeOutput(output, proof); _expectNoChangeInNumberOfExecutedOutputs(numberOfExecutedOutputsBefore); - vm.prank(_tokenOwner); - _erc721Token.safeTransferFrom(_tokenOwner, address(_appContract), TOKEN_ID); + _contracts.dev.testNonFungibleToken.mint(address(_appContract), TOKEN_ID); _expectEmitOutputExecuted(output, proof); _appContract.executeOutput(output, proof); @@ -1619,9 +2365,7 @@ contract ApplicationTest is internal { uint256 numberOfExecutedOutputsBefore = _appContract.getNumberOfExecutedOutputs(); - vm.prank(_tokenOwner); - bool success = _erc20Token.transfer(address(_appContract), TRANSFER_AMOUNT); - assertTrue(success, ""); + _contracts.dev.testFungibleToken.mint(address(_appContract), TRANSFER_AMOUNT); uint256 recipientBalance = _erc20Token.balanceOf(address(_recipient)); uint256 appBalance = _erc20Token.balanceOf(address(_appContract)); @@ -1665,10 +2409,8 @@ contract ApplicationTest is _appContract.executeOutput(output, proof); _expectNoChangeInNumberOfExecutedOutputs(numberOfExecutedOutputsBefore); - vm.prank(_tokenOwner); - _erc1155Token.safeTransferFrom( - _tokenOwner, address(_appContract), TOKEN_ID, INITIAL_SUPPLY, "" - ); + _contracts.dev.testMultiToken + .mint(address(_appContract), TOKEN_ID, INITIAL_SUPPLY); uint256 recipientBalance = _erc1155Token.balanceOf(_recipient, TOKEN_ID); uint256 appBalance = _erc1155Token.balanceOf(address(_appContract), TOKEN_ID); @@ -1711,10 +2453,8 @@ contract ApplicationTest is _appContract.executeOutput(output, proof); _expectNoChangeInNumberOfExecutedOutputs(numberOfExecutedOutputsBefore); - vm.prank(_tokenOwner); - _erc1155Token.safeBatchTransferFrom( - _tokenOwner, address(_appContract), _tokenIds, _initialSupplies, "" - ); + _contracts.dev.testMultiToken + .mintBatch(address(_appContract), _tokenIds, _initialSupplies); uint256 batchLength = _initialSupplies.length; uint256[] memory appBalances = new uint256[](batchLength); diff --git a/test/dapp/ApplicationFactory.t.sol b/test/dapp/ApplicationFactory.t.sol index cc819b50..adac190c 100644 --- a/test/dapp/ApplicationFactory.t.sol +++ b/test/dapp/ApplicationFactory.t.sol @@ -283,6 +283,18 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { appContract.wasOutputExecuted(vm.randomUint()), "initially, wasOutputExecuted(...) = false" ); + assertEq( + address(appContract.getRefundOutputBuilder()), + address(_contracts.core.refundOutputBuilder), + "getRefundOutputBuilder() != RefundOutputBuilder" + ); + assertEq( + appContract.getNumberOfIssuedRefunds(), 0, "getNumberOfIssuedRefunds() != 0" + ); + assertFalse( + appContract.wasRefundForInputIssued(vm.randomUint()), + "initially, wasRefundForInputIssued(...) = false" + ); assertEq(appContract.getNumberOfWithdrawals(), 0, "getNumberOfWithdrawals() != 0"); assertFalse( appContract.wereAccountFundsWithdrawn(vm.randomUint()), diff --git a/test/library/LibDelegateCallVoucher.t.sol b/test/library/LibDelegateCallVoucher.t.sol new file mode 100644 index 00000000..6518f7d6 --- /dev/null +++ b/test/library/LibDelegateCallVoucher.t.sol @@ -0,0 +1,31 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {DelegateCallVoucher} from "src/common/DelegateCallVoucher.sol"; +import {Outputs} from "src/common/Outputs.sol"; +import {LibBytes} from "src/library/LibBytes.sol"; +import {LibDelegateCallVoucher} from "src/library/LibDelegateCallVoucher.sol"; + +contract LibDelegateCallVoucherTest is Test { + using LibBytes for bytes; + using LibDelegateCallVoucher for DelegateCallVoucher; + + function testEncode(DelegateCallVoucher calldata delegateCallVoucher) external pure { + bytes memory output = delegateCallVoucher.encode(); + (bool isValid, bytes4 selector, bytes memory args) = output.consumeBytes4(); + assertTrue(isValid, "Encoded delegate-call voucher is not valid output"); + assertEq(selector, Outputs.DelegateCallVoucher.selector, "Invalid selector"); + uint256 payloadLength = delegateCallVoucher.payload.length; + uint256 payloadWordCount = (payloadLength + 31) >> 5; + assertEq(args.length, (3 + payloadWordCount) * 32, "Invalid arguments length"); + address arg1; + bytes memory arg2; + (arg1, arg2) = abi.decode(args, (address, bytes)); + assertEq(arg1, delegateCallVoucher.destination, "Invalid destination"); + assertEq(arg2, delegateCallVoucher.payload, "Invalid payload"); + } +} diff --git a/test/library/LibErc1155BatchDeposit.t.sol b/test/library/LibErc1155BatchDeposit.t.sol new file mode 100644 index 00000000..91dd0186 --- /dev/null +++ b/test/library/LibErc1155BatchDeposit.t.sol @@ -0,0 +1,43 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {IERC1155} from "@openzeppelin-contracts-5.2.0/token/ERC1155/IERC1155.sol"; + +import {Erc1155BatchDeposit} from "src/common/Erc1155BatchDeposit.sol"; +import {Voucher} from "src/common/Voucher.sol"; +import {LibBytes} from "src/library/LibBytes.sol"; +import {LibErc1155BatchDeposit} from "src/library/LibErc1155BatchDeposit.sol"; + +contract LibErc1155BatchDepositTest is Test { + using LibErc1155BatchDeposit for Erc1155BatchDeposit; + using LibBytes for bytes; + + function testBuildRefund(Erc1155BatchDeposit calldata deposit, address appContract) + external + pure + { + Voucher memory voucher = deposit.buildRefund(appContract); + assertEq(voucher.destination, address(deposit.token), "destination"); + assertEq(voucher.value, 0, "voucher value"); + bool isPayloadValid; + bytes4 selector; + bytes memory args; + (isPayloadValid, selector, args) = voucher.payload.consumeBytes4(); + assertTrue(isPayloadValid, "is payload valid"); + assertEq(selector, IERC1155.safeBatchTransferFrom.selector); + address arg1; + address arg2; + uint256[] memory arg3; + uint256[] memory arg4; + (arg1, arg2, arg3, arg4) = + abi.decode(args, (address, address, uint256[], uint256[])); + assertEq(arg1, appContract, "from"); + assertEq(arg2, deposit.sender, "to"); + assertEq(arg3, deposit.tokenIds, "tokenId"); + assertEq(arg4, deposit.values, "transfer value"); + } +} diff --git a/test/library/LibErc1155SingleDeposit.t.sol b/test/library/LibErc1155SingleDeposit.t.sol new file mode 100644 index 00000000..4163b1cc --- /dev/null +++ b/test/library/LibErc1155SingleDeposit.t.sol @@ -0,0 +1,42 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {IERC1155} from "@openzeppelin-contracts-5.2.0/token/ERC1155/IERC1155.sol"; + +import {Erc1155SingleDeposit} from "src/common/Erc1155SingleDeposit.sol"; +import {Voucher} from "src/common/Voucher.sol"; +import {LibBytes} from "src/library/LibBytes.sol"; +import {LibErc1155SingleDeposit} from "src/library/LibErc1155SingleDeposit.sol"; + +contract LibErc1155SingleDepositTest is Test { + using LibErc1155SingleDeposit for Erc1155SingleDeposit; + using LibBytes for bytes; + + function testBuildRefund(Erc1155SingleDeposit calldata deposit, address appContract) + external + pure + { + Voucher memory voucher = deposit.buildRefund(appContract); + assertEq(voucher.destination, address(deposit.token), "destination"); + assertEq(voucher.value, 0, "voucher value"); + bool isPayloadValid; + bytes4 selector; + bytes memory args; + (isPayloadValid, selector, args) = voucher.payload.consumeBytes4(); + assertTrue(isPayloadValid, "is payload valid"); + assertEq(selector, IERC1155.safeTransferFrom.selector); + address arg1; + address arg2; + uint256 arg3; + uint256 arg4; + (arg1, arg2, arg3, arg4) = abi.decode(args, (address, address, uint256, uint256)); + assertEq(arg1, appContract, "from"); + assertEq(arg2, deposit.sender, "to"); + assertEq(arg3, deposit.tokenId, "tokenId"); + assertEq(arg4, deposit.value, "transfer value"); + } +} diff --git a/test/library/LibErc20Deposit.t.sol b/test/library/LibErc20Deposit.t.sol new file mode 100644 index 00000000..08763cc2 --- /dev/null +++ b/test/library/LibErc20Deposit.t.sol @@ -0,0 +1,40 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {IERC20} from "@openzeppelin-contracts-5.2.0/token/ERC20/IERC20.sol"; + +import {DelegateCallVoucher} from "src/common/DelegateCallVoucher.sol"; +import {Erc20Deposit} from "src/common/Erc20Deposit.sol"; +import {ISafeERC20Transfer} from "src/delegatecall/ISafeERC20Transfer.sol"; +import {LibBytes} from "src/library/LibBytes.sol"; +import {LibErc20Deposit} from "src/library/LibErc20Deposit.sol"; + +contract LibErc20DepositTest is Test { + using LibErc20Deposit for Erc20Deposit; + using LibBytes for bytes; + + function testBuildRefund( + Erc20Deposit calldata deposit, + ISafeERC20Transfer safeTransfer + ) external pure { + DelegateCallVoucher memory dcVoucher = deposit.buildRefund(safeTransfer); + assertEq(dcVoucher.destination, address(safeTransfer), "destination"); + bool isPayloadValid; + bytes4 selector; + bytes memory args; + (isPayloadValid, selector, args) = dcVoucher.payload.consumeBytes4(); + assertTrue(isPayloadValid, "is payload valid"); + assertEq(selector, ISafeERC20Transfer.safeTransfer.selector, "selector"); + IERC20 arg1; + address arg2; + uint256 arg3; + (arg1, arg2, arg3) = abi.decode(args, (IERC20, address, uint256)); + assertEq(address(arg1), address(deposit.token), "token"); + assertEq(arg2, deposit.sender, "sender"); + assertEq(arg3, deposit.value, "value"); + } +} diff --git a/test/library/LibErc721Deposit.t.sol b/test/library/LibErc721Deposit.t.sol new file mode 100644 index 00000000..3cf0c703 --- /dev/null +++ b/test/library/LibErc721Deposit.t.sol @@ -0,0 +1,38 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {Erc721Deposit} from "src/common/Erc721Deposit.sol"; +import {Voucher} from "src/common/Voucher.sol"; +import {LibBytes} from "src/library/LibBytes.sol"; +import {LibErc721Deposit} from "src/library/LibErc721Deposit.sol"; + +contract LibErc721DepositTest is Test { + using LibErc721Deposit for Erc721Deposit; + using LibBytes for bytes; + + function testBuildRefund(Erc721Deposit calldata deposit, address appContract) + external + pure + { + Voucher memory voucher = deposit.buildRefund(appContract); + assertEq(voucher.destination, address(deposit.token), "destination"); + assertEq(voucher.value, 0, "value"); + bool isPayloadValid; + bytes4 selector; + bytes memory args; + (isPayloadValid, selector, args) = voucher.payload.consumeBytes4(); + assertTrue(isPayloadValid, "is payload valid"); + assertEq(selector, bytes4(keccak256("safeTransferFrom(address,address,uint256)"))); + address arg1; + address arg2; + uint256 arg3; + (arg1, arg2, arg3) = abi.decode(args, (address, address, uint256)); + assertEq(arg1, appContract, "from"); + assertEq(arg2, deposit.sender, "to"); + assertEq(arg3, deposit.tokenId, "tokenId"); + } +} diff --git a/test/library/LibEtherDeposit.t.sol b/test/library/LibEtherDeposit.t.sol new file mode 100644 index 00000000..38f1073b --- /dev/null +++ b/test/library/LibEtherDeposit.t.sol @@ -0,0 +1,21 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {EtherDeposit} from "src/common/EtherDeposit.sol"; +import {Voucher} from "src/common/Voucher.sol"; +import {LibEtherDeposit} from "src/library/LibEtherDeposit.sol"; + +contract LibEtherDepositTest is Test { + using LibEtherDeposit for EtherDeposit; + + function testBuildRefund(EtherDeposit calldata deposit) external pure { + Voucher memory voucher = deposit.buildRefund(); + assertEq(voucher.destination, deposit.sender, "destination"); + assertEq(voucher.value, deposit.value, "value"); + assertEq(voucher.payload.length, 0, "payload length"); + } +} diff --git a/test/library/LibVoucher.t.sol b/test/library/LibVoucher.t.sol new file mode 100644 index 00000000..271b35a7 --- /dev/null +++ b/test/library/LibVoucher.t.sol @@ -0,0 +1,33 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {Outputs} from "src/common/Outputs.sol"; +import {Voucher} from "src/common/Voucher.sol"; +import {LibBytes} from "src/library/LibBytes.sol"; +import {LibVoucher} from "src/library/LibVoucher.sol"; + +contract LibVoucherTest is Test { + using LibBytes for bytes; + using LibVoucher for Voucher; + + function testEncode(Voucher calldata voucher) external pure { + bytes memory output = voucher.encode(); + (bool isValid, bytes4 selector, bytes memory args) = output.consumeBytes4(); + assertTrue(isValid, "Encoded voucher is not valid output"); + assertEq(selector, Outputs.Voucher.selector, "Invalid selector"); + uint256 payloadLength = voucher.payload.length; + uint256 payloadWordCount = (payloadLength + 31) >> 5; + assertEq(args.length, (4 + payloadWordCount) * 32, "Invalid arguments length"); + address arg1; + uint256 arg2; + bytes memory arg3; + (arg1, arg2, arg3) = abi.decode(args, (address, uint256, bytes)); + assertEq(arg1, voucher.destination, "Invalid destination"); + assertEq(arg2, voucher.value, "Invalid value"); + assertEq(arg3, voucher.payload, "Invalid payload"); + } +} diff --git a/test/refund/RefundOutputBuilder.t.sol b/test/refund/RefundOutputBuilder.t.sol new file mode 100644 index 00000000..441e7a19 --- /dev/null +++ b/test/refund/RefundOutputBuilder.t.sol @@ -0,0 +1,287 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; +import {Vm} from "forge-std-1.9.6/src/Vm.sol"; + +import {IERC1155} from "@openzeppelin-contracts-5.2.0/token/ERC1155/IERC1155.sol"; +import {IERC20} from "@openzeppelin-contracts-5.2.0/token/ERC20/IERC20.sol"; + +import {Erc1155BatchDeposit} from "src/common/Erc1155BatchDeposit.sol"; +import {Erc1155SingleDeposit} from "src/common/Erc1155SingleDeposit.sol"; +import {Erc20Deposit} from "src/common/Erc20Deposit.sol"; +import {Erc721Deposit} from "src/common/Erc721Deposit.sol"; +import {EtherDeposit} from "src/common/EtherDeposit.sol"; +import {Outputs} from "src/common/Outputs.sol"; +import {ISafeERC20Transfer} from "src/delegatecall/ISafeERC20Transfer.sol"; +import {SafeERC20Transfer} from "src/delegatecall/SafeERC20Transfer.sol"; +import {IInputBox} from "src/inputs/IInputBox.sol"; +import {InputBox} from "src/inputs/InputBox.sol"; +import {LibBytes} from "src/library/LibBytes.sol"; +import {ERC1155BatchPortal} from "src/portals/ERC1155BatchPortal.sol"; +import {ERC1155SinglePortal} from "src/portals/ERC1155SinglePortal.sol"; +import {ERC20Portal} from "src/portals/ERC20Portal.sol"; +import {ERC721Portal} from "src/portals/ERC721Portal.sol"; +import {EtherPortal} from "src/portals/EtherPortal.sol"; +import {IERC1155BatchPortal} from "src/portals/IERC1155BatchPortal.sol"; +import {IERC1155SinglePortal} from "src/portals/IERC1155SinglePortal.sol"; +import {IERC20Portal} from "src/portals/IERC20Portal.sol"; +import {IERC721Portal} from "src/portals/IERC721Portal.sol"; +import {IEtherPortal} from "src/portals/IEtherPortal.sol"; +import {IRefundOutputBuilder} from "src/refund/IRefundOutputBuilder.sol"; +import {IRefundOutputBuilderErrors} from "src/refund/IRefundOutputBuilderErrors.sol"; +import {RefundOutputBuilder} from "src/refund/RefundOutputBuilder.sol"; + +import {InputBoxTestUtils} from "../util/InputBoxTestUtils.sol"; +import {LibAddressArray} from "../util/LibAddressArray.sol"; +import {LibDepositEncoder} from "../util/LibDepositEncoder.sol"; +import {VersionGetterTestUtils} from "../util/VersionGetterTestUtils.sol"; + +contract RefundOutputBuilderTest is Test, InputBoxTestUtils, VersionGetterTestUtils { + using LibBytes for bytes; + using LibAddressArray for Vm; + using LibDepositEncoder for EtherDeposit; + using LibDepositEncoder for Erc20Deposit; + using LibDepositEncoder for Erc721Deposit; + using LibDepositEncoder for Erc1155SingleDeposit; + using LibDepositEncoder for Erc1155BatchDeposit; + + IInputBox _inputBox; + IEtherPortal _etherPortal; + IERC20Portal _erc20Portal; + IERC721Portal _erc721Portal; + IERC1155SinglePortal _erc1155SinglePortal; + IERC1155BatchPortal _erc1155BatchPortal; + ISafeERC20Transfer _safeErc20Transfer; + IRefundOutputBuilder _refundOutputBuilder; + + function setUp() external { + _inputBox = new InputBox(); + _etherPortal = new EtherPortal(_inputBox); + _erc20Portal = new ERC20Portal(_inputBox); + _erc721Portal = new ERC721Portal(_inputBox); + _erc1155SinglePortal = new ERC1155SinglePortal(_inputBox); + _erc1155BatchPortal = new ERC1155BatchPortal(_inputBox); + _safeErc20Transfer = new SafeERC20Transfer(); + _refundOutputBuilder = new RefundOutputBuilder( + _etherPortal, + _erc20Portal, + _erc721Portal, + _erc1155SinglePortal, + _erc1155BatchPortal, + _safeErc20Transfer + ); + } + + function testVersion() external view { + _testVersion(_refundOutputBuilder); + } + + function testBuildRefundOutputRevertsUnknownInputSender(bytes calldata inputPayload) + external + { + address appContract = _newActiveAppMock(); + + address[] memory portalAddresses = new address[](5); + portalAddresses[0] = address(_etherPortal); + portalAddresses[1] = address(_erc20Portal); + portalAddresses[2] = address(_erc721Portal); + portalAddresses[3] = address(_erc1155SinglePortal); + portalAddresses[4] = address(_erc1155BatchPortal); + + address inputSender = vm.randomAddressNotIn(portalAddresses); + + vm.prank(vm.randomAddress()); + try _refundOutputBuilder.buildRefundOutput( + appContract, inputSender, inputPayload + ) { + revert("Expected UnknownInputSender error"); + } catch (bytes memory errorData) { + (bool isError, bytes4 sel, bytes memory args) = errorData.consumeBytes4(); + assertTrue(isError, "is error"); + assertEq(sel, IRefundOutputBuilderErrors.UnknownInputSender.selector); + address arg1 = abi.decode(args, (address)); + assertEq(arg1, inputSender, "UnknownInputSender.inputSender"); + } + } + + function testBuildRefundOutputForEtherDeposit( + EtherDeposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external { + address appContract = _newActiveAppMock(); + + vm.prank(vm.randomAddress()); + bytes memory output = _refundOutputBuilder.buildRefundOutput( + appContract, + address(_etherPortal), // inputSender + deposit.encode(extraData) // inputPayload + ); + + (bool isOutput, bytes4 sel, bytes memory args) = output.consumeBytes4(); + assertTrue(isOutput, "is output"); + assertEq(sel, Outputs.Voucher.selector, "is voucher"); + + address destination; + uint256 value; + bytes memory payload; + (destination, value, payload) = abi.decode(args, (address, uint256, bytes)); + assertEq(destination, deposit.sender, "voucher destination"); + assertEq(value, deposit.value, "voucher value"); + assertEq(payload.length, 0, "voucher payload length"); + } + + function testBuildRefundOutputForErc20Deposit( + Erc20Deposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external { + address appContract = _newActiveAppMock(); + + vm.prank(vm.randomAddress()); + bytes memory output = _refundOutputBuilder.buildRefundOutput( + appContract, + address(_erc20Portal), // inputSender + deposit.encode(extraData) // inputPayload + ); + + (bool isOutput, bytes4 sel, bytes memory args) = output.consumeBytes4(); + assertTrue(isOutput, "is output"); + assertEq(sel, Outputs.DelegateCallVoucher.selector, "is delegate-call voucher"); + + address destination; + bytes memory payload; + (destination, payload) = abi.decode(args, (address, bytes)); + assertEq(destination, address(_safeErc20Transfer), "voucher destination"); + + (bool isCall, bytes4 sel2, bytes memory args2) = payload.consumeBytes4(); + assertTrue(isCall, "is Solidity function call"); + assertEq(sel2, ISafeERC20Transfer.safeTransfer.selector, "call selector"); + + IERC20 token; + address to; + uint256 value; + (token, to, value) = abi.decode(args2, (IERC20, address, uint256)); + assertEq(address(token), address(deposit.token), "transfer token"); + assertEq(to, deposit.sender, "transfer destination"); + assertEq(value, deposit.value, "transfer value"); + } + + function testBuildRefundOutputForErc721Deposit( + Erc721Deposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external { + address appContract = _newActiveAppMock(); + + vm.prank(vm.randomAddress()); + bytes memory output = _refundOutputBuilder.buildRefundOutput( + appContract, + address(_erc721Portal), // inputSender + deposit.encode(extraData) // inputPayload + ); + + (bool isOutput, bytes4 sel, bytes memory args) = output.consumeBytes4(); + assertTrue(isOutput, "is output"); + assertEq(sel, Outputs.Voucher.selector, "is voucher"); + + address destination; + uint256 value; + bytes memory payload; + (destination, value, payload) = abi.decode(args, (address, uint256, bytes)); + assertEq(destination, address(deposit.token), "voucher destination"); + assertEq(value, 0, "voucher value"); + + (bool isCall, bytes4 sel2, bytes memory args2) = payload.consumeBytes4(); + assertTrue(isCall, "is Solidity function call"); + assertEq(sel2, bytes4(keccak256("safeTransferFrom(address,address,uint256)"))); + + address from; + address to; + uint256 tokenId; + (from, to, tokenId) = abi.decode(args2, (address, address, uint256)); + assertEq(from, appContract, "transfer origin"); + assertEq(to, deposit.sender, "transfer destination"); + assertEq(tokenId, deposit.tokenId, "transfer token ID"); + } + + function testBuildRefundOutputForErc1155SingleDeposit( + Erc1155SingleDeposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external { + address appContract = _newActiveAppMock(); + + vm.prank(vm.randomAddress()); + bytes memory output = _refundOutputBuilder.buildRefundOutput( + appContract, + address(_erc1155SinglePortal), // inputSender + deposit.encode(extraData) // inputPayload + ); + + (bool isOutput, bytes4 sel, bytes memory args) = output.consumeBytes4(); + assertTrue(isOutput, "is output"); + assertEq(sel, Outputs.Voucher.selector, "is voucher"); + + address destination; + uint256 value; + bytes memory payload; + (destination, value, payload) = abi.decode(args, (address, uint256, bytes)); + assertEq(destination, address(deposit.token), "voucher destination"); + assertEq(value, 0, "voucher value"); + + (bool isCall, bytes4 sel2, bytes memory args2) = payload.consumeBytes4(); + assertTrue(isCall, "is Solidity function call"); + assertEq(sel2, IERC1155.safeTransferFrom.selector); + + address from; + address to; + uint256 tokenId; + uint256 depositValue; + (from, to, tokenId, depositValue) = + abi.decode(args2, (address, address, uint256, uint256)); + assertEq(from, appContract, "transfer origin"); + assertEq(to, deposit.sender, "transfer destination"); + assertEq(tokenId, deposit.tokenId, "transfer token ID"); + assertEq(depositValue, deposit.value, "transfer value"); + } + + function testBuildRefundOutputForErc1155BatchDeposit( + Erc1155BatchDeposit calldata deposit, + LibDepositEncoder.ExtraData calldata extraData + ) external { + address appContract = _newActiveAppMock(); + + vm.prank(vm.randomAddress()); + bytes memory output = _refundOutputBuilder.buildRefundOutput( + appContract, + address(_erc1155BatchPortal), // inputSender + deposit.encode(extraData) // inputPayload + ); + + (bool isOutput, bytes4 sel, bytes memory args) = output.consumeBytes4(); + assertTrue(isOutput, "is output"); + assertEq(sel, Outputs.Voucher.selector, "is voucher"); + + address destination; + uint256 value; + bytes memory payload; + (destination, value, payload) = abi.decode(args, (address, uint256, bytes)); + assertEq(destination, address(deposit.token), "voucher destination"); + assertEq(value, 0, "voucher value"); + + (bool isCall, bytes4 sel2, bytes memory args2) = payload.consumeBytes4(); + assertTrue(isCall, "is Solidity function call"); + assertEq(sel2, IERC1155.safeBatchTransferFrom.selector); + + address from; + address to; + uint256[] memory tokenIds; + uint256[] memory depositValues; + (from, to, tokenIds, depositValues) = + abi.decode(args2, (address, address, uint256[], uint256[])); + assertEq(from, appContract, "transfer origin"); + assertEq(to, deposit.sender, "transfer destination"); + assertEq(tokenIds, deposit.tokenIds, "transfer token IDs"); + assertEq(depositValues, deposit.values, "transfer values"); + } +} diff --git a/test/util/LibDepositDecoder.sol b/test/util/LibDepositDecoder.sol new file mode 100644 index 00000000..2933a2e8 --- /dev/null +++ b/test/util/LibDepositDecoder.sol @@ -0,0 +1,53 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Erc1155BatchDeposit} from "src/common/Erc1155BatchDeposit.sol"; +import {Erc1155SingleDeposit} from "src/common/Erc1155SingleDeposit.sol"; +import {Erc20Deposit} from "src/common/Erc20Deposit.sol"; +import {Erc721Deposit} from "src/common/Erc721Deposit.sol"; +import {EtherDeposit} from "src/common/EtherDeposit.sol"; +import {InputEncoding} from "src/common/InputEncoding.sol"; + +library LibDepositDecoder { + function decodeEtherDeposit(bytes calldata payload) + external + pure + returns (EtherDeposit memory deposit) + { + return InputEncoding.decodeEtherDeposit(payload); + } + + function decodeErc20Deposit(bytes calldata payload) + external + pure + returns (Erc20Deposit memory deposit) + { + return InputEncoding.decodeErc20Deposit(payload); + } + + function decodeErc721Deposit(bytes calldata payload) + external + pure + returns (Erc721Deposit memory deposit) + { + return InputEncoding.decodeErc721Deposit(payload); + } + + function decodeErc1155SingleDeposit(bytes calldata payload) + external + pure + returns (Erc1155SingleDeposit memory deposit) + { + return InputEncoding.decodeErc1155SingleDeposit(payload); + } + + function decodeErc1155BatchDeposit(bytes calldata payload) + external + pure + returns (Erc1155BatchDeposit memory deposit) + { + return InputEncoding.decodeErc1155BatchDeposit(payload); + } +} diff --git a/test/util/LibDepositEncoder.sol b/test/util/LibDepositEncoder.sol new file mode 100644 index 00000000..222dcc16 --- /dev/null +++ b/test/util/LibDepositEncoder.sol @@ -0,0 +1,86 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.30; + +import {Erc1155BatchDeposit} from "src/common/Erc1155BatchDeposit.sol"; +import {Erc1155SingleDeposit} from "src/common/Erc1155SingleDeposit.sol"; +import {Erc20Deposit} from "src/common/Erc20Deposit.sol"; +import {Erc721Deposit} from "src/common/Erc721Deposit.sol"; +import {EtherDeposit} from "src/common/EtherDeposit.sol"; +import {InputEncoding} from "src/common/InputEncoding.sol"; + +library LibDepositEncoder { + /// @notice Extra data not present in deposit structures. + /// @param baseLayerData Base-layer data + /// @param execLayerData Execution-layer data + /// @dev Ether and ERC-20 deposits don't use the base-layer data. + struct ExtraData { + bytes baseLayerData; + bytes execLayerData; + } + + function encode(EtherDeposit calldata deposit, ExtraData calldata extraData) + external + pure + returns (bytes memory payload) + { + return InputEncoding.encodeEtherDeposit( + deposit.sender, deposit.value, extraData.execLayerData + ); + } + + function encode(Erc20Deposit calldata deposit, ExtraData calldata extraData) + external + pure + returns (bytes memory payload) + { + return InputEncoding.encodeERC20Deposit( + deposit.token, deposit.sender, deposit.value, extraData.execLayerData + ); + } + + function encode(Erc721Deposit calldata deposit, ExtraData calldata extraData) + external + pure + returns (bytes memory payload) + { + return InputEncoding.encodeERC721Deposit( + deposit.token, + deposit.sender, + deposit.tokenId, + extraData.baseLayerData, + extraData.execLayerData + ); + } + + function encode(Erc1155SingleDeposit calldata deposit, ExtraData calldata extraData) + external + pure + returns (bytes memory payload) + { + return InputEncoding.encodeSingleERC1155Deposit( + deposit.token, + deposit.sender, + deposit.tokenId, + deposit.value, + extraData.baseLayerData, + extraData.execLayerData + ); + } + + function encode(Erc1155BatchDeposit calldata deposit, ExtraData calldata extraData) + external + pure + returns (bytes memory payload) + { + return InputEncoding.encodeBatchERC1155Deposit( + deposit.token, + deposit.sender, + deposit.tokenIds, + deposit.values, + extraData.baseLayerData, + extraData.execLayerData + ); + } +} diff --git a/test/util/LibUint256Array.sol b/test/util/LibUint256Array.sol index 4f5a90f0..c48ad034 100644 --- a/test/util/LibUint256Array.sol +++ b/test/util/LibUint256Array.sol @@ -36,6 +36,16 @@ library LibUint256Array { } } + function randomUintGe(Vm vm, uint256[] memory array) + internal + returns (uint256[] memory newArray) + { + newArray = new uint256[](array.length); + for (uint256 i; i < array.length; ++i) { + newArray[i] = vm.randomUint(array[i], type(uint256).max); + } + } + function sequence(uint256 start, uint256 n) internal pure diff --git a/test/util/LibUint256Array.t.sol b/test/util/LibUint256Array.t.sol index 016cda1c..17d726c8 100644 --- a/test/util/LibUint256Array.t.sol +++ b/test/util/LibUint256Array.t.sol @@ -133,6 +133,14 @@ contract LibUint256ArrayTest is Test { } } + function testRandomUintGe(uint256[] memory array) external { + uint256[] memory newArray = vm.randomUintGe(array); + assertEq(newArray.length, array.length); + for (uint256 i; i < array.length; ++i) { + assertGe(newArray[i], array[i]); + } + } + function testAddAndSub(uint8 n) external { uint256[] memory a = new uint256[](n); uint256[] memory b = new uint256[](n); From 56a9fcfeb29277c77a5cf433e44c40bedc4b9dfd Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Sun, 21 Jun 2026 00:36:10 -0300 Subject: [PATCH 2/3] Replace data availability with input box --- src/common/DataAvailability.sol | 32 --------------- src/dapp/Application.sol | 41 +++++-------------- src/dapp/ApplicationFactory.sol | 17 ++++---- src/dapp/IApplication.sol | 15 +++---- src/dapp/IApplicationFactory.sol | 17 ++++---- src/dapp/ISelfHostedApplicationFactory.sol | 9 +++-- src/dapp/SelfHostedApplicationFactory.sol | 9 +++-- test/dapp/Application.t.sol | 6 +-- test/dapp/ApplicationFactory.t.sol | 42 ++++++++++---------- test/dapp/SelfHostedApplicationFactory.t.sol | 15 +++---- 10 files changed, 72 insertions(+), 131 deletions(-) delete mode 100644 src/common/DataAvailability.sol diff --git a/src/common/DataAvailability.sol b/src/common/DataAvailability.sol deleted file mode 100644 index cad3b998..00000000 --- a/src/common/DataAvailability.sol +++ /dev/null @@ -1,32 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.30; - -import {IInputBox} from "../inputs/IInputBox.sol"; - -/// forge-lint: disable-start(mixed-case-function) - -/// @title Data Availability -/// @notice Defines the signatures of data availability solutions. -interface DataAvailability { - /// @notice The application receives inputs only from - /// a contract that implements the `IInputBox` interface. - /// @param inputBox The input box contract address - function InputBox(IInputBox inputBox) external; - - /// @notice The application receives inputs from - /// a contract that implements the `IInputBox` interface, - /// and from Espresso, starting from a given block height, - /// and for a given namespace ID. - /// @param inputBox The input box contract address - /// @param fromBlock Height of first Espresso block to consider - /// @param namespaceId The Espresso namespace ID - function InputBoxAndEspresso( - IInputBox inputBox, - uint256 fromBlock, - uint32 namespaceId - ) external; -} - -/// forge-lint: disable-end(mixed-case-function) diff --git a/src/dapp/Application.sol b/src/dapp/Application.sol index 91847565..3b6fb827 100644 --- a/src/dapp/Application.sol +++ b/src/dapp/Application.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.30; import {IOwnable} from "../access/IOwnable.sol"; import {AccountValidityProof} from "../common/AccountValidityProof.sol"; import {CanonicalMachine} from "../common/CanonicalMachine.sol"; -import {DataAvailability} from "../common/DataAvailability.sol"; import {Inputs} from "../common/Inputs.sol"; import {OutputValidityProof} from "../common/OutputValidityProof.sol"; import {Outputs} from "../common/Outputs.sol"; @@ -56,6 +55,10 @@ contract Application is /// @dev See the `getTemplateHash` function. bytes32 immutable TEMPLATE_HASH; + /// @notice The input box contract. + /// @dev See the `getInputBox` function. + IInputBox immutable INPUT_BOX; + /// @notice The guardian address. /// @dev See the `getGuardian` function. address immutable GUARDIAN; @@ -96,10 +99,6 @@ contract Application is /// @dev See the `getOutputsMerkleRootValidator` and `migrateToOutputsMerkleRootValidator` functions. IOutputsMerkleRootValidator internal _outputsMerkleRootValidator; - /// @notice The data availability solution. - /// @dev See the `getDataAvailability` function. - bytes internal _dataAvailability; - /// @notice Whether the application has been foreclosed by the guardian. /// @dev See the `isForeclosed` function. bool internal _isForeclosed; @@ -130,7 +129,7 @@ contract Application is /// @param outputsMerkleRootValidator The initial outputs Merkle root validator contract /// @param initialOwner The initial application owner /// @param templateHash The initial machine state hash - /// @param dataAvailability The data availability solution + /// @param inputBox The input box contract /// @param refundOutputBuilder The refund output builder /// @param withdrawalConfig The withdrawal configuration /// @dev Reverts if the initial application owner address is zero. @@ -138,7 +137,7 @@ contract Application is IOutputsMerkleRootValidator outputsMerkleRootValidator, address initialOwner, bytes32 templateHash, - bytes memory dataAvailability, + IInputBox inputBox, IRefundOutputBuilder refundOutputBuilder, WithdrawalConfig memory withdrawalConfig ) Ownable(initialOwner) { @@ -147,6 +146,7 @@ contract Application is IApplicationFactoryErrors.InvalidWithdrawalConfig(withdrawalConfig) ); TEMPLATE_HASH = templateHash; + INPUT_BOX = inputBox; GUARDIAN = withdrawalConfig.guardian; LOG2_LEAVES_PER_ACCOUNT = withdrawalConfig.log2LeavesPerAccount; LOG2_MAX_NUM_OF_ACCOUNTS = withdrawalConfig.log2MaxNumOfAccounts; @@ -154,7 +154,6 @@ contract Application is REFUND_OUTPUT_BUILDER = refundOutputBuilder; WITHDRAWAL_OUTPUT_BUILDER = withdrawalConfig.withdrawalOutputBuilder; _outputsMerkleRootValidator = outputsMerkleRootValidator; - _dataAvailability = dataAvailability; } /// @notice Accept Ether transfers. @@ -389,7 +388,7 @@ contract Application is view override { - IInputBox inputBox = _getInputBox(); + IInputBox inputBox = getInputBox(); uint256 numOfInputs = inputBox.getNumberOfInputs(address(this)); require(inputIndex < numOfInputs, InvalidInputIndex(inputIndex, numOfInputs)); bytes32 stInputHash = inputBox.getInputHash(address(this), inputIndex); @@ -446,9 +445,8 @@ contract Application is return _outputsMerkleRootValidator; } - /// @inheritdoc IApplication - function getDataAvailability() public view override returns (bytes memory) { - return _dataAvailability; + function getInputBox() public view override returns (IInputBox) { + return INPUT_BOX; } /// @inheritdoc IApplication @@ -562,25 +560,6 @@ contract Application is _; } - /// @notice Get the input box contract used as data availability. - function _getInputBox() internal view returns (IInputBox inputBox) { - bool hasSelector; - bytes32 selector; - bytes memory arguments; - - (hasSelector, selector, arguments) = getDataAvailability().consumeBytes4(); - - require(hasSelector, UnknownDataAvailability()); - - if (selector == DataAvailability.InputBox.selector) { - inputBox = abi.decode(arguments, (IInputBox)); - } else if (selector == DataAvailability.InputBoxAndEspresso.selector) { - (inputBox,,) = abi.decode(arguments, (IInputBox, uint256, uint32)); - } else { - revert UnknownDataAvailability(); - } - } - /// @notice Get the log (base 2) of the number of bytes in the machine memory that are /// reserved for the accounts drive. function _getLog2AccountsDriveSize() internal view returns (uint8) { diff --git a/src/dapp/ApplicationFactory.sol b/src/dapp/ApplicationFactory.sol index a8478206..6e6e2a9c 100644 --- a/src/dapp/ApplicationFactory.sol +++ b/src/dapp/ApplicationFactory.sol @@ -8,6 +8,7 @@ import {Create2} from "@openzeppelin-contracts-5.2.0/utils/Create2.sol"; import {RollupsContract} from "../common/RollupsContract.sol"; import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IOutputsMerkleRootValidator} from "../consensus/IOutputsMerkleRootValidator.sol"; +import {IInputBox} from "../inputs/IInputBox.sol"; import {IRefundOutputBuilder} from "../refund/IRefundOutputBuilder.sol"; import {Application} from "./Application.sol"; import {IApplication} from "./IApplication.sol"; @@ -28,14 +29,14 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig ) external override returns (IApplication appContract) { appContract = new Application( outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, REFUND_OUTPUT_BUILDER, withdrawalConfig ); @@ -44,7 +45,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, appContract ); @@ -54,7 +55,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external override returns (IApplication appContract) { @@ -62,7 +63,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, REFUND_OUTPUT_BUILDER, withdrawalConfig ); @@ -71,7 +72,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, appContract ); @@ -81,7 +82,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external view override returns (address) { @@ -94,7 +95,7 @@ contract ApplicationFactory is IApplicationFactory, RollupsContract { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, REFUND_OUTPUT_BUILDER, withdrawalConfig ) diff --git a/src/dapp/IApplication.sol b/src/dapp/IApplication.sol index 76889ea3..e0afd5ff 100644 --- a/src/dapp/IApplication.sol +++ b/src/dapp/IApplication.sol @@ -10,6 +10,7 @@ import {IVersionGetter} from "../common/IVersionGetter.sol"; import {OutputValidityProof} from "../common/OutputValidityProof.sol"; import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IOutputsMerkleRootValidator} from "../consensus/IOutputsMerkleRootValidator.sol"; +import {IInputBox} from "../inputs/IInputBox.sol"; import {IRefundOutputBuilder} from "../refund/IRefundOutputBuilder.sol"; import {IRefundOutputBuilderErrors} from "../refund/IRefundOutputBuilderErrors.sol"; import {IWithdrawalOutputBuilder} from "../withdrawal/IWithdrawalOutputBuilder.sol"; @@ -103,10 +104,6 @@ interface IApplication is /// and therefore some actions cannot be performed anymore. error Foreclosed(); - /// @notice Raised when trying to decode the data availability byte array, - /// but either it is ill-formed or encodes an unknown data availability solution. - error UnknownDataAvailability(); - /// @notice Raised when trying to validate an input with an invalid index. /// @param invalidInputIndex The invalid input index provided for validation /// @param numOfInputs The actual number of inputs to the application @@ -121,7 +118,7 @@ interface IApplication is /// @notice Raised when decoding an ill-formed input. /// @dev This error should never be raised if the application uses - /// the canonical input box contract as on-chain data availability. + /// the canonical input box contract. error IllFormedInput(); /// @notice Raised when trying to issue a refund for a finalized input. @@ -268,10 +265,8 @@ interface IApplication is view returns (IOutputsMerkleRootValidator); - /// @notice Get the data availability solution used by application. - /// @return Solidity ABI-encoded function call that describes - /// the source of inputs that should be fed to the application. - function getDataAvailability() external view returns (bytes memory); + /// @notice Get the input box contract used by application. + function getInputBox() external view returns (IInputBox); /// @notice Get number of block in which contract was deployed function getDeploymentBlockNumber() external view returns (uint256); @@ -355,7 +350,7 @@ interface IApplication is /// @notice Validates an input that was sent to the application. /// @param inputIndex The index of the input in the application's input box. /// @param inputHash The hash of the input that was sent to the application - /// @dev May raise `UnknownDataAvailability`, `InvalidInputIndex` or `InvalidInputHash`. + /// @dev May raise `InvalidInputIndex` or `InvalidInputHash`. function validateInputHash(uint256 inputIndex, bytes32 inputHash) external view; /// @notice Get the withdrawal output builder, which gets static-called diff --git a/src/dapp/IApplicationFactory.sol b/src/dapp/IApplicationFactory.sol index 8efb9398..de7ea46e 100644 --- a/src/dapp/IApplicationFactory.sol +++ b/src/dapp/IApplicationFactory.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.30; import {IVersionGetter} from "../common/IVersionGetter.sol"; import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IOutputsMerkleRootValidator} from "../consensus/IOutputsMerkleRootValidator.sol"; +import {IInputBox} from "../inputs/IInputBox.sol"; import {IApplication} from "./IApplication.sol"; import {IApplicationFactoryErrors} from "./IApplicationFactoryErrors.sol"; @@ -17,14 +18,14 @@ interface IApplicationFactory is IVersionGetter, IApplicationFactoryErrors { /// @param outputsMerkleRootValidator The initial outputs Merkle root validator contract /// @param appOwner The initial application owner /// @param templateHash The initial machine state hash - /// @param dataAvailability The data availability solution + /// @param inputBox The input box contract /// @param appContract The application contract /// @dev MUST be triggered on a successful call to `newApplication`. event ApplicationCreated( IOutputsMerkleRootValidator indexed outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes dataAvailability, + IInputBox inputBox, WithdrawalConfig withdrawalConfig, IApplication appContract ); @@ -35,7 +36,7 @@ interface IApplicationFactory is IVersionGetter, IApplicationFactoryErrors { /// @param outputsMerkleRootValidator The initial outputs Merkle root validator contract /// @param appOwner The initial application owner /// @param templateHash The initial machine state hash - /// @param dataAvailability The data availability solution + /// @param inputBox The input box contract /// @param withdrawalConfig The withdrawal configuration /// @return The application /// @dev On success, MUST emit an `ApplicationCreated` event. @@ -44,7 +45,7 @@ interface IApplicationFactory is IVersionGetter, IApplicationFactoryErrors { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig ) external returns (IApplication); @@ -52,7 +53,7 @@ interface IApplicationFactory is IVersionGetter, IApplicationFactoryErrors { /// @param outputsMerkleRootValidator The initial outputs Merkle root validator contract /// @param appOwner The initial application owner /// @param templateHash The initial machine state hash - /// @param dataAvailability The data availability solution + /// @param inputBox The input box contract /// @param withdrawalConfig The withdrawal configuration /// @param salt The salt used to deterministically generate the application contract address /// @return The application @@ -62,7 +63,7 @@ interface IApplicationFactory is IVersionGetter, IApplicationFactoryErrors { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external returns (IApplication); @@ -71,7 +72,7 @@ interface IApplicationFactory is IVersionGetter, IApplicationFactoryErrors { /// @param outputsMerkleRootValidator The initial outputs Merkle root validator contract /// @param appOwner The initial application owner /// @param templateHash The initial machine state hash - /// @param dataAvailability The data availability solution + /// @param inputBox The input box contract /// @param withdrawalConfig The withdrawal configuration /// @param salt The salt used to deterministically generate the application contract address /// @return The deterministic application contract address @@ -81,7 +82,7 @@ interface IApplicationFactory is IVersionGetter, IApplicationFactoryErrors { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external view returns (address); diff --git a/src/dapp/ISelfHostedApplicationFactory.sol b/src/dapp/ISelfHostedApplicationFactory.sol index bd2234e2..877dd373 100644 --- a/src/dapp/ISelfHostedApplicationFactory.sol +++ b/src/dapp/ISelfHostedApplicationFactory.sol @@ -8,6 +8,7 @@ import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IConsensusFactoryErrors} from "../consensus/IConsensusFactoryErrors.sol"; import {IAuthority} from "../consensus/authority/IAuthority.sol"; import {IAuthorityFactory} from "../consensus/authority/IAuthorityFactory.sol"; +import {IInputBox} from "../inputs/IInputBox.sol"; import {IApplication} from "./IApplication.sol"; import {IApplicationFactory} from "./IApplicationFactory.sol"; import {IApplicationFactoryErrors} from "./IApplicationFactoryErrors.sol"; @@ -32,7 +33,7 @@ interface ISelfHostedApplicationFactory is /// @param claimStagingPeriod The claim staging period /// @param appOwner The initial application owner /// @param templateHash The initial machine state hash - /// @param dataAvailability The data availability solution + /// @param inputBox The input box contract /// @param withdrawalConfig The withdrawal configuration /// @param salt The salt used to deterministically generate the addresses /// @return The application contract @@ -46,7 +47,7 @@ interface ISelfHostedApplicationFactory is uint256 claimStagingPeriod, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external returns (IApplication, IAuthority); @@ -58,7 +59,7 @@ interface ISelfHostedApplicationFactory is /// @param claimStagingPeriod The claim staging period /// @param appOwner The initial application owner /// @param templateHash The initial machine state hash - /// @param dataAvailability The data availability solution + /// @param inputBox The input box contract /// @param withdrawalConfig The withdrawal configuration /// @param salt The salt used to deterministically generate the addresses /// @return The application address @@ -69,7 +70,7 @@ interface ISelfHostedApplicationFactory is uint256 claimStagingPeriod, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external view returns (address, address); diff --git a/src/dapp/SelfHostedApplicationFactory.sol b/src/dapp/SelfHostedApplicationFactory.sol index feca7e2f..dc8beb7d 100644 --- a/src/dapp/SelfHostedApplicationFactory.sol +++ b/src/dapp/SelfHostedApplicationFactory.sol @@ -8,6 +8,7 @@ import {WithdrawalConfig} from "../common/WithdrawalConfig.sol"; import {IOutputsMerkleRootValidator} from "../consensus/IOutputsMerkleRootValidator.sol"; import {IAuthority} from "../consensus/authority/IAuthority.sol"; import {IAuthorityFactory} from "../consensus/authority/IAuthorityFactory.sol"; +import {IInputBox} from "../inputs/IInputBox.sol"; import {IApplication} from "./IApplication.sol"; import {IApplicationFactory} from "./IApplicationFactory.sol"; import {ISelfHostedApplicationFactory} from "./ISelfHostedApplicationFactory.sol"; @@ -48,7 +49,7 @@ contract SelfHostedApplicationFactory is ISelfHostedApplicationFactory, RollupsC uint256 claimStagingPeriod, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external returns (IApplication application, IAuthority authority) { @@ -57,7 +58,7 @@ contract SelfHostedApplicationFactory is ISelfHostedApplicationFactory, RollupsC ); application = APPLICATION_FACTORY.newApplication( - authority, appOwner, templateHash, dataAvailability, withdrawalConfig, salt + authority, appOwner, templateHash, inputBox, withdrawalConfig, salt ); } @@ -67,7 +68,7 @@ contract SelfHostedApplicationFactory is ISelfHostedApplicationFactory, RollupsC uint256 claimStagingPeriod, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external view returns (address application, address authority) { @@ -79,7 +80,7 @@ contract SelfHostedApplicationFactory is ISelfHostedApplicationFactory, RollupsC IOutputsMerkleRootValidator(authority), appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ); diff --git a/test/dapp/Application.t.sol b/test/dapp/Application.t.sol index dde79239..1565ee2f 100644 --- a/test/dapp/Application.t.sol +++ b/test/dapp/Application.t.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.30; import {AccountValidityProof} from "src/common/AccountValidityProof.sol"; import {BinaryMerkleTreeErrors} from "src/common/BinaryMerkleTreeErrors.sol"; import {CanonicalMachine} from "src/common/CanonicalMachine.sol"; -import {DataAvailability} from "src/common/DataAvailability.sol"; import {OutputValidityProof} from "src/common/OutputValidityProof.sol"; import {Outputs} from "src/common/Outputs.sol"; import {WithdrawalConfig} from "src/common/WithdrawalConfig.sol"; @@ -87,7 +86,6 @@ contract ApplicationTest is address _appOwner; address _authorityOwner; address _recipient; - bytes _dataAvailability; string[] _outputNames; string[] _accountNames; uint256[] _tokenIds; @@ -1600,8 +1598,6 @@ contract ApplicationTest is _erc20Token = _contracts.dev.testFungibleToken; _erc721Token = _contracts.dev.testNonFungibleToken; _erc1155Token = _contracts.dev.testMultiToken; - _dataAvailability = - abi.encodeCall(DataAvailability.InputBox, (_contracts.core.inputBox)); _safeErc20Transfer = _contracts.core.safeErc20Transfer; } @@ -1621,7 +1617,7 @@ contract ApplicationTest is CLAIM_STAGING_PERIOD, _appOwner, _templateHash, - _dataAvailability, + _contracts.core.inputBox, _withdrawalConfig, SALT ); diff --git a/test/dapp/ApplicationFactory.t.sol b/test/dapp/ApplicationFactory.t.sol index adac190c..6666ae50 100644 --- a/test/dapp/ApplicationFactory.t.sol +++ b/test/dapp/ApplicationFactory.t.sol @@ -9,6 +9,7 @@ import {IOutputsMerkleRootValidator} from "src/consensus/IOutputsMerkleRootValid import {IApplication} from "src/dapp/IApplication.sol"; import {IApplicationFactory} from "src/dapp/IApplicationFactory.sol"; import {IApplicationFactoryErrors} from "src/dapp/IApplicationFactoryErrors.sol"; +import {IInputBox} from "src/inputs/IInputBox.sol"; import {LibWithdrawalConfig} from "src/library/LibWithdrawalConfig.sol"; import {Ownable} from "@openzeppelin-contracts-5.2.0/access/Ownable.sol"; @@ -38,7 +39,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig ) external { vm.roll(blockNumber); @@ -46,11 +47,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { vm.recordLogs(); try _factory.newApplication( - outputsMerkleRootValidator, - appOwner, - templateHash, - dataAvailability, - withdrawalConfig + outputsMerkleRootValidator, appOwner, templateHash, inputBox, withdrawalConfig ) returns ( IApplication appContract ) { @@ -60,7 +57,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, appContract, blockNumber, @@ -77,7 +74,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external { @@ -87,7 +84,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ); @@ -98,7 +95,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ) returns ( @@ -116,7 +113,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, appContract, blockNumber, @@ -132,7 +129,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ), @@ -145,7 +142,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { outputsMerkleRootValidator, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ) { @@ -163,7 +160,7 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { IOutputsMerkleRootValidator outputsMerkleRootValidator, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig memory withdrawalConfig, IApplication appContract, uint256 blockNumber, @@ -188,11 +185,12 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { ( address appOwner_, bytes32 templateHash_, - bytes memory dataAvailability_, + IInputBox inputBox_, WithdrawalConfig memory withdrawalConfig_, IApplication app_ ) = abi.decode( - log.data, (address, bytes32, bytes, WithdrawalConfig, IApplication) + log.data, + (address, bytes32, IInputBox, WithdrawalConfig, IApplication) ); assertEq(appOwner, appOwner_, "ApplicationCreated.owner != owner"); @@ -202,9 +200,9 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { "ApplicationCreated.templateHash != templateHash" ); assertEq( - dataAvailability, - dataAvailability_, - "ApplicationCreated.dataAvailability != dataAvailability" + address(inputBox), + address(inputBox_), + "ApplicationCreated.inputBox != inputBox" ); assertEq( abi.encode(withdrawalConfig), @@ -265,9 +263,9 @@ contract ApplicationFactoryTest is RollupsTest, VersionGetterTestUtils { "getWithdrawalOutputBuilder() != withdrawalConfig.withdrawalOutputBuilder" ); assertEq( - appContract.getDataAvailability(), - dataAvailability, - "getDataAvailability() != dataAvailability" + address(appContract.getInputBox()), + address(inputBox), + "getInputBox() != inputBox" ); assertEq( appContract.getDeploymentBlockNumber(), diff --git a/test/dapp/SelfHostedApplicationFactory.t.sol b/test/dapp/SelfHostedApplicationFactory.t.sol index 78c8dbc6..2937bae0 100644 --- a/test/dapp/SelfHostedApplicationFactory.t.sol +++ b/test/dapp/SelfHostedApplicationFactory.t.sol @@ -14,6 +14,7 @@ import {IApplication} from "src/dapp/IApplication.sol"; import {IApplicationFactory} from "src/dapp/IApplicationFactory.sol"; import {IApplicationFactoryErrors} from "src/dapp/IApplicationFactoryErrors.sol"; import {ISelfHostedApplicationFactory} from "src/dapp/ISelfHostedApplicationFactory.sol"; +import {IInputBox} from "src/inputs/IInputBox.sol"; import {LibWithdrawalConfig} from "src/library/LibWithdrawalConfig.sol"; import {LibBytes} from "../util/LibBytes.sol"; @@ -53,7 +54,7 @@ contract SelfHostedApplicationFactoryTest is RollupsTest, VersionGetterTestUtils uint256 claimStagingPeriod, address appOwner, bytes32 templateHash, - bytes calldata dataAvailability, + IInputBox inputBox, WithdrawalConfig calldata withdrawalConfig, bytes32 salt ) external { @@ -68,7 +69,7 @@ contract SelfHostedApplicationFactoryTest is RollupsTest, VersionGetterTestUtils claimStagingPeriod, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ); @@ -79,7 +80,7 @@ contract SelfHostedApplicationFactoryTest is RollupsTest, VersionGetterTestUtils claimStagingPeriod, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ) returns ( @@ -156,9 +157,9 @@ contract SelfHostedApplicationFactoryTest is RollupsTest, VersionGetterTestUtils "app.getWithdrawalOutputBuilder() != withdrawalConfig.withdrawalOutputBuilder" ); assertEq( - application.getDataAvailability(), - dataAvailability, - "app.getDataAvailability() != dataAvailability" + address(application.getInputBox()), + address(inputBox), + "app.getInputBox() != inputBox" ); assertEq( application.getDeploymentBlockNumber(), @@ -176,7 +177,7 @@ contract SelfHostedApplicationFactoryTest is RollupsTest, VersionGetterTestUtils claimStagingPeriod, appOwner, templateHash, - dataAvailability, + inputBox, withdrawalConfig, salt ); From e1092cd8759030b881cd72a29ac46678e0b18404 Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Thu, 25 Jun 2026 10:16:19 -0300 Subject: [PATCH 3/3] Generate Rust bindings only for a subset of contracts - This commit fixes CI, which produced bindings for test utility contracts, which happen to generate invalid Rust code (overloaded library function). --- Makefile | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Makefile b/Makefile index aff2b3ea..105670a4 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,9 @@ .PHONY: release-artifacts .PHONY: rust-bindings +NOOP = +SPACE = $(NOOP) $(NOOP) + PROJECT_NAME := cartesi-rollups-contracts PROJECT_MAJOR_VERSION := 3 @@ -129,6 +132,33 @@ MAINNET_CHAIN_IDS += $(OPT_MAINNET_CHAIN_ID) LIVENET_CHAIN_IDS := $(TESTNET_CHAIN_IDS) $(MAINNET_CHAIN_IDS) +FORGE_BIND_CONTRACTS += IApplication +FORGE_BIND_CONTRACTS += IApplicationFactory +FORGE_BIND_CONTRACTS += IAuthority +FORGE_BIND_CONTRACTS += IAuthorityFactory +FORGE_BIND_CONTRACTS += IConsensus +FORGE_BIND_CONTRACTS += IERC1155BatchPortal +FORGE_BIND_CONTRACTS += IERC1155SinglePortal +FORGE_BIND_CONTRACTS += IERC20Portal +FORGE_BIND_CONTRACTS += IERC721Portal +FORGE_BIND_CONTRACTS += IEtherPortal +FORGE_BIND_CONTRACTS += IInputBox +FORGE_BIND_CONTRACTS += Inputs +FORGE_BIND_CONTRACTS += IOutputsMerkleRootValidator +FORGE_BIND_CONTRACTS += IQuorum +FORGE_BIND_CONTRACTS += IQuorumFactory +FORGE_BIND_CONTRACTS += IRefundOutputBuilder +FORGE_BIND_CONTRACTS += ISafeERC20Transfer +FORGE_BIND_CONTRACTS += ISelfHostedApplicationFactory +FORGE_BIND_CONTRACTS += IUsdWithdrawalOutputBuilder +FORGE_BIND_CONTRACTS += IUsdWithdrawalOutputBuilderFactory +FORGE_BIND_CONTRACTS += IWithdrawalOutputBuilder +FORGE_BIND_CONTRACTS += Outputs +FORGE_BIND_CONTRACTS += TestFungibleToken +FORGE_BIND_CONTRACTS += TestMultiToken +FORGE_BIND_CONTRACTS += TestNonFungibleToken + +FORGE_BIND_OPTS += --select "$(subst $(SPACE),|,$(FORGE_BIND_CONTRACTS))" FORGE_BIND_OPTS += --crate-name "$(PROJECT_NAME)" FORGE_BIND_OPTS += --crate-version "$(PROJECT_VERSION)" FORGE_BIND_OPTS += --crate-license "Apache-2.0"