From 5559379f172eb8a1a5da39195e0cd33e7a6a3b10 Mon Sep 17 00:00:00 2001 From: Zehui Zheng Date: Wed, 24 Apr 2024 18:36:12 +0800 Subject: [PATCH] feat: safe erc20 delegatecall --- .changeset/lucky-elephants-play.md | 5 + contracts/delegatecall/SafeERC20Transfer.sol | 15 +++ test/foundry/dapp/Application.t.sol | 112 +++++++++++++++++-- 3 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 .changeset/lucky-elephants-play.md create mode 100644 contracts/delegatecall/SafeERC20Transfer.sol diff --git a/.changeset/lucky-elephants-play.md b/.changeset/lucky-elephants-play.md new file mode 100644 index 00000000..6b5569ae --- /dev/null +++ b/.changeset/lucky-elephants-play.md @@ -0,0 +1,5 @@ +--- +"@cartesi/rollups": minor +--- + +Add a contract for safe ERC20 transfers. This can be used by delegatecall vouchers. diff --git a/contracts/delegatecall/SafeERC20Transfer.sol b/contracts/delegatecall/SafeERC20Transfer.sol new file mode 100644 index 00000000..2832c7e9 --- /dev/null +++ b/contracts/delegatecall/SafeERC20Transfer.sol @@ -0,0 +1,15 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.20; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract SafeERC20Transfer { + using SafeERC20 for IERC20; + + function safeTransfer(IERC20 token, address to, uint256 value) external { + token.safeTransfer(to, value); + } +} diff --git a/test/foundry/dapp/Application.t.sol b/test/foundry/dapp/Application.t.sol index 1595646e..d0bee282 100644 --- a/test/foundry/dapp/Application.t.sol +++ b/test/foundry/dapp/Application.t.sol @@ -19,9 +19,11 @@ import {InputBox} from "contracts/inputs/InputBox.sol"; 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 {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 {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; @@ -35,8 +37,6 @@ import {SimpleERC20} from "../util/SimpleERC20.sol"; import {SimpleERC721} from "../util/SimpleERC721.sol"; import {ExternalLibMerkle32} from "../library/LibMerkle32.t.sol"; -import "forge-std/console.sol"; - contract ApplicationTest is ERC165Test { using LibEmulator for LibEmulator.State; using ExternalLibMerkle32 for bytes32[]; @@ -48,6 +48,7 @@ contract ApplicationTest is ERC165Test { IERC721 _erc721Token; IInputBox _inputBox; IPortal[] _portals; + SafeERC20Transfer _safeERC20Transfer; LibEmulator.State _emulator; address _appOwner; address _authorityOwner; @@ -302,8 +303,8 @@ contract ApplicationTest is ERC165Test { _appContract.executeOutput(output, proof); } - function testExecuteErc20TransferVoucher() external { - string memory name = "Erc20TransferVoucher"; + function testExecuteERC20TransferVoucher() external { + string memory name = "ERC20TransferVoucher"; bytes memory output = _getOutput(name); OutputValidityProof memory proof = _getProof(name); @@ -353,8 +354,8 @@ contract ApplicationTest is ERC165Test { _appContract.executeOutput(output, proof); } - function testExecuteErc721TransferVoucher() external { - string memory name = "Erc721TransferVoucher"; + function testExecuteERC721TransferVoucher() external { + string memory name = "ERC721TransferVoucher"; bytes memory output = _getOutput(name); OutputValidityProof memory proof = _getProof(name); @@ -425,6 +426,77 @@ contract ApplicationTest is ERC165Test { _appContract.executeOutput(output, proof); } + function testExecuteERC20TransferDelegateCallVoucher() 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" + ); + + // test revert + + 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(); + + // test success + + 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); + } + // ------- // ERC-165 // ------- @@ -474,6 +546,7 @@ contract ApplicationTest is ERC165Test { _appOwner, _templateHash ); + _safeERC20Transfer = new SafeERC20Transfer(); } function _addOutputs() internal { @@ -511,7 +584,7 @@ contract ApplicationTest is ERC165Test { _finishEpoch(); _nameOutput( - "Erc20TransferVoucher", + "ERC20TransferVoucher", _addOutput( _encodeVoucher( address(_erc20Token), @@ -524,7 +597,7 @@ contract ApplicationTest is ERC165Test { ) ); _nameOutput( - "Erc721TransferVoucher", + "ERC721TransferVoucher", _addOutput( _encodeVoucher( address(_erc721Token), @@ -541,6 +614,21 @@ contract ApplicationTest is ERC165Test { _finishInput(); _finishEpoch(); + _nameOutput( + "ERC20DelegateCallVoucher", + _addOutput( + _encodeDelegateCallVoucher( + address(_safeERC20Transfer), + abi.encodeCall( + SafeERC20Transfer.safeTransfer, + (_erc20Token, _recipient, _transferAmount) + ) + ) + ) + ); + _finishInput(); + _finishEpoch(); + // Test input with no outputs _finishInput(); _finishEpoch(); @@ -560,6 +648,14 @@ contract ApplicationTest is ERC165Test { return abi.encodeCall(Outputs.Voucher, (destination, value, payload)); } + function _encodeDelegateCallVoucher( + address destination, + bytes memory payload + ) internal pure returns (bytes memory) { + return + abi.encodeCall(Outputs.DelegateCallVoucher, (destination, payload)); + } + function _addOutput( bytes memory output ) internal returns (LibEmulator.OutputId memory) {