diff --git a/packages/protocol/contracts/shared/bridge/EtherBridgeWrapper.sol b/packages/protocol/contracts/shared/bridge/EtherBridgeWrapper.sol deleted file mode 100644 index 224523b2017..00000000000 --- a/packages/protocol/contracts/shared/bridge/EtherBridgeWrapper.sol +++ /dev/null @@ -1,235 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "./Bridge.sol"; -import "../../shared/based/ITaiko.sol"; -import "src/layer1/based/ITaikoInbox.sol"; -import "src/shared/libs/LibAddress.sol"; -import "src/shared/common/EssentialContract.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; - -contract EtherBridgeWrapper is EssentialContract { - using Address for address; - using LibAddress for address; - - /// @dev Represents an operation to send Ether to another chain. - struct EtherBridgeOp { - // Destination chain ID. - uint64 destChainId; - // The owner of the bridge message on the destination chain. - address destOwner; - // Recipient address. - address to; - // Processing fee for the relayer. - uint64 fee; - // Gas limit for the operation. - uint32 gasLimit; - // Amount of Ether to be sent. - uint256 amount; - // Added solver fee - uint256 solverFee; - } - - /// @dev Represents an operation to solve an Ether bridging intent - struct SolverOp { - uint256 nonce; - address to; - uint256 amount; - // Fields for L2 batch verification - uint64 l2BatchId; - bytes32 l2BatchMetaHash; - } - - /// @notice Emitted when Ether is sent to another chain. - event EtherSent( - bytes32 indexed msgHash, - address indexed from, - address indexed to, - uint256 amount, - uint256 solverFee - ); - - /// @notice Emitted when Ether is received from another chain. - event EtherReceived( - bytes32 indexed msgHash, - address indexed from, - address indexed to, - address solver, - uint64 srcChainId, - uint256 amount, - uint256 solverFee - ); - - /// @notice Emitted when a bridging intent is solved - event EtherSolved(bytes32 indexed solverCondition, address solver); - - error InvalidAmount(); - error InsufficientValue(); - error EtherBridgePermissionDenied(); - error EtherBridgeInvalidToAddr(); - error VaultNotOnL1(); - error VaultMetahashMismatch(); - error VaultAlreadySolved(); - - /// @notice Mapping from solver condition to the address of solver - mapping(bytes32 solverCondition => address solver) public solverConditionToSolver; - - /// @notice Initializes the contract. - /// @param _owner The owner of this contract. msg.sender will be used if this value is zero. - /// @param _sharedResolver The {IResolver} used by multipel rollups. - function init(address _owner, address _sharedResolver) external initializer { - __Essential_init(_owner, _sharedResolver); - } - - /// @notice Sends Ether to another chain. - /// @param _op Options for sending Ether. - /// @return message_ The constructed message. - function sendToken(EtherBridgeOp calldata _op) - external - payable - whenNotPaused - nonReentrant - returns (IBridge.Message memory message_) - { - if (_op.amount == 0) revert InvalidAmount(); - if (msg.value < _op.amount + _op.fee + _op.solverFee) revert InsufficientValue(); - - address bridge = resolve(LibStrings.B_BRIDGE, false); - - // Generate solver condition if solver fee is specified - bytes32 solverCondition; - if (_op.solverFee > 0) { - uint256 _nonce = IBridge(bridge).nextMessageId(); - solverCondition = getSolverCondition(_nonce, _op.to, _op.amount); - } - - bytes memory data = abi.encodeCall( - this.onMessageInvocation, - abi.encode(msg.sender, _op.to, _op.amount, _op.solverFee, solverCondition) - ); - - IBridge.Message memory message = IBridge.Message({ - id: 0, // will receive a new value - from: address(0), // will receive a new value - srcChainId: 0, // will receive a new value - destChainId: _op.destChainId, - srcOwner: msg.sender, - destOwner: _op.destOwner != address(0) ? _op.destOwner : msg.sender, - to: resolve(_op.destChainId, name(), false), - value: _op.amount + _op.solverFee, - fee: _op.fee, - gasLimit: _op.gasLimit, - data: data - }); - - bytes32 msgHash; - (msgHash, message_) = IBridge(bridge).sendMessage{ value: msg.value }(message); - - emit EtherSent({ - msgHash: msgHash, - from: message_.srcOwner, - to: _op.to, - amount: _op.amount, - solverFee: _op.solverFee - }); - } - - /// @notice Handles incoming Ether bridge messages. - /// @param _data The encoded message data. - function onMessageInvocation(bytes calldata _data) - external - payable - whenNotPaused - nonReentrant - { - // `onlyFromBridge` checked in checkProcessMessageContext - IBridge.Context memory ctx = checkProcessMessageContext(); - - (address from, address to, uint256 amount, uint256 solverFee, bytes32 solverCondition) = - abi.decode(_data, (address, address, uint256, uint256, bytes32)); - - // Don't allow sending to disallowed addresses - checkToAddress(to); - - address recipient = to; - - // If the bridging intent has been solved, the solver becomes the recipient - address solver = solverConditionToSolver[solverCondition]; - if (solver != address(0)) { - recipient = solver; - delete solverConditionToSolver[solverCondition]; - } - - // Transfer Ether to recipient - recipient.sendEtherAndVerify(amount + solverFee); - - emit EtherReceived({ - msgHash: ctx.msgHash, - from: from, - to: to, - solver: solver, - srcChainId: ctx.srcChainId, - amount: amount, - solverFee: solverFee - }); - } - - /// @notice Lets a solver fulfil a bridging intent by transferring Ether to the recipient. - /// @param _op Parameters for the solve operation - function solve(SolverOp memory _op) external payable nonReentrant whenNotPaused { - if (_op.l2BatchMetaHash != 0) { - // Verify that the required L2 batch containing the intent transaction has been proposed - address taiko = resolve(LibStrings.B_TAIKO, false); - if (!ITaiko(taiko).isOnL1()) revert VaultNotOnL1(); - - bytes32 l2BatchMetaHash = ITaikoInbox(taiko).getBatch(_op.l2BatchId).metaHash; - if (l2BatchMetaHash != _op.l2BatchMetaHash) revert VaultMetahashMismatch(); - } - - // Record the solver's address - bytes32 solverCondition = getSolverCondition(_op.nonce, _op.to, _op.amount); - if (solverConditionToSolver[solverCondition] != address(0)) revert VaultAlreadySolved(); - solverConditionToSolver[solverCondition] = msg.sender; - - // Transfer the Ether to the recipient - _op.to.sendEtherAndVerify(_op.amount); - - emit EtherSolved(solverCondition, msg.sender); - } - - /// @notice Returns the solver condition for a bridging intent - /// @param _nonce Unique numeric value to prevent nonce collision - /// @param _to Recipient on destination chain - /// @param _amount Amount of Ether expected by the recipient - /// @return solver condition - function getSolverCondition( - uint256 _nonce, - address _to, - uint256 _amount - ) - public - pure - returns (bytes32) - { - return keccak256(abi.encodePacked(_nonce, _to, _amount)); - } - - function checkProcessMessageContext() - internal - view - onlyFromNamed(LibStrings.B_BRIDGE) - returns (IBridge.Context memory ctx_) - { - ctx_ = IBridge(msg.sender).context(); - address selfOnSourceChain = resolve(ctx_.srcChainId, name(), false); - if (ctx_.from != selfOnSourceChain) revert EtherBridgePermissionDenied(); - } - - function checkToAddress(address _to) internal view { - if (_to == address(0) || _to == address(this)) revert EtherBridgeInvalidToAddr(); - } - - function name() public pure returns (bytes32) { - return LibStrings.B_ETHER_BRIDGE_WRAPPER; - } -} diff --git a/packages/protocol/contracts/shared/libs/LibStrings.sol b/packages/protocol/contracts/shared/libs/LibStrings.sol index 90f99eda9e2..7f119da566f 100644 --- a/packages/protocol/contracts/shared/libs/LibStrings.sol +++ b/packages/protocol/contracts/shared/libs/LibStrings.sol @@ -10,7 +10,6 @@ library LibStrings { bytes32 internal constant B_BRIDGE_WATCHDOG = bytes32("bridge_watchdog"); bytes32 internal constant B_BRIDGED_ERC1155 = bytes32("bridged_erc1155"); bytes32 internal constant B_BRIDGED_ERC20 = bytes32("bridged_erc20"); - bytes32 internal constant B_ETHER_BRIDGE_WRAPPER = bytes32("ether_bridge_wrapper"); bytes32 internal constant B_BRIDGED_ERC721 = bytes32("bridged_erc721"); bytes32 internal constant B_CHAIN_WATCHDOG = bytes32("chain_watchdog"); bytes32 internal constant B_ERC1155_VAULT = bytes32("erc1155_vault"); diff --git a/packages/protocol/contracts/shared/tokenvault/ERC20Vault.sol b/packages/protocol/contracts/shared/tokenvault/ERC20Vault.sol index fec4e67afaa..9150c4e54f5 100644 --- a/packages/protocol/contracts/shared/tokenvault/ERC20Vault.sol +++ b/packages/protocol/contracts/shared/tokenvault/ERC20Vault.sol @@ -184,6 +184,7 @@ contract ERC20Vault is BaseVault { /// @param solver The address of the solver event ERC20Solved(bytes32 indexed solverCondition, address solver); + error VAULT_INSUFFICIENT_ETHER(); error VAULT_ALREADY_SOLVED(); error VAULT_BTOKEN_BLACKLISTED(); error VAULT_CTOKEN_MISMATCH(); @@ -272,10 +273,9 @@ contract ERC20Vault is BaseVault { }); } - /// @notice Transfers ERC20 tokens to this vault and sends a message to the - /// destination chain so the user can receive the same amount of tokens by - /// invoking the message call. - /// @param _op Option for sending ERC20 tokens. + /// @notice Transfers ERC20 tokens or Ether to this vault and sends a message to the + /// destination chain so the user can receive the same amount by invoking the message call. + /// @param _op Option for sending tokens/ether. /// @return message_ The constructed message. function sendToken(BridgeTransferOp calldata _op) external @@ -285,9 +285,12 @@ contract ERC20Vault is BaseVault { returns (IBridge.Message memory message_) { if (_op.amount == 0) revert VAULT_INVALID_AMOUNT(); - if (_op.token == address(0)) revert VAULT_INVALID_TOKEN(); - if (btokenDenylist[_op.token]) revert VAULT_BTOKEN_BLACKLISTED(); - if (msg.value < _op.fee) revert VAULT_INSUFFICIENT_FEE(); + if (msg.value < _op.fee + (_op.token == address(0) ? _op.amount + _op.solverFee : 0)) { + revert VAULT_INSUFFICIENT_ETHER(); + } + if (_op.token != address(0) && btokenDenylist[_op.token]) { + revert VAULT_BTOKEN_BLACKLISTED(); + } address bridge = resolve(LibStrings.B_BRIDGE, false); @@ -355,8 +358,14 @@ contract ERC20Vault is BaseVault { delete solverConditionToSolver[solverCondition]; } - address token = _transferTokens(ctoken, tokenRecipient, amount + solverFee); - to.sendEtherAndVerify(msg.value); + address token; + { + uint256 amountToTransfer = amount + solverFee; + token = _transferTokens(ctoken, tokenRecipient, amountToTransfer); + to.sendEtherAndVerify( + ctoken.addr == address(0) ? msg.value - amountToTransfer : msg.value + ); + } emit TokenReceived({ msgHash: ctx.msgHash, @@ -390,8 +399,11 @@ contract ERC20Vault is BaseVault { abi.decode(data, (CanonicalERC20, address, address, uint256, uint256, bytes32)); // Transfer the ETH and tokens back to the owner - address token = _transferTokens(ctoken, _message.srcOwner, amount + solverFee); - _message.srcOwner.sendEtherAndVerify(_message.value); + uint256 amountToReturn = amount + solverFee; + address token = _transferTokens(ctoken, _message.srcOwner, amountToReturn); + _message.srcOwner.sendEtherAndVerify( + ctoken.addr == address(0) ? _message.value - amountToReturn : _message.value + ); emit TokenReleased({ msgHash: _msgHash, @@ -402,36 +414,42 @@ contract ERC20Vault is BaseVault { }); } - /// @notice Lets a solver fulfil a bridging intent by transferring the bridged token amount - // to the recipient. + /// @notice Lets a solver fulfil a bridging intent by transferring tokens/ether to the + /// recipient. /// @param _op Parameters for the solve operation - function solve(SolverOp memory _op) external nonReentrant whenNotPaused { + function solve(SolverOp memory _op) external payable nonReentrant whenNotPaused { if (_op.l2BatchMetaHash != 0) { // Verify that the required L2 batch containing the intent transaction has been proposed address taiko = resolve(LibStrings.B_TAIKO, false); - require(ITaiko(taiko).isOnL1(), VAULT_NOT_ON_L1()); + if (!ITaiko(taiko).isOnL1()) revert VAULT_NOT_ON_L1(); bytes32 l2BatchMetaHash = ITaikoInbox(taiko).getBatch(_op.l2BatchId).metaHash; - require(l2BatchMetaHash == _op.l2BatchMetaHash, VAULT_METAHASH_MISMATCH()); + if (l2BatchMetaHash != _op.l2BatchMetaHash) revert VAULT_METAHASH_MISMATCH(); } // Record the solver's address bytes32 solverCondition = getSolverCondition(_op.nonce, _op.token, _op.to, _op.amount); - require(solverConditionToSolver[solverCondition] == address(0), VAULT_ALREADY_SOLVED()); - + if (solverConditionToSolver[solverCondition] != address(0)) revert VAULT_ALREADY_SOLVED(); solverConditionToSolver[solverCondition] = msg.sender; - // Transfer the amount to the recipient - IERC20(_op.token).transferFrom(msg.sender, _op.to, _op.amount); + // Handle transfer based on token type + if (_op.token == address(0)) { + // For Ether transfers + if (msg.value != _op.amount) revert VAULT_INVALID_AMOUNT(); + _op.to.sendEtherAndVerify(_op.amount); + } else { + // For ERC20 tokens + IERC20(_op.token).safeTransferFrom(msg.sender, _op.to, _op.amount); + } emit ERC20Solved(solverCondition, msg.sender); } /// @notice Returns the solver condition for a bridging intent /// @param _nonce Unique numeric value to prevent nonce collision - /// @param _token Address of the ERC20 token on destination chain - /// @param _amount Amount of tokens expected by the recipient + /// @param _token Token address (address(0) for Ether) /// @param _to Recipient on destination chain + /// @param _amount Amount of tokens/ether expected by the recipient /// @return solver condition function getSolverCondition( uint256 _nonce, @@ -459,16 +477,21 @@ contract ERC20Vault is BaseVault { private returns (address token_) { - if (_ctoken.chainId == block.chainid) { + if (_ctoken.addr == address(0)) { + // Handle Ether transfer + _to.sendEtherAndVerify(_amount); + token_ = address(0); + } else if (_ctoken.chainId == block.chainid) { token_ = _ctoken.addr; IERC20(token_).safeTransfer(_to, _amount); } else { token_ = _getOrDeployBridgedToken(_ctoken); - //For native bridged tokens (like USDC), the mint() signature is the same, so no need to - // check. IBridgedERC20(token_).mint(_to, _amount); } - _consumeTokenQuota(token_, _amount); + + if (token_ != address(0)) { + _consumeTokenQuota(token_, _amount); + } } /// @dev Handles the message on the source chain and returns the encoded @@ -495,20 +518,20 @@ contract ERC20Vault is BaseVault { uint256 balanceChangeSolverFee_ ) { - // An identifier hash for the solver condition on destination chain bytes32 solverCondition; - // If it's a bridged token - CanonicalERC20 storage _ctoken = bridgedToCanonical[_op.token]; - if (_ctoken.addr != address(0)) { - ctoken_ = _ctoken; - // Following the "transfer and burn" pattern, as used by USDC + if (_op.token == address(0)) { + balanceChangeAmount_ = _op.amount; + balanceChangeSolverFee_ = _op.solverFee; + } else if (bridgedToCanonical[_op.token].addr != address(0)) { + // Handle bridged token + ctoken_ = bridgedToCanonical[_op.token]; IERC20(_op.token).safeTransferFrom(msg.sender, address(this), _op.amount); IBridgedERC20(_op.token).burn(_op.amount); balanceChangeAmount_ = _op.amount; balanceChangeSolverFee_ = _op.solverFee; } else { - // If it's a canonical token + // Handle canonical token ctoken_ = CanonicalERC20({ chainId: uint64(block.chainid), addr: _op.token, @@ -517,18 +540,14 @@ contract ERC20Vault is BaseVault { name: safeName(_op.token) }); - // Query the balance then query it again to get the actual amount of - // token transferred into this address, this is more accurate than - // simply using `amount` -- some contract may deduct a fee from the - // transferred amount. balanceChangeAmount_ = _transferTokenAndReturnBalanceDiff(_op.token, _op.amount); balanceChangeSolverFee_ = _transferTokenAndReturnBalanceDiff(_op.token, _op.solverFee); } - // Prepare solver condition for allowing fast withdrawal on L1 + // Prepare solver condition if (_op.solverFee > 0) { uint256 _nonce = IBridge(_bridge).nextMessageId(); - solverCondition = getSolverCondition(_nonce, _ctoken.addr, _op.to, balanceChangeAmount_); + solverCondition = getSolverCondition(_nonce, ctoken_.addr, _op.to, balanceChangeAmount_); } msgData_ = abi.encodeCall( diff --git a/packages/protocol/test/shared/CommonTest.sol b/packages/protocol/test/shared/CommonTest.sol index 990fccefaeb..a53a072a930 100644 --- a/packages/protocol/test/shared/CommonTest.sol +++ b/packages/protocol/test/shared/CommonTest.sol @@ -20,7 +20,6 @@ import "src/shared/tokenvault/ERC20Vault.sol"; import "src/shared/tokenvault/ERC721Vault.sol"; import "src/shared/tokenvault/ERC1155Vault.sol"; import "src/shared/bridge/Bridge.sol"; -import "src/shared/bridge/EtherBridgeWrapper.sol"; import "src/shared/bridge/QuotaManager.sol"; import "src/layer1/token/TaikoToken.sol"; import "test/shared/helpers/SignalService_WithoutProofVerification.sol"; @@ -209,16 +208,6 @@ abstract contract CommonTest is Test, Script { ); } - function deployEtherBridgeWrapper() internal returns (EtherBridgeWrapper) { - return EtherBridgeWrapper( - deploy({ - name: "ether_bridge_wrapper", - impl: address(new EtherBridgeWrapper()), - data: abi.encodeCall(EtherBridgeWrapper.init, (address(0), address(resolver))) - }) - ); - } - function deployQuotaManager() internal returns (QuotaManager) { return QuotaManager( deploy({ diff --git a/packages/protocol/test/shared/bridge/EtherBridgeWrapper.h.sol b/packages/protocol/test/shared/bridge/EtherBridgeWrapper.h.sol deleted file mode 100644 index a353e3700c9..00000000000 --- a/packages/protocol/test/shared/bridge/EtherBridgeWrapper.h.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "../CommonTest.sol"; -import "src/shared/bridge/EtherBridgeWrapper.sol"; -import "src/layer1/based/ITaikoInbox.sol"; - -contract PrankTaikoInbox { - ITaikoInbox.Batch internal batch; - - function setBatch(ITaikoInbox.Batch memory _batch) external { - batch = _batch; - } - - function getBatch(uint64) external view returns (ITaikoInbox.Batch memory) { - return batch; - } - - function isOnL1() external pure returns (bool) { - return true; - } -} - -contract PrankDestBridge { - EtherBridgeWrapper destWrapper; - TContext ctx; - - struct TContext { - bytes32 msgHash; - address sender; - uint64 srcChainId; - } - - constructor(EtherBridgeWrapper _wrapper) { - destWrapper = _wrapper; - } - - function context() public view returns (TContext memory) { - return ctx; - } - - function sendReceiveEtherToWrapper( - address from, - address to, - uint256 amount, - uint256 solverFee, - bytes32 solverCondition, - bytes32 msgHash, - address srcWrapper, - uint64 srcChainId, - uint256 mockLibInvokeMsgValue - ) - public - { - ctx.sender = srcWrapper; - ctx.msgHash = msgHash; - ctx.srcChainId = srcChainId; - - destWrapper.onMessageInvocation{ value: mockLibInvokeMsgValue }( - abi.encode(from, to, amount, solverFee, solverCondition) - ); - - ctx.sender = address(0); - ctx.msgHash = bytes32(0); - ctx.srcChainId = 0; - } -} diff --git a/packages/protocol/test/shared/bridge/EtherBridgeWrapper.t.sol b/packages/protocol/test/shared/bridge/EtherBridgeWrapper.t.sol deleted file mode 100644 index 89f5ba5a568..00000000000 --- a/packages/protocol/test/shared/bridge/EtherBridgeWrapper.t.sol +++ /dev/null @@ -1,256 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import "../CommonTest.sol"; -import "./EtherBridgeWrapper.h.sol"; - -contract TestEtherBridgeWrapper is CommonTest { - // Contracts on Ethereum - SignalService private eSignalService; - PrankDestBridge private eBridge; - PrankTaikoInbox private taikoInbox; - EtherBridgeWrapper private eWrapper; - - // Contracts on Taiko - SignalService private tSignalService; - Bridge private tBridge; - EtherBridgeWrapper private tWrapper; - - function setUpOnEthereum() internal override { - eSignalService = deploySignalService(address(new SignalService_WithoutProofVerification())); - eWrapper = deployEtherBridgeWrapper(); - eBridge = new PrankDestBridge(eWrapper); - taikoInbox = new PrankTaikoInbox(); - - register("bridge", address(eBridge)); - register("taiko", address(taikoInbox)); - - vm.deal(address(eBridge), 100 ether); - vm.deal(David, 100 ether); - } - - function setUpOnTaiko() internal override { - tSignalService = deploySignalService(address(new SignalService_WithoutProofVerification())); - tBridge = deployBridge(address(new Bridge())); - tWrapper = deployEtherBridgeWrapper(); - - register("bridge", address(tBridge)); - - vm.deal(Alice, 100 ether); - vm.deal(Bob, 100 ether); - } - - function test_wrapper_send_ether_revert_if_insufficient_value() public { - vm.chainId(taikoChainId); - vm.startPrank(Alice); - vm.expectRevert(EtherBridgeWrapper.InsufficientValue.selector); - tWrapper.sendToken{ value: 0.9 ether }( - EtherBridgeWrapper.EtherBridgeOp( - ethereumChainId, - address(0), - Bob, - 0.1 ether, // fee - 1_000_000, - 1 ether, // amount - 0 // solverFee - ) - ); - } - - function test_wrapper_send_ether_no_processing_fee() public { - vm.chainId(taikoChainId); - vm.startPrank(Alice); - - uint256 amount = 1 ether; - uint256 aliceBalanceBefore = Alice.balance; - uint256 wrapperBalanceBefore = address(tWrapper).balance; - - IBridge.Message memory message = tWrapper.sendToken{ value: amount }( - EtherBridgeWrapper.EtherBridgeOp( - ethereumChainId, - address(0), - Bob, - 0, // fee - 1_000_000, - amount, - 0 // solverFee - ) - ); - - uint256 aliceBalanceAfter = Alice.balance; - uint256 wrapperBalanceAfter = address(tWrapper).balance; - - assertEq(aliceBalanceBefore - aliceBalanceAfter, amount); - assertEq(wrapperBalanceAfter - wrapperBalanceBefore, 0); - assertEq(message.value, amount); - } - - function test_wrapper_send_ether_with_processing_fee() public { - vm.chainId(taikoChainId); - vm.startPrank(Alice); - - uint256 amount = 1 ether; - uint256 fee = 0.1 ether; - uint256 aliceBalanceBefore = Alice.balance; - - IBridge.Message memory message = tWrapper.sendToken{ value: amount + fee }( - EtherBridgeWrapper.EtherBridgeOp( - ethereumChainId, - address(0), - Bob, - uint64(fee), - 1_000_000, - amount, - 0 // solverFee - ) - ); - - uint256 aliceBalanceAfter = Alice.balance; - assertEq(aliceBalanceBefore - aliceBalanceAfter, amount + fee); - assertEq(message.value, amount); - assertEq(message.fee, fee); - } - - function test_wrapper_send_ether_reverts_invalid_amount() public { - vm.startPrank(Alice); - - vm.expectRevert(EtherBridgeWrapper.InvalidAmount.selector); - tWrapper.sendToken( - EtherBridgeWrapper.EtherBridgeOp( - ethereumChainId, - address(0), - Bob, - 0, - 1_000_000, - 0, // amount = 0 - 0 - ) - ); - } - - function test_wrapper_receive_ether() public { - vm.startPrank(Alice); - vm.chainId(ethereumChainId); - - uint256 amount = 1 ether; - address to = Bob; - uint256 toBalanceBefore = to.balance; - - eBridge.sendReceiveEtherToWrapper( - Alice, - to, - amount, - 0, // solverFee - bytes32(0), // solverCondition - bytes32(0), // msgHash - address(tWrapper), - taikoChainId, - amount // mockLibInvokeMsgValue - ); - - uint256 toBalanceAfter = to.balance; - assertEq(toBalanceAfter - toBalanceBefore, amount); - } - - function test_wrapper_receive_ether_solved() public { - vm.chainId(ethereumChainId); - - uint256 amount = 1 ether; - uint256 solverFee = 0.1 ether; - address to = Bob; - address solver = David; - bytes32 solverCondition = tWrapper.getSolverCondition(1, to, amount); - - vm.deal(solver, 10 ether); - vm.startPrank(solver); - - uint256 solverBalanceBefore = solver.balance; - uint256 toBalanceBefore = to.balance; - - { - uint64 l2BatchId = 1; - bytes32 l2BatchMetaHash = bytes32("metahash"); - - ITaikoInbox.Batch memory batch; - batch.metaHash = l2BatchMetaHash; - taikoInbox.setBatch(batch); - - eWrapper.solve{ value: amount }( - EtherBridgeWrapper.SolverOp(1, to, amount, l2BatchId, l2BatchMetaHash) - ); - } - - uint256 totalValue = amount + solverFee; - eBridge.sendReceiveEtherToWrapper( - Alice, - to, - amount, - solverFee, - solverCondition, - bytes32(0), - address(tWrapper), - taikoChainId, - totalValue - ); - - uint256 toBalanceAfter = to.balance; - assertEq(toBalanceAfter - toBalanceBefore, amount); - - uint256 solverBalanceAfter = solver.balance; - assertEq(solverBalanceAfter - solverBalanceBefore, solverFee); - - assertTrue(eWrapper.solverConditionToSolver(solverCondition) == address(0)); - } - - function test_wrapper_solve_reverts_when_already_solved() public { - vm.chainId(ethereumChainId); - - uint256 amount = 1 ether; - address to = James; - address solver = David; - uint256 nonce = 1; - - vm.deal(solver, 10 ether); - vm.startPrank(solver); - - uint64 l2BatchId = 1; - bytes32 l2BatchMetaHash = bytes32("metahash1"); - - ITaikoInbox.Batch memory batch; - batch.metaHash = l2BatchMetaHash; - taikoInbox.setBatch(batch); - - eWrapper.solve{ value: amount }( - EtherBridgeWrapper.SolverOp(nonce, to, amount, l2BatchId, l2BatchMetaHash) - ); - - vm.expectRevert(EtherBridgeWrapper.VaultAlreadySolved.selector); - eWrapper.solve{ value: amount }( - EtherBridgeWrapper.SolverOp(nonce, to, amount, l2BatchId, l2BatchMetaHash) - ); - } - - function test_wrapper_solve_reverts_when_metahash_mismatched() public { - vm.chainId(ethereumChainId); - - uint256 amount = 1 ether; - address to = James; - address solver = David; - uint256 nonce = 1; - - vm.startPrank(solver); - - uint64 l2BatchId = 1; - bytes32 l2BatchMetaHash = bytes32("metahash1"); - bytes32 mismatchedMetaHash = bytes32("metahash2"); - - ITaikoInbox.Batch memory batch; - batch.metaHash = l2BatchMetaHash; - taikoInbox.setBatch(batch); - - vm.expectRevert(EtherBridgeWrapper.VaultMetahashMismatch.selector); - eWrapper.solve{ value: amount }( - EtherBridgeWrapper.SolverOp(nonce, to, amount, l2BatchId, mismatchedMetaHash) - ); - } -} diff --git a/packages/protocol/test/shared/tokenvault/ERC20Vault.h.sol b/packages/protocol/test/shared/tokenvault/ERC20Vault.h.sol index d3c6a8fca6e..611fac22968 100644 --- a/packages/protocol/test/shared/tokenvault/ERC20Vault.h.sol +++ b/packages/protocol/test/shared/tokenvault/ERC20Vault.h.sol @@ -83,4 +83,37 @@ contract PrankDestBridge { ctx.msgHash = bytes32(0); ctx.srcChainId = 0; } + + function sendReceiveEtherToERC20Vault( + address from, + address to, + uint64 amount, + uint64 solverFee, + bytes32 solverCondition, + bytes32 msgHash, + address srcChainERC20Vault, + uint64 srcChainId, + uint256 mockLibInvokeMsgValue + ) + public + { + ctx.sender = srcChainERC20Vault; + ctx.msgHash = msgHash; + ctx.srcChainId = srcChainId; + + destERC20Vault.onMessageInvocation{ value: mockLibInvokeMsgValue }( + abi.encode( + ERC20Vault.CanonicalERC20(0, address(0), 0, "", ""), + from, + to, + amount, + solverFee, + solverCondition + ) + ); + + ctx.sender = address(0); + ctx.msgHash = bytes32(0); + ctx.srcChainId = 0; + } } diff --git a/packages/protocol/test/shared/tokenvault/ERC20Vault.t.sol b/packages/protocol/test/shared/tokenvault/ERC20Vault.t.sol index 3eaa83fbc83..045c988d434 100644 --- a/packages/protocol/test/shared/tokenvault/ERC20Vault.t.sol +++ b/packages/protocol/test/shared/tokenvault/ERC20Vault.t.sol @@ -62,7 +62,7 @@ contract TestERC20Vault is CommonTest { function test_20Vault_send_erc20_revert_if_allowance_not_set() public { vm.startPrank(Alice); - vm.expectRevert(BaseVault.VAULT_INSUFFICIENT_FEE.selector); + vm.expectRevert(ERC20Vault.VAULT_INSUFFICIENT_ETHER.selector); eVault.sendToken( ERC20Vault.BridgeTransferOp( taikoChainId, address(0), Bob, 1, address(eERC20Token1), 1_000_000, 1 wei, 0 @@ -155,19 +155,6 @@ contract TestERC20Vault is CommonTest { ); } - function test_20Vault_send_erc20_reverts_invalid_token_address() public { - vm.startPrank(Alice); - - uint64 amount = 1; - - vm.expectRevert(ERC20Vault.VAULT_INVALID_TOKEN.selector); - eVault.sendToken( - ERC20Vault.BridgeTransferOp( - taikoChainId, address(0), Bob, 0, address(0), 1_000_000, amount, 0 - ) - ); - } - function test_20Vault_receive_erc20_canonical_to_dest_chain_transfers_from_canonical_token() public { @@ -238,182 +225,6 @@ contract TestERC20Vault is CommonTest { assertEq(David.balance, etherAmount); } - function test_20Vault_receive_erc20_solved() public { - vm.chainId(taikoChainId); - - eERC20Token1.mint(address(eVault)); - - uint64 amount = 1; - uint64 solverFee = 2; - address to = Bob; - address solver = David; - bytes32 solverCondition = eVault.getSolverCondition(1, address(eERC20Token1), to, amount); - - eERC20Token1.mint(address(solver)); - - vm.startPrank(solver); - - uint256 solverBalanceBefore = eERC20Token1.balanceOf(solver); - uint256 eVaultBalanceBefore = eERC20Token1.balanceOf(address(eVault)); - uint256 toBalanceBefore = eERC20Token1.balanceOf(to); - - { - uint64 blockId = 1; - bytes32 blockMetaHash = bytes32("metahash"); - - ITaikoInbox.Batch memory batch; - batch.metaHash = blockMetaHash; - taikoInbox.setBatch(batch); - - eERC20Token1.approve(address(eVault), 2); - - eVault.solve( - ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) - ); - } - - tBridge.sendReceiveERC20ToERC20Vault( - erc20ToCanonicalERC20(taikoChainId), - Alice, - to, - amount, - solverFee, - solverCondition, - bytes32(0), - address(eVault), - ethereumChainId, - 0 - ); - - uint256 eVaultBalanceAfter = eERC20Token1.balanceOf(address(eVault)); - assertEq(eVaultBalanceBefore - eVaultBalanceAfter, amount + solverFee); - - uint256 toBalanceAfter = eERC20Token1.balanceOf(to); - assertEq(toBalanceAfter - toBalanceBefore, amount); - - uint256 solverBalanceAfter = eERC20Token1.balanceOf(solver); - assertEq(solverBalanceAfter - solverBalanceBefore, solverFee); - - assertTrue(eVault.solverConditionToSolver(solverCondition) == address(0)); - } - - function test_20Vault_receive_erc20_solved_with_ether_to_james() public { - vm.chainId(taikoChainId); - - eERC20Token1.mint(address(eVault)); - - uint64 amount = 1; - uint64 solverFee = 2; - address to = James; - address solver = David; - bytes32 solverCondition = eVault.getSolverCondition(1, address(eERC20Token1), to, amount); - uint256 etherAmount = 0.1 ether; - - eERC20Token1.mint(address(solver)); - - vm.startPrank(solver); - - uint256 solverBalanceBefore = eERC20Token1.balanceOf(solver); - uint256 eVaultBalanceBefore = eERC20Token1.balanceOf(address(eVault)); - uint256 toBalanceBefore = eERC20Token1.balanceOf(to); - - { - uint64 blockId = 1; - bytes32 blockMetaHash = bytes32("metahash"); - - ITaikoInbox.Batch memory batch; - batch.metaHash = blockMetaHash; - taikoInbox.setBatch(batch); - - eERC20Token1.approve(address(eVault), 2); - - eVault.solve( - ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) - ); - } - - tBridge.sendReceiveERC20ToERC20Vault( - erc20ToCanonicalERC20(taikoChainId), - Alice, - to, - amount, - solverFee, - solverCondition, - bytes32(0), - address(eVault), - ethereumChainId, - etherAmount - ); - - uint256 eVaultBalanceAfter = eERC20Token1.balanceOf(address(eVault)); - assertEq(eVaultBalanceBefore - eVaultBalanceAfter, amount + solverFee); - - uint256 toBalanceAfter = eERC20Token1.balanceOf(to); - assertEq(toBalanceAfter - toBalanceBefore, amount); - - uint256 solverBalanceAfter = eERC20Token1.balanceOf(solver); - assertEq(solverBalanceAfter - solverBalanceBefore, solverFee); - - assertEq(James.balance, etherAmount); - assertTrue(eVault.solverConditionToSolver(solverCondition) == address(0)); - } - - function test_20Vault_solve_reverts_when_already_solved() public { - vm.chainId(taikoChainId); - - uint64 amount = 1; - address to = James; - address solver = David; - - eERC20Token1.mint(address(solver)); - - vm.startPrank(solver); - - uint64 blockId = 1; - bytes32 blockMetaHash = bytes32("metahash1"); - - ITaikoInbox.Batch memory batch; - batch.metaHash = blockMetaHash; - taikoInbox.setBatch(batch); - - eERC20Token1.approve(address(eVault), 2); - eVault.solve( - ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) - ); - - vm.expectRevert(ERC20Vault.VAULT_ALREADY_SOLVED.selector); - eVault.solve( - ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) - ); - } - - function test_20Vault_solve_reverts_when_metadata_is_mismatched() public { - vm.chainId(taikoChainId); - - uint64 amount = 1; - address to = James; - address solver = David; - - eERC20Token1.mint(address(solver)); - - vm.startPrank(solver); - - uint64 blockId = 1; - bytes32 blockMetaHash = bytes32("metahash1"); - bytes32 mismatchedBlockMetahash = bytes32("metahash2"); - - ITaikoInbox.Batch memory batch; - batch.metaHash = blockMetaHash; - taikoInbox.setBatch(batch); - - vm.expectRevert(ERC20Vault.VAULT_METAHASH_MISMATCH.selector); - eVault.solve( - ERC20Vault.SolverOp( - 1, address(eERC20Token1), to, amount, blockId, mismatchedBlockMetahash - ) - ); - } - function test_20Vault_receive_erc20_non_canonical_to_dest_chain_deploys_new_bridged_token_and_mints( ) public diff --git a/packages/protocol/test/shared/tokenvault/ERC20Vault_solver.t.sol b/packages/protocol/test/shared/tokenvault/ERC20Vault_solver.t.sol new file mode 100644 index 00000000000..ea4ed71c235 --- /dev/null +++ b/packages/protocol/test/shared/tokenvault/ERC20Vault_solver.t.sol @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./ERC20Vault.h.sol"; +import "../helpers/FreeMintERC20Token.sol"; +import "src/layer1/based/ITaikoInbox.sol"; + +contract TestERC20Vault_solver is CommonTest { + // Contracts on Ethereum + SignalService private eSignalService; + PrankTaikoInbox private taikoInbox; + PrankDestBridge private eBridge; + ERC20Vault private eVault; + FreeMintERC20Token private eERC20Token1; + FreeMintERC20Token private eERC20Token2; + + // Contracts on Taiko + SignalService private tSignalService; + Bridge private tBridge; + ERC20Vault private tVault; + BridgedERC20 private tUSDC; + BridgedERC20 private tUSDT; + BridgedERC20 private tStETH; + + function setUpOnEthereum() internal override { + eSignalService = deploySignalService( + address(new SignalService_WithoutProofVerification(address(resolver))) + ); + eVault = deployERC20Vault(); + eBridge = new PrankDestBridge(eVault); + taikoInbox = new PrankTaikoInbox(); + + eERC20Token1 = new FreeMintERC20Token("ERC20", "ERC20"); + eERC20Token1.mint(Alice); + + eERC20Token2 = new FreeMintERC20Token("", "123456abcdefgh"); + eERC20Token2.mint(Alice); + + register("bridge", address(eBridge)); + register("taiko", address(taikoInbox)); + + vm.deal(address(eBridge), 100 ether); + } + + function setUpOnTaiko() internal override { + tSignalService = deploySignalService( + address(new SignalService_WithoutProofVerification(address(resolver))) + ); + tVault = deployERC20Vault(); + tBridge = deployBridge(address(new Bridge(address(resolver)))); + + register("bridge", address(tBridge)); + register("bridged_erc20", address(new BridgedERC20(address(resolver)))); + + tUSDC = deployBridgedERC20(randAddress(), 100, 18, "USDC", "USDC coin"); + tUSDT = deployBridgedERC20(randAddress(), 100, 18, "USDT", "USDT coin"); + tStETH = deployBridgedERC20(randAddress(), 100, 18, "tStETH", "Lido Staked ETH"); + + vm.deal(Alice, 1 ether); + vm.deal(Bob, 1 ether); + } + + function test_20Vault_receive_erc20_solved() public { + eERC20Token1.mint(address(eVault)); + + uint64 amount = 1; + uint64 solverFee = 2; + address to = Bob; + address solver = David; + bytes32 solverCondition = eVault.getSolverCondition(1, address(eERC20Token1), to, amount); + + eERC20Token1.mint(address(solver)); + + vm.startPrank(solver); + + uint256 solverBalanceBefore = eERC20Token1.balanceOf(solver); + uint256 eVaultBalanceBefore = eERC20Token1.balanceOf(address(eVault)); + uint256 toBalanceBefore = eERC20Token1.balanceOf(to); + + { + uint64 blockId = 1; + bytes32 blockMetaHash = bytes32("metahash"); + + ITaikoInbox.Batch memory batch; + batch.metaHash = blockMetaHash; + taikoInbox.setBatch(batch); + + eERC20Token1.approve(address(eVault), 2); + + eVault.solve( + ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) + ); + } + + eBridge.sendReceiveERC20ToERC20Vault( + erc20ToCanonicalERC20(ethereumChainId), + Alice, + to, + amount, + solverFee, + solverCondition, + bytes32(0), + address(tVault), + taikoChainId, + 0 + ); + + uint256 eVaultBalanceAfter = eERC20Token1.balanceOf(address(eVault)); + assertEq(eVaultBalanceBefore - eVaultBalanceAfter, amount + solverFee); + + uint256 toBalanceAfter = eERC20Token1.balanceOf(to); + assertEq(toBalanceAfter - toBalanceBefore, amount); + + uint256 solverBalanceAfter = eERC20Token1.balanceOf(solver); + assertEq(solverBalanceAfter - solverBalanceBefore, solverFee); + + assertTrue(eVault.solverConditionToSolver(solverCondition) == address(0)); + } + + function test_20Vault_receive_erc20_solved_with_ether_to_james() public { + eERC20Token1.mint(address(eVault)); + + uint64 amount = 1; + uint64 solverFee = 2; + address to = James; + address solver = David; + bytes32 solverCondition = eVault.getSolverCondition(1, address(eERC20Token1), to, amount); + uint256 etherAmount = 0.1 ether; + + eERC20Token1.mint(address(solver)); + + vm.startPrank(solver); + + uint256 solverBalanceBefore = eERC20Token1.balanceOf(solver); + uint256 eVaultBalanceBefore = eERC20Token1.balanceOf(address(eVault)); + uint256 toBalanceBefore = eERC20Token1.balanceOf(to); + + { + uint64 blockId = 1; + bytes32 blockMetaHash = bytes32("metahash"); + + ITaikoInbox.Batch memory batch; + batch.metaHash = blockMetaHash; + taikoInbox.setBatch(batch); + + eERC20Token1.approve(address(eVault), 2); + + eVault.solve( + ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) + ); + } + + eBridge.sendReceiveERC20ToERC20Vault( + erc20ToCanonicalERC20(ethereumChainId), + Alice, + to, + amount, + solverFee, + solverCondition, + bytes32(0), + address(tVault), + taikoChainId, + etherAmount + ); + + uint256 eVaultBalanceAfter = eERC20Token1.balanceOf(address(eVault)); + assertEq(eVaultBalanceBefore - eVaultBalanceAfter, amount + solverFee); + + uint256 toBalanceAfter = eERC20Token1.balanceOf(to); + assertEq(toBalanceAfter - toBalanceBefore, amount); + + uint256 solverBalanceAfter = eERC20Token1.balanceOf(solver); + assertEq(solverBalanceAfter - solverBalanceBefore, solverFee); + + assertEq(James.balance, etherAmount); + assertTrue(eVault.solverConditionToSolver(solverCondition) == address(0)); + } + + function test_20Vault_solve_reverts_when_already_solved() public { + uint64 amount = 1; + address to = James; + address solver = David; + + eERC20Token1.mint(address(solver)); + + vm.startPrank(solver); + + uint64 blockId = 1; + bytes32 blockMetaHash = bytes32("metahash1"); + + ITaikoInbox.Batch memory batch; + batch.metaHash = blockMetaHash; + taikoInbox.setBatch(batch); + + eERC20Token1.approve(address(eVault), 2); + eVault.solve( + ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) + ); + + vm.expectRevert(ERC20Vault.VAULT_ALREADY_SOLVED.selector); + eVault.solve( + ERC20Vault.SolverOp(1, address(eERC20Token1), to, amount, blockId, blockMetaHash) + ); + } + + function test_20Vault_solve_reverts_when_metadata_is_mismatched() public { + uint64 amount = 1; + address to = James; + address solver = David; + + eERC20Token1.mint(address(solver)); + + vm.startPrank(solver); + + uint64 blockId = 1; + bytes32 blockMetaHash = bytes32("metahash1"); + bytes32 mismatchedBlockMetahash = bytes32("metahash2"); + + ITaikoInbox.Batch memory batch; + batch.metaHash = blockMetaHash; + taikoInbox.setBatch(batch); + + vm.expectRevert(ERC20Vault.VAULT_METAHASH_MISMATCH.selector); + eVault.solve( + ERC20Vault.SolverOp( + 1, address(eERC20Token1), to, amount, blockId, mismatchedBlockMetahash + ) + ); + } + + function erc20ToCanonicalERC20(uint64 chainId) + internal + view + returns (ERC20Vault.CanonicalERC20 memory) + { + return ERC20Vault.CanonicalERC20({ + chainId: chainId, + addr: address(eERC20Token1), + decimals: eERC20Token1.decimals(), + symbol: eERC20Token1.symbol(), + name: eERC20Token1.name() + }); + } + + function test_20Vault_send_ether_revert_if_insufficient_value() public { + vm.chainId(taikoChainId); + vm.startPrank(Alice); + vm.expectRevert(ERC20Vault.VAULT_INSUFFICIENT_ETHER.selector); + tVault.sendToken{ value: 0 }( + ERC20Vault.BridgeTransferOp( + ethereumChainId, address(0), Bob, 0, address(0), 1_000_000, 1 wei, 0 + ) + ); + } + + function test_20Vault_send_ether_no_processing_fee() public { + vm.chainId(taikoChainId); + vm.startPrank(Alice); + + uint256 amount = 2 wei; + uint256 aliceBalanceBefore = Alice.balance; + + tVault.sendToken{ value: amount }( + ERC20Vault.BridgeTransferOp( + ethereumChainId, address(0), Bob, 0, address(0), 1_000_000, amount, 0 + ) + ); + + uint256 aliceBalanceAfter = Alice.balance; + assertEq(aliceBalanceBefore - aliceBalanceAfter, amount); + } + + function test_20Vault_send_ether_processing_fee_reverts_if_msg_value_too_low() public { + vm.chainId(taikoChainId); + vm.startPrank(Alice); + + uint256 amount = 2 wei; + uint64 fee = 1 wei; + + vm.expectRevert(ERC20Vault.VAULT_INSUFFICIENT_ETHER.selector); + tVault.sendToken{ value: amount }( + ERC20Vault.BridgeTransferOp( + ethereumChainId, address(0), Bob, fee, address(0), 1_000_000, amount, 0 + ) + ); + } + + function test_20Vault_send_ether_processing_fee() public { + vm.chainId(taikoChainId); + vm.startPrank(Alice); + + uint256 amount = 2 wei; + uint64 fee = 1 wei; + uint256 totalValue = amount + fee; + + uint256 aliceBalanceBefore = Alice.balance; + + tVault.sendToken{ value: totalValue }( + ERC20Vault.BridgeTransferOp( + ethereumChainId, address(0), Bob, fee, address(0), 1_000_000, amount, 0 + ) + ); + + uint256 aliceBalanceAfter = Alice.balance; + assertEq(aliceBalanceBefore - aliceBalanceAfter, totalValue); + } + + function test_20Vault_send_ether_with_solver_fee() public { + vm.chainId(taikoChainId); + vm.startPrank(Alice); + + uint256 amount = 2 wei; + uint64 fee = 1 wei; + uint256 solverFee = 1 wei; + uint256 totalValue = amount + fee + solverFee; + + uint256 aliceBalanceBefore = Alice.balance; + + tVault.sendToken{ value: totalValue }( + ERC20Vault.BridgeTransferOp( + ethereumChainId, address(0), Bob, fee, address(0), 1_000_000, amount, solverFee + ) + ); + + uint256 aliceBalanceAfter = Alice.balance; + assertEq(aliceBalanceBefore - aliceBalanceAfter, totalValue); + } + + function test_20Vault_solve_ether() public { + uint256 amount = 1 wei; + address to = James; + address solver = David; + + vm.deal(solver, 1 wei); + + vm.startPrank(solver); + + uint256 solverBalanceBefore = solver.balance; + uint256 toBalanceBefore = to.balance; + + uint64 blockId = 1; + bytes32 blockMetaHash = bytes32("metahash"); + + ITaikoInbox.Batch memory batch; + batch.metaHash = blockMetaHash; + taikoInbox.setBatch(batch); + + eVault.solve{ value: amount }( + ERC20Vault.SolverOp(1, address(0), to, amount, blockId, blockMetaHash) + ); + + uint256 toBalanceAfter = to.balance; + uint256 solverBalanceAfter = solver.balance; + + assertEq(toBalanceAfter - toBalanceBefore, amount); + assertEq(solverBalanceBefore - solverBalanceAfter, amount); + } + + function test_20Vault_onMessageRecalled_ether() public { + vm.chainId(taikoChainId); + vm.startPrank(Alice); + + uint256 amount = 2 wei; + uint256 aliceBalanceBefore = Alice.balance; + + IBridge.Message memory message = tVault.sendToken{ value: amount }( + ERC20Vault.BridgeTransferOp( + ethereumChainId, address(0), Bob, 0, address(0), 1_000_000, amount, 0 + ) + ); + + uint256 aliceBalanceAfter = Alice.balance; + assertEq(aliceBalanceBefore - aliceBalanceAfter, amount); + + tBridge.recallMessage(message, bytes("")); + + uint256 aliceBalanceAfterRecall = Alice.balance; + assertEq(aliceBalanceAfterRecall, aliceBalanceBefore); + } + + function test_20Vault_receive_ether_solved() public { + uint64 amount = 1 ether; + uint64 solverFee = 0.1 ether; + address to = Bob; + address solver = David; + bytes32 solverCondition = eVault.getSolverCondition(1, address(0), to, amount); + + vm.deal(solver, amount); + + vm.startPrank(solver); + + uint256 solverBalanceBefore = solver.balance; + uint256 toBalanceBefore = to.balance; + + { + uint64 blockId = 1; + bytes32 blockMetaHash = bytes32("metahash"); + + ITaikoInbox.Batch memory batch; + batch.metaHash = blockMetaHash; + taikoInbox.setBatch(batch); + + eVault.solve{ value: amount }( + ERC20Vault.SolverOp(1, address(0), to, amount, blockId, blockMetaHash) + ); + } + + uint256 ethAmount = amount + solverFee; + eBridge.sendReceiveEtherToERC20Vault( + Alice, + to, + amount, + solverFee, + solverCondition, + bytes32(0), + address(tVault), + taikoChainId, + ethAmount + ); + + uint256 toBalanceAfter = to.balance; + assertEq(toBalanceAfter - toBalanceBefore, amount); + + uint256 solverBalanceAfter = solver.balance; + assertEq(solverBalanceAfter - solverBalanceBefore, solverFee); + + assertTrue(eVault.solverConditionToSolver(solverCondition) == address(0)); + } +}