diff --git a/.changeset/olive-crabs-carry.md b/.changeset/olive-crabs-carry.md new file mode 100644 index 00000000..dd8583d9 --- /dev/null +++ b/.changeset/olive-crabs-carry.md @@ -0,0 +1,6 @@ +--- +"@cartesi/rollups": major +--- + +Added contract `AssetTransferToENS` that can be used as a destination for `DELEGATECALL` vouchers to transfer assets to ENS-identified accounts. +Added library `LibAddress` for safe low level call and safe delegate call. diff --git a/contracts/dapp/Application.sol b/contracts/dapp/Application.sol index d4039547..42131f3a 100644 --- a/contracts/dapp/Application.sol +++ b/contracts/dapp/Application.sol @@ -11,8 +11,8 @@ import {LibOutputValidityProof} from "../library/LibOutputValidityProof.sol"; import {OutputValidityProof} from "../common/OutputValidityProof.sol"; import {Outputs} from "../common/Outputs.sol"; import {InputRange} from "../common/InputRange.sol"; -import {LibError} from "../library/LibError.sol"; import {LibInputRange} from "../library/LibInputRange.sol"; +import {LibAddress} from "../library/LibAddress.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; @@ -30,7 +30,7 @@ contract Application is ReentrancyGuard { using BitMaps for BitMaps.BitMap; - using LibError for bytes; + using LibAddress for address; using LibOutputValidityProof for OutputValidityProof; using LibInputRange for InputRange; @@ -203,14 +203,7 @@ contract Application is (address, uint256, bytes) ); - bool success; - bytes memory returndata; - - (success, returndata) = destination.call{value: value}(payload); - - if (!success) { - returndata.raise(); - } + destination.safeCall(value, payload); } /// @notice Executes a delegatecall voucher @@ -221,13 +214,6 @@ contract Application is (destination, payload) = abi.decode(arguments, (address, bytes)); - bool success; - bytes memory returndata; - - (success, returndata) = destination.delegatecall(payload); - - if (!success) { - returndata.raise(); - } + destination.safeDelegateCall(payload); } } diff --git a/contracts/delegatecall/AssetTransferToENS.sol b/contracts/delegatecall/AssetTransferToENS.sol new file mode 100644 index 00000000..07363824 --- /dev/null +++ b/contracts/delegatecall/AssetTransferToENS.sol @@ -0,0 +1,86 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.20; + +import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol"; +import {AddrResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/AddrResolver.sol"; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +import {LibAddress} from "../library/LibAddress.sol"; + +contract AssetTransferToENS { + using LibAddress for address; + using SafeERC20 for IERC20; + + ENS immutable _ens; + + constructor(ENS ens) { + _ens = ens; + } + + function sendEtherToENS( + bytes32 node, + uint256 value, + bytes memory payload + ) external { + address recipient = _resolveENS(node); + recipient.safeCall(value, payload); + } + + function sendERC20ToENS( + IERC20 token, + bytes32 node, + uint256 value + ) external { + address recipient = _resolveENS(node); + token.safeTransfer(recipient, value); + } + + function sendERC721ToENS( + IERC721 token, + bytes32 node, + uint256 tokenId, + bytes calldata data + ) external { + address recipient = _resolveENS(node); + token.safeTransferFrom(address(this), recipient, tokenId, data); + } + + function sendERC1155ToENS( + IERC1155 token, + bytes32 node, + uint256 id, + uint256 value, + bytes calldata data + ) external { + address recipient = _resolveENS(node); + token.safeTransferFrom(address(this), recipient, id, value, data); + } + + function sendBatchERC1155ToENS( + IERC1155 token, + bytes32 node, + uint256[] memory ids, + uint256[] memory values, + bytes calldata data + ) external { + address recipient = _resolveENS(node); + token.safeBatchTransferFrom( + address(this), + recipient, + ids, + values, + data + ); + } + + function _resolveENS(bytes32 node) internal view returns (address) { + AddrResolver resolver = AddrResolver(_ens.resolver(node)); + return resolver.addr(node); + } +} diff --git a/contracts/library/LibAddress.sol b/contracts/library/LibAddress.sol new file mode 100644 index 00000000..3a79f9f2 --- /dev/null +++ b/contracts/library/LibAddress.sol @@ -0,0 +1,48 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {LibError} from "../library/LibError.sol"; + +library LibAddress { + using LibError for bytes; + + /// @notice Perform a low level call and raise error if failed + /// @param destination The address that will be called + /// @param value The amount of Wei to be transferred through the call + /// @param payload The payload, which—in the case of Solidity + /// contracts—encodes a function call + function safeCall( + address destination, + uint256 value, + bytes memory payload + ) internal { + bool success; + bytes memory returndata; + + (success, returndata) = destination.call{value: value}(payload); + + if (!success) { + returndata.raise(); + } + } + + /// @notice Perform a delegate call and raise error if failed + /// @param destination The address that will be called + /// @param payload The payload, which—in the case of Solidity + /// libraries—encodes a function call + function safeDelegateCall( + address destination, + bytes memory payload + ) internal { + bool success; + bytes memory returndata; + + (success, returndata) = destination.delegatecall(payload); + + if (!success) { + returndata.raise(); + } + } +} diff --git a/package.json b/package.json index 5dc59e6d..fa59a978 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ }, "dependencies": { "@cartesi/util": "6.1.0", - "@openzeppelin/contracts": "5.0.2" + "@openzeppelin/contracts": "5.0.2", + "@ensdomains/ens-contracts": "1.1.4" }, "devDependencies": { "@changesets/cli": "^2.27.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35aee161..ca8b58bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@cartesi/util': specifier: 6.1.0 version: 6.1.0 + '@ensdomains/ens-contracts': + specifier: 1.1.4 + version: 1.1.4 '@openzeppelin/contracts': specifier: 5.0.2 version: 5.0.2 @@ -328,6 +331,25 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@ensdomains/buffer@0.1.1: + resolution: {integrity: sha512-92SfSiNS8XorgU7OUBHo/i1ZU7JV7iz/6bKuLPNVsMxV79/eI7fJR6jfJJc40zAHjs3ha+Xo965Idomlq3rqnw==} + dev: false + + /@ensdomains/ens-contracts@1.1.4: + resolution: {integrity: sha512-kjdcjaznMtE2lwjAVTX2irs8mgNgJCVuB5hnhFhiMaO8dR/tlHQ5UhtZjhSYRhkZd0hLXYrMkXp6thnwpG+ltg==} + dependencies: + '@ensdomains/buffer': 0.1.1 + '@ensdomains/solsha1': 0.0.3 + '@openzeppelin/contracts': 4.9.6 + dns-packet: 5.6.1 + dev: false + + /@ensdomains/solsha1@0.0.3: + resolution: {integrity: sha512-uhuG5LzRt/UJC0Ux83cE2rCKwSleRePoYdQVcqPN1wyf3/ekMzT/KZUF9+v7/AG5w9jlMLCQkUM50vfjr0Yu9Q==} + dependencies: + hash-test-vectors: 1.3.2 + dev: false + /@ethereumjs/rlp@4.0.1: resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} engines: {node: '>=14'} @@ -691,6 +713,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@leichtgewicht/ip-codec@2.0.5: + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + dev: false + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: @@ -1046,6 +1072,10 @@ packages: hardhat: 2.22.2(ts-node@10.9.2)(typescript@5.4.3) dev: true + /@openzeppelin/contracts@4.9.6: + resolution: {integrity: sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==} + dev: false + /@openzeppelin/contracts@5.0.2: resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} dev: false @@ -2299,6 +2329,13 @@ packages: path-type: 4.0.0 dev: true + /dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -3240,6 +3277,10 @@ packages: safe-buffer: 5.2.1 dev: true + /hash-test-vectors@1.3.2: + resolution: {integrity: sha512-PKd/fitmsrlWGh3OpKbgNLE04ZQZsvs1ZkuLoQpeIKuwx+6CYVNdW6LaPIS1QAdZvV40+skk0w4YomKnViUnvQ==} + dev: false + /hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} dependencies: diff --git a/test/foundry/dapp/Application.t.sol b/test/foundry/dapp/Application.t.sol index d0bee282..fd941552 100644 --- a/test/foundry/dapp/Application.t.sol +++ b/test/foundry/dapp/Application.t.sol @@ -20,21 +20,27 @@ import {InputRange} from "contracts/common/InputRange.sol"; import {OutputValidityProof} from "contracts/common/OutputValidityProof.sol"; import {Outputs} from "contracts/common/Outputs.sol"; import {SafeERC20Transfer} from "contracts/delegatecall/SafeERC20Transfer.sol"; +import {AssetTransferToENS} from "contracts/delegatecall/AssetTransferToENS.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20Errors, IERC721Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import {IERC20Errors, IERC721Errors, IERC1155Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol"; +import {AddrResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/AddrResolver.sol"; + import {ERC165Test} from "../util/ERC165Test.sol"; import {EtherReceiver} from "../util/EtherReceiver.sol"; import {LibEmulator} from "../util/LibEmulator.sol"; import {SimpleERC20} from "../util/SimpleERC20.sol"; import {SimpleERC721} from "../util/SimpleERC721.sol"; +import {SimpleSingleERC1155, SimpleBatchERC1155} from "../util/SimpleERC1155.sol"; import {ExternalLibMerkle32} from "../library/LibMerkle32.t.sol"; contract ApplicationTest is ERC165Test { @@ -46,30 +52,39 @@ contract ApplicationTest is ERC165Test { IConsensus _consensus; IERC20 _erc20Token; IERC721 _erc721Token; + IERC1155 _erc1155SingleToken; + IERC1155 _erc1155BatchToken; IInputBox _inputBox; IPortal[] _portals; SafeERC20Transfer _safeERC20Transfer; + AssetTransferToENS _assetTransferToENS; + ENS _ens; + AddrResolver _resolver; + LibEmulator.State _emulator; address _appOwner; address _authorityOwner; address _recipient; address _tokenOwner; - - mapping(string => LibEmulator.OutputId) _outputIdsByName; string[] _outputNames; - bytes4[] _interfaceIds; + uint256[] _tokenIds; + uint256[] _initialSupplies; + uint256[] _transferAmounts; + mapping(string => LibEmulator.OutputId) _outputIdsByName; bytes32 constant _templateHash = keccak256("templateHash"); uint256 constant _initialSupply = 1000000000000000000000000000000000000; uint256 constant _tokenId = 88888888; uint256 constant _transferAmount = 42; + bytes32 constant _ensNode = keccak256("user.eth"); function setUp() public { _initVariables(); _deployContracts(); _addOutputs(); _submitClaims(); + _mockENS(); } // ----------- @@ -119,13 +134,6 @@ contract ApplicationTest is ERC165Test { assertEq(appContract.getPortals(), portals); } - function assertEq(IPortal[] memory a, IPortal[] memory b) internal pure { - assertEq(a.length, b.length); - for (uint256 i; i < a.length; ++i) { - assertEq(address(a[i]), address(b[i])); - } - } - // ------------------- // consensus migration // ------------------- @@ -214,43 +222,7 @@ contract ApplicationTest is ERC165Test { bytes memory output = _getOutput(name); OutputValidityProof memory proof = _getProof(name); - assertLt( - address(_appContract).balance, - _transferAmount, - "Application contract does not have enough Ether" - ); - - vm.expectRevert(); - _appContract.executeOutput(output, proof); - - vm.deal(address(_appContract), _transferAmount); - - uint256 recipientBalance = _recipient.balance; - uint256 appBalance = address(_appContract).balance; - - _expectEmitOutputExecuted(output, proof); - - _appContract.executeOutput(output, proof); - - assertEq( - _recipient.balance, - recipientBalance + _transferAmount, - "Recipient should have received the transfer amount" - ); - - assertEq( - address(_appContract).balance, - appBalance - _transferAmount, - "Application contract should have the transfer amount deducted" - ); - - assertTrue( - _wasOutputExecuted(proof), - "Output should be marked as executed" - ); - - _expectRevertOutputNotReexecutable(output); - _appContract.executeOutput(output, proof); + _testEtherTransfer(output, proof); } function testExecuteEtherMintVoucher() external { @@ -258,49 +230,7 @@ contract ApplicationTest is ERC165Test { bytes memory output = _getOutput(name); OutputValidityProof memory proof = _getProof(name); - assertLt( - address(_appContract).balance, - _transferAmount, - "Application contract does not have enough Ether" - ); - - vm.expectRevert(); - _appContract.executeOutput(output, proof); - - vm.deal(address(_appContract), _transferAmount); - - uint256 recipientBalance = address(_etherReceiver).balance; - uint256 appBalance = address(_appContract).balance; - uint256 balanceOf = _etherReceiver.balanceOf(address(_appContract)); - - _expectEmitOutputExecuted(output, proof); - _appContract.executeOutput(output, proof); - - assertEq( - address(_etherReceiver).balance, - recipientBalance + _transferAmount, - "Recipient should have received the transfer amount" - ); - - assertEq( - address(_appContract).balance, - appBalance - _transferAmount, - "Application contract should have the transfer amount deducted" - ); - - assertEq( - _etherReceiver.balanceOf(address(_appContract)), - balanceOf + _transferAmount, - "Application contract should have the transfer amount minted" - ); - - assertTrue( - _wasOutputExecuted(proof), - "Output should be marked as executed" - ); - - _expectRevertOutputNotReexecutable(output); - _appContract.executeOutput(output, proof); + _testEtherMint(output, proof); } function testExecuteERC20TransferVoucher() external { @@ -359,44 +289,7 @@ contract ApplicationTest is ERC165Test { bytes memory output = _getOutput(name); OutputValidityProof memory proof = _getProof(name); - assertEq( - _erc721Token.ownerOf(_tokenId), - _tokenOwner, - "The NFT is initially owned by `_tokenOwner`" - ); - - vm.expectRevert( - abi.encodeWithSelector( - IERC721Errors.ERC721InsufficientApproval.selector, - address(_appContract), - _tokenId - ) - ); - _appContract.executeOutput(output, proof); - - vm.prank(_tokenOwner); - _erc721Token.safeTransferFrom( - _tokenOwner, - address(_appContract), - _tokenId - ); - - _expectEmitOutputExecuted(output, proof); - _appContract.executeOutput(output, proof); - - assertEq( - _erc721Token.ownerOf(_tokenId), - _recipient, - "The NFT is then transferred to the recipient" - ); - - assertTrue( - _wasOutputExecuted(proof), - "Output should be marked as executed" - ); - - _expectRevertOutputNotReexecutable(output); - _appContract.executeOutput(output, proof); + _testERC721Transfer(output, proof); } function testExecuteEmptyOutput() external { @@ -426,68 +319,183 @@ contract ApplicationTest is ERC165Test { _appContract.executeOutput(output, proof); } - function testExecuteERC20TransferDelegateCallVoucher() external { + function testExecuteERC20TransferDelegateCallVoucherFail() external { string memory name = "ERC20DelegateCallVoucher"; bytes memory output = _getOutput(name); OutputValidityProof memory proof = _getProof(name); - assertLt( - _erc20Token.balanceOf(address(_appContract)), - _transferAmount, - "Application contract does not have enough ERC-20 tokens" - ); + _testERC20Fail(output, proof); + } - // test revert + function testExecuteERC20TransferDelegateCallVoucherSuccess() external { + string memory name = "ERC20DelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); - vm.expectRevert( - abi.encodeWithSelector( - IERC20Errors.ERC20InsufficientBalance.selector, - address(_appContract), - _erc20Token.balanceOf(address(_appContract)), - _transferAmount - ) - ); - _appContract.executeOutput(output, proof); + _testERC20Success(output, proof); + } - // test return false + function testEtherTransferToENS() external { + string memory name = "EtherToENSDelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); + + _testEtherTransfer(output, proof); + } + + function testEtherTransferWithPayloadToENS() external { + string memory name = "EtherWithPayloadToENSDelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); vm.mockCall( - address(_erc20Token), - abi.encodeCall(_erc20Token.transfer, (_recipient, _transferAmount)), - abi.encode(false) + address(_resolver), + abi.encodeWithSignature("addr(bytes32)", (_ensNode)), + abi.encode(address(_etherReceiver)) ); + + _testEtherMint(output, proof); + } + + function testERC20TransferToENSFail() external { + string memory name = "ERC20ToENSDelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); + + _testERC20Fail(output, proof); + } + + function testERC20TransferToENSSuccess() external { + string memory name = "ERC20ToENSDelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); + + _testERC20Success(output, proof); + } + + function testERC721TransferToENS() external { + string memory name = "ERC721ToENSDelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); + + _testERC721Transfer(output, proof); + } + + function testERC1155SingleTransferToENS() external { + string memory name = "ERC1155SingleToENSDelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); + vm.expectRevert( abi.encodeWithSelector( - SafeERC20.SafeERC20FailedOperation.selector, - address(_erc20Token) + IERC1155Errors.ERC1155InsufficientBalance.selector, + address(_appContract), + 0, + _transferAmount, + _tokenId ) ); _appContract.executeOutput(output, proof); - vm.clearMockedCalls(); - - // test success vm.prank(_tokenOwner); - _erc20Token.transfer(address(_appContract), _transferAmount); + _erc1155SingleToken.safeTransferFrom( + _tokenOwner, + address(_appContract), + _tokenId, + _initialSupply, + "" + ); - uint256 recipientBalance = _erc20Token.balanceOf(address(_recipient)); - uint256 appBalance = _erc20Token.balanceOf(address(_appContract)); + uint256 recipientBalance = _erc1155SingleToken.balanceOf( + _recipient, + _tokenId + ); + uint256 appBalance = _erc1155SingleToken.balanceOf( + address(_appContract), + _tokenId + ); _expectEmitOutputExecuted(output, proof); _appContract.executeOutput(output, proof); assertEq( - _erc20Token.balanceOf(address(_recipient)), + _erc1155SingleToken.balanceOf(address(_appContract), _tokenId), + appBalance - _transferAmount, + "Application contract should have the transfer amount deducted" + ); + assertEq( + _erc1155SingleToken.balanceOf(_recipient, _tokenId), recipientBalance + _transferAmount, "Recipient should have received the transfer amount" ); - assertEq( - _erc20Token.balanceOf(address(_appContract)), - appBalance - _transferAmount, - "Application contract should have the transfer amount deducted" + assertTrue( + _wasOutputExecuted(proof), + "Output should be marked as executed" + ); + + _expectRevertOutputNotReexecutable(output); + _appContract.executeOutput(output, proof); + } + + function testERC1155BatchTransferToENS() external { + string memory name = "ERC1155BatchToENSDelegateCallVoucher"; + bytes memory output = _getOutput(name); + OutputValidityProof memory proof = _getProof(name); + + vm.expectRevert( + abi.encodeWithSelector( + IERC1155Errors.ERC1155InsufficientBalance.selector, + address(_appContract), + 0, + _transferAmounts[0], + _tokenIds[0] + ) + ); + _appContract.executeOutput(output, proof); + + vm.prank(_tokenOwner); + _erc1155BatchToken.safeBatchTransferFrom( + _tokenOwner, + address(_appContract), + _tokenIds, + _initialSupplies, + "" ); + uint256 batchLength = _initialSupplies.length; + uint256[] memory appBalances = new uint256[](batchLength); + uint256[] memory recipientBalances = new uint256[](batchLength); + for (uint256 i; i < batchLength; ++i) { + appBalances[i] = _erc1155BatchToken.balanceOf( + address(_appContract), + _tokenIds[i] + ); + recipientBalances[i] = _erc1155BatchToken.balanceOf( + _recipient, + _tokenIds[i] + ); + } + + _expectEmitOutputExecuted(output, proof); + _appContract.executeOutput(output, proof); + + for (uint256 i; i < _tokenIds.length; ++i) { + assertEq( + _erc1155BatchToken.balanceOf( + address(_appContract), + _tokenIds[i] + ), + appBalances[i] - _transferAmounts[i], + "Application contract should have the transfer amount deducted" + ); + assertEq( + _erc1155BatchToken.balanceOf(_recipient, _tokenIds[i]), + recipientBalances[i] + _transferAmounts[i], + "Recipient should have received the transfer amount" + ); + } + assertTrue( _wasOutputExecuted(proof), "Output should be marked as executed" @@ -523,15 +531,34 @@ contract ApplicationTest is ERC165Test { _appOwner = _newAddr(); _recipient = _newAddr(); _tokenOwner = _newAddr(); + _ens = ENS(_newAddr()); + _resolver = AddrResolver(_newAddr()); _interfaceIds.push(type(IApplication).interfaceId); _interfaceIds.push(type(IERC721Receiver).interfaceId); _interfaceIds.push(type(IERC1155Receiver).interfaceId); + for (uint256 i; i < 7; ++i) { + _tokenIds.push(i); + _initialSupplies.push(_initialSupply); + _transferAmounts.push( + bound(uint256(keccak256(abi.encode(i))), 1, _initialSupply) + ); + } } function _deployContracts() internal { _etherReceiver = new EtherReceiver(); _erc20Token = new SimpleERC20(_tokenOwner, _initialSupply); _erc721Token = new SimpleERC721(_tokenOwner, _tokenId); + _erc1155SingleToken = new SimpleSingleERC1155( + _tokenOwner, + _tokenId, + _initialSupply + ); + _erc1155BatchToken = new SimpleBatchERC1155( + _tokenOwner, + _tokenIds, + _initialSupplies + ); _inputBox = new InputBox(); _portals.push(new EtherPortal(_inputBox)); _portals.push(new ERC20Portal(_inputBox)); @@ -547,6 +574,7 @@ contract ApplicationTest is ERC165Test { _templateHash ); _safeERC20Transfer = new SafeERC20Transfer(); + _assetTransferToENS = new AssetTransferToENS(_ens); } function _addOutputs() internal { @@ -629,12 +657,103 @@ contract ApplicationTest is ERC165Test { _finishInput(); _finishEpoch(); - // Test input with no outputs - _finishInput(); - _finishEpoch(); - } - - function _encodeNotice( + _nameOutput( + "EtherToENSDelegateCallVoucher", + _addOutput( + _encodeDelegateCallVoucher( + address(_assetTransferToENS), + abi.encodeCall( + AssetTransferToENS.sendEtherToENS, + (_ensNode, _transferAmount, "") + ) + ) + ) + ); + _nameOutput( + "EtherWithPayloadToENSDelegateCallVoucher", + _addOutput( + _encodeDelegateCallVoucher( + address(_assetTransferToENS), + abi.encodeCall( + AssetTransferToENS.sendEtherToENS, + ( + _ensNode, + _transferAmount, + abi.encodeCall(EtherReceiver.mint, ()) + ) + ) + ) + ) + ); + _nameOutput( + "ERC20ToENSDelegateCallVoucher", + _addOutput( + _encodeDelegateCallVoucher( + address(_assetTransferToENS), + abi.encodeCall( + AssetTransferToENS.sendERC20ToENS, + (_erc20Token, _ensNode, _transferAmount) + ) + ) + ) + ); + _nameOutput( + "ERC721ToENSDelegateCallVoucher", + _addOutput( + _encodeDelegateCallVoucher( + address(_assetTransferToENS), + abi.encodeCall( + AssetTransferToENS.sendERC721ToENS, + (_erc721Token, _ensNode, _tokenId, "") + ) + ) + ) + ); + _nameOutput( + "ERC1155SingleToENSDelegateCallVoucher", + _addOutput( + _encodeDelegateCallVoucher( + address(_assetTransferToENS), + abi.encodeCall( + AssetTransferToENS.sendERC1155ToENS, + ( + _erc1155SingleToken, + _ensNode, + _tokenId, + _transferAmount, + "" + ) + ) + ) + ) + ); + _nameOutput( + "ERC1155BatchToENSDelegateCallVoucher", + _addOutput( + _encodeDelegateCallVoucher( + address(_assetTransferToENS), + abi.encodeCall( + AssetTransferToENS.sendBatchERC1155ToENS, + ( + _erc1155BatchToken, + _ensNode, + _tokenIds, + _transferAmounts, + "" + ) + ) + ) + ) + ); + _finishInput(); + _finishEpoch(); + + // Test input with no outputs + _finishInput(); + _finishEpoch(); + } + + function _encodeNotice( bytes memory payload ) internal pure returns (bytes memory) { return abi.encodeCall(Outputs.Notice, (payload)); @@ -752,4 +871,233 @@ contract ApplicationTest is ERC165Test { proof.outputIndexWithinInput ); } + + function _mockENS() internal { + vm.mockCall( + address(_ens), + abi.encodeCall(ENS.resolver, (_ensNode)), + abi.encode(_resolver) + ); + vm.mockCall( + address(_resolver), + abi.encodeWithSignature("addr(bytes32)", (_ensNode)), + abi.encode(_recipient) + ); + } + + function assertEq(IPortal[] memory a, IPortal[] memory b) internal pure { + assertEq(a.length, b.length); + for (uint256 i; i < a.length; ++i) { + assertEq(address(a[i]), address(b[i])); + } + } + + function _testEtherTransfer( + bytes memory output, + OutputValidityProof memory proof + ) internal { + assertLt( + address(_appContract).balance, + _transferAmount, + "Application contract does not have enough Ether" + ); + + vm.expectRevert(); + _appContract.executeOutput(output, proof); + + vm.deal(address(_appContract), _transferAmount); + + uint256 recipientBalance = _recipient.balance; + uint256 appBalance = address(_appContract).balance; + + _expectEmitOutputExecuted(output, proof); + + _appContract.executeOutput(output, proof); + + assertEq( + _recipient.balance, + recipientBalance + _transferAmount, + "Recipient should have received the transfer amount" + ); + + assertEq( + address(_appContract).balance, + appBalance - _transferAmount, + "Application contract should have the transfer amount deducted" + ); + + assertTrue( + _wasOutputExecuted(proof), + "Output should be marked as executed" + ); + + _expectRevertOutputNotReexecutable(output); + _appContract.executeOutput(output, proof); + } + + function _testEtherMint( + bytes memory output, + OutputValidityProof memory proof + ) internal { + assertLt( + address(_appContract).balance, + _transferAmount, + "Application contract does not have enough Ether" + ); + + vm.expectRevert(); + _appContract.executeOutput(output, proof); + + vm.deal(address(_appContract), _transferAmount); + + uint256 recipientBalance = address(_etherReceiver).balance; + uint256 appBalance = address(_appContract).balance; + uint256 balanceOf = _etherReceiver.balanceOf(address(_appContract)); + + _expectEmitOutputExecuted(output, proof); + _appContract.executeOutput(output, proof); + + assertEq( + address(_etherReceiver).balance, + recipientBalance + _transferAmount, + "Recipient should have received the transfer amount" + ); + + assertEq( + address(_appContract).balance, + appBalance - _transferAmount, + "Application contract should have the transfer amount deducted" + ); + + assertEq( + _etherReceiver.balanceOf(address(_appContract)), + balanceOf + _transferAmount, + "Application contract should have the transfer amount minted" + ); + + assertTrue( + _wasOutputExecuted(proof), + "Output should be marked as executed" + ); + + _expectRevertOutputNotReexecutable(output); + _appContract.executeOutput(output, proof); + } + + function _testERC721Transfer( + bytes memory output, + OutputValidityProof memory proof + ) internal { + assertEq( + _erc721Token.ownerOf(_tokenId), + _tokenOwner, + "The NFT is initially owned by `_tokenOwner`" + ); + + vm.expectRevert( + abi.encodeWithSelector( + IERC721Errors.ERC721InsufficientApproval.selector, + address(_appContract), + _tokenId + ) + ); + _appContract.executeOutput(output, proof); + + vm.prank(_tokenOwner); + _erc721Token.safeTransferFrom( + _tokenOwner, + address(_appContract), + _tokenId + ); + + _expectEmitOutputExecuted(output, proof); + _appContract.executeOutput(output, proof); + + assertEq( + _erc721Token.ownerOf(_tokenId), + _recipient, + "The NFT is then transferred to the recipient" + ); + + assertTrue( + _wasOutputExecuted(proof), + "Output should be marked as executed" + ); + + _expectRevertOutputNotReexecutable(output); + _appContract.executeOutput(output, proof); + } + + function _testERC20Fail( + bytes memory output, + OutputValidityProof memory proof + ) internal { + // test revert + + assertLt( + _erc20Token.balanceOf(address(_appContract)), + _transferAmount, + "Application contract does not have enough ERC-20 tokens" + ); + + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, + address(_appContract), + _erc20Token.balanceOf(address(_appContract)), + _transferAmount + ) + ); + _appContract.executeOutput(output, proof); + + // test return false + + vm.mockCall( + address(_erc20Token), + abi.encodeCall(_erc20Token.transfer, (_recipient, _transferAmount)), + abi.encode(false) + ); + vm.expectRevert( + abi.encodeWithSelector( + SafeERC20.SafeERC20FailedOperation.selector, + address(_erc20Token) + ) + ); + _appContract.executeOutput(output, proof); + vm.clearMockedCalls(); + } + + function _testERC20Success( + bytes memory output, + OutputValidityProof memory proof + ) internal { + vm.prank(_tokenOwner); + _erc20Token.transfer(address(_appContract), _transferAmount); + + uint256 recipientBalance = _erc20Token.balanceOf(address(_recipient)); + uint256 appBalance = _erc20Token.balanceOf(address(_appContract)); + + _expectEmitOutputExecuted(output, proof); + _appContract.executeOutput(output, proof); + + assertEq( + _erc20Token.balanceOf(address(_recipient)), + recipientBalance + _transferAmount, + "Recipient should have received the transfer amount" + ); + + assertEq( + _erc20Token.balanceOf(address(_appContract)), + appBalance - _transferAmount, + "Application contract should have the transfer amount deducted" + ); + + assertTrue( + _wasOutputExecuted(proof), + "Output should be marked as executed" + ); + + _expectRevertOutputNotReexecutable(output); + _appContract.executeOutput(output, proof); + } } diff --git a/test/foundry/util/SimpleERC1155.sol b/test/foundry/util/SimpleERC1155.sol new file mode 100644 index 00000000..b5c822f7 --- /dev/null +++ b/test/foundry/util/SimpleERC1155.sol @@ -0,0 +1,27 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +/// @title A Simple ERC-20 Contract +pragma solidity ^0.8.22; + +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +contract SimpleSingleERC1155 is ERC1155 { + constructor( + address tokenOwner, + uint256 tokenId, + uint256 supply + ) ERC1155("SimpleSingleERC1155") { + _mint(tokenOwner, tokenId, supply, ""); + } +} + +contract SimpleBatchERC1155 is ERC1155 { + constructor( + address tokenOwner, + uint256[] memory tokenIds, + uint256[] memory supplies + ) ERC1155("SimpleBatchERC1155") { + _mintBatch(tokenOwner, tokenIds, supplies, ""); + } +}