diff --git a/helix-contract/contracts/interfaces/IMessager.sol b/helix-contract/contracts/interfaces/IMessager.sol index dcdc830d..59f07d2e 100644 --- a/helix-contract/contracts/interfaces/IMessager.sol +++ b/helix-contract/contracts/interfaces/IMessager.sol @@ -14,5 +14,4 @@ interface ILowLevelMessageReceiver { interface IMessageId { function latestSentMessageId() external view returns(bytes32); function latestRecvMessageId() external view returns(bytes32); - function messageDeliveredOrSlashed(bytes32 messageId) external view returns(bool); } diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol index 37c05705..8f3366c3 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -12,26 +12,24 @@ import "../../../utils/TokenTransferHelper.sol"; // When sending cross-chain transactions, the user locks the Token in the contract, and when the message reaches the target chain, the corresponding mapped asset (xToken) will be issued; // if the target chain fails to issue the xToken, the user can send a reverse message on the target chain to unlock the original asset. contract xTokenBacking is xTokenBridgeBase { - struct LockedInfo { - bytes32 hash; - bool hasRefundForFailed; - } - address public wToken; - // (transferId => lockedInfo) - // Token => xToken - mapping(bytes32 => LockedInfo) public lockedMessages; - // (transferId => lockedInfo) - // xToken => Token - mapping(bytes32 => bool) public unlockedTransferIds; - // save original token => xToken to prevent unregistered token lock mapping(bytes32 => address) public originalToken2xTokens; - event TokenLocked(bytes32 transferId, uint256 remoteChainId, address token, address sender, address recipient, uint256 amount, uint256 fee); + event TokenLocked( + bytes32 transferId, + bytes32 messageId, + uint256 nonce, + uint256 remoteChainId, + address token, + address sender, + address recipient, + uint256 amount, + uint256 fee + ); event TokenUnlocked(bytes32 transferId, uint256 remoteChainId, address token, address recipient, uint256 amount); - event RemoteIssuingFailure(bytes32 refundId, bytes32 transferId, address mappingToken, address originalSender, uint256 amount, uint256 fee); + event RemoteIssuingFailure(bytes32 transferId, bytes32 messageId, address xToken, address originalSender, uint256 amount, uint256 fee); event TokenUnlockedForFailed(bytes32 transferId, uint256 remoteChainId, address token, address recipient, uint256 amount); // the wToken is the wrapped native token's address @@ -54,16 +52,22 @@ contract xTokenBacking is xTokenBridgeBase { _setDailyLimit(_originalToken, _dailyLimit); } + // We use nonce to ensure that messages are not duplicated + // especially in reorg scenarios, the destination chain use nonce to filter out duplicate deliveries. function lockAndRemoteIssuing( uint256 _remoteChainId, address _originalToken, address _recipient, uint256 _amount, + uint256 _nonce, bytes memory _extParams ) external payable { bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); require(originalToken2xTokens[key] != address(0), "token not registered"); + bytes32 transferId = getTransferId(_nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount); + _requestTransfer(transferId); + uint256 prepaid = msg.value; // lock token if (address(0) == _originalToken) { @@ -81,27 +85,30 @@ contract xTokenBacking is xTokenBridgeBase { } bytes memory issuxToken = encodeIssuexToken( _originalToken, + msg.sender, _recipient, - _amount + _amount, + _nonce ); - bytes32 transferId = _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); - bytes32 lockMessageHash = keccak256(abi.encodePacked(transferId, _remoteChainId, _originalToken, msg.sender, _amount)); - require(lockedMessages[transferId].hash == bytes32(0), "the locked message exist"); - lockedMessages[transferId] = LockedInfo(lockMessageHash, false); - emit TokenLocked(transferId, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); + bytes32 messageId = _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); + emit TokenLocked(transferId, messageId, _nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); } function encodeIssuexToken( address _originalToken, + address _originalSender, address _recipient, - uint256 _amount + uint256 _amount, + uint256 _nonce ) public view returns(bytes memory) { return abi.encodeWithSelector( IxTokenIssuing.issuexToken.selector, block.chainid, _originalToken, + _originalSender, _recipient, - _amount + _amount, + _nonce ); } @@ -109,14 +116,16 @@ contract xTokenBacking is xTokenBridgeBase { function unlockFromRemote( uint256 _remoteChainId, address _originalToken, + address _originSender, address _recipient, - uint256 _amount + uint256 _amount, + uint256 _nonce ) external calledByMessager(_remoteChainId) whenNotPaused { expendDailyLimit(_originalToken, _amount); - bytes32 transferId = _latestRecvMessageId(_remoteChainId); - require(unlockedTransferIds[transferId] == false, "message has been accepted"); - unlockedTransferIds[transferId] = true; + bytes32 transferId = getTransferId(_nonce, block.chainid, _originalToken, _originSender, _recipient, _amount); + require(filledTransfers[transferId] == TRANSFER_UNFILLED, "message has been accepted"); + filledTransfers[transferId] = TRANSFER_DELIVERED; // native token do not use guard if (address(0) == _originalToken) { @@ -162,45 +171,49 @@ contract xTokenBacking is xTokenBridgeBase { } // send message to Issuing when unlock failed - // 1. message was not accepted by backing to unlock tokens - // 2. message was delivered by messager, and can't be received again - // this method can be retried function requestRemoteIssuingForUnlockFailure( - bytes32 _transferId, uint256 _remoteChainId, address _originalToken, address _originalSender, + address _recipient, uint256 _amount, + uint256 _nonce, bytes memory _extParams ) external payable { + require(_originalSender == msg.sender || _recipient == msg.sender || dao == msg.sender, "invalid msgSender"); + bytes32 transferId = getTransferId(_nonce, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); // must not exist in successful issue list - require(unlockedTransferIds[_transferId] == false, "success message can't refund for failed"); - // if the msg is pending, xToken can expire the message - // on arbitrum, the low gasLimit may cause this case - _assertMessageIsDelivered(_remoteChainId, _transferId); + uint256 filledTransfer = filledTransfers[transferId]; + require(filledTransfer != TRANSFER_DELIVERED, "success message can't refund for failed"); + if (filledTransfer != TRANSFER_REFUNDED) { + filledTransfers[transferId] = TRANSFER_REFUNDED; + } bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( - _transferId, _originalToken, _originalSender, - _amount + _recipient, + _amount, + _nonce ); - bytes32 refundId = _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); - emit RemoteIssuingFailure(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); + bytes32 messageId = _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); + emit RemoteIssuingFailure(transferId, messageId, _originalToken, _originalSender, _amount, msg.value); } function encodeIssuingForUnlockFailureFromRemote( - bytes32 _transferId, address _originalToken, address _originalSender, - uint256 _amount + address _recipient, + uint256 _amount, + uint256 _nonce ) public view returns(bytes memory) { return abi.encodeWithSelector( IxTokenIssuing.handleIssuingForUnlockFailureFromRemote.selector, block.chainid, - _transferId, _originalToken, _originalSender, - _amount + _recipient, + _amount, + _nonce ); } @@ -211,22 +224,20 @@ contract xTokenBacking is xTokenBridgeBase { // 2. the locked message exist and the information(hash) matched function handleUnlockForIssuingFailureFromRemote( uint256 _remoteChainId, - bytes32 _transferId, address _originalToken, - address _originSender, - uint256 _amount + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce ) external calledByMessager(_remoteChainId) whenNotPaused { - LockedInfo memory lockedMessage = lockedMessages[_transferId]; - require(lockedMessage.hasRefundForFailed == false, "the locked message has been refund"); - bytes32 messageHash = keccak256(abi.encodePacked(_transferId, _remoteChainId, _originalToken, _originSender, _amount)); - require(lockedMessage.hash == messageHash, "message is not matched"); - lockedMessages[_transferId].hasRefundForFailed = true; + bytes32 transferId = keccak256(abi.encodePacked(_nonce, _remoteChainId, _originalToken, _originalSender, _recipient, _amount)); + _handleRefund(transferId); if (_originalToken == address(0)) { - TokenTransferHelper.safeTransferNative(_originSender, _amount); + TokenTransferHelper.safeTransferNative(_originalSender, _amount); } else { - TokenTransferHelper.safeTransfer(_originalToken, _originSender, _amount); + TokenTransferHelper.safeTransfer(_originalToken, _originalSender, _amount); } - emit TokenUnlockedForFailed(_transferId, _remoteChainId, _originalToken, _originSender, _amount); + emit TokenUnlockedForFailed(transferId, _remoteChainId, _originalToken, _originalSender, _amount); } } diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol index a7d0a499..ecc12823 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -12,11 +12,19 @@ import "../../../utils/TokenTransferHelper.sol"; // Backing or Issuing contract will inherit the contract. // This contract define the access authorization, the message channel contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLimit { + uint256 constant public TRANSFER_UNFILLED = 0x00; + uint256 constant public TRANSFER_DELIVERED = 0x01; + uint256 constant public TRANSFER_REFUNDED = 0x02; struct MessagerService { address sendService; address receiveService; } + struct RequestInfo { + bool isRequested; + bool hasRefundForFailed; + } + // the version is to issue different xTokens for different version of bridge. string public version; // the protocol fee for each time user send transaction @@ -27,6 +35,14 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim // remoteChainId => info mapping(uint256 => MessagerService) public messagers; + // transferId => RequestInfo + mapping(bytes32 => RequestInfo) public requestInfos; + + // transferId => result + // 1. 0x01: filled by receive message + // 2. 0x02: filled by refund operation + mapping(bytes32 => uint256) public filledTransfers; + // must be called by message service configured modifier calledByMessager(uint256 _remoteChainId) { address receiveService = messagers[_remoteChainId].receiveService; @@ -87,20 +103,27 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim messageId = IMessageId(service.sendService).latestSentMessageId(); } - // check a special message is delivered by message service - // the delivered message can't be received any more - function _assertMessageIsDelivered(uint256 _remoteChainId, bytes32 _transferId) view internal { - MessagerService memory service = messagers[_remoteChainId]; - require(service.receiveService != address(0), "bridge not configured"); - require(IMessageId(service.receiveService).messageDeliveredOrSlashed(_transferId), "message not delivered"); + function _requestTransfer(bytes32 _transferId) internal { + require(requestInfos[_transferId].isRequested == false, "request exist"); + requestInfos[_transferId].isRequested = true; } - // the latest received message id - // when this method is called in the receive method, it's the current received message's id - function _latestRecvMessageId(uint256 _remoteChainId) view internal returns(bytes32) { - MessagerService memory service = messagers[_remoteChainId]; - require(service.receiveService != address(0), "invalid remoteChainId"); - return IMessageId(service.receiveService).latestRecvMessageId(); + function _handleRefund(bytes32 _transferId) internal { + RequestInfo memory requestInfo = requestInfos[_transferId]; + require(requestInfo.isRequested == true, "request not exist"); + require(requestInfo.hasRefundForFailed == false, "request has been refund"); + requestInfos[_transferId].hasRefundForFailed = true; + } + + function getTransferId( + uint256 _nonce, + uint256 _targetChainId, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount + ) public pure returns(bytes32) { + return keccak256(abi.encodePacked(_nonce, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); } // settings diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol index c0a024da..c56b4fbf 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -1,28 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import "./xTokenErc20.sol"; import "./xTokenBridgeBase.sol"; +import "./xTokenErc20.sol"; import "../interfaces/IxTokenBacking.sol"; import "../../interfaces/IGuard.sol"; import "../../../utils/TokenTransferHelper.sol"; contract xTokenIssuing is xTokenBridgeBase { - struct BurnInfo { - bytes32 hash; - bool hasRefundForFailed; - } - struct OriginalTokenInfo { uint256 chainId; address token; } - // transferId => BurnInfo - mapping(bytes32 => BurnInfo) public burnMessages; - // transferId => bool - mapping(bytes32 => bool) public issueTransferIds; - // original Token => xToken mapping is saved in Issuing Contract // salt => xToken address mapping(bytes32 => address) public xTokens; @@ -33,7 +23,17 @@ contract xTokenIssuing is xTokenBridgeBase { event IssuingERC20Updated(uint256 originalChainId, address originalToken, address xToken, address oldxToken); event RemoteUnlockForIssuingFailureRequested(bytes32 refundId, bytes32 transferId, address originalToken, address originalSender, uint256 amount, uint256 fee); event xTokenIssued(bytes32 transferId, uint256 remoteChainId, address originalToken, address xToken, address recipient, uint256 amount); - event BurnAndRemoteUnlocked(bytes32 transferId, uint256 remoteChainId, address sender, address recipient, address originalToken, address xToken, uint256 amount, uint256 fee); + event BurnAndRemoteUnlocked( + bytes32 transferId, + bytes32 messageId, + uint256 nonce, + uint256 remoteChainId, + address sender, + address recipient, + address originalToken, + uint256 amount, + uint256 fee + ); event TokenRemintForFailed(bytes32 transferId, uint256 originalChainId, address originalToken, address xToken, address originalSender, uint256 amount); function registerxToken( @@ -95,18 +95,20 @@ contract xTokenIssuing is xTokenBridgeBase { function issuexToken( uint256 _remoteChainId, address _originalToken, + address _originalSender, address _recipient, - uint256 _amount + uint256 _amount, + uint256 _nonce ) external calledByMessager(_remoteChainId) whenNotPaused { - bytes32 transferId = _latestRecvMessageId(_remoteChainId); + bytes32 transferId = getTransferId(_nonce, block.chainid, _originalToken, _originalSender, _recipient, _amount); bytes32 salt = xTokenSalt(_remoteChainId, _originalToken); address xToken = xTokens[salt]; require(xToken != address(0), "xToken not exist"); require(_amount > 0, "can not receive amount zero"); expendDailyLimit(xToken, _amount); - require(issueTransferIds[transferId] == false, "message has been accepted"); - issueTransferIds[transferId] = true; + require(filledTransfers[transferId] == TRANSFER_UNFILLED, "message has been accepted"); + filledTransfers[transferId] = TRANSFER_DELIVERED; address _guard = guard; if (_guard != address(0)) { @@ -124,77 +126,93 @@ contract xTokenIssuing is xTokenBridgeBase { address _xToken, address _recipient, uint256 _amount, + uint256 _nonce, bytes memory _extParams ) external payable { require(_amount > 0, "can not transfer amount zero"); OriginalTokenInfo memory originalInfo = originalTokens[_xToken]; + bytes32 transferId = getTransferId(_nonce, originalInfo.chainId, originalInfo.token, msg.sender, _recipient, _amount); + _requestTransfer(transferId); // transfer to this and then burn TokenTransferHelper.safeTransferFrom(_xToken, msg.sender, address(this), _amount); xTokenErc20(_xToken).burn(address(this), _amount); bytes memory remoteUnlockCall = encodeUnlockFromRemote( originalInfo.token, + msg.sender, _recipient, - _amount + _amount, + _nonce ); - bytes32 transferId = _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); + bytes32 messageId = _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); - require(burnMessages[transferId].hash == bytes32(0), "message exist"); - bytes32 messageHash = keccak256(abi.encodePacked(transferId, originalInfo.chainId, _xToken, msg.sender, _amount)); - burnMessages[transferId] = BurnInfo(messageHash, false); - emit BurnAndRemoteUnlocked(transferId, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _xToken, _amount, msg.value); + emit BurnAndRemoteUnlocked(transferId, messageId, _nonce, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _amount, msg.value); } function encodeUnlockFromRemote( address _originalToken, + address _originalSender, address _recipient, - uint256 _amount + uint256 _amount, + uint256 _nonce ) public view returns(bytes memory) { return abi.encodeWithSelector( IxTokenBacking.unlockFromRemote.selector, block.chainid, _originalToken, + _originalSender, _recipient, - _amount + _amount, + _nonce ); } // send unlock message when issuing failed // 1. message has been delivered // 2. xtoken not issued + // this method can retry function requestRemoteUnlockForIssuingFailure( - bytes32 _transferId, uint256 _originalChainId, address _originalToken, address _originalSender, + address _recipient, uint256 _amount, + uint256 _nonce, bytes memory _extParams ) external payable { - require(issueTransferIds[_transferId] == false, "success message can't refund for failed"); - _assertMessageIsDelivered(_originalChainId, _transferId); + require(_originalSender == msg.sender || _recipient == msg.sender || dao == msg.sender, "invalid msgSender"); + bytes32 transferId = getTransferId(_nonce, _originalChainId, _originalToken, _originalSender, _recipient, _amount); + uint256 filledTransfer = filledTransfers[transferId]; + require(filledTransfer != TRANSFER_DELIVERED, "success message can't refund for failed"); + if (filledTransfer != TRANSFER_REFUNDED) { + filledTransfers[transferId] = TRANSFER_REFUNDED; + } bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( - _transferId, _originalToken, _originalSender, - _amount + _recipient, + _amount, + _nonce ); - bytes32 refundId = _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); - emit RemoteUnlockForIssuingFailureRequested(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); + bytes32 messageId = _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); + emit RemoteUnlockForIssuingFailureRequested(transferId, messageId, _originalToken, _originalSender, _amount, msg.value); } function encodeUnlockForIssuingFailureFromRemote( - bytes32 _transferId, address _originalToken, address _originalSender, - uint256 _amount + address _recipient, + uint256 _amount, + uint256 _nonce ) public view returns(bytes memory) { return abi.encodeWithSelector( IxTokenBacking.handleUnlockForIssuingFailureFromRemote.selector, block.chainid, - _transferId, _originalToken, _originalSender, - _amount + _recipient, + _amount, + _nonce ); } @@ -205,24 +223,21 @@ contract xTokenIssuing is xTokenBridgeBase { // 2. the burn information(hash) matched function handleIssuingForUnlockFailureFromRemote( uint256 _originalChainId, - bytes32 _transferId, address _originalToken, address _originalSender, - uint256 _amount + address _recipient, + uint256 _amount, + uint256 _nonce ) external calledByMessager(_originalChainId) whenNotPaused { - BurnInfo memory burnInfo = burnMessages[_transferId]; - require(burnInfo.hasRefundForFailed == false, "Backing:the burn message has been refund"); + bytes32 transferId = getTransferId(_nonce, _originalChainId, _originalToken, _originalSender, _recipient, _amount); + _handleRefund(transferId); bytes32 salt = xTokenSalt(_originalChainId, _originalToken); address xToken = xTokens[salt]; require(xToken != address(0), "xToken not exist"); - bytes32 messageHash = keccak256(abi.encodePacked(_transferId, _originalChainId, xToken, _originalSender, _amount)); - require(burnInfo.hash == messageHash, "message is not matched"); - burnMessages[_transferId].hasRefundForFailed = true; - xTokenErc20(xToken).mint(_originalSender, _amount); - emit TokenRemintForFailed(_transferId, _originalChainId, _originalToken, xToken, _originalSender, _amount); + emit TokenRemintForFailed(transferId, _originalChainId, _originalToken, xToken, _originalSender, _amount); } function xTokenSalt( @@ -231,5 +246,4 @@ contract xTokenIssuing is xTokenBridgeBase { ) public view returns(bytes32) { return keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); } -} - +} diff --git a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol index 199a3e1f..a190f1ad 100644 --- a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol @@ -5,15 +5,18 @@ interface IxTokenBacking { function unlockFromRemote( uint256 remoteChainId, address originalToken, + address originalSender, address recipient, - uint256 amount + uint256 amount, + uint256 nonce ) external; function handleUnlockForIssuingFailureFromRemote( uint256 remoteChainId, - bytes32 transferId, address originalToken, address originalSender, - uint256 amount + address recipient, + uint256 amount, + uint256 nonce ) external; } diff --git a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol index ae035e26..00f59685 100644 --- a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol @@ -3,17 +3,20 @@ pragma solidity >=0.8.17; interface IxTokenIssuing { function handleIssuingForUnlockFailureFromRemote( - uint256 remoteChainId, - bytes32 transferId, + uint256 originalChainId, address originalToken, address originalSender, - uint256 amount + address recipient, + uint256 amount, + uint256 nonce ) external; function issuexToken( uint256 remoteChainId, address originalToken, + address originalSender, address recipient, - uint256 amount + uint256 amount, + uint256 nonce ) external; } diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol index 03dc5e36..fcf7ac2d 100644 --- a/helix-contract/contracts/messagers/MsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -5,9 +5,6 @@ import "../utils/AccessController.sol"; import "../interfaces/IMessageLine.sol"; contract MsglineMessager is Application, AccessController { - // expire time = 1 hour - uint256 constant public SLASH_EXPIRE_TIME = 3600; - IMessageLine public immutable msgline; struct RemoteMessager { @@ -24,12 +21,8 @@ contract MsglineMessager is Application, AccessController { mapping(bytes32=>address) public remoteAppReceivers; mapping(bytes32=>address) public remoteAppSenders; - // transferId => timestamp - mapping(bytes32=>uint256) public slashTransferIds; - event CallerUnMatched(uint256 srcAppChainId, bytes32 transferId, address srcAppAddress); event CallResult(uint256 srcAppChainId, bytes32 transferId, bool result); - event MessageStartSlash(bytes32 transferId, uint256 expiredTimestamp); modifier onlyWhiteList() { require(whiteList[msg.sender], "msg.sender not in whitelist"); @@ -91,10 +84,6 @@ contract MsglineMessager is Application, AccessController { bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); bytes32 transferId = latestRecvMessageId(); - if (_messageSlashed(transferId)) { - return; - } - // check remote appSender if (_remoteAppAddress != remoteAppSenders[key]) { emit CallerUnMatched(_srcAppChainId, transferId, _remoteAppAddress); @@ -105,19 +94,6 @@ contract MsglineMessager is Application, AccessController { emit CallResult(_srcAppChainId, transferId, success); } - // We need to assume that transferId is unpredictable - function slashMessage(bytes32 _transferId) external { - require(slashTransferIds[_transferId] == 0, "!slash"); - uint256 expiredTimestamp = block.timestamp + SLASH_EXPIRE_TIME; - slashTransferIds[_transferId] = expiredTimestamp; - emit MessageStartSlash(_transferId, expiredTimestamp); - } - - function _messageSlashed(bytes32 _transferId) internal view returns(bool) { - uint256 slashTimestamp = slashTransferIds[_transferId]; - return slashTimestamp > 0 && slashTimestamp < block.timestamp; - } - function latestSentMessageId() external view returns(bytes32) { return msgline.sentMessageId(); } @@ -126,10 +102,6 @@ contract MsglineMessager is Application, AccessController { return msgline.recvMessageId(); } - function messageDeliveredOrSlashed(bytes32 _transferId) external view returns(bool) { - return msgline.dones(_transferId) || _messageSlashed(_transferId); - } - function messagePayload(address _from, address _to, bytes memory _message) public view returns(bytes memory) { return abi.encodeWithSelector( MsglineMessager.receiveMessage.selector, diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index 5e180f5f..43eaf14c 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -18,11 +18,12 @@ describe("xtoken tests", () => { }); it("test_msglinebased_xtoken_flow", async function () { - const [owner, relayer, user, slasher] = await ethers.getSigners(); + const [owner, user01, user02] = await ethers.getSigners(); const dao = owner.address; const backingChainId = 31337; const issuingChainId = 31337; const nativeTokenAddress = "0x0000000000000000000000000000000000000000"; + let globalNonce = 10001; const xTokens = {}; @@ -96,6 +97,11 @@ describe("xtoken tests", () => { await issuingGuard.deployed(); await issuingGuard.setDepositor(issuing.address, true); + function generateNonce() { + globalNonce += 1; + return globalNonce; + } + async function registerToken( originalTokenAddress, originalChainName, @@ -128,7 +134,7 @@ describe("xtoken tests", () => { console.log("register original token finished, address:", xTokenAddress); xTokens[originalTokenAddress] = xTokenAddress; const xToken = await ethers.getContractAt("Erc20", xTokenAddress); - await xToken.approve(issuing.address, ethers.utils.parseEther("1000000000")); + await xToken.connect(user02).approve(issuing.address, ethers.utils.parseEther("1000000000")); return xTokenAddress; } @@ -143,22 +149,24 @@ describe("xtoken tests", () => { async function lockAndRemoteIssuing( originalAddress, - recipient, amount, fee, usingGuard, result ) { + const recipient = user02.address; + const nonce = generateNonce(); const xTokenAddress = xTokens[originalAddress]; const balanceRecipientBefore = await balanceOf(xTokenAddress, recipient); const balanceBackingBefore = await balanceOf(originalAddress, backing.address); - const transaction = await backing.lockAndRemoteIssuing( + const transaction = await backing.connect(user01).lockAndRemoteIssuing( issuingChainId, originalAddress, recipient, amount, + nonce, 0, {value: ethers.utils.parseEther(fee)} ) @@ -167,11 +175,10 @@ describe("xtoken tests", () => { const balanceRecipientAfter = await balanceOf(xTokenAddress, recipient); const balanceBackingAfter = await balanceOf(originalAddress, backing.address); - const messageId = await backingMessager.latestSentMessageId(); - const lockInfo = await backing.lockedMessages(messageId); - - expect(lockInfo.hash).not.to.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); - expect(lockInfo.hasRefundForFailed).to.equal(false); + const transferId = await backing.getTransferId(nonce, issuingChainId, originalAddress, user01.address, recipient, amount); + const requestInfo = await backing.requestInfos(transferId); + expect(requestInfo.isRequested).to.equal(true); + expect(requestInfo.hasRefundForFailed).to.equal(false); if (result == true && !usingGuard) { expect(balanceRecipientAfter - balanceRecipientBefore).to.equal(amount); expect(balanceBackingAfter - balanceBackingBefore).to.equal(amount); @@ -179,26 +186,29 @@ describe("xtoken tests", () => { expect(balanceRecipientAfter - balanceRecipientBefore).to.equal(0); expect(balanceBackingAfter - balanceBackingBefore).to.equal(amount); } + return nonce; } async function burnAndRemoteUnlock( originalAddress, - recipient, amount, fee, usingGuard, result ) { + const recipient = user01.address; + const nonce = generateNonce(); const xTokenAddress = xTokens[originalAddress]; - const balanceUserBefore = await balanceOf(xTokenAddress, owner.address); + const balanceUserBefore = await balanceOf(xTokenAddress, user02.address); const balanceRecipientBefore = await balanceOf(originalAddress, recipient); const balanceBackingBefore = await balanceOf(originalAddress, backing.address); - const transaction = await issuing.burnAndRemoteUnlock( + const transaction = await issuing.connect(user02).burnAndRemoteUnlock( xTokenAddress, recipient, amount, + nonce, 0, {value: ethers.utils.parseEther(fee)} ); @@ -207,12 +217,12 @@ describe("xtoken tests", () => { const balanceRecipientAfter = await balanceOf(originalAddress, recipient); const balanceBackingAfter = await balanceOf(originalAddress, backing.address); - const balanceUserAfter = await balanceOf(xTokenAddress, owner.address); + const balanceUserAfter = await balanceOf(xTokenAddress, user02.address); - const messageId = await issuingMessager.latestSentMessageId(); - const burnInfo = await issuing.burnMessages(messageId); - expect(burnInfo.hash).not.to.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); - expect(burnInfo.hasRefundForFailed).to.equal(false); + const transferId = await backing.getTransferId(nonce, backingChainId, originalAddress, user02.address, recipient, amount); + const requestInfo = await issuing.requestInfos(transferId); + expect(requestInfo.isRequested).to.equal(true); + expect(requestInfo.hasRefundForFailed).to.equal(false); expect(balanceUserBefore.sub(balanceUserAfter)).to.equal(amount); if (result && !usingGuard) { @@ -227,25 +237,27 @@ describe("xtoken tests", () => { } expect(balanceRecipientAfter.sub(balanceRecipientBefore)).to.equal(0); } + return nonce; } async function requestRemoteUnlockForIssuingFailure( - transferId, originalToken, - originalSender, amount, + nonce, fee, result ) { + const originalSender = user01.address; + const recipient = user02.address; const balanceBackingBefore = await balanceOf(originalToken, backing.address); const balanceSenderBefore = await balanceOf(originalToken, originalSender); - const balanceSlasherBefore = await balanceOf(originalToken, slasher.address); - const transaction = await issuing.connect(slasher).requestRemoteUnlockForIssuingFailure( - transferId, + const transaction = await issuing.requestRemoteUnlockForIssuingFailure( backingChainId, originalToken, originalSender, + recipient, amount, + nonce, 0, { value: ethers.utils.parseEther(fee), @@ -254,18 +266,17 @@ describe("xtoken tests", () => { ); const balanceSenderAfter = await balanceOf(originalToken, originalSender); const balanceBackingAfter = await balanceOf(originalToken, backing.address); - const balanceSlasherAfter = await balanceOf(originalToken, slasher.address); let receipt = await transaction.wait(); let gasFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice); - expect(balanceSlasherBefore.sub(balanceSlasherAfter)).to.be.equal(gasFee.add(ethers.utils.parseEther(fee))); - const lockInfo = await backing.lockedMessages(transferId); - expect(lockInfo.hash).not.to.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); + const transferId = await backing.getTransferId(nonce, issuingChainId, originalToken, originalSender, recipient, amount); + const requestInfo = await backing.requestInfos(transferId); if (result) { expect(balanceSenderAfter.sub(balanceSenderBefore)).to.be.equal(amount); expect(balanceBackingBefore.sub(balanceBackingAfter)).to.be.equal(amount); - expect(lockInfo.hasRefundForFailed).to.equal(true); + expect(requestInfo.isRequested).to.equal(true); + expect(requestInfo.hasRefundForFailed).to.equal(true); } else { expect(balanceSenderAfter.sub(balanceSenderBefore)).to.be.equal(0); expect(balanceBackingBefore.sub(balanceBackingAfter)).to.be.equal(0); @@ -273,22 +284,25 @@ describe("xtoken tests", () => { } async function requestRemoteIssuingForUnlockFailure( - transferId, originalToken, - originalSender, amount, + nonce, fee, result ) { + const originalSender = user02.address; + const recipient = user01.address; + const xTokenAddress = xTokens[originalToken]; const balanceSenderBefore = await balanceOf(xTokenAddress, originalSender); await backing.requestRemoteIssuingForUnlockFailure( - transferId, issuingChainId, originalToken, originalSender, + recipient, amount, + nonce, 0, {value: ethers.utils.parseEther(fee)} ); @@ -352,7 +366,6 @@ describe("xtoken tests", () => { await expect(lockAndRemoteIssuing( nativeTokenAddress, - owner.address, 100, "0.9", false, @@ -360,18 +373,16 @@ describe("xtoken tests", () => { )).to.be.revertedWith("fee is not enough"); // success lock and remote xtoken - await lockAndRemoteIssuing( + const nonce01 = await lockAndRemoteIssuing( nativeTokenAddress, - owner.address, 500, "1.1", false, true ); // success burn and remote unlock - await burnAndRemoteUnlock( + const nonce02 = await burnAndRemoteUnlock( nativeTokenAddress, - user.address, 100, "1.1", false, @@ -380,26 +391,23 @@ describe("xtoken tests", () => { // test refund failed if the message has been successed await expect(requestRemoteUnlockForIssuingFailure( - await issuingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, - 100, + 500, + nonce01, "1.1", true )).to.be.revertedWith("success message can't refund for failed"); await expect(requestRemoteIssuingForUnlockFailure( - await backingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, 100, + nonce02, "1.1", true )).to.be.revertedWith("success message can't refund for failed"); // lock exceed daily limit - await lockAndRemoteIssuing( + const nonce03 = await lockAndRemoteIssuing( nativeTokenAddress, - owner.address, 501, "1.1", false, @@ -407,46 +415,41 @@ describe("xtoken tests", () => { ); // refund (when isssuing failed) await requestRemoteUnlockForIssuingFailure( - await issuingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, 501, + nonce03, "1.1", true ); // the params not right // 1. amount await requestRemoteUnlockForIssuingFailure( - await issuingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, 500, + nonce03, "1.1", false ); // receiver await requestRemoteUnlockForIssuingFailure( - await issuingMessager.latestRecvMessageId(), nativeTokenAddress, - relayer.address, 501, + nonce03, "1.1", false ); // refund twice await requestRemoteUnlockForIssuingFailure( - await issuingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, 501, + nonce03, "1.1", false ); // burn failed await mockBackingMsgline.setRecvFailed(); - await burnAndRemoteUnlock( + const nonce04 = await burnAndRemoteUnlock( nativeTokenAddress, - user.address, 100, "1.1", false, @@ -454,28 +457,25 @@ describe("xtoken tests", () => { ); // invalid args await requestRemoteIssuingForUnlockFailure( - await backingMessager.latestRecvMessageId(), nativeTokenAddress, - user.address, 101, + nonce04, "1.1", false ); // refund (when unlock failed) await requestRemoteIssuingForUnlockFailure( - await backingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, 100, + nonce04, "1.1", true ); // refund twice await requestRemoteIssuingForUnlockFailure( - await backingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, 100, + nonce04, "1.1", false ); @@ -485,42 +485,42 @@ describe("xtoken tests", () => { await issuing.updateGuard(issuingGuard.address); // lock -> issuing using guard - await lockAndRemoteIssuing( + const nonce05 = await lockAndRemoteIssuing( nativeTokenAddress, - owner.address, 10, "1.1", true,//using guard true ); + const transferId = await backing.getTransferId(nonce05, issuingChainId, nativeTokenAddress, user01.address, user02.address, 10); await guardClaim( issuingGuard, issuing.address, - await issuingMessager.latestRecvMessageId(), + transferId, await getBlockTimestamp(), [guards[0], guards[1]], xTokens[nativeTokenAddress], - owner.address, + user02.address, 10 ); // burn -> unlock using guard (native token) - await burnAndRemoteUnlock( + const nonce06 = await burnAndRemoteUnlock( nativeTokenAddress, - user.address, 20, "1.1", true, //using guard true ); + const transferId06 = await backing.getTransferId(nonce06, backingChainId, nativeTokenAddress, user02.address, user01.address, 20); await guardClaim( backingGuard, backing.address, - await backingMessager.latestRecvMessageId(), + transferId06, await getBlockTimestamp(), [guards[0], guards[1]], // native token must be claimed by wtoken weth.address, - user.address, + user01.address, 20 ); // claim twice @@ -531,51 +531,20 @@ describe("xtoken tests", () => { await getBlockTimestamp(), [guards[0], guards[1]], weth.address, - user.address, + user01.address, 20 )).to.be.revertedWith("Guard: Invalid id to claim"); // test message slashed await mockIssuingMsgline.setNeverDelivered(); // this message will be never delivered - await lockAndRemoteIssuing( + const nonce07 = await lockAndRemoteIssuing( nativeTokenAddress, - owner.address, 10, "1.1", true, false ); - // can't request refund - await expect(requestRemoteUnlockForIssuingFailure( - await backingMessager.latestSentMessageId(), - nativeTokenAddress, - owner.address, - 10, - "1.1", - false - )).to.be.revertedWith("message not delivered"); - // start expire - await issuingMessager.slashMessage(backingMessager.latestSentMessageId()); - // still can't refund - await expect(requestRemoteUnlockForIssuingFailure( - await backingMessager.latestSentMessageId(), - nativeTokenAddress, - owner.address, - 10, - "1.1", - false - )).to.be.revertedWith("message not delivered"); - // time expired, and refund successed - await network.provider.send("evm_increaseTime", [Number(await issuingMessager.SLASH_EXPIRE_TIME())]); - requestRemoteUnlockForIssuingFailure( - await backingMessager.latestSentMessageId(), - nativeTokenAddress, - owner.address, - 10, - "1.1", - true - ); }); });