From 11fd1c4e4eb712f963c24cc646f00bdb3f38a80c Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 6 Dec 2023 11:49:06 +0800 Subject: [PATCH 01/26] init xtoken bridge v3 --- .../contracts/interfaces/IMessageLine.sol | 30 +++ .../contracts/interfaces/IMessager.sol | 18 ++ .../mapping-token/v3/base/xTokenBacking.sol | 185 ++++++++++++++++++ .../v3/base/xTokenBridgeBase.sol | 83 ++++++++ .../mapping-token/v3/base/xTokenErc20.sol | 118 +++++++++++ .../mapping-token/v3/base/xTokenIssuing.sol | 180 +++++++++++++++++ .../v3/interfaces/IxTokenBacking.sol | 16 ++ .../v3/interfaces/IxTokenIssuing.sol | 17 ++ .../mapping-token/v3/xTokenBridge.sol.bak | 172 ++++++++++++++++ .../messagers/DarwiniaMsglineMessager.sol | 119 +++++++++++ .../contracts/utils/AccessController.sol | 42 ++++ .../contracts/utils/TokenTransferHelper.sol | 43 ++++ 12 files changed, 1023 insertions(+) create mode 100644 helix-contract/contracts/interfaces/IMessageLine.sol create mode 100644 helix-contract/contracts/interfaces/IMessager.sol create mode 100644 helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol create mode 100644 helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol create mode 100644 helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol create mode 100644 helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol create mode 100644 helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol create mode 100644 helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol create mode 100644 helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak create mode 100644 helix-contract/contracts/messagers/DarwiniaMsglineMessager.sol create mode 100644 helix-contract/contracts/utils/AccessController.sol create mode 100644 helix-contract/contracts/utils/TokenTransferHelper.sol diff --git a/helix-contract/contracts/interfaces/IMessageLine.sol b/helix-contract/contracts/interfaces/IMessageLine.sol new file mode 100644 index 00000000..33440856 --- /dev/null +++ b/helix-contract/contracts/interfaces/IMessageLine.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +interface IMessageLine { + function send(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external payable; + function fee(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external view returns (uint256); + function sentMessageId() external view returns(bytes32); + function recvMessageId() external view returns(bytes32); + function dones(bytes32) external view returns(bool); +} + +abstract contract Application { + function _msgSender() internal view returns (address payable _line) { + _line = payable(msg.sender); + } + + function _fromChainId() internal pure returns (uint256 _msgDataFromChainId) { + require(msg.data.length >= 52, "!fromChainId"); + assembly { + _msgDataFromChainId := calldataload(sub(calldatasize(), 52)) + } + } + + function _xmsgSender() internal pure returns (address payable _from) { + require(msg.data.length >= 20, "!line"); + assembly { + _from := shr(96, calldataload(sub(calldatasize(), 20))) + } + } +} diff --git a/helix-contract/contracts/interfaces/IMessager.sol b/helix-contract/contracts/interfaces/IMessager.sol new file mode 100644 index 00000000..34d3aa12 --- /dev/null +++ b/helix-contract/contracts/interfaces/IMessager.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; +} + +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +} + +interface IMessageId { + function latestSentMessageId() external view returns(bytes32); + function latestRecvMessageId() external view returns(bytes32); + function messageDelivered(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 new file mode 100644 index 00000000..dd0faf37 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "./xTokenBridgeBase.sol"; +import "../interfaces/IxTokenIssuing.sol"; +import "../../interfaces/IGuard.sol"; +import "../../interfaces/IWToken.sol"; +import "../../../utils/TokenTransferHelper.sol"; + +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 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 TokenUnlockedForFailed(bytes32 transferId, uint256 remoteChainId, address token, address recipient, uint256 amount); + + function registerOriginalToken( + uint256 _remoteChainId, + address _originalToken, + address _xToken + ) external onlyDao { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); + originalToken2xTokens[key] = _xToken; + } + + function lockAndRemoteIssuing( + uint256 _remoteChainId, + address _originalToken, + address _recipient, + uint256 _amount, + bytes memory _extParams + ) external payable { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); + require(originalToken2xTokens[key] != address(0), "token not registered"); + + uint256 prepaid = msg.value; + // lock token + if (address(0) == _originalToken) { + // native token + require(msg.value > _amount, "invalid value"); + prepaid -= _amount; + } else { + // erc20 token + TokenTransferHelper.safeTransferFrom( + _originalToken, + msg.sender, + address(this), + _amount + ); + } + bytes memory issueMappingToken = abi.encodeWithSelector( + IxTokenIssuing.issuexToken.selector, + block.chainid, + _originalToken, + _recipient, + _amount + ); + bytes32 transferId = _sendMessage(_remoteChainId, issueMappingToken, prepaid, _extParams); + bytes32 lockMessageHash = keccak256(abi.encodePacked(transferId, _remoteChainId, _originalToken, msg.sender, _amount)); + lockedMessages[transferId] = LockedInfo(lockMessageHash, false); + emit TokenLocked(transferId, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); + } + + // in backing contract, it only know the original token info + // in issuing contract, it know the mapping relationship of original token and it's mapping token xToken + // we use original token info in messages + + // method for backing + // receive unlock original token message from remote issuing contract + function unlockFromRemote( + uint256 _remoteChainId, + address _originalToken, + address _recipient, + uint256 _amount + ) external calledByMessager(_remoteChainId) whenNotPaused { + bytes32 transferId = _latestRecvMessageId(_remoteChainId); + require(unlockedTransferIds[transferId] == false, "message has been accepted"); + unlockedTransferIds[transferId] = true; + + // native token do not use guard + if (address(0) == _originalToken) { + _unlockNativeToken(transferId, _recipient, _amount); + } else { + _unlockErc20Token(transferId, _originalToken, _recipient, _amount); + } + emit TokenUnlocked(transferId, _remoteChainId, _originalToken, _recipient, _amount); + } + + function _unlockNativeToken( + bytes32 _transferId, + address _recipient, + uint256 _amount + ) internal { + address _guard = guard; + if (_guard == address(0)) { + TokenTransferHelper.safeTransferNative(_recipient, _amount); + } else { + IWToken(wToken).deposit{value: _amount}(); + // see https://github.com/helix-bridge/contracts/issues/18 + uint allowance = IERC20(wToken).allowance(address(this), _guard); + require(IERC20(wToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(_transferId), wToken, _recipient, _amount); + } + } + + function _unlockErc20Token( + bytes32 _transferId, + address _token, + address _recipient, + uint256 _amount + ) internal { + address _guard = guard; + if (_guard == address(0)) { + TokenTransferHelper.safeTransfer(_token, _recipient, _amount); + } else { + uint allowance = IERC20(_token).allowance(address(this), _guard); + require(IERC20(_token).approve(_guard, allowance + _amount), "Backing:approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(_transferId), _token, _recipient, _amount); + } + } + + function requestRemoteIssuingForUnlockFailure( + bytes32 _transferId, + uint256 _remoteChainId, + address _originalToken, + address _originalSender, + uint256 _amount, + bytes memory _extParams + ) external payable { + // must not exist in successful issue list + require(unlockedTransferIds[_transferId] == false, "success message can't refund for failed"); + _assertMessageIsDelivered(_remoteChainId, _transferId); + bytes memory unlockForFailed = abi.encodeWithSelector( + IxTokenIssuing.handleIssuingForUnlockFailureFromRemote.selector, + block.chainid, + _transferId, + _originalToken, + _originalSender, + _amount + ); + bytes32 refundId = _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); + emit RemoteIssuingFailure(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); + } + + // when lock and issuing failed + // receive unlock(refund) message from remote issuing contract + // this will refund original token to original sender + function handleUnlockForIssuingFailureFromRemote( + uint256 _remoteChainId, + bytes32 _transferId, + address _originalToken, + address _originSender, + uint256 _amount + ) external calledByMessager(_remoteChainId) whenNotPaused { + LockedInfo memory lockedMessage = lockedMessages[_transferId]; + require(lockedMessage.hasRefundForFailed == false, "the locked message has been refund"); + bytes32 messageHash = keccak256(abi.encodePacked(_remoteChainId, _transferId, _originalToken, _originSender, _amount)); + require(lockedMessage.hash == messageHash, "message is not matched"); + lockedMessages[_transferId].hasRefundForFailed = true; + if (_originalToken == address(0)) { + TokenTransferHelper.safeTransferNative(_originSender, _amount); + } else { + TokenTransferHelper.safeTransfer(_originalToken, _originSender, _amount); + } + emit TokenUnlockedForFailed(_transferId, _remoteChainId, _originalToken, _originSender, _amount); + } +} + diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol new file mode 100644 index 00000000..536a0d87 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@zeppelin-solidity/contracts/security/Pausable.sol"; +import "../../../interfaces/IMessager.sol"; +import "../../../utils/AccessController.sol"; +import "../../../utils/DailyLimit.sol"; +import "../../../utils/TokenTransferHelper.sol"; + +contract xTokenBridgeBase is Pausable, AccessController, DailyLimit { + struct MessagerService { + address sendService; + address receiveService; + } + + string public version; + uint256 public protocolFee; + address public guard; + // remoteChainId => info + mapping(uint256 => MessagerService) public messagers; + + // common method + modifier calledByMessager(uint256 _remoteChainId) { + address receiveService = messagers[_remoteChainId].receiveService; + require(receiveService == msg.sender, "invalid messager"); + _; + } + + receive() external payable {} + + function setSendService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { + messagers[_remoteChainId].sendService = _service; + ILowLevelMessageSender(_service).registerRemoteReceiver(_remoteChainId, _remoteBridge); + } + + function setReceiveService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { + messagers[_remoteChainId].receiveService = _service; + ILowLevelMessageReceiver(_service).registerRemoteSender(_remoteChainId, _remoteBridge); + } + + function withdrawProtocolFee(address _receiver, uint256 _amount) external onlyDao { + require(_amount <= protocolFee, "not enough fee"); + TokenTransferHelper.safeTransferNative(_receiver, _amount); + } + + function _sendMessage( + uint256 _remoteChainId, + bytes memory _payload, + uint256 feePrepaid, + bytes memory _extParams + ) internal whenNotPaused returns(bytes32 messageId) { + MessagerService memory service = messagers[_remoteChainId]; + require(service.sendService != address(0), "bridge not configured"); + ILowLevelMessageSender(service.sendService).sendMessage{value: feePrepaid - protocolFee}( + _remoteChainId, + _payload, + _extParams + ); + messageId = IMessageId(service.sendService).latestSentMessageId(); + } + + 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).messageDelivered(_transferId), "message not delivered"); + } + + 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(); + } + + // settings + function updateGuard(address _guard) external onlyDao { + guard = _guard; + } + + function setDailyLimit(address _token, uint256 _dailyLimit) external onlyDao { + _setDailyLimit(_token, _dailyLimit); + } +} + diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol new file mode 100644 index 00000000..7ef47161 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@zeppelin-solidity/contracts/access/Ownable.sol"; +import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "@zeppelin-solidity/contracts/utils/math/SafeMath.sol"; + +contract xTokenErc20 is IERC20, Ownable { + using SafeMath for uint256; + + mapping (address => uint256) private _balances; + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string public name; + string public symbol; + uint8 public decimals; + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + _transferOwnership(_msgSender()); + } + + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, msg.sender, _allowances[sender][msg.sender].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } + + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue)); + return true; + } + + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + // only factory contract can mint with the lock proof from ethereum + function mint(address account, uint256 amount) external onlyOwner { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + if (account != msg.sender && owner() != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { + _approve(account, msg.sender, _allowances[account][msg.sender].sub(amount, "ERC20: decreased allowance below zero")); + } + _burn(account, amount); + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} + diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol new file mode 100644 index 00000000..43f76095 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./xTokenErc20.sol"; +import "./xTokenBridgeBase.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) burnMessages; + // transferId => bool + mapping(bytes32 => bool) public issueTransferIds; + + // original Token => xToken mapping is saved in Issuing Contract + // salt => xToken address + mapping(bytes32 => address) xTokens; + // xToken => Origin Token Info + mapping(address => OriginalTokenInfo) originalTokens; + + event IssuingERC20Created(uint256 originalChainId, address originalToken, address xToken); + event IssuingERC20Updated(uint256 originalChainId, address originalToken, address xToken, address oldxToken); + event RemoteUnlockForIssuingFailureRequested(bytes32 refundId, bytes32 transferId, address originalToken, address originalSender, uint256 amount, uint256 fee); + + function registerxToken( + uint256 _originalChainId, + address _originalToken, + string memory _originalChainName, + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _dailyLimit + ) external onlyDao returns (address xToken) { + bytes32 salt = keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); + require(xTokens[salt] == address(0), "contract has been deployed"); + bytes memory bytecode = type(xTokenErc20).creationCode; + bytes memory bytecodeWithInitdata = abi.encodePacked( + bytecode, + abi.encode( + string(abi.encodePacked(_name, "[", _originalChainName, ">")), + string(abi.encodePacked("x", _symbol)), + _decimals + )); + assembly { + xToken := create2(0, add(bytecodeWithInitdata, 0x20), mload(bytecodeWithInitdata), salt) + if iszero(extcodesize(xToken)) { revert(0, 0) } + } + xTokens[salt] = xToken; + originalTokens[xToken] = OriginalTokenInfo(_originalChainId, _originalToken); + _setDailyLimit(xToken, _dailyLimit); + emit IssuingERC20Created(_originalChainId, _originalToken, xToken); + } + + function updatexToken( + uint256 _originalChainId, + address _originalToken, + address _xToken + ) external onlyDao { + bytes32 salt = keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); + address oldxToken = xTokens[salt]; + if (oldxToken != address(0)) { + require(xTokenErc20(oldxToken).totalSupply() == 0, "can't delete old xToken"); + delete originalTokens[oldxToken]; + } + xTokens[salt] = _xToken; + originalTokens[_xToken] = OriginalTokenInfo(_originalChainId, _originalToken); + emit IssuingERC20Updated(_originalChainId, _originalToken, _xToken, oldxToken); + } + + // receive issuing xToken message from remote backing contract + function issuexToken( + uint256 _remoteChainId, + address _originalToken, + address _recipient, + uint256 _amount + ) external calledByMessager(_remoteChainId) whenNotPaused { + bytes32 transferId = _latestRecvMessageId(_remoteChainId); + bytes32 salt = keccak256(abi.encodePacked(_remoteChainId, _originalToken, version)); + address xToken = xTokens[salt]; + require(xToken != address(0), "xToken not exist"); + require(_amount > 0, "can not receive amount zero"); + + require(issueTransferIds[transferId] == false, "message has been accepted"); + issueTransferIds[transferId] = true; + + address _guard = guard; + if (_guard != address(0)) { + xTokenErc20(xToken).mint(address(this), _amount); + uint allowance = xTokenErc20(xToken).allowance(address(this), _guard); + require(xTokenErc20(xToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(transferId), xToken, _recipient, _amount); + } else { + xTokenErc20(xToken).mint(_recipient, _amount); + } + // emit event + } + + function burnAndRemoteUnlock( + address _xToken, + address _recipient, + uint256 _amount, + bytes memory _extParams + ) external payable { + require(_amount > 0, "can not transfer amount zero"); + OriginalTokenInfo memory originalInfo = originalTokens[_xToken]; + // transfer to this and then burn + TokenTransferHelper.safeTransferFrom(_xToken, msg.sender, address(this), _amount); + xTokenErc20(_xToken).burn(address(this), _amount); + + bytes memory remoteUnlockCall = abi.encodeWithSelector( + IxTokenBacking.unlockFromRemote.selector, + originalInfo.token, + _recipient, + _amount + ); + bytes32 transferId = _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); + + require(burnMessages[transferId].hash == bytes32(0), "message exist"); + bytes32 messageHash = keccak256(abi.encodePacked(transferId, _xToken, msg.sender, _amount)); + burnMessages[transferId] = BurnInfo(messageHash, false); + //emit BurnAndRemoteUnlocked(transferId, msg.sender, _recipient, _xToken, _amount, fee); + } + + function requestRemoteUnlockForIssuingFailure( + bytes32 _transferId, + uint256 _originalChainId, + address _originalToken, + address _originalSender, + uint256 _amount, + bytes memory _extParams + ) external payable { + require(issueTransferIds[_transferId] == false, "success message can't refund for failed"); + _assertMessageIsDelivered(_originalChainId, _transferId); + bytes memory handleUnlockForFailed = abi.encodeWithSelector( + IxTokenBacking.handleUnlockForIssuingFailureFromRemote.selector, + _transferId, + _originalToken, + _originalSender, + _amount + ); + bytes32 refundId = _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); + emit RemoteUnlockForIssuingFailureRequested(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); + } + + // when burn and unlock failed + // receive reIssue(refund) message from remote backing contract + // this will refund xToken to original sender + function handleIssuingForUnlockFailureFromRemote( + uint256 _originalChainId, + bytes32 _transferId, + address _originalToken, + address _originalSender, + uint256 _amount + ) external calledByMessager(_originalChainId) whenNotPaused { + BurnInfo memory burnInfo = burnMessages[_transferId]; + require(burnInfo.hasRefundForFailed == false, "Backing:the burn message has been refund"); + bytes32 messageHash = keccak256(abi.encodePacked(_transferId, _originalChainId, _originalSender, _originalToken, _amount)); + require(burnInfo.hash == messageHash, "message is not matched"); + burnMessages[_transferId].hasRefundForFailed = true; + + bytes32 salt = keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); + address xToken = xTokens[salt]; + require(xToken != address(0), "xToken not exist"); + + xTokenErc20(xToken).mint(_originalSender, _amount); + //emit TokenRemintForFailed(_transferId, xToken, _originalSender, _amount); + } +} + diff --git a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol new file mode 100644 index 00000000..06f842e5 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +interface IxTokenBacking { + function unlockFromRemote( + address originalToken, + address recipient, + uint256 amount + ) external; + + function handleUnlockForIssuingFailureFromRemote( + address originalToken, + address originalSender, + uint256 amount + ) external; +} diff --git a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol new file mode 100644 index 00000000..eb258f59 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +interface IxTokenIssuing { + function handleIssuingForUnlockFailureFromRemote( + bytes32 transferId, + address originalToken, + address originalSender, + uint256 amount + ) external; + + function issuexToken( + address originalToken, + address recipient, + uint256 amount + ) external; +} diff --git a/helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak b/helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak new file mode 100644 index 00000000..943b7bf1 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./base/xTokenBacking.sol"; +import "./base/xTokenIssuing.sol"; +import "./interfaces/IMessager.sol"; +import "../../utils/AccessController.sol"; +import "../../utils/DailyLimit.sol"; + +contract xTokenMessageLineMessager is Application { + struct RemoteBridgeInfo { + address messager; + address tokenBridge; + } + + string public version; + uint256 public protocolFee; + // remoteChainId => info + mapping(uint256 => RemoteBridgeInfo) bridges; + + // common method + modifier calledByMessager() { + uint256 _remoteChainId = _fromChainId(); + RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; + require(msg.sender == bridgeInfo.messager, "invalid msg sender"); + require(_xmsgSender() == bridgeInfo.tokenBridge, "invalid source msg sender"); + _; + } + + function withdrawProtocolFee(address _receiver, uint256 _amount) external onlyDao { + require(_amount <= protocolFee, "not enough fee"); + TokenTransferHelper.safeTransferNative(_receiver, _amount); + } + + function totalFee(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) external returns(uint256) { + RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; + require(bridgeInfo.messager != address(0), "bridge not configured"); + return IMessager(bridgeInfo.messager).fee(_remoteChainId, bridgeInfo.tokenBridge, _payload, _extParams) + protocolFee; + } + + function _sendMessage( + uint256 _remoteChainId, + bytes memory _payload, + uint256 feePrepaid, + bytes memory _extParams + ) internal whenNotPaused override returns(bytes32 messageId, uint256 fee) { + RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; + require(bridgeInfo.messager != address(0), "bridge not configured"); + fee = IMessager(bridgeInfo.messager).fee(_remoteChainId, bridgeInfo.tokenBridge, _payload, _extParams); + IMessager(bridgeInfo.messager).send{value: feePrepaid - fee - protocolFee}( + _remoteChainId, + bridgeInfo.tokenBridge, + _payload, + _extParams + ); + messageId = IMessager(bridgeInfo.messager).sentMessageId(); + } + + function _assertMessageIsDelivered(uint256 _remoteChainId, bytes32 _transferId) internal override { + RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; + require(bridgeInfo.messager != address(0), "bridge not configured"); + require(IMessager(bridgeInfo.messager).dones(_transferId), "message not delivered"); + } + + function _messageId() internal returns(bytes32) { + return IMessager(msg.sender).recvMessageId(); + } + + function _version() internal returns(string memory) { + return version; + } + + // settings + function updateBackingGuard(address _guard) external onlyDao { + _updateBackingGuard(_guard); + } + + function updateIssuingGuard(address _guard) external onlyDao { + _updateIssuingGuard(_guard); + } + + function registerOriginalToken( + uint256 _remoteChainId, + address _originalToken, + address _xToken + ) external onlyDao { + _registerOriginalToken(_remoteChainId, _originalToken, _xToken); + } + + function registerxToken( + uint256 _originalChainId, + address _originalToken, + string memory _originalChainName, + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _dailyLimit + ) external onlyDao { + address xToken = _registerxToken( + _originalChainId, + _originalToken, + _originalChainName, + _name, + _symbol, + _decimals + ); + _setDailyLimit(xToken, _dailyLimit); + } + + function updatexToken( + uint256 _originalChainId, + address _originalToken, + address _xToken + ) external onlyDao { + _updatexToken(_originalChainId, _originalToken, _xToken); + } + + function setDailyLimit(address _token, uint256 _dailyLimit) external onlyDao { + _setDailyLimit(_token, _dailyLimit); + } + + // in backing contract, it only know the original token info + // in issuing contract, it know the mapping relationship of original token and it's mapping token xToken + // we use original token info in messages + + // method for backing + // receive unlock original token message from remote issuing contract + function unlockFromRemote( + address _originalToken, + address _recipient, + uint256 _amount + ) external calledByMessager whenNotPaused { + bytes32 transferId = _messageId(); + _unlockFromRemote(_fromChainId(), transferId, _originalToken, _recipient, _amount); + } + + // when lock and issuing failed + // receive unlock(refund) message from remote issuing contract + // this will refund original token to original sender + function handleUnlockForIssuingFailureFromRemote( + address _originalToken, + address _originalSender, + uint256 _amount + ) external calledByMessager whenNotPaused { + bytes32 transferId = _messageId(); + _handleUnlockForIssuingFailureFromRemote(_fromChainId(), transferId, _originalToken, _originalSender, _amount); + } + + // method for issuing + // receive issuing xToken message from remote backing contract + function issuexToken( + address _originalToken, + address _recipient, + uint256 _amount + ) external calledByMessager whenNotPaused { + bytes32 transferId = _messageId(); + _issuexToken(_fromChainId(), transferId, _originalToken, _recipient, _amount); + } + + // when burn and unlock failed + // receive reIssue(refund) message from remote backing contract + // this will refund xToken to original sender + function handleIssuingForUnlockFailureFromRemote( + address _originalToken, + address _originalSender, + uint256 _amount + ) external calledByMessager whenNotPaused { + bytes32 transferId = _messageId(); + _handleIssuingForUnlockFailureFromRemote(_fromChainId(), transferId, _originalToken, _originalSender, _amount); + } +} + diff --git a/helix-contract/contracts/messagers/DarwiniaMsglineMessager.sol b/helix-contract/contracts/messagers/DarwiniaMsglineMessager.sol new file mode 100644 index 00000000..cf56b891 --- /dev/null +++ b/helix-contract/contracts/messagers/DarwiniaMsglineMessager.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "../utils/AccessController.sol"; +import "../interfaces/IMessageLine.sol"; + +contract DarwiniaMsglineMessager is Application, AccessController { + IMessageLine public immutable msgline; + + struct RemoteMessager { + uint256 msglineRemoteChainId; + address messager; + } + + mapping(address=>bool) public whiteList; + // app remoteChainId => msgline remote messager + mapping(uint256=>RemoteMessager) public remoteMessagers; + + // token bridge pair + // hash(msglineRemoteChainId, localAppAddress) => remoteAppAddress + mapping(bytes32=>address) public remoteAppReceivers; + mapping(bytes32=>address) public remoteAppSenders; + + event CallerUnMatched(uint256 srcAppChainId, address srcAppAddress); + event CallResult(uint256 srcAppChainId, bool result); + + modifier onlyWhiteList() { + require(whiteList[msg.sender], "invalid msg.sender"); + _; + } + + modifier onlyMsgline() { + require(msg.sender == address(msgline), "invalid caller"); + _; + } + + //event CallResult(string sourceChain, string srcAddress, bool successed); + + constructor(address _dao, address _msgline) { + _initialize(_dao); + msgline = IMessageLine(_msgline); + } + + function setRemoteMessager(uint256 _appRemoteChainId, uint256 _msglineRemoteChainId, address _remoteMessager) onlyDao external { + remoteMessagers[_appRemoteChainId] = RemoteMessager(_msglineRemoteChainId, _remoteMessager); + } + + function setWhiteList(address _caller, bool _enable) external onlyDao { + whiteList[_caller] = _enable; + } + + function registerRemoteReceiver(uint256 _remoteChainId, address _remoteBridge) onlyWhiteList external { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.msglineRemoteChainId, msg.sender)); + remoteAppReceivers[key] = _remoteBridge; + } + + function registerRemoteSender(uint256 _remoteChainId, address _remoteBridge) onlyWhiteList external { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.msglineRemoteChainId, msg.sender)); + remoteAppSenders[key] = _remoteBridge; + } + + function sendMessage(uint256 _remoteChainId, bytes memory _message, bytes memory _params) onlyWhiteList external payable { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.msglineRemoteChainId, msg.sender)); + address remoteAppAddress = remoteAppReceivers[key]; + require(remoteAppAddress != address(0), "app pair not registered"); + bytes memory msglinePayload = messagePayload(msg.sender, remoteAppAddress, _message); + msgline.send{ value: msg.value }( + remoteMessager.msglineRemoteChainId, + remoteMessager.messager, + msglinePayload, + _params + ); + } + + function receiveMessage(uint256 _srcAppChainId, address _remoteAppAddress, address _localAppAddress, bytes memory _message) onlyMsgline external { + uint256 srcChainId = _fromChainId(); + RemoteMessager memory remoteMessager = remoteMessagers[_srcAppChainId]; + require(srcChainId == remoteMessager.msglineRemoteChainId, "invalid remote chainid"); + require(remoteMessager.messager == _xmsgSender(), "invalid remote messager"); + bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); + // check remote appSender + if (_remoteAppAddress != remoteAppSenders[key]) { + emit CallerUnMatched(_srcAppChainId, _remoteAppAddress); + return; + } + (bool success,) = _localAppAddress.call(_message); + // don't revert to prevent message block + emit CallResult(_srcAppChainId, success); + } + + function latestSentMessageId() external view returns(bytes32) { + return msgline.sentMessageId(); + } + + function latestRecvMessageId() external view returns(bytes32) { + return msgline.recvMessageId(); + } + + function messageDelivered(bytes32 messageId) external view returns(bool) { + return msgline.dones(messageId); + } + + function messagePayload(address _from, address _to, bytes memory _message) public view returns(bytes memory) { + return abi.encodeWithSelector( + DarwiniaMsglineMessager.receiveMessage.selector, + block.chainid, + _from, + _to, + _message + ); + } +} + diff --git a/helix-contract/contracts/utils/AccessController.sol b/helix-contract/contracts/utils/AccessController.sol new file mode 100644 index 00000000..c74d5fe0 --- /dev/null +++ b/helix-contract/contracts/utils/AccessController.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; + } + + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; + } +} + diff --git a/helix-contract/contracts/utils/TokenTransferHelper.sol b/helix-contract/contracts/utils/TokenTransferHelper.sol new file mode 100644 index 00000000..bd94b59c --- /dev/null +++ b/helix-contract/contracts/utils/TokenTransferHelper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; + +library TokenTransferHelper { + function safeTransfer( + address token, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transfer.selector, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transfer token failed"); + } + + function safeTransferFrom( + address token, + address sender, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transferFrom.selector, + sender, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transferFrom token failed"); + } + + function safeTransferNative( + address receiver, + uint256 amount + ) internal { + (bool success,) = payable(receiver).call{value: amount}(""); + require(success, "lnBridgeHelper:transfer native token failed"); + } +} + From 25f514616557ae989d6c732880c0c1fc0f64831b Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 6 Dec 2023 11:51:16 +0800 Subject: [PATCH 02/26] rm unused --- .../mapping-token/v3/xTokenBridge.sol.bak | 172 ------------------ 1 file changed, 172 deletions(-) delete mode 100644 helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak diff --git a/helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak b/helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak deleted file mode 100644 index 943b7bf1..00000000 --- a/helix-contract/contracts/mapping-token/v3/xTokenBridge.sol.bak +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import "./base/xTokenBacking.sol"; -import "./base/xTokenIssuing.sol"; -import "./interfaces/IMessager.sol"; -import "../../utils/AccessController.sol"; -import "../../utils/DailyLimit.sol"; - -contract xTokenMessageLineMessager is Application { - struct RemoteBridgeInfo { - address messager; - address tokenBridge; - } - - string public version; - uint256 public protocolFee; - // remoteChainId => info - mapping(uint256 => RemoteBridgeInfo) bridges; - - // common method - modifier calledByMessager() { - uint256 _remoteChainId = _fromChainId(); - RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; - require(msg.sender == bridgeInfo.messager, "invalid msg sender"); - require(_xmsgSender() == bridgeInfo.tokenBridge, "invalid source msg sender"); - _; - } - - function withdrawProtocolFee(address _receiver, uint256 _amount) external onlyDao { - require(_amount <= protocolFee, "not enough fee"); - TokenTransferHelper.safeTransferNative(_receiver, _amount); - } - - function totalFee(uint256 _remoteChainId, bytes memory _payload, bytes memory _extParams) external returns(uint256) { - RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; - require(bridgeInfo.messager != address(0), "bridge not configured"); - return IMessager(bridgeInfo.messager).fee(_remoteChainId, bridgeInfo.tokenBridge, _payload, _extParams) + protocolFee; - } - - function _sendMessage( - uint256 _remoteChainId, - bytes memory _payload, - uint256 feePrepaid, - bytes memory _extParams - ) internal whenNotPaused override returns(bytes32 messageId, uint256 fee) { - RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; - require(bridgeInfo.messager != address(0), "bridge not configured"); - fee = IMessager(bridgeInfo.messager).fee(_remoteChainId, bridgeInfo.tokenBridge, _payload, _extParams); - IMessager(bridgeInfo.messager).send{value: feePrepaid - fee - protocolFee}( - _remoteChainId, - bridgeInfo.tokenBridge, - _payload, - _extParams - ); - messageId = IMessager(bridgeInfo.messager).sentMessageId(); - } - - function _assertMessageIsDelivered(uint256 _remoteChainId, bytes32 _transferId) internal override { - RemoteBridgeInfo memory bridgeInfo = bridges[_remoteChainId]; - require(bridgeInfo.messager != address(0), "bridge not configured"); - require(IMessager(bridgeInfo.messager).dones(_transferId), "message not delivered"); - } - - function _messageId() internal returns(bytes32) { - return IMessager(msg.sender).recvMessageId(); - } - - function _version() internal returns(string memory) { - return version; - } - - // settings - function updateBackingGuard(address _guard) external onlyDao { - _updateBackingGuard(_guard); - } - - function updateIssuingGuard(address _guard) external onlyDao { - _updateIssuingGuard(_guard); - } - - function registerOriginalToken( - uint256 _remoteChainId, - address _originalToken, - address _xToken - ) external onlyDao { - _registerOriginalToken(_remoteChainId, _originalToken, _xToken); - } - - function registerxToken( - uint256 _originalChainId, - address _originalToken, - string memory _originalChainName, - string memory _name, - string memory _symbol, - uint8 _decimals, - uint256 _dailyLimit - ) external onlyDao { - address xToken = _registerxToken( - _originalChainId, - _originalToken, - _originalChainName, - _name, - _symbol, - _decimals - ); - _setDailyLimit(xToken, _dailyLimit); - } - - function updatexToken( - uint256 _originalChainId, - address _originalToken, - address _xToken - ) external onlyDao { - _updatexToken(_originalChainId, _originalToken, _xToken); - } - - function setDailyLimit(address _token, uint256 _dailyLimit) external onlyDao { - _setDailyLimit(_token, _dailyLimit); - } - - // in backing contract, it only know the original token info - // in issuing contract, it know the mapping relationship of original token and it's mapping token xToken - // we use original token info in messages - - // method for backing - // receive unlock original token message from remote issuing contract - function unlockFromRemote( - address _originalToken, - address _recipient, - uint256 _amount - ) external calledByMessager whenNotPaused { - bytes32 transferId = _messageId(); - _unlockFromRemote(_fromChainId(), transferId, _originalToken, _recipient, _amount); - } - - // when lock and issuing failed - // receive unlock(refund) message from remote issuing contract - // this will refund original token to original sender - function handleUnlockForIssuingFailureFromRemote( - address _originalToken, - address _originalSender, - uint256 _amount - ) external calledByMessager whenNotPaused { - bytes32 transferId = _messageId(); - _handleUnlockForIssuingFailureFromRemote(_fromChainId(), transferId, _originalToken, _originalSender, _amount); - } - - // method for issuing - // receive issuing xToken message from remote backing contract - function issuexToken( - address _originalToken, - address _recipient, - uint256 _amount - ) external calledByMessager whenNotPaused { - bytes32 transferId = _messageId(); - _issuexToken(_fromChainId(), transferId, _originalToken, _recipient, _amount); - } - - // when burn and unlock failed - // receive reIssue(refund) message from remote backing contract - // this will refund xToken to original sender - function handleIssuingForUnlockFailureFromRemote( - address _originalToken, - address _originalSender, - uint256 _amount - ) external calledByMessager whenNotPaused { - bytes32 transferId = _messageId(); - _handleIssuingForUnlockFailureFromRemote(_fromChainId(), transferId, _originalToken, _originalSender, _amount); - } -} - From b086d835e33a084e2f30d9a79084921f9a7bd411 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Thu, 7 Dec 2023 13:31:44 +0800 Subject: [PATCH 03/26] unit test --- .../mapping-token/v3/base/xTokenBacking.sol | 52 ++- .../v3/base/xTokenBridgeBase.sol | 21 +- .../mapping-token/v3/base/xTokenIssuing.sol | 76 +++- .../v3/interfaces/IxTokenBacking.sol | 3 + .../v3/interfaces/IxTokenIssuing.sol | 2 + ...sglineMessager.sol => MsglineMessager.sol} | 7 +- .../contracts/messagers/mock/MockMsgline.sol | 66 +++ helix-contract/test/6_test_xtoken_v3.js | 410 ++++++++++++++++++ 8 files changed, 605 insertions(+), 32 deletions(-) rename helix-contract/contracts/messagers/{DarwiniaMsglineMessager.sol => MsglineMessager.sol} (96%) create mode 100644 helix-contract/contracts/messagers/mock/MockMsgline.sol create mode 100644 helix-contract/test/6_test_xtoken_v3.js diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol index dd0faf37..31b5026d 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -31,13 +31,19 @@ contract xTokenBacking is xTokenBridgeBase { event RemoteIssuingFailure(bytes32 refundId, bytes32 transferId, address mappingToken, address originalSender, uint256 amount, uint256 fee); event TokenUnlockedForFailed(bytes32 transferId, uint256 remoteChainId, address token, address recipient, uint256 amount); + function setwToken(address _wtoken) external onlyDao { + wToken = _wtoken; + } + function registerOriginalToken( uint256 _remoteChainId, address _originalToken, - address _xToken + address _xToken, + uint256 _dailyLimit ) external onlyDao { bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); originalToken2xTokens[key] = _xToken; + _setDailyLimit(_originalToken, _dailyLimit); } function lockAndRemoteIssuing( @@ -65,19 +71,31 @@ contract xTokenBacking is xTokenBridgeBase { _amount ); } - bytes memory issueMappingToken = abi.encodeWithSelector( - IxTokenIssuing.issuexToken.selector, - block.chainid, + bytes memory issuxToken = encodeIssuexToken( _originalToken, _recipient, _amount ); - bytes32 transferId = _sendMessage(_remoteChainId, issueMappingToken, prepaid, _extParams); + bytes32 transferId = _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); bytes32 lockMessageHash = keccak256(abi.encodePacked(transferId, _remoteChainId, _originalToken, msg.sender, _amount)); lockedMessages[transferId] = LockedInfo(lockMessageHash, false); emit TokenLocked(transferId, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); } + function encodeIssuexToken( + address _originalToken, + address _recipient, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.issuexToken.selector, + block.chainid, + _originalToken, + _recipient, + _amount + ); + } + // in backing contract, it only know the original token info // in issuing contract, it know the mapping relationship of original token and it's mapping token xToken // we use original token info in messages @@ -90,6 +108,8 @@ contract xTokenBacking is xTokenBridgeBase { address _recipient, uint256 _amount ) external calledByMessager(_remoteChainId) whenNotPaused { + expendDailyLimit(_originalToken, _amount); + bytes32 transferId = _latestRecvMessageId(_remoteChainId); require(unlockedTransferIds[transferId] == false, "message has been accepted"); unlockedTransferIds[transferId] = true; @@ -147,9 +167,7 @@ contract xTokenBacking is xTokenBridgeBase { // must not exist in successful issue list require(unlockedTransferIds[_transferId] == false, "success message can't refund for failed"); _assertMessageIsDelivered(_remoteChainId, _transferId); - bytes memory unlockForFailed = abi.encodeWithSelector( - IxTokenIssuing.handleIssuingForUnlockFailureFromRemote.selector, - block.chainid, + bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( _transferId, _originalToken, _originalSender, @@ -159,6 +177,22 @@ contract xTokenBacking is xTokenBridgeBase { emit RemoteIssuingFailure(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); } + function encodeIssuingForUnlockFailureFromRemote( + bytes32 _transferId, + address _originalToken, + address _originalSender, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.handleIssuingForUnlockFailureFromRemote.selector, + block.chainid, + _transferId, + _originalToken, + _originalSender, + _amount + ); + } + // when lock and issuing failed // receive unlock(refund) message from remote issuing contract // this will refund original token to original sender @@ -171,7 +205,7 @@ contract xTokenBacking is xTokenBridgeBase { ) external calledByMessager(_remoteChainId) whenNotPaused { LockedInfo memory lockedMessage = lockedMessages[_transferId]; require(lockedMessage.hasRefundForFailed == false, "the locked message has been refund"); - bytes32 messageHash = keccak256(abi.encodePacked(_remoteChainId, _transferId, _originalToken, _originSender, _amount)); + bytes32 messageHash = keccak256(abi.encodePacked(_transferId, _remoteChainId, _originalToken, _originSender, _amount)); require(lockedMessage.hash == messageHash, "message is not matched"); lockedMessages[_transferId].hasRefundForFailed = true; if (_originalToken == address(0)) { diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol index 536a0d87..c02753ce 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; +import "@zeppelin-solidity/contracts/proxy/utils/Initializable.sol"; import "@zeppelin-solidity/contracts/security/Pausable.sol"; import "../../../interfaces/IMessager.sol"; import "../../../utils/AccessController.sol"; import "../../../utils/DailyLimit.sol"; import "../../../utils/TokenTransferHelper.sol"; -contract xTokenBridgeBase is Pausable, AccessController, DailyLimit { +contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLimit { struct MessagerService { address sendService; address receiveService; @@ -15,6 +16,7 @@ contract xTokenBridgeBase is Pausable, AccessController, DailyLimit { string public version; uint256 public protocolFee; + uint256 public protocolFeeReserved; address public guard; // remoteChainId => info mapping(uint256 => MessagerService) public messagers; @@ -28,6 +30,19 @@ contract xTokenBridgeBase is Pausable, AccessController, DailyLimit { receive() external payable {} + function initialize(address _dao, string calldata _version) public initializer { + _initialize(_dao); + version = _version; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } + function setSendService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { messagers[_remoteChainId].sendService = _service; ILowLevelMessageSender(_service).registerRemoteReceiver(_remoteChainId, _remoteBridge); @@ -39,7 +54,8 @@ contract xTokenBridgeBase is Pausable, AccessController, DailyLimit { } function withdrawProtocolFee(address _receiver, uint256 _amount) external onlyDao { - require(_amount <= protocolFee, "not enough fee"); + require(_amount <= protocolFeeReserved, "not enough fee"); + protocolFeeReserved -= _amount; TokenTransferHelper.safeTransferNative(_receiver, _amount); } @@ -51,6 +67,7 @@ contract xTokenBridgeBase is Pausable, AccessController, DailyLimit { ) internal whenNotPaused returns(bytes32 messageId) { MessagerService memory service = messagers[_remoteChainId]; require(service.sendService != address(0), "bridge not configured"); + protocolFeeReserved += protocolFee; ILowLevelMessageSender(service.sendService).sendMessage{value: feePrepaid - protocolFee}( _remoteChainId, _payload, diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol index 43f76095..53cbfd80 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -19,19 +19,22 @@ contract xTokenIssuing is xTokenBridgeBase { } // transferId => BurnInfo - mapping(bytes32 => BurnInfo) burnMessages; + 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) xTokens; + mapping(bytes32 => address) public xTokens; // xToken => Origin Token Info - mapping(address => OriginalTokenInfo) originalTokens; + mapping(address => OriginalTokenInfo) public originalTokens; event IssuingERC20Created(uint256 originalChainId, address originalToken, address xToken); 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 TokenRemintForFailed(bytes32 transferId, uint256 originalChainId, address originalToken, address xToken, address originalSender, uint256 amount); function registerxToken( uint256 _originalChainId, @@ -42,7 +45,7 @@ contract xTokenIssuing is xTokenBridgeBase { uint8 _decimals, uint256 _dailyLimit ) external onlyDao returns (address xToken) { - bytes32 salt = keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); require(xTokens[salt] == address(0), "contract has been deployed"); bytes memory bytecode = type(xTokenErc20).creationCode; bytes memory bytecodeWithInitdata = abi.encodePacked( @@ -67,7 +70,7 @@ contract xTokenIssuing is xTokenBridgeBase { address _originalToken, address _xToken ) external onlyDao { - bytes32 salt = keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); address oldxToken = xTokens[salt]; if (oldxToken != address(0)) { require(xTokenErc20(oldxToken).totalSupply() == 0, "can't delete old xToken"); @@ -86,10 +89,11 @@ contract xTokenIssuing is xTokenBridgeBase { uint256 _amount ) external calledByMessager(_remoteChainId) whenNotPaused { bytes32 transferId = _latestRecvMessageId(_remoteChainId); - bytes32 salt = keccak256(abi.encodePacked(_remoteChainId, _originalToken, version)); + 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; @@ -103,7 +107,7 @@ contract xTokenIssuing is xTokenBridgeBase { } else { xTokenErc20(xToken).mint(_recipient, _amount); } - // emit event + emit xTokenIssued(transferId, _remoteChainId, _originalToken, xToken, _recipient, _amount); } function burnAndRemoteUnlock( @@ -118,8 +122,7 @@ contract xTokenIssuing is xTokenBridgeBase { TokenTransferHelper.safeTransferFrom(_xToken, msg.sender, address(this), _amount); xTokenErc20(_xToken).burn(address(this), _amount); - bytes memory remoteUnlockCall = abi.encodeWithSelector( - IxTokenBacking.unlockFromRemote.selector, + bytes memory remoteUnlockCall = encodeUnlockFromRemote( originalInfo.token, _recipient, _amount @@ -127,9 +130,23 @@ contract xTokenIssuing is xTokenBridgeBase { bytes32 transferId = _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); require(burnMessages[transferId].hash == bytes32(0), "message exist"); - bytes32 messageHash = keccak256(abi.encodePacked(transferId, _xToken, msg.sender, _amount)); + bytes32 messageHash = keccak256(abi.encodePacked(transferId, originalInfo.chainId, _xToken, msg.sender, _amount)); burnMessages[transferId] = BurnInfo(messageHash, false); - //emit BurnAndRemoteUnlocked(transferId, msg.sender, _recipient, _xToken, _amount, fee); + emit BurnAndRemoteUnlocked(transferId, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _xToken, _amount, msg.value); + } + + function encodeUnlockFromRemote( + address _originalToken, + address _recipient, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.unlockFromRemote.selector, + block.chainid, + _originalToken, + _recipient, + _amount + ); } function requestRemoteUnlockForIssuingFailure( @@ -142,8 +159,7 @@ contract xTokenIssuing is xTokenBridgeBase { ) external payable { require(issueTransferIds[_transferId] == false, "success message can't refund for failed"); _assertMessageIsDelivered(_originalChainId, _transferId); - bytes memory handleUnlockForFailed = abi.encodeWithSelector( - IxTokenBacking.handleUnlockForIssuingFailureFromRemote.selector, + bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( _transferId, _originalToken, _originalSender, @@ -153,6 +169,22 @@ contract xTokenIssuing is xTokenBridgeBase { emit RemoteUnlockForIssuingFailureRequested(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); } + function encodeUnlockForIssuingFailureFromRemote( + bytes32 _transferId, + address _originalToken, + address _originalSender, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.handleUnlockForIssuingFailureFromRemote.selector, + block.chainid, + _transferId, + _originalToken, + _originalSender, + _amount + ); + } + // when burn and unlock failed // receive reIssue(refund) message from remote backing contract // this will refund xToken to original sender @@ -165,16 +197,24 @@ contract xTokenIssuing is xTokenBridgeBase { ) external calledByMessager(_originalChainId) whenNotPaused { BurnInfo memory burnInfo = burnMessages[_transferId]; require(burnInfo.hasRefundForFailed == false, "Backing:the burn message has been refund"); - bytes32 messageHash = keccak256(abi.encodePacked(_transferId, _originalChainId, _originalSender, _originalToken, _amount)); - require(burnInfo.hash == messageHash, "message is not matched"); - burnMessages[_transferId].hasRefundForFailed = true; - bytes32 salt = keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); + 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, xToken, _originalSender, _amount); + emit TokenRemintForFailed(_transferId, _originalChainId, _originalToken, xToken, _originalSender, _amount); + } + + function xTokenSalt( + uint256 _originalChainId, + address _originalToken + ) 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 06f842e5..199a3e1f 100644 --- a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol @@ -3,12 +3,15 @@ pragma solidity >=0.8.17; interface IxTokenBacking { function unlockFromRemote( + uint256 remoteChainId, address originalToken, address recipient, uint256 amount ) external; function handleUnlockForIssuingFailureFromRemote( + uint256 remoteChainId, + bytes32 transferId, address originalToken, address originalSender, uint256 amount diff --git a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol index eb258f59..ae035e26 100644 --- a/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.17; interface IxTokenIssuing { function handleIssuingForUnlockFailureFromRemote( + uint256 remoteChainId, bytes32 transferId, address originalToken, address originalSender, @@ -10,6 +11,7 @@ interface IxTokenIssuing { ) external; function issuexToken( + uint256 remoteChainId, address originalToken, address recipient, uint256 amount diff --git a/helix-contract/contracts/messagers/DarwiniaMsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol similarity index 96% rename from helix-contract/contracts/messagers/DarwiniaMsglineMessager.sol rename to helix-contract/contracts/messagers/MsglineMessager.sol index cf56b891..fa4d84a0 100644 --- a/helix-contract/contracts/messagers/DarwiniaMsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import "../utils/AccessController.sol"; import "../interfaces/IMessageLine.sol"; -contract DarwiniaMsglineMessager is Application, AccessController { +contract MsglineMessager is Application, AccessController { IMessageLine public immutable msgline; struct RemoteMessager { @@ -25,7 +25,7 @@ contract DarwiniaMsglineMessager is Application, AccessController { event CallResult(uint256 srcAppChainId, bool result); modifier onlyWhiteList() { - require(whiteList[msg.sender], "invalid msg.sender"); + require(whiteList[msg.sender], "msg.sender not in whitelist"); _; } @@ -84,6 +84,7 @@ contract DarwiniaMsglineMessager is Application, AccessController { require(srcChainId == remoteMessager.msglineRemoteChainId, "invalid remote chainid"); require(remoteMessager.messager == _xmsgSender(), "invalid remote messager"); bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); + // check remote appSender if (_remoteAppAddress != remoteAppSenders[key]) { emit CallerUnMatched(_srcAppChainId, _remoteAppAddress); @@ -108,7 +109,7 @@ contract DarwiniaMsglineMessager is Application, AccessController { function messagePayload(address _from, address _to, bytes memory _message) public view returns(bytes memory) { return abi.encodeWithSelector( - DarwiniaMsglineMessager.receiveMessage.selector, + MsglineMessager.receiveMessage.selector, block.chainid, _from, _to, diff --git a/helix-contract/contracts/messagers/mock/MockMsgline.sol b/helix-contract/contracts/messagers/mock/MockMsgline.sol new file mode 100644 index 00000000..b5f14ca8 --- /dev/null +++ b/helix-contract/contracts/messagers/mock/MockMsgline.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +contract MockMessageLine { + mapping(bytes32 => bool) public dones; + bool public failedFlag; + + uint256 public sendNonce; + uint256 public recvNonce; + + address public remote; + + function setRemote(address _remote) external { + remote = _remote; + } + + function setRecvFailed() external { + failedFlag = true; + } + + function send( + uint256 toChainId, + address toDapp, + bytes calldata message, + bytes calldata params + ) external payable { + require(msg.value >= 1 ether, "fee is not enough"); + sendNonce += 1; + MockMessageLine(remote).recv(block.chainid, msg.sender, toDapp, message); + } + + function recv( + uint256 sourceChainId, + address sourceSender, + address toDapp, + bytes calldata message + ) public { + recvNonce += 1; + dones[bytes32(recvNonce)] = true; + if (failedFlag) { + failedFlag = false; + return; + } + + toDapp.call(abi.encodePacked(message, sourceChainId, sourceSender)); + } + + function fee( + uint256 toChainId, + address toDapp, + bytes calldata message, + bytes calldata params + ) external view returns (uint256) { + return 1 ether; + } + + function sentMessageId() external view returns(bytes32) { + return bytes32(sendNonce); + } + + function recvMessageId() external view returns(bytes32) { + return bytes32(recvNonce); + } +} + + diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js new file mode 100644 index 00000000..16a9372d --- /dev/null +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -0,0 +1,410 @@ +const { expect } = require("chai"); +const { solidity } = require("ethereum-waffle"); +const chai = require("chai"); +const ethUtil = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const secp256k1 = require('secp256k1'); + +chai.use(solidity); + +async function getBlockTimestamp() { + const blockNumber = await ethers.provider.getBlockNumber(); + const block = await ethers.provider.getBlock(blockNumber); + return block.timestamp; +} + +describe("xtoken tests", () => { + before(async () => { + }); + + it("test_msglinebased_xtoken_flow", async function () { + const [owner, relayer, user, slasher] = await ethers.getSigners(); + const dao = owner.address; + const backingChainId = 31337; + const issuingChainId = 31337; + const nativeTokenAddress = "0x0000000000000000000000000000000000000000"; + + const xTokens = {}; + + // deploy mock msgline + const mockMsglineContract = await ethers.getContractFactory("MockMessageLine"); + const mockBackingMsgline = await mockMsglineContract.deploy(); + await mockBackingMsgline.deployed(); + const mockIssuingMsgline = await mockMsglineContract.deploy(); + await mockIssuingMsgline.deployed(); + await mockBackingMsgline.setRemote(mockIssuingMsgline.address); + await mockIssuingMsgline.setRemote(mockBackingMsgline.address); + console.log("mock msgline deployed address:", mockBackingMsgline.address, mockIssuingMsgline.address); + + // deploy msgline messager + const msglineMessagerContract = await ethers.getContractFactory("MsglineMessager"); + const backingMessager = await msglineMessagerContract.deploy(dao, mockBackingMsgline.address); + await backingMessager.deployed(); + console.log("backing messager deployed address:", backingMessager.address); + const issuingMessager = await msglineMessagerContract.deploy(dao, mockIssuingMsgline.address); + await issuingMessager.deployed(); + console.log("issuing messager deployed address:", issuingMessager.address); + + // deploy backing + const xTokenBackingContract = await ethers.getContractFactory("xTokenBacking"); + const backing = await xTokenBackingContract.deploy(); + await backing.deployed(); + console.log("backing deployed address:", backing.address); + await backing.initialize(dao, "v1.0.0"); + + // deploy issuing + const xTokenIssuingContract = await ethers.getContractFactory("xTokenIssuing"); + const issuing = await xTokenIssuingContract.deploy(); + await issuing.deployed(); + console.log("issuing deployed address:", issuing.address); + await issuing.initialize(dao, "v1.0.0"); + + await backingMessager.setRemoteMessager(issuingChainId, issuingChainId, issuingMessager.address); + await issuingMessager.setRemoteMessager(backingChainId, backingChainId, backingMessager.address); + await backingMessager.setWhiteList(backing.address, true); + await issuingMessager.setWhiteList(issuing.address, true); + + await backing.setSendService(issuingChainId, issuing.address, backingMessager.address); + await backing.setReceiveService(issuingChainId, issuing.address, backingMessager.address); + await issuing.setSendService(backingChainId, backing.address, issuingMessager.address); + await issuing.setReceiveService(backingChainId, backing.address, issuingMessager.address); + console.log("configure backing & issuing finished"); + + // use a mapping erc20 as original token + const wethName = "Ethereum Wrapped ETH"; + const wethSymbol = "WETH"; + const wethContract = await ethers.getContractFactory("WToken"); + const weth = await wethContract.deploy(wethName, wethSymbol, 18); + await weth.deployed(); + await backing.setwToken(weth.address); + + async function registerToken( + originalTokenAddress, + originalChainName, + originalTokenName, + originalTokenSymbol, + originalTokenDecimals, + dailyLimit + ) { + // register xtoken + await issuing.registerxToken( + backingChainId, + originalTokenAddress, + originalChainName, + originalTokenName, + originalTokenSymbol, + originalTokenDecimals, + dailyLimit + ); + console.log("register xtoken finished"); + + const xTokenSalt = await issuing.xTokenSalt(backingChainId, originalTokenAddress); + const xTokenAddress = await issuing.xTokens(xTokenSalt); + // register native token + await backing.registerOriginalToken( + issuingChainId, + originalTokenAddress, + xTokenAddress, + dailyLimit + ); + 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")); + return xTokenAddress; + } + + async function balanceOf(tokenAddress, account) { + if (tokenAddress == nativeTokenAddress) { + return await ethers.provider.getBalance(account); + } else { + const token = await ethers.getContractAt("Erc20", tokenAddress); + return await token.balanceOf(account); + } + } + + async function lockAndRemoteIssuing( + originalAddress, + recipient, + amount, + fee, + result + ) { + const xTokenAddress = xTokens[originalAddress]; + + const balanceRecipientBefore = await balanceOf(xTokenAddress, recipient); + const balanceBackingBefore = await balanceOf(originalAddress, backing.address); + + const transaction = await backing.lockAndRemoteIssuing( + issuingChainId, + originalAddress, + recipient, + amount, + 0, + {value: ethers.utils.parseEther(fee)} + ) + const receipt = await transaction.wait(); + console.log("lockAndRemoteIssuing gasUsed: ", receipt.cumulativeGasUsed); + + 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); + if (result == true) { + expect(balanceRecipientAfter - balanceRecipientBefore).to.equal(amount); + expect(balanceBackingAfter - balanceBackingBefore).to.equal(amount); + } else { + expect(balanceRecipientAfter - balanceRecipientBefore).to.equal(0); + expect(balanceBackingAfter - balanceBackingBefore).to.equal(amount); + } + } + + async function burnAndRemoteUnlock( + originalAddress, + recipient, + amount, + fee, + result + ) { + const xTokenAddress = xTokens[originalAddress]; + + const balanceRecipientBefore = await balanceOf(xTokenAddress, recipient); + const balanceBackingBefore = await balanceOf(originalAddress, backing.address); + + const transaction = await issuing.burnAndRemoteUnlock( + xTokenAddress, + recipient, + amount, + 0, + {value: ethers.utils.parseEther(fee)} + ); + const receipt = await transaction.wait(); + console.log("burnAndRemoteUnlock gasUsed: ", receipt.cumulativeGasUsed); + + const balanceRecipientAfter = await balanceOf(xTokenAddress, recipient); + const balanceBackingAfter = await balanceOf(originalAddress, backing.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); + + if (result) { + expect(balanceRecipientBefore - balanceRecipientAfter).to.equal(amount); + expect(balanceBackingBefore - balanceBackingAfter).to.equal(amount); + } else { + expect(balanceRecipientBefore - balanceRecipientAfter).to.equal(amount); + expect(balanceBackingBefore - balanceBackingAfter).to.equal(0); + } + } + + async function requestRemoteUnlockForIssuingFailure( + transferId, + originalToken, + originalSender, + amount, + fee, + result + ) { + 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, + backingChainId, + originalToken, + originalSender, + amount, + 0, + { + value: ethers.utils.parseEther(fee), + gasPrice: 10000000000, + } + ); + 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"); + if (result) { + expect(balanceSenderAfter.sub(balanceSenderBefore)).to.be.equal(amount); + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.be.equal(amount); + expect(lockInfo.hasRefundForFailed).to.equal(true); + } else { + expect(balanceSenderAfter.sub(balanceSenderBefore)).to.be.equal(0); + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.be.equal(0); + } + } + + async function requestRemoteIssuingForUnlockFailure( + transferId, + originalToken, + originalSender, + amount, + fee, + result + ) { + const xTokenAddress = xTokens[originalToken]; + + const balanceSenderBefore = await balanceOf(xTokenAddress, originalSender); + await backing.requestRemoteIssuingForUnlockFailure( + transferId, + issuingChainId, + originalToken, + originalSender, + amount, + 0, + {value: ethers.utils.parseEther(fee)} + ); + const balanceSenderAfter = await balanceOf(xTokenAddress, originalSender); + if (result) { + expect(balanceSenderAfter.sub(balanceSenderBefore)).to.equal(amount); + } else { + expect(balanceSenderAfter.sub(balanceSenderBefore)).to.equal(0); + } + } + + await registerToken( + nativeTokenAddress, + "ethereum", + "native token", + "eth", + 18, + 1000 + ); + + await expect(lockAndRemoteIssuing( + nativeTokenAddress, + owner.address, + 100, + "0.9", + true + )).to.be.revertedWith("fee is not enough"); + + // success lock and remote xtoken + await lockAndRemoteIssuing( + nativeTokenAddress, + owner.address, + 500, + "1.1", + true + ); + // success burn and remote unlock + await burnAndRemoteUnlock( + nativeTokenAddress, + owner.address, + 100, + "1.1", + true + ); + + // test refund failed if the message has been successed + await expect(requestRemoteUnlockForIssuingFailure( + await issuingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 100, + "1.1", + true + )).to.be.revertedWith("success message can't refund for failed"); + await expect(requestRemoteIssuingForUnlockFailure( + await backingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 100, + "1.1", + true + )).to.be.revertedWith("success message can't refund for failed"); + + // lock exceed daily limit + await lockAndRemoteIssuing( + nativeTokenAddress, + owner.address, + 501, + "1.1", + false + ); + // refund (when isssuing failed) + await requestRemoteUnlockForIssuingFailure( + await issuingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 501, + "1.1", + true + ); + // the params not right + // 1. amount + await requestRemoteUnlockForIssuingFailure( + await issuingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 500, + "1.1", + false + ); + // receiver + await requestRemoteUnlockForIssuingFailure( + await issuingMessager.latestRecvMessageId(), + nativeTokenAddress, + relayer.address, + 501, + "1.1", + false + ); + // refund twice + await requestRemoteUnlockForIssuingFailure( + await issuingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 501, + "1.1", + false + ); + // burn failed + await mockBackingMsgline.setRecvFailed(); + await burnAndRemoteUnlock( + nativeTokenAddress, + owner.address, + 100, + "1.1", + false + ); + // invalid args + await requestRemoteIssuingForUnlockFailure( + await backingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 101, + "1.1", + false + ); + // refund (when unlock failed) + await requestRemoteIssuingForUnlockFailure( + await backingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 100, + "1.1", + true + ); + // refund twice + await requestRemoteIssuingForUnlockFailure( + await backingMessager.latestRecvMessageId(), + nativeTokenAddress, + owner.address, + 100, + "1.1", + false + ); + }); +}); + From ec70d52e83ab57f0ea0c2f2ea5ee96900ff28d5a Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Fri, 8 Dec 2023 14:42:12 +0800 Subject: [PATCH 04/26] test guard --- .../contracts/mapping-token/v2/Guard.sol | 4 +- .../contracts/utils/TokenTransferHelper.sol | 6 +- helix-contract/test/6_test_xtoken_v3.js | 149 ++++++++++++++++-- 3 files changed, 143 insertions(+), 16 deletions(-) diff --git a/helix-contract/contracts/mapping-token/v2/Guard.sol b/helix-contract/contracts/mapping-token/v2/Guard.sol index 767c0668..14ae1588 100644 --- a/helix-contract/contracts/mapping-token/v2/Guard.sol +++ b/helix-contract/contracts/mapping-token/v2/Guard.sol @@ -17,7 +17,7 @@ contract Guard is GuardRegistry, Pausable { address public depositor; address public operator; - event TokenDeposit(uint256 id, address token, address recipient, uint256 amount); + event TokenDeposit(uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); event TokenClaimed(uint256 id); constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime, address _depositor) { @@ -68,7 +68,7 @@ contract Guard is GuardRegistry, Pausable { uint256 amount ) public onlyDepositor whenNotPaused { depositors[id] = hash(abi.encodePacked(block.timestamp, token, recipient, amount)); - emit TokenDeposit(id, token, recipient, amount); + emit TokenDeposit(id, block.timestamp, token, recipient, amount); } function claimById( diff --git a/helix-contract/contracts/utils/TokenTransferHelper.sol b/helix-contract/contracts/utils/TokenTransferHelper.sol index bd94b59c..2751f8ea 100644 --- a/helix-contract/contracts/utils/TokenTransferHelper.sol +++ b/helix-contract/contracts/utils/TokenTransferHelper.sol @@ -14,7 +14,7 @@ library TokenTransferHelper { receiver, amount )); - require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transfer token failed"); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transfer token failed"); } function safeTransferFrom( @@ -29,7 +29,7 @@ library TokenTransferHelper { receiver, amount )); - require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transferFrom token failed"); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transferFrom token failed"); } function safeTransferNative( @@ -37,7 +37,7 @@ library TokenTransferHelper { uint256 amount ) internal { (bool success,) = payable(receiver).call{value: amount}(""); - require(success, "lnBridgeHelper:transfer native token failed"); + require(success, "helix:transfer native token failed"); } } diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index 16a9372d..dee72ef2 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -78,6 +78,22 @@ describe("xtoken tests", () => { await weth.deployed(); await backing.setwToken(weth.address); + let guards = []; + for (let i = 0; i < 3; i++) { + const wallet = ethers.Wallet.createRandom(); + guards.push(wallet); + } + guards = guards.sort((x, y) => { + return x.address.toLowerCase().localeCompare(y.address.toLowerCase()) + }); + + const guardBackingContract = await ethers.getContractFactory("Guard"); + const backingGuard = await guardBackingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60, backing.address); + await backingGuard.deployed(); + const guardIssuingContract = await ethers.getContractFactory("Guard"); + const issuingGuard = await guardIssuingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60, issuing.address); + await issuingGuard.deployed(); + async function registerToken( originalTokenAddress, originalChainName, @@ -128,6 +144,7 @@ describe("xtoken tests", () => { recipient, amount, fee, + usingGuard, result ) { const xTokenAddress = xTokens[originalAddress]; @@ -153,7 +170,7 @@ describe("xtoken tests", () => { expect(lockInfo.hash).not.to.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); expect(lockInfo.hasRefundForFailed).to.equal(false); - if (result == true) { + if (result == true && !usingGuard) { expect(balanceRecipientAfter - balanceRecipientBefore).to.equal(amount); expect(balanceBackingAfter - balanceBackingBefore).to.equal(amount); } else { @@ -167,11 +184,13 @@ describe("xtoken tests", () => { recipient, amount, fee, + usingGuard, result ) { const xTokenAddress = xTokens[originalAddress]; - const balanceRecipientBefore = await balanceOf(xTokenAddress, recipient); + const balanceUserBefore = await balanceOf(xTokenAddress, owner.address); + const balanceRecipientBefore = await balanceOf(originalAddress, recipient); const balanceBackingBefore = await balanceOf(originalAddress, backing.address); const transaction = await issuing.burnAndRemoteUnlock( @@ -184,20 +203,27 @@ describe("xtoken tests", () => { const receipt = await transaction.wait(); console.log("burnAndRemoteUnlock gasUsed: ", receipt.cumulativeGasUsed); - const balanceRecipientAfter = await balanceOf(xTokenAddress, recipient); + const balanceRecipientAfter = await balanceOf(originalAddress, recipient); const balanceBackingAfter = await balanceOf(originalAddress, backing.address); + const balanceUserAfter = await balanceOf(xTokenAddress, owner.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); + expect(balanceUserBefore.sub(balanceUserAfter)).to.equal(amount); - if (result) { - expect(balanceRecipientBefore - balanceRecipientAfter).to.equal(amount); - expect(balanceBackingBefore - balanceBackingAfter).to.equal(amount); + if (result && !usingGuard) { + expect(balanceRecipientAfter.sub(balanceRecipientBefore)).to.equal(amount); + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.equal(amount); } else { - expect(balanceRecipientBefore - balanceRecipientAfter).to.equal(amount); - expect(balanceBackingBefore - balanceBackingAfter).to.equal(0); + // if successfully unlock native token by guard + if (nativeTokenAddress == originalAddress && result && usingGuard) { + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.equal(amount); + } else { + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.equal(0); + } + expect(balanceRecipientAfter.sub(balanceRecipientBefore)).to.equal(0); } } @@ -272,6 +298,47 @@ describe("xtoken tests", () => { } } + async function guardClaim( + guard, + depositer, + id, + timestamp, + wallets, + token, + recipient, + amount + ) { + // encode value + const structHash = + ethUtil.keccak256( + abi.rawEncode( + ['bytes4', 'bytes'], + [abi.methodID('claim', [ 'uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), + abi.rawEncode(['uint256', 'uint256', 'address', 'address', 'uint256'], + [id, timestamp, token, recipient, amount]) + ] + ) + ); + const dataHash = await guard.encodeDataHash(structHash); + const signatures = wallets.map((wallet) => { + const address = wallet.address; + const privateKey = ethers.utils.arrayify(wallet.privateKey); + const signatureECDSA = secp256k1.ecdsaSign(ethers.utils.arrayify(dataHash), privateKey); + const ethRecID = signatureECDSA.recid + 27; + const signature = Uint8Array.from( + signatureECDSA.signature.join().split(',').concat(ethRecID) + ); + return ethers.utils.hexlify(signature); + }); + const balanceBackingBefore = await balanceOf(token, depositer); + const balanceRecipientBefore = await balanceOf(token, recipient); + await guard.claim(id, timestamp, token, recipient, amount, signatures); + const balanceBackingAfter = await balanceOf(token, depositer); + const balanceRecipientAfter = await balanceOf(token, recipient); + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.equal(amount); + expect(balanceRecipientAfter.sub(balanceRecipientBefore)).to.equal(amount); + } + await registerToken( nativeTokenAddress, "ethereum", @@ -286,6 +353,7 @@ describe("xtoken tests", () => { owner.address, 100, "0.9", + false, true )).to.be.revertedWith("fee is not enough"); @@ -295,14 +363,16 @@ describe("xtoken tests", () => { owner.address, 500, "1.1", + false, true ); // success burn and remote unlock await burnAndRemoteUnlock( nativeTokenAddress, - owner.address, + user.address, 100, "1.1", + false, true ); @@ -330,6 +400,7 @@ describe("xtoken tests", () => { owner.address, 501, "1.1", + false, false ); // refund (when isssuing failed) @@ -373,16 +444,17 @@ describe("xtoken tests", () => { await mockBackingMsgline.setRecvFailed(); await burnAndRemoteUnlock( nativeTokenAddress, - owner.address, + user.address, 100, "1.1", + false, false ); // invalid args await requestRemoteIssuingForUnlockFailure( await backingMessager.latestRecvMessageId(), nativeTokenAddress, - owner.address, + user.address, 101, "1.1", false @@ -405,6 +477,61 @@ describe("xtoken tests", () => { "1.1", false ); + + // using guard + await backing.updateGuard(backingGuard.address); + await issuing.updateGuard(issuingGuard.address); + + // lock -> issuing using guard + await lockAndRemoteIssuing( + nativeTokenAddress, + owner.address, + 10, + "1.1", + true,//using guard + true + ); + await guardClaim( + issuingGuard, + issuing.address, + await issuingMessager.latestRecvMessageId(), + await getBlockTimestamp(), + [guards[0], guards[1]], + xTokens[nativeTokenAddress], + owner.address, + 10 + ); + // burn -> unlock using guard (native token) + await burnAndRemoteUnlock( + nativeTokenAddress, + user.address, + 20, + "1.1", + true, //using guard + true + ); + await guardClaim( + backingGuard, + backing.address, + await backingMessager.latestRecvMessageId(), + await getBlockTimestamp(), + [guards[0], guards[1]], + // native token must be claimed by wtoken + weth.address, + user.address, + 20 + ); + // claim twice + await expect(guardClaim( + backingGuard, + backing.address, + await backingMessager.latestRecvMessageId(), + await getBlockTimestamp(), + [guards[0], guards[1]], + weth.address, + user.address, + 20 + )).to.be.revertedWith("Guard: Invalid id to claim"); }); }); From ab04b365f048db6322fdb45b9bbaa04deffa5158 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Fri, 8 Dec 2023 17:55:05 +0800 Subject: [PATCH 05/26] deploy on test net --- helix-contract/address/xtoken-dev.json | 22 + .../v3/base/xTokenBridgeBase.sol | 4 + .../deploy/deploy_msgline_messager.js | 58 + helix-contract/deploy/deploy_xtoken_config.js | 70 + helix-contract/deploy/deploy_xtoken_logic.js | 58 + helix-contract/deploy/deploy_xtoken_proxy.js | 66 + helix-contract/deploy/flatten-xtoken.sh | 6 + .../flatten/xtoken-v3/MsglineMessager.sol | 211 +++ .../flatten/xtoken-v3/xTokenBacking.sol | 1133 ++++++++++++ .../flatten/xtoken-v3/xTokenErc20.sol | 554 ++++++ .../flatten/xtoken-v3/xTokenIssuing.sol | 1551 +++++++++++++++++ 11 files changed, 3733 insertions(+) create mode 100644 helix-contract/address/xtoken-dev.json create mode 100644 helix-contract/deploy/deploy_msgline_messager.js create mode 100644 helix-contract/deploy/deploy_xtoken_config.js create mode 100644 helix-contract/deploy/deploy_xtoken_logic.js create mode 100644 helix-contract/deploy/deploy_xtoken_proxy.js create mode 100644 helix-contract/deploy/flatten-xtoken.sh create mode 100644 helix-contract/flatten/xtoken-v3/MsglineMessager.sol create mode 100644 helix-contract/flatten/xtoken-v3/xTokenBacking.sol create mode 100644 helix-contract/flatten/xtoken-v3/xTokenErc20.sol create mode 100644 helix-contract/flatten/xtoken-v3/xTokenIssuing.sol diff --git a/helix-contract/address/xtoken-dev.json b/helix-contract/address/xtoken-dev.json new file mode 100644 index 00000000..42021f62 --- /dev/null +++ b/helix-contract/address/xtoken-dev.json @@ -0,0 +1,22 @@ +{ + "messagers": { + "crab": { + "msglineMessager": "0x8FC1319581fcb95e86461216B2Cad86DC78D5681" + }, + "sepolia": { + "msglineMessager": "0x35A314e53e2fdDfeCA7b743042AaCfB1ABAF0aDe" + } + }, + "backingProxy": { + "crab": "0xd082Af5a732dB55F0fe016c100C40c26FB267bf3" + }, + "backingLogic": { + "crab": "0x4b9a3b5a9929dc2F9766739dd4457e2941f6A0c9" + }, + "issuingProxy": { + "sepolia": "0x97c0a7162317786E1030b7D023c2B8525Fc28497" + }, + "issuingLogic": { + "sepolia": "0x09Fe8eA52CD9F3bFF11E84378cfc99441910AD3f" + } +} diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol index c02753ce..4d9cecba 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -43,6 +43,10 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim _pause(); } + function setProtocolFee(uint256 _protocolFee) external onlyOperator { + protocolFee = _protocolFee; + } + function setSendService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { messagers[_remoteChainId].sendService = _service; ILowLevelMessageSender(_service).registerRemoteReceiver(_remoteChainId, _remoteBridge); diff --git a/helix-contract/deploy/deploy_msgline_messager.js b/helix-contract/deploy/deploy_msgline_messager.js new file mode 100644 index 00000000..b0e07834 --- /dev/null +++ b/helix-contract/deploy/deploy_msgline_messager.js @@ -0,0 +1,58 @@ +const ethUtil = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const secp256k1 = require('secp256k1'); +const fs = require("fs"); + +var ProxyDeployer = require("./proxy.js"); + +const privateKey = process.env.PRIKEY + +const crabNetwork = { + name: "crab", + url: "https://crab-rpc.darwinia.network", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", + msgline: "0x000000000EFcBAdA3793cC59c62D79b9f56Ae48F", +}; + +const sepoliaNetwork = { + name: "sepolia", + url: "https://rpc-sepolia.rockx.com", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", + msgline: "0x000000000EFcBAdA3793cC59c62D79b9f56Ae48F", +}; + +function wallet(url) { + const provider = new ethers.providers.JsonRpcProvider(url); + const wallet = new ethers.Wallet(privateKey, provider); + return wallet; +} + +async function deployMessager(wallet, dao, msgline, deployer) { + const messagerContract = await ethers.getContractFactory("MsglineMessager", wallet); + const messager = await messagerContract.deploy(dao, msgline); + await messager.deployed(); + console.log("finish to deploy messager, address:", messager.address); + return messager.address; +} + +async function deploy() { + const walletCrab = wallet(crabNetwork.url); + await deployMessager(walletCrab, crabNetwork.dao, crabNetwork.msgline, crabNetwork.deployer); + + const walletSepolia = wallet(sepoliaNetwork.url); + await deployMessager(walletSepolia, sepoliaNetwork.dao, sepoliaNetwork.msgline, sepoliaNetwork.deployer); +} + +async function main() { + await deploy(); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); + diff --git a/helix-contract/deploy/deploy_xtoken_config.js b/helix-contract/deploy/deploy_xtoken_config.js new file mode 100644 index 00000000..004f838e --- /dev/null +++ b/helix-contract/deploy/deploy_xtoken_config.js @@ -0,0 +1,70 @@ +const ethUtil = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const secp256k1 = require('secp256k1'); +const fs = require("fs"); + +var ProxyDeployer = require("./proxy.js"); + +const privateKey = process.env.PRIKEY + +const crabNetwork = { + name: "crab", + url: "https://crab-rpc.darwinia.network", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + messager: "0x8FC1319581fcb95e86461216B2Cad86DC78D5681", + backing: "0xd082Af5a732dB55F0fe016c100C40c26FB267bf3", + chainid: 44 +}; + +const sepoliaNetwork = { + name: "sepolia", + url: "https://rpc-sepolia.rockx.com", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + messager: "0x35A314e53e2fdDfeCA7b743042AaCfB1ABAF0aDe", + issuing: "0x97c0a7162317786E1030b7D023c2B8525Fc28497", + chainid: 11155111 +}; + +function wallet(url) { + const provider = new ethers.providers.JsonRpcProvider(url); + const wallet = new ethers.Wallet(privateKey, provider); + return wallet; +} + +async function deploy() { + const backingNetwork = crabNetwork; + const issuingNetwork = sepoliaNetwork; + const walletBacking = wallet(crabNetwork.url); + const walletIssuing = wallet(sepoliaNetwork.url); + + // connect messager + const backingMessager = await ethers.getContractAt("MsglineMessager", backingNetwork.messager, walletBacking); + const issuingMessager = await ethers.getContractAt("MsglineMessager", issuingNetwork.messager, walletIssuing); + await backingMessager.setRemoteMessager(issuingNetwork.chainid, issuingNetwork.chainid, issuingMessager.address); + await issuingMessager.setRemoteMessager(backingNetwork.chainid, backingNetwork.chainid, backingMessager.address); + console.log("connect messager successed"); + // xTokenBridge <> messager authorize + const backing = await ethers.getContractAt("xTokenBacking", backingNetwork.backing, walletBacking); + const issuing = await ethers.getContractAt("xTokenIssuing", issuingNetwork.issuing, walletIssuing); + await backingMessager.setWhiteList(backing.address, true); + await issuingMessager.setWhiteList(issuing.address, true); + console.log("messager authorize xtoken bridge successed"); + + await backing.setSendService(issuingNetwork.chainid, issuing.address, backingMessager.address); + await backing.setReceiveService(issuingNetwork.chainid, issuing.address, backingMessager.address); + await issuing.setSendService(backingNetwork.chainid, backing.address, issuingMessager.address); + await issuing.setReceiveService(backingNetwork.chainid, backing.address, issuingMessager.address); + console.log("xtoken bridge connect remote successed"); +} + +async function main() { + await deploy(); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); + diff --git a/helix-contract/deploy/deploy_xtoken_logic.js b/helix-contract/deploy/deploy_xtoken_logic.js new file mode 100644 index 00000000..a74ad4c7 --- /dev/null +++ b/helix-contract/deploy/deploy_xtoken_logic.js @@ -0,0 +1,58 @@ +const ethUtil = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const secp256k1 = require('secp256k1'); + +var Create2 = require("./create2.js"); + +const privateKey = process.env.PRIKEY + +const crabNetwork = { + url: "https://crab-rpc.darwinia.network", + deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", +}; + +const sepoliaNetwork = { + url: "https://rpc-sepolia.rockx.com", + deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", +} + +function wallet(url) { + const provider = new ethers.providers.JsonRpcProvider(url); + const wallet = new ethers.Wallet(privateKey, provider); + return wallet; +} + +async function deployxTokenBacking(wallet, deployerAddress, salt) { + const bridgeContract = await ethers.getContractFactory("xTokenBacking", wallet); + const bytecode = Create2.getDeployedBytecode(bridgeContract, [], []); + const address = await Create2.deploy(deployerAddress, wallet, bytecode, salt); + console.log("finish to deploy xToken backing logic, address: ", address); + return address; +} + +async function deployxTokenIssuing(wallet, deployerAddress, salt) { + const bridgeContract = await ethers.getContractFactory("xTokenIssuing", wallet); + const bytecode = Create2.getDeployedBytecode(bridgeContract, [], []); + const address = await Create2.deploy(deployerAddress, wallet, bytecode, salt); + console.log("finish to deploy xToken Issuing logic, address: ", address); + return address; +} + +// 2. deploy mapping token factory +async function main() { + // deploy backing on crab + const walletCrab = wallet(crabNetwork.url); + const backingLogic = await deployxTokenBacking(walletCrab, crabNetwork.deployer, "xTokenBacking-logic-v1.0.1"); + + // deploy issuing on sepolia + const walletSpeolia = wallet(sepoliaNetwork.url); + const issuingLogic = await deployxTokenIssuing(walletSpeolia, sepoliaNetwork.deployer, "xTokenIssuing-logic-v1.0.0"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); + diff --git a/helix-contract/deploy/deploy_xtoken_proxy.js b/helix-contract/deploy/deploy_xtoken_proxy.js new file mode 100644 index 00000000..f599ad9c --- /dev/null +++ b/helix-contract/deploy/deploy_xtoken_proxy.js @@ -0,0 +1,66 @@ +const ethUtil = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const secp256k1 = require('secp256k1'); +const fs = require("fs"); + +var ProxyDeployer = require("./proxy.js"); + +const privateKey = process.env.PRIKEY + +const crabNetwork = { + name: "crab", + url: "https://crab-rpc.darwinia.network", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + proxyAdmin: "0xE3979fFa68BBa1F53c6F502c8F5788B370d28730", + deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", +}; + +const sepoliaNetwork = { + name: "sepolia", + url: "https://rpc-sepolia.rockx.com", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + proxyAdmin: "0xE3979fFa68BBa1F53c6F502c8F5788B370d28730", + deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", +}; + +function wallet(url) { + const provider = new ethers.providers.JsonRpcProvider(url); + const wallet = new ethers.Wallet(privateKey, provider); + return wallet; +} + +async function deployxTokenProxy(wallet, salt, dao, proxyAdminAddress, logicAddress, deployer) { + const bridgeContract = await ethers.getContractFactory("xTokenBridgeBase", wallet); + const proxy = await ProxyDeployer.deployProxyContract2( + deployer, + salt, + proxyAdminAddress, + bridgeContract, + logicAddress, + [dao, salt], + wallet); + console.log("finish to deploy xtoken bridge proxy, address:", proxy); + return proxy; +} + +async function deploy() { + const walletCrab = wallet(crabNetwork.url); + const backingLogic = "0x7733Df6aCd438bCaDC70Deb36757C25Ee4820C11"; + await deployxTokenProxy(walletCrab, "xtoken-backing-1.0.0", crabNetwork.dao, crabNetwork.proxyAdmin, backingLogic, crabNetwork.deployer); + + const walletSepolia = wallet(sepoliaNetwork.url); + const issuingLogic = "0x09Fe8eA52CD9F3bFF11E84378cfc99441910AD3f"; + await deployxTokenProxy(walletSepolia, "xtoken-issuing-1.0.0", sepoliaNetwork.dao, sepoliaNetwork.proxyAdmin, issuingLogic, sepoliaNetwork.deployer); +} + +async function main() { + await deploy(); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); + diff --git a/helix-contract/deploy/flatten-xtoken.sh b/helix-contract/deploy/flatten-xtoken.sh new file mode 100644 index 00000000..df3cccf0 --- /dev/null +++ b/helix-contract/deploy/flatten-xtoken.sh @@ -0,0 +1,6 @@ +path=flatten/xtoken-v3 +mkdir -p $path +yarn flat contracts/mapping-token/v3/base/xTokenBacking.sol --output $path/xTokenBacking.sol +yarn flat contracts/mapping-token/v3/base/xTokenIssuing.sol --output $path/xTokenIssuing.sol +yarn flat contracts/mapping-token/v3/base/xTokenErc20.sol --output $path/xTokenErc20.sol +yarn flat contracts/messagers/MsglineMessager.sol --output $path/MsglineMessager.sol diff --git a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol new file mode 100644 index 00000000..69731772 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/8/2023 + **/ + +pragma solidity ^0.8.17; + +// File contracts/utils/AccessController.sol +// License-Identifier: MIT + +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; + } + + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; + } +} + +// File contracts/interfaces/IMessageLine.sol +// License-Identifier: MIT + +interface IMessageLine { + function send(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external payable; + function fee(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external view returns (uint256); + function sentMessageId() external view returns(bytes32); + function recvMessageId() external view returns(bytes32); + function dones(bytes32) external view returns(bool); +} + +abstract contract Application { + function _msgSender() internal view returns (address payable _line) { + _line = payable(msg.sender); + } + + function _fromChainId() internal pure returns (uint256 _msgDataFromChainId) { + require(msg.data.length >= 52, "!fromChainId"); + assembly { + _msgDataFromChainId := calldataload(sub(calldatasize(), 52)) + } + } + + function _xmsgSender() internal pure returns (address payable _from) { + require(msg.data.length >= 20, "!line"); + assembly { + _from := shr(96, calldataload(sub(calldatasize(), 20))) + } + } +} + +// File contracts/messagers/MsglineMessager.sol +// License-Identifier: MIT + + +contract MsglineMessager is Application, AccessController { + IMessageLine public immutable msgline; + + struct RemoteMessager { + uint256 msglineRemoteChainId; + address messager; + } + + mapping(address=>bool) public whiteList; + // app remoteChainId => msgline remote messager + mapping(uint256=>RemoteMessager) public remoteMessagers; + + // token bridge pair + // hash(msglineRemoteChainId, localAppAddress) => remoteAppAddress + mapping(bytes32=>address) public remoteAppReceivers; + mapping(bytes32=>address) public remoteAppSenders; + + event CallerUnMatched(uint256 srcAppChainId, address srcAppAddress); + event CallResult(uint256 srcAppChainId, bool result); + + modifier onlyWhiteList() { + require(whiteList[msg.sender], "msg.sender not in whitelist"); + _; + } + + modifier onlyMsgline() { + require(msg.sender == address(msgline), "invalid caller"); + _; + } + + //event CallResult(string sourceChain, string srcAddress, bool successed); + + constructor(address _dao, address _msgline) { + _initialize(_dao); + msgline = IMessageLine(_msgline); + } + + function setRemoteMessager(uint256 _appRemoteChainId, uint256 _msglineRemoteChainId, address _remoteMessager) onlyDao external { + remoteMessagers[_appRemoteChainId] = RemoteMessager(_msglineRemoteChainId, _remoteMessager); + } + + function setWhiteList(address _caller, bool _enable) external onlyDao { + whiteList[_caller] = _enable; + } + + function registerRemoteReceiver(uint256 _remoteChainId, address _remoteBridge) onlyWhiteList external { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.msglineRemoteChainId, msg.sender)); + remoteAppReceivers[key] = _remoteBridge; + } + + function registerRemoteSender(uint256 _remoteChainId, address _remoteBridge) onlyWhiteList external { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.msglineRemoteChainId, msg.sender)); + remoteAppSenders[key] = _remoteBridge; + } + + function sendMessage(uint256 _remoteChainId, bytes memory _message, bytes memory _params) onlyWhiteList external payable { + RemoteMessager memory remoteMessager = remoteMessagers[_remoteChainId]; + require(remoteMessager.messager != address(0), "remote not configured"); + bytes32 key = keccak256(abi.encodePacked(remoteMessager.msglineRemoteChainId, msg.sender)); + address remoteAppAddress = remoteAppReceivers[key]; + require(remoteAppAddress != address(0), "app pair not registered"); + bytes memory msglinePayload = messagePayload(msg.sender, remoteAppAddress, _message); + msgline.send{ value: msg.value }( + remoteMessager.msglineRemoteChainId, + remoteMessager.messager, + msglinePayload, + _params + ); + } + + function receiveMessage(uint256 _srcAppChainId, address _remoteAppAddress, address _localAppAddress, bytes memory _message) onlyMsgline external { + uint256 srcChainId = _fromChainId(); + RemoteMessager memory remoteMessager = remoteMessagers[_srcAppChainId]; + require(srcChainId == remoteMessager.msglineRemoteChainId, "invalid remote chainid"); + require(remoteMessager.messager == _xmsgSender(), "invalid remote messager"); + bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); + + // check remote appSender + if (_remoteAppAddress != remoteAppSenders[key]) { + emit CallerUnMatched(_srcAppChainId, _remoteAppAddress); + return; + } + (bool success,) = _localAppAddress.call(_message); + // don't revert to prevent message block + emit CallResult(_srcAppChainId, success); + } + + function latestSentMessageId() external view returns(bytes32) { + return msgline.sentMessageId(); + } + + function latestRecvMessageId() external view returns(bytes32) { + return msgline.recvMessageId(); + } + + function messageDelivered(bytes32 messageId) external view returns(bool) { + return msgline.dones(messageId); + } + + function messagePayload(address _from, address _to, bytes memory _message) public view returns(bytes memory) { + return abi.encodeWithSelector( + MsglineMessager.receiveMessage.selector, + block.chainid, + _from, + _to, + _message + ); + } +} \ No newline at end of file diff --git a/helix-contract/flatten/xtoken-v3/xTokenBacking.sol b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol new file mode 100644 index 00000000..8c764df9 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol @@ -0,0 +1,1133 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/8/2023 + **/ + +pragma solidity ^0.8.17; + +// File contracts/mapping-token/interfaces/IGuard.sol +// License-Identifier: MIT + + +interface IGuard { + function deposit(uint256 id, address token, address recipient, uint256 amount) external; +} + +// File contracts/mapping-token/interfaces/IWToken.sol +// License-Identifier: MIT + + +interface IWToken { + function deposit() external payable; + function withdraw(uint wad) external; +} + +// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +// File contracts/utils/TokenTransferHelper.sol +// License-Identifier: MIT + +library TokenTransferHelper { + function safeTransfer( + address token, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transfer.selector, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transfer token failed"); + } + + function safeTransferFrom( + address token, + address sender, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transferFrom.selector, + sender, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transferFrom token failed"); + } + + function safeTransferNative( + address receiver, + uint256 amount + ) internal { + (bool success,) = payable(receiver).call{value: amount}(""); + require(success, "helix:transfer native token failed"); + } +} + +// File contracts/interfaces/IMessager.sol +// License-Identifier: MIT + +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; +} + +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +} + +interface IMessageId { + function latestSentMessageId() external view returns(bytes32); + function latestRecvMessageId() external view returns(bytes32); + function messageDelivered(bytes32 messageId) external view returns(bool); +} + +// File contracts/utils/DailyLimit.sol +// License-Identifier: MIT + + +/// @title relay with daily limit - Allows the relay to mint token in a daily limit. +contract DailyLimit { + + event DailyLimitChange(address token, uint dailyLimit); + + mapping(address => uint) public dailyLimit; + // deprecated, slot for upgrade + mapping(address => uint) public _slotReserved; + mapping(address => uint) public spentToday; + + uint constant public SPEND_BIT_LENGTH = 192; + uint constant public LASTDAY_BIT_LENGTH = 64; + + /// ==== Internal functions ==== + + /// @dev Contract constructor sets initial owners, required number of confirmations and daily mint limit. + /// @param _token Token address. + /// @param _dailyLimit Amount in wei, which can be mint without confirmations on a daily basis. + function _setDailyLimit(address _token, uint _dailyLimit) + internal + { + require(_dailyLimit < type(uint192).max, "DaliyLimit: overflow uint192"); + dailyLimit[_token] = _dailyLimit; + } + + /// @dev Allows to change the daily limit. + /// @param _token Token address. + /// @param _dailyLimit Amount in wei. + function _changeDailyLimit(address _token, uint _dailyLimit) + internal + { + require(_dailyLimit < type(uint192).max, "DaliyLimit: overflow uint192"); + dailyLimit[_token] = _dailyLimit; + emit DailyLimitChange(_token, _dailyLimit); + } + + /// @dev Allows to change the daily limit. + /// @param token Token address. + /// @param amount Amount in wei. + function expendDailyLimit(address token, uint amount) + internal + { + uint spentInfo = spentToday[token]; + uint lastday = spentInfo >> SPEND_BIT_LENGTH; + uint lastspent = spentInfo << LASTDAY_BIT_LENGTH >> LASTDAY_BIT_LENGTH; + if (block.timestamp > lastday + 24 hours) { + require(amount <= dailyLimit[token], "DailyLimit: amount exceed daily limit"); + spentToday[token] = (block.timestamp << SPEND_BIT_LENGTH) + amount; + return; + } + require(lastspent + amount <= dailyLimit[token] && amount <= dailyLimit[token], "DailyLimit: exceed daily limit"); + spentToday[token] = spentInfo + amount; + } + + /// ==== Web3 call functions ==== + + /// @dev Returns maximum withdraw amount. + /// @param token Token address. + /// @return Returns amount. + function calcMaxWithdraw(address token) + public + view + returns (uint) + { + uint spentInfo = spentToday[token]; + uint lastday = spentInfo >> SPEND_BIT_LENGTH; + uint lastspent = spentInfo << LASTDAY_BIT_LENGTH >> LASTDAY_BIT_LENGTH; + if (block.timestamp > lastday + 24 hours) { + return dailyLimit[token]; + } + + if (dailyLimit[token] < lastspent) { + return 0; + } + + return dailyLimit[token] - lastspent; + } +} + +// File contracts/utils/AccessController.sol +// License-Identifier: MIT + +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; + } + + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; + } +} + +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + +// File @zeppelin-solidity/contracts/utils/Address.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol) + + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success, ) = recipient.call{value: amount}(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + require(isContract(target), "Address: call to non-contract"); + + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + require(isContract(target), "Address: delegate call to non-contract"); + + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} + +// File @zeppelin-solidity/contracts/proxy/utils/Initializable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/utils/Initializable.sol) + + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ``` + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. + */ + modifier initializer() { + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _initialized = 1; + if (isTopLevelCall) { + _initializing = true; + } + _; + if (isTopLevelCall) { + _initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * `initializer` is equivalent to `reinitializer(1)`, so a reinitializer may be used after the original + * initialization step. This is essential to configure modules that are added through upgrades and that require + * initialization. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + */ + modifier reinitializer(uint8 version) { + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _initialized = version; + _initializing = true; + _; + _initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + require(_initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + */ + function _disableInitializers() internal virtual { + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized < type(uint8).max) { + _initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } +} + +// File contracts/mapping-token/v3/base/xTokenBridgeBase.sol +// License-Identifier: MIT + + + + + + +contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLimit { + struct MessagerService { + address sendService; + address receiveService; + } + + string public version; + uint256 public protocolFee; + uint256 public protocolFeeReserved; + address public guard; + // remoteChainId => info + mapping(uint256 => MessagerService) public messagers; + + // common method + modifier calledByMessager(uint256 _remoteChainId) { + address receiveService = messagers[_remoteChainId].receiveService; + require(receiveService == msg.sender, "invalid messager"); + _; + } + + receive() external payable {} + + function initialize(address _dao, string calldata _version) public initializer { + _initialize(_dao); + version = _version; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } + + function setProtocolFee(uint256 _protocolFee) external onlyOperator { + protocolFee = _protocolFee; + } + + function setSendService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { + messagers[_remoteChainId].sendService = _service; + ILowLevelMessageSender(_service).registerRemoteReceiver(_remoteChainId, _remoteBridge); + } + + function setReceiveService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { + messagers[_remoteChainId].receiveService = _service; + ILowLevelMessageReceiver(_service).registerRemoteSender(_remoteChainId, _remoteBridge); + } + + function withdrawProtocolFee(address _receiver, uint256 _amount) external onlyDao { + require(_amount <= protocolFeeReserved, "not enough fee"); + protocolFeeReserved -= _amount; + TokenTransferHelper.safeTransferNative(_receiver, _amount); + } + + function _sendMessage( + uint256 _remoteChainId, + bytes memory _payload, + uint256 feePrepaid, + bytes memory _extParams + ) internal whenNotPaused returns(bytes32 messageId) { + MessagerService memory service = messagers[_remoteChainId]; + require(service.sendService != address(0), "bridge not configured"); + protocolFeeReserved += protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: feePrepaid - protocolFee}( + _remoteChainId, + _payload, + _extParams + ); + messageId = IMessageId(service.sendService).latestSentMessageId(); + } + + 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).messageDelivered(_transferId), "message not delivered"); + } + + 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(); + } + + // settings + function updateGuard(address _guard) external onlyDao { + guard = _guard; + } + + function setDailyLimit(address _token, uint256 _dailyLimit) external onlyDao { + _setDailyLimit(_token, _dailyLimit); + } +} + +// File contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol +// License-Identifier: MIT + +interface IxTokenIssuing { + function handleIssuingForUnlockFailureFromRemote( + uint256 remoteChainId, + bytes32 transferId, + address originalToken, + address originalSender, + uint256 amount + ) external; + + function issuexToken( + uint256 remoteChainId, + address originalToken, + address recipient, + uint256 amount + ) external; +} + +// File contracts/mapping-token/v3/base/xTokenBacking.sol +// License-Identifier: MIT + + + + + + +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 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 TokenUnlockedForFailed(bytes32 transferId, uint256 remoteChainId, address token, address recipient, uint256 amount); + + function setwToken(address _wtoken) external onlyDao { + wToken = _wtoken; + } + + function registerOriginalToken( + uint256 _remoteChainId, + address _originalToken, + address _xToken, + uint256 _dailyLimit + ) external onlyDao { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); + originalToken2xTokens[key] = _xToken; + _setDailyLimit(_originalToken, _dailyLimit); + } + + function lockAndRemoteIssuing( + uint256 _remoteChainId, + address _originalToken, + address _recipient, + uint256 _amount, + bytes memory _extParams + ) external payable { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); + require(originalToken2xTokens[key] != address(0), "token not registered"); + + uint256 prepaid = msg.value; + // lock token + if (address(0) == _originalToken) { + // native token + require(msg.value > _amount, "invalid value"); + prepaid -= _amount; + } else { + // erc20 token + TokenTransferHelper.safeTransferFrom( + _originalToken, + msg.sender, + address(this), + _amount + ); + } + bytes memory issuxToken = encodeIssuexToken( + _originalToken, + _recipient, + _amount + ); + bytes32 transferId = _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); + bytes32 lockMessageHash = keccak256(abi.encodePacked(transferId, _remoteChainId, _originalToken, msg.sender, _amount)); + lockedMessages[transferId] = LockedInfo(lockMessageHash, false); + emit TokenLocked(transferId, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); + } + + function encodeIssuexToken( + address _originalToken, + address _recipient, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.issuexToken.selector, + block.chainid, + _originalToken, + _recipient, + _amount + ); + } + + // in backing contract, it only know the original token info + // in issuing contract, it know the mapping relationship of original token and it's mapping token xToken + // we use original token info in messages + + // method for backing + // receive unlock original token message from remote issuing contract + function unlockFromRemote( + uint256 _remoteChainId, + address _originalToken, + address _recipient, + uint256 _amount + ) external calledByMessager(_remoteChainId) whenNotPaused { + expendDailyLimit(_originalToken, _amount); + + bytes32 transferId = _latestRecvMessageId(_remoteChainId); + require(unlockedTransferIds[transferId] == false, "message has been accepted"); + unlockedTransferIds[transferId] = true; + + // native token do not use guard + if (address(0) == _originalToken) { + _unlockNativeToken(transferId, _recipient, _amount); + } else { + _unlockErc20Token(transferId, _originalToken, _recipient, _amount); + } + emit TokenUnlocked(transferId, _remoteChainId, _originalToken, _recipient, _amount); + } + + function _unlockNativeToken( + bytes32 _transferId, + address _recipient, + uint256 _amount + ) internal { + address _guard = guard; + if (_guard == address(0)) { + TokenTransferHelper.safeTransferNative(_recipient, _amount); + } else { + IWToken(wToken).deposit{value: _amount}(); + // see https://github.com/helix-bridge/contracts/issues/18 + uint allowance = IERC20(wToken).allowance(address(this), _guard); + require(IERC20(wToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(_transferId), wToken, _recipient, _amount); + } + } + + function _unlockErc20Token( + bytes32 _transferId, + address _token, + address _recipient, + uint256 _amount + ) internal { + address _guard = guard; + if (_guard == address(0)) { + TokenTransferHelper.safeTransfer(_token, _recipient, _amount); + } else { + uint allowance = IERC20(_token).allowance(address(this), _guard); + require(IERC20(_token).approve(_guard, allowance + _amount), "Backing:approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(_transferId), _token, _recipient, _amount); + } + } + + function requestRemoteIssuingForUnlockFailure( + bytes32 _transferId, + uint256 _remoteChainId, + address _originalToken, + address _originalSender, + uint256 _amount, + bytes memory _extParams + ) external payable { + // must not exist in successful issue list + require(unlockedTransferIds[_transferId] == false, "success message can't refund for failed"); + _assertMessageIsDelivered(_remoteChainId, _transferId); + bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( + _transferId, + _originalToken, + _originalSender, + _amount + ); + bytes32 refundId = _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); + emit RemoteIssuingFailure(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); + } + + function encodeIssuingForUnlockFailureFromRemote( + bytes32 _transferId, + address _originalToken, + address _originalSender, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.handleIssuingForUnlockFailureFromRemote.selector, + block.chainid, + _transferId, + _originalToken, + _originalSender, + _amount + ); + } + + // when lock and issuing failed + // receive unlock(refund) message from remote issuing contract + // this will refund original token to original sender + function handleUnlockForIssuingFailureFromRemote( + uint256 _remoteChainId, + bytes32 _transferId, + address _originalToken, + address _originSender, + uint256 _amount + ) 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; + if (_originalToken == address(0)) { + TokenTransferHelper.safeTransferNative(_originSender, _amount); + } else { + TokenTransferHelper.safeTransfer(_originalToken, _originSender, _amount); + } + emit TokenUnlockedForFailed(_transferId, _remoteChainId, _originalToken, _originSender, _amount); + } +} \ No newline at end of file diff --git a/helix-contract/flatten/xtoken-v3/xTokenErc20.sol b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol new file mode 100644 index 00000000..41299649 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol @@ -0,0 +1,554 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/8/2023 + **/ + +pragma solidity ^0.8.17; + +// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +// File @zeppelin-solidity/contracts/utils/math/SafeMath.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) + + +// CAUTION +// This version of SafeMath should only be used with Solidity 0.8 or later, +// because it relies on the compiler's built in overflow checks. + +/** + * @dev Wrappers over Solidity's arithmetic operations. + * + * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler + * now has built in overflow checking. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return a % b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {trySub}. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b <= a, errorMessage); + return a - b; + } + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a / b; + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting with custom message when dividing by zero. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {tryMod}. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a % b; + } + } +} + +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/access/Ownable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) + + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _transferOwnership(_msgSender()); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} + +// File contracts/mapping-token/v3/base/xTokenErc20.sol +// License-Identifier: MIT + + + +contract xTokenErc20 is IERC20, Ownable { + using SafeMath for uint256; + + mapping (address => uint256) private _balances; + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string public name; + string public symbol; + uint8 public decimals; + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + _transferOwnership(_msgSender()); + } + + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, msg.sender, _allowances[sender][msg.sender].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } + + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue)); + return true; + } + + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + // only factory contract can mint with the lock proof from ethereum + function mint(address account, uint256 amount) external onlyOwner { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + if (account != msg.sender && owner() != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { + _approve(account, msg.sender, _allowances[account][msg.sender].sub(amount, "ERC20: decreased allowance below zero")); + } + _burn(account, amount); + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} \ No newline at end of file diff --git a/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol new file mode 100644 index 00000000..074a6f39 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol @@ -0,0 +1,1551 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/8/2023 + **/ + +pragma solidity ^0.8.17; + +// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +// File contracts/utils/TokenTransferHelper.sol +// License-Identifier: MIT + +library TokenTransferHelper { + function safeTransfer( + address token, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transfer.selector, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transfer token failed"); + } + + function safeTransferFrom( + address token, + address sender, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transferFrom.selector, + sender, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "helix:transferFrom token failed"); + } + + function safeTransferNative( + address receiver, + uint256 amount + ) internal { + (bool success,) = payable(receiver).call{value: amount}(""); + require(success, "helix:transfer native token failed"); + } +} + +// File contracts/mapping-token/interfaces/IGuard.sol +// License-Identifier: MIT + + +interface IGuard { + function deposit(uint256 id, address token, address recipient, uint256 amount) external; +} + +// File contracts/utils/DailyLimit.sol +// License-Identifier: MIT + + +/// @title relay with daily limit - Allows the relay to mint token in a daily limit. +contract DailyLimit { + + event DailyLimitChange(address token, uint dailyLimit); + + mapping(address => uint) public dailyLimit; + // deprecated, slot for upgrade + mapping(address => uint) public _slotReserved; + mapping(address => uint) public spentToday; + + uint constant public SPEND_BIT_LENGTH = 192; + uint constant public LASTDAY_BIT_LENGTH = 64; + + /// ==== Internal functions ==== + + /// @dev Contract constructor sets initial owners, required number of confirmations and daily mint limit. + /// @param _token Token address. + /// @param _dailyLimit Amount in wei, which can be mint without confirmations on a daily basis. + function _setDailyLimit(address _token, uint _dailyLimit) + internal + { + require(_dailyLimit < type(uint192).max, "DaliyLimit: overflow uint192"); + dailyLimit[_token] = _dailyLimit; + } + + /// @dev Allows to change the daily limit. + /// @param _token Token address. + /// @param _dailyLimit Amount in wei. + function _changeDailyLimit(address _token, uint _dailyLimit) + internal + { + require(_dailyLimit < type(uint192).max, "DaliyLimit: overflow uint192"); + dailyLimit[_token] = _dailyLimit; + emit DailyLimitChange(_token, _dailyLimit); + } + + /// @dev Allows to change the daily limit. + /// @param token Token address. + /// @param amount Amount in wei. + function expendDailyLimit(address token, uint amount) + internal + { + uint spentInfo = spentToday[token]; + uint lastday = spentInfo >> SPEND_BIT_LENGTH; + uint lastspent = spentInfo << LASTDAY_BIT_LENGTH >> LASTDAY_BIT_LENGTH; + if (block.timestamp > lastday + 24 hours) { + require(amount <= dailyLimit[token], "DailyLimit: amount exceed daily limit"); + spentToday[token] = (block.timestamp << SPEND_BIT_LENGTH) + amount; + return; + } + require(lastspent + amount <= dailyLimit[token] && amount <= dailyLimit[token], "DailyLimit: exceed daily limit"); + spentToday[token] = spentInfo + amount; + } + + /// ==== Web3 call functions ==== + + /// @dev Returns maximum withdraw amount. + /// @param token Token address. + /// @return Returns amount. + function calcMaxWithdraw(address token) + public + view + returns (uint) + { + uint spentInfo = spentToday[token]; + uint lastday = spentInfo >> SPEND_BIT_LENGTH; + uint lastspent = spentInfo << LASTDAY_BIT_LENGTH >> LASTDAY_BIT_LENGTH; + if (block.timestamp > lastday + 24 hours) { + return dailyLimit[token]; + } + + if (dailyLimit[token] < lastspent) { + return 0; + } + + return dailyLimit[token] - lastspent; + } +} + +// File contracts/utils/AccessController.sol +// License-Identifier: MIT + +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; + } + + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; + } +} + +// File contracts/interfaces/IMessager.sol +// License-Identifier: MIT + +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; +} + +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +} + +interface IMessageId { + function latestSentMessageId() external view returns(bytes32); + function latestRecvMessageId() external view returns(bytes32); + function messageDelivered(bytes32 messageId) external view returns(bool); +} + +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + +// File @zeppelin-solidity/contracts/utils/Address.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol) + + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success, ) = recipient.call{value: amount}(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + require(isContract(target), "Address: call to non-contract"); + + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + require(isContract(target), "Address: delegate call to non-contract"); + + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} + +// File @zeppelin-solidity/contracts/proxy/utils/Initializable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/utils/Initializable.sol) + + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ``` + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. + */ + modifier initializer() { + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _initialized = 1; + if (isTopLevelCall) { + _initializing = true; + } + _; + if (isTopLevelCall) { + _initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * `initializer` is equivalent to `reinitializer(1)`, so a reinitializer may be used after the original + * initialization step. This is essential to configure modules that are added through upgrades and that require + * initialization. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + */ + modifier reinitializer(uint8 version) { + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _initialized = version; + _initializing = true; + _; + _initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + require(_initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + */ + function _disableInitializers() internal virtual { + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized < type(uint8).max) { + _initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } +} + +// File contracts/mapping-token/v3/base/xTokenBridgeBase.sol +// License-Identifier: MIT + + + + + + +contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLimit { + struct MessagerService { + address sendService; + address receiveService; + } + + string public version; + uint256 public protocolFee; + uint256 public protocolFeeReserved; + address public guard; + // remoteChainId => info + mapping(uint256 => MessagerService) public messagers; + + // common method + modifier calledByMessager(uint256 _remoteChainId) { + address receiveService = messagers[_remoteChainId].receiveService; + require(receiveService == msg.sender, "invalid messager"); + _; + } + + receive() external payable {} + + function initialize(address _dao, string calldata _version) public initializer { + _initialize(_dao); + version = _version; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } + + function setProtocolFee(uint256 _protocolFee) external onlyOperator { + protocolFee = _protocolFee; + } + + function setSendService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { + messagers[_remoteChainId].sendService = _service; + ILowLevelMessageSender(_service).registerRemoteReceiver(_remoteChainId, _remoteBridge); + } + + function setReceiveService(uint256 _remoteChainId, address _remoteBridge, address _service) external onlyDao { + messagers[_remoteChainId].receiveService = _service; + ILowLevelMessageReceiver(_service).registerRemoteSender(_remoteChainId, _remoteBridge); + } + + function withdrawProtocolFee(address _receiver, uint256 _amount) external onlyDao { + require(_amount <= protocolFeeReserved, "not enough fee"); + protocolFeeReserved -= _amount; + TokenTransferHelper.safeTransferNative(_receiver, _amount); + } + + function _sendMessage( + uint256 _remoteChainId, + bytes memory _payload, + uint256 feePrepaid, + bytes memory _extParams + ) internal whenNotPaused returns(bytes32 messageId) { + MessagerService memory service = messagers[_remoteChainId]; + require(service.sendService != address(0), "bridge not configured"); + protocolFeeReserved += protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: feePrepaid - protocolFee}( + _remoteChainId, + _payload, + _extParams + ); + messageId = IMessageId(service.sendService).latestSentMessageId(); + } + + 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).messageDelivered(_transferId), "message not delivered"); + } + + 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(); + } + + // settings + function updateGuard(address _guard) external onlyDao { + guard = _guard; + } + + function setDailyLimit(address _token, uint256 _dailyLimit) external onlyDao { + _setDailyLimit(_token, _dailyLimit); + } +} + +// File @zeppelin-solidity/contracts/access/Ownable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) + + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _transferOwnership(_msgSender()); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} + +// File @zeppelin-solidity/contracts/utils/math/SafeMath.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) + + +// CAUTION +// This version of SafeMath should only be used with Solidity 0.8 or later, +// because it relies on the compiler's built in overflow checks. + +/** + * @dev Wrappers over Solidity's arithmetic operations. + * + * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler + * now has built in overflow checking. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return a % b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {trySub}. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b <= a, errorMessage); + return a - b; + } + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a / b; + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting with custom message when dividing by zero. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {tryMod}. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a % b; + } + } +} + +// File contracts/mapping-token/v3/base/xTokenErc20.sol +// License-Identifier: MIT + + + +contract xTokenErc20 is IERC20, Ownable { + using SafeMath for uint256; + + mapping (address => uint256) private _balances; + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string public name; + string public symbol; + uint8 public decimals; + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + _transferOwnership(_msgSender()); + } + + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, msg.sender, _allowances[sender][msg.sender].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } + + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue)); + return true; + } + + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + // only factory contract can mint with the lock proof from ethereum + function mint(address account, uint256 amount) external onlyOwner { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + if (account != msg.sender && owner() != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { + _approve(account, msg.sender, _allowances[account][msg.sender].sub(amount, "ERC20: decreased allowance below zero")); + } + _burn(account, amount); + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} + +// File contracts/mapping-token/v3/interfaces/IxTokenBacking.sol +// License-Identifier: MIT + +interface IxTokenBacking { + function unlockFromRemote( + uint256 remoteChainId, + address originalToken, + address recipient, + uint256 amount + ) external; + + function handleUnlockForIssuingFailureFromRemote( + uint256 remoteChainId, + bytes32 transferId, + address originalToken, + address originalSender, + uint256 amount + ) external; +} + +// File contracts/mapping-token/v3/base/xTokenIssuing.sol +// License-Identifier: MIT + + + + + +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; + // xToken => Origin Token Info + mapping(address => OriginalTokenInfo) public originalTokens; + + event IssuingERC20Created(uint256 originalChainId, address originalToken, address xToken); + 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 TokenRemintForFailed(bytes32 transferId, uint256 originalChainId, address originalToken, address xToken, address originalSender, uint256 amount); + + function registerxToken( + uint256 _originalChainId, + address _originalToken, + string memory _originalChainName, + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _dailyLimit + ) external onlyDao returns (address xToken) { + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); + require(xTokens[salt] == address(0), "contract has been deployed"); + bytes memory bytecode = type(xTokenErc20).creationCode; + bytes memory bytecodeWithInitdata = abi.encodePacked( + bytecode, + abi.encode( + string(abi.encodePacked(_name, "[", _originalChainName, ">")), + string(abi.encodePacked("x", _symbol)), + _decimals + )); + assembly { + xToken := create2(0, add(bytecodeWithInitdata, 0x20), mload(bytecodeWithInitdata), salt) + if iszero(extcodesize(xToken)) { revert(0, 0) } + } + xTokens[salt] = xToken; + originalTokens[xToken] = OriginalTokenInfo(_originalChainId, _originalToken); + _setDailyLimit(xToken, _dailyLimit); + emit IssuingERC20Created(_originalChainId, _originalToken, xToken); + } + + function updatexToken( + uint256 _originalChainId, + address _originalToken, + address _xToken + ) external onlyDao { + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); + address oldxToken = xTokens[salt]; + if (oldxToken != address(0)) { + require(xTokenErc20(oldxToken).totalSupply() == 0, "can't delete old xToken"); + delete originalTokens[oldxToken]; + } + xTokens[salt] = _xToken; + originalTokens[_xToken] = OriginalTokenInfo(_originalChainId, _originalToken); + emit IssuingERC20Updated(_originalChainId, _originalToken, _xToken, oldxToken); + } + + // receive issuing xToken message from remote backing contract + function issuexToken( + uint256 _remoteChainId, + address _originalToken, + address _recipient, + uint256 _amount + ) external calledByMessager(_remoteChainId) whenNotPaused { + bytes32 transferId = _latestRecvMessageId(_remoteChainId); + 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; + + address _guard = guard; + if (_guard != address(0)) { + xTokenErc20(xToken).mint(address(this), _amount); + uint allowance = xTokenErc20(xToken).allowance(address(this), _guard); + require(xTokenErc20(xToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(transferId), xToken, _recipient, _amount); + } else { + xTokenErc20(xToken).mint(_recipient, _amount); + } + emit xTokenIssued(transferId, _remoteChainId, _originalToken, xToken, _recipient, _amount); + } + + function burnAndRemoteUnlock( + address _xToken, + address _recipient, + uint256 _amount, + bytes memory _extParams + ) external payable { + require(_amount > 0, "can not transfer amount zero"); + OriginalTokenInfo memory originalInfo = originalTokens[_xToken]; + // 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, + _recipient, + _amount + ); + bytes32 transferId = _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); + } + + function encodeUnlockFromRemote( + address _originalToken, + address _recipient, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.unlockFromRemote.selector, + block.chainid, + _originalToken, + _recipient, + _amount + ); + } + + function requestRemoteUnlockForIssuingFailure( + bytes32 _transferId, + uint256 _originalChainId, + address _originalToken, + address _originalSender, + uint256 _amount, + bytes memory _extParams + ) external payable { + require(issueTransferIds[_transferId] == false, "success message can't refund for failed"); + _assertMessageIsDelivered(_originalChainId, _transferId); + bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( + _transferId, + _originalToken, + _originalSender, + _amount + ); + bytes32 refundId = _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); + emit RemoteUnlockForIssuingFailureRequested(refundId, _transferId, _originalToken, _originalSender, _amount, msg.value); + } + + function encodeUnlockForIssuingFailureFromRemote( + bytes32 _transferId, + address _originalToken, + address _originalSender, + uint256 _amount + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.handleUnlockForIssuingFailureFromRemote.selector, + block.chainid, + _transferId, + _originalToken, + _originalSender, + _amount + ); + } + + // when burn and unlock failed + // receive reIssue(refund) message from remote backing contract + // this will refund xToken to original sender + function handleIssuingForUnlockFailureFromRemote( + uint256 _originalChainId, + bytes32 _transferId, + address _originalToken, + address _originalSender, + uint256 _amount + ) external calledByMessager(_originalChainId) whenNotPaused { + BurnInfo memory burnInfo = burnMessages[_transferId]; + require(burnInfo.hasRefundForFailed == false, "Backing:the burn message has been refund"); + + 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); + } + + function xTokenSalt( + uint256 _originalChainId, + address _originalToken + ) public view returns(bytes32) { + return keccak256(abi.encodePacked(_originalChainId, _originalToken, version)); + } +} \ No newline at end of file From 0371e88eb535a5ae0dd1850f5438784aa65a0791 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Mon, 11 Dec 2023 21:41:32 +0800 Subject: [PATCH 06/26] redeploy on testnet --- helix-contract/address/xtoken-dev.json | 12 +++---- .../mapping-token/v3/base/xTokenBacking.sol | 31 +++++++++++++------ .../v3/base/xTokenBridgeBase.sol | 19 +++++++++--- .../mapping-token/v3/base/xTokenIssuing.sol | 5 +++ .../contracts/messagers/MsglineMessager.sol | 13 ++++---- helix-contract/deploy/deploy_xtoken_config.js | 8 ++--- helix-contract/deploy/deploy_xtoken_proxy.js | 4 +-- helix-contract/deploy/flatten-xtoken.sh | 1 + 8 files changed, 60 insertions(+), 33 deletions(-) diff --git a/helix-contract/address/xtoken-dev.json b/helix-contract/address/xtoken-dev.json index 42021f62..e9675ddc 100644 --- a/helix-contract/address/xtoken-dev.json +++ b/helix-contract/address/xtoken-dev.json @@ -1,22 +1,22 @@ { "messagers": { "crab": { - "msglineMessager": "0x8FC1319581fcb95e86461216B2Cad86DC78D5681" + "msglineMessager": "0xCAb1f69C671f1548fd3dE5d63852E9B9181a0D0E" }, "sepolia": { - "msglineMessager": "0x35A314e53e2fdDfeCA7b743042AaCfB1ABAF0aDe" + "msglineMessager": "0x527B67a61C6E1344C359Af2e241aAFeb0c3a9DE9" } }, "backingProxy": { - "crab": "0xd082Af5a732dB55F0fe016c100C40c26FB267bf3" + "crab": "0xb137BDf1Ad5392027832f54a4409685Ef52Aa9dA" }, "backingLogic": { - "crab": "0x4b9a3b5a9929dc2F9766739dd4457e2941f6A0c9" + "crab": "0x0cDc94088C40B461C3c9cF44DD38B328BDca95e9" }, "issuingProxy": { - "sepolia": "0x97c0a7162317786E1030b7D023c2B8525Fc28497" + "sepolia": "0x44A001aF6AcD2d5f5cB82FCB14Af3d497D56faB4" }, "issuingLogic": { - "sepolia": "0x09Fe8eA52CD9F3bFF11E84378cfc99441910AD3f" + "sepolia": "0x97cd4227eFC7AEd96CD027994dE2e3E9ACc1b394" } } diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol index 31b5026d..c0a88052 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -8,6 +8,9 @@ import "../../interfaces/IGuard.sol"; import "../../interfaces/IWToken.sol"; import "../../../utils/TokenTransferHelper.sol"; +// The contract implements the backing side of the Helix xToken protocol. +// 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; @@ -31,10 +34,15 @@ contract xTokenBacking is xTokenBridgeBase { event RemoteIssuingFailure(bytes32 refundId, bytes32 transferId, address mappingToken, 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 + // this is used to unlock token to guard function setwToken(address _wtoken) external onlyDao { wToken = _wtoken; } + // register token on source chain + // this is used to prevent the unregistered token's transfer + // and must be registered on the target chain before function registerOriginalToken( uint256 _remoteChainId, address _originalToken, @@ -78,6 +86,7 @@ contract xTokenBacking is xTokenBridgeBase { ); 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); } @@ -96,11 +105,6 @@ contract xTokenBacking is xTokenBridgeBase { ); } - // in backing contract, it only know the original token info - // in issuing contract, it know the mapping relationship of original token and it's mapping token xToken - // we use original token info in messages - - // method for backing // receive unlock original token message from remote issuing contract function unlockFromRemote( uint256 _remoteChainId, @@ -132,11 +136,12 @@ contract xTokenBacking is xTokenBridgeBase { if (_guard == address(0)) { TokenTransferHelper.safeTransferNative(_recipient, _amount); } else { - IWToken(wToken).deposit{value: _amount}(); - // see https://github.com/helix-bridge/contracts/issues/18 - uint allowance = IERC20(wToken).allowance(address(this), _guard); - require(IERC20(wToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); - IGuard(_guard).deposit(uint256(_transferId), wToken, _recipient, _amount); + address _wToken = wToken; + // when use guard, we deposit native token to the wToken contract + IWToken(_wToken).deposit{value: _amount}(); + uint allowance = IERC20(_wToken).allowance(address(this), _guard); + require(IERC20(_wToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(_transferId), _wToken, _recipient, _amount); } } @@ -156,6 +161,10 @@ 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, @@ -196,6 +205,8 @@ contract xTokenBacking is xTokenBridgeBase { // when lock and issuing failed // receive unlock(refund) message from remote issuing contract // this will refund original token to original sender + // 1. the message is not refunded before + // 2. the locked message exist and the information(hash) matched function handleUnlockForIssuingFailureFromRemote( uint256 _remoteChainId, bytes32 _transferId, diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol index 4d9cecba..04cc8919 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -8,20 +8,26 @@ import "../../../utils/AccessController.sol"; import "../../../utils/DailyLimit.sol"; import "../../../utils/TokenTransferHelper.sol"; +// The Base contract for xToken protocol +// Backing or Issuing contract will inherit the contract. +// This contract define the access authorization, the message channel contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLimit { struct MessagerService { address sendService; address receiveService; } + // the version is to issue different xTokens for different version of bridge. string public version; + // the protocol fee for each time user send transaction uint256 public protocolFee; + // the reserved protocol fee in the contract uint256 public protocolFeeReserved; address public guard; // remoteChainId => info mapping(uint256 => MessagerService) public messagers; - // common method + // must be called by message service configured modifier calledByMessager(uint256 _remoteChainId) { address receiveService = messagers[_remoteChainId].receiveService; require(receiveService == msg.sender, "invalid messager"); @@ -66,13 +72,14 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim function _sendMessage( uint256 _remoteChainId, bytes memory _payload, - uint256 feePrepaid, + uint256 _feePrepaid, bytes memory _extParams ) internal whenNotPaused returns(bytes32 messageId) { MessagerService memory service = messagers[_remoteChainId]; require(service.sendService != address(0), "bridge not configured"); - protocolFeeReserved += protocolFee; - ILowLevelMessageSender(service.sendService).sendMessage{value: feePrepaid - protocolFee}( + uint256 _protocolFee = protocolFee; + protocolFeeReserved += _protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: _feePrepaid - _protocolFee}( _remoteChainId, _payload, _extParams @@ -80,12 +87,16 @@ 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).messageDelivered(_transferId), "message not delivered"); } + // 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"); diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol index 53cbfd80..9bb90f58 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -149,6 +149,9 @@ contract xTokenIssuing is xTokenBridgeBase { ); } + // send unlock message when issuing failed + // 1. message has been delivered + // 2. xtoken not issued function requestRemoteUnlockForIssuingFailure( bytes32 _transferId, uint256 _originalChainId, @@ -188,6 +191,8 @@ contract xTokenIssuing is xTokenBridgeBase { // when burn and unlock failed // receive reIssue(refund) message from remote backing contract // this will refund xToken to original sender + // 1. the transfer not refund before + // 2. the burn information(hash) matched function handleIssuingForUnlockFailureFromRemote( uint256 _originalChainId, bytes32 _transferId, diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol index fa4d84a0..4c73c228 100644 --- a/helix-contract/contracts/messagers/MsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -21,8 +21,8 @@ contract MsglineMessager is Application, AccessController { mapping(bytes32=>address) public remoteAppReceivers; mapping(bytes32=>address) public remoteAppSenders; - event CallerUnMatched(uint256 srcAppChainId, address srcAppAddress); - event CallResult(uint256 srcAppChainId, bool result); + event CallerUnMatched(uint256 srcAppChainId, bytes32 transferId, address srcAppAddress); + event CallResult(uint256 srcAppChainId, bytes32 transferId, bool result); modifier onlyWhiteList() { require(whiteList[msg.sender], "msg.sender not in whitelist"); @@ -34,8 +34,6 @@ contract MsglineMessager is Application, AccessController { _; } - //event CallResult(string sourceChain, string srcAddress, bool successed); - constructor(address _dao, address _msgline) { _initialize(_dao); msgline = IMessageLine(_msgline); @@ -84,22 +82,23 @@ contract MsglineMessager is Application, AccessController { require(srcChainId == remoteMessager.msglineRemoteChainId, "invalid remote chainid"); require(remoteMessager.messager == _xmsgSender(), "invalid remote messager"); bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); + bytes32 transferId = latestRecvMessageId(); // check remote appSender if (_remoteAppAddress != remoteAppSenders[key]) { - emit CallerUnMatched(_srcAppChainId, _remoteAppAddress); + emit CallerUnMatched(_srcAppChainId, transferId, _remoteAppAddress); return; } (bool success,) = _localAppAddress.call(_message); // don't revert to prevent message block - emit CallResult(_srcAppChainId, success); + emit CallResult(_srcAppChainId, transferId, success); } function latestSentMessageId() external view returns(bytes32) { return msgline.sentMessageId(); } - function latestRecvMessageId() external view returns(bytes32) { + function latestRecvMessageId() public view returns(bytes32) { return msgline.recvMessageId(); } diff --git a/helix-contract/deploy/deploy_xtoken_config.js b/helix-contract/deploy/deploy_xtoken_config.js index 004f838e..5ad953d2 100644 --- a/helix-contract/deploy/deploy_xtoken_config.js +++ b/helix-contract/deploy/deploy_xtoken_config.js @@ -11,8 +11,8 @@ const crabNetwork = { name: "crab", url: "https://crab-rpc.darwinia.network", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", - messager: "0x8FC1319581fcb95e86461216B2Cad86DC78D5681", - backing: "0xd082Af5a732dB55F0fe016c100C40c26FB267bf3", + messager: "0xCAb1f69C671f1548fd3dE5d63852E9B9181a0D0E", + backing: "0xb137BDf1Ad5392027832f54a4409685Ef52Aa9dA", chainid: 44 }; @@ -20,8 +20,8 @@ const sepoliaNetwork = { name: "sepolia", url: "https://rpc-sepolia.rockx.com", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", - messager: "0x35A314e53e2fdDfeCA7b743042AaCfB1ABAF0aDe", - issuing: "0x97c0a7162317786E1030b7D023c2B8525Fc28497", + messager: "0x527B67a61C6E1344C359Af2e241aAFeb0c3a9DE9", + issuing: "0x44A001aF6AcD2d5f5cB82FCB14Af3d497D56faB4", chainid: 11155111 }; diff --git a/helix-contract/deploy/deploy_xtoken_proxy.js b/helix-contract/deploy/deploy_xtoken_proxy.js index f599ad9c..d031ec2a 100644 --- a/helix-contract/deploy/deploy_xtoken_proxy.js +++ b/helix-contract/deploy/deploy_xtoken_proxy.js @@ -45,11 +45,11 @@ async function deployxTokenProxy(wallet, salt, dao, proxyAdminAddress, logicAddr async function deploy() { const walletCrab = wallet(crabNetwork.url); - const backingLogic = "0x7733Df6aCd438bCaDC70Deb36757C25Ee4820C11"; + const backingLogic = "0x0cDc94088C40B461C3c9cF44DD38B328BDca95e9"; await deployxTokenProxy(walletCrab, "xtoken-backing-1.0.0", crabNetwork.dao, crabNetwork.proxyAdmin, backingLogic, crabNetwork.deployer); const walletSepolia = wallet(sepoliaNetwork.url); - const issuingLogic = "0x09Fe8eA52CD9F3bFF11E84378cfc99441910AD3f"; + const issuingLogic = "0x97cd4227eFC7AEd96CD027994dE2e3E9ACc1b394"; await deployxTokenProxy(walletSepolia, "xtoken-issuing-1.0.0", sepoliaNetwork.dao, sepoliaNetwork.proxyAdmin, issuingLogic, sepoliaNetwork.deployer); } diff --git a/helix-contract/deploy/flatten-xtoken.sh b/helix-contract/deploy/flatten-xtoken.sh index df3cccf0..06e08da0 100644 --- a/helix-contract/deploy/flatten-xtoken.sh +++ b/helix-contract/deploy/flatten-xtoken.sh @@ -3,4 +3,5 @@ mkdir -p $path yarn flat contracts/mapping-token/v3/base/xTokenBacking.sol --output $path/xTokenBacking.sol yarn flat contracts/mapping-token/v3/base/xTokenIssuing.sol --output $path/xTokenIssuing.sol yarn flat contracts/mapping-token/v3/base/xTokenErc20.sol --output $path/xTokenErc20.sol +yarn flat contracts/mapping-token/v2/Guard.sol --output $path/Guard.sol yarn flat contracts/messagers/MsglineMessager.sol --output $path/MsglineMessager.sol From 108e3abbd1c8e533803f84083752d59a815b6a05 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 13 Dec 2023 14:44:16 +0800 Subject: [PATCH 07/26] audit suggestion repair --- .../contracts/mapping-token/v2/Guard.sol | 43 ++++++++++-------- .../mapping-token/v3/base/xTokenBacking.sol | 2 + .../mapping-token/v3/base/xTokenErc20.sol | 45 ++++++++++++++----- .../mapping-token/v3/base/xTokenIssuing.sol | 10 ++++- helix-contract/test/6_test_xtoken_v3.js | 14 +++--- 5 files changed, 79 insertions(+), 35 deletions(-) diff --git a/helix-contract/contracts/mapping-token/v2/Guard.sol b/helix-contract/contracts/mapping-token/v2/Guard.sol index 14ae1588..e13cef69 100644 --- a/helix-contract/contracts/mapping-token/v2/Guard.sol +++ b/helix-contract/contracts/mapping-token/v2/Guard.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity >=0.8.10; +pragma solidity >=0.8.17; import "@zeppelin-solidity/contracts/security/Pausable.sol"; import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; @@ -11,24 +11,23 @@ import "../interfaces/IWToken.sol"; contract Guard is GuardRegistry, Pausable { using SafeMath for uint256; - mapping(uint256 => bytes32) depositors; + mapping(uint256 => bytes32) deposits; uint256 public maxUnclaimableTime; - address public depositor; + mapping(address => bool) depositors; address public operator; - event TokenDeposit(uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); + event TokenDeposit(address sender, uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); event TokenClaimed(uint256 id); - constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime, address _depositor) { + constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime) { maxUnclaimableTime = _maxUnclaimableTime; - depositor = _depositor; operator = msg.sender; initialize(_guards, _threshold); } modifier onlyDepositor() { - require(msg.sender == depositor, "Guard: Invalid depositor"); + require(depositors[msg.sender] == true, "Guard: Invalid depositor"); _; } @@ -50,6 +49,10 @@ contract Guard is GuardRegistry, Pausable { operator = newOperator; } + function setDepositor(address depositor, bool enable) external onlyOperator { + depositors[depositor] = enable; + } + function setMaxUnclaimableTime(uint256 _maxUnclaimableTime) external onlyOperator { maxUnclaimableTime = _maxUnclaimableTime; } @@ -67,11 +70,12 @@ contract Guard is GuardRegistry, Pausable { address recipient, uint256 amount ) public onlyDepositor whenNotPaused { - depositors[id] = hash(abi.encodePacked(block.timestamp, token, recipient, amount)); - emit TokenDeposit(id, block.timestamp, token, recipient, amount); + deposits[id] = hash(abi.encodePacked(msg.sender, block.timestamp, token, recipient, amount)); + emit TokenDeposit(msg.sender, id, block.timestamp, token, recipient, amount); } function claimById( + address from, uint256 id, uint256 timestamp, address token, @@ -79,18 +83,18 @@ contract Guard is GuardRegistry, Pausable { uint256 amount, bool isNative ) internal { - require(hash(abi.encodePacked(timestamp, token, recipient, amount)) == depositors[id], "Guard: Invalid id to claim"); + require(hash(abi.encodePacked(from, timestamp, token, recipient, amount)) == deposits[id], "Guard: Invalid id to claim"); require(amount > 0, "Guard: Invalid amount to claim"); if (isNative) { - require(IERC20(token).transferFrom(depositor, address(this), amount), "Guard: claim native token failed"); + require(IERC20(token).transferFrom(from, address(this), amount), "Guard: claim native token failed"); uint256 balanceBefore = address(this).balance; IWToken(token).withdraw(amount); require(address(this).balance == balanceBefore.add(amount), "Guard: token is not wrapped by native token"); payable(recipient).transfer(amount); } else { - require(IERC20(token).transferFrom(depositor, recipient, amount), "Guard: claim token failed"); + require(IERC20(token).transferFrom(from, recipient, amount), "Guard: claim token failed"); } - delete depositors[id]; + delete deposits[id]; emit TokenClaimed(id); } @@ -100,6 +104,7 @@ contract Guard is GuardRegistry, Pausable { * @param signatures the signatures of the guards which to claim tokens. */ function claim( + address from, uint256 id, uint256 timestamp, address token, @@ -107,8 +112,8 @@ contract Guard is GuardRegistry, Pausable { uint256 amount, bytes[] memory signatures ) public { - verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(id, timestamp, token, recipient, amount), signatures); - claimById(id, timestamp, token, recipient, amount, false); + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, false); } /** @@ -117,6 +122,7 @@ contract Guard is GuardRegistry, Pausable { * @param signatures the signatures of the guards which to claim tokens. */ function claimNative( + address from, uint256 id, uint256 timestamp, address token, @@ -124,8 +130,8 @@ contract Guard is GuardRegistry, Pausable { uint256 amount, bytes[] memory signatures ) public { - verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(id, timestamp, token, recipient, amount), signatures); - claimById(id, timestamp, token, recipient, amount, true); + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, true); } /** @@ -133,6 +139,7 @@ contract Guard is GuardRegistry, Pausable { * @param id the id to be claimed */ function claimByTimeout( + address from, uint256 id, uint256 timestamp, address token, @@ -141,7 +148,7 @@ contract Guard is GuardRegistry, Pausable { bool isNative ) public whenNotPaused { require(timestamp < block.timestamp && block.timestamp - timestamp > maxUnclaimableTime, "Guard: claim at invalid time"); - claimById(id, timestamp, token, recipient, amount, isNative); + claimById(from, id, timestamp, token, recipient, amount, isNative); } function hash(bytes memory value) public pure returns (bytes32) { diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol index c0a88052..37c05705 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -175,6 +175,8 @@ contract xTokenBacking is xTokenBridgeBase { ) external payable { // 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); bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( _transferId, diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol index 7ef47161..f3cbe3dc 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import "@zeppelin-solidity/contracts/access/Ownable.sol"; import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "@zeppelin-solidity/contracts/utils/math/SafeMath.sol"; -contract xTokenErc20 is IERC20, Ownable { +contract xTokenErc20 is IERC20 { using SafeMath for uint256; mapping (address => uint256) private _balances; @@ -17,11 +16,37 @@ contract xTokenErc20 is IERC20, Ownable { string public symbol; uint8 public decimals; + address public owner; + address public pendingOwner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + modifier onlyOwner() { + require(owner == msg.sender, "Ownable: caller is not the owner"); + _; + } + constructor(string memory _name, string memory _symbol, uint8 _decimals) { name = _name; symbol = _symbol; decimals = _decimals; - _transferOwnership(_msgSender()); + _transferOwnership(msg.sender); + } + + function _transferOwnership(address newOwner) internal { + address oldOwner = owner; + owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } + + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + } + + function acceptOwnership() external { + require(pendingOwner == msg.sender, "invalid pending owner"); + _transferOwnership(pendingOwner); + pendingOwner = address(0); } function totalSupply() public view override returns (uint256) { @@ -37,8 +62,8 @@ contract xTokenErc20 is IERC20, Ownable { return true; } - function allowance(address owner, address spender) public view virtual override returns (uint256) { - return _allowances[owner][spender]; + function allowance(address account, address spender) public view virtual override returns (uint256) { + return _allowances[account][spender]; } function approve(address spender, uint256 amount) public virtual override returns (bool) { @@ -79,7 +104,7 @@ contract xTokenErc20 is IERC20, Ownable { } function burn(address account, uint256 amount) external { - if (account != msg.sender && owner() != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { + if (account != msg.sender && owner != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { _approve(account, msg.sender, _allowances[account][msg.sender].sub(amount, "ERC20: decreased allowance below zero")); } _burn(account, amount); @@ -105,12 +130,12 @@ contract xTokenErc20 is IERC20, Ownable { emit Transfer(account, address(0), amount); } - function _approve(address owner, address spender, uint256 amount) internal virtual { - require(owner != address(0), "ERC20: approve from the zero address"); + function _approve(address account, address spender, uint256 amount) internal virtual { + require(account != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address"); - _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); + _allowances[account][spender] = amount; + emit Approval(account, spender, amount); } function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol index 9bb90f58..57c9db68 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -73,7 +73,6 @@ contract xTokenIssuing is xTokenBridgeBase { bytes32 salt = xTokenSalt(_originalChainId, _originalToken); address oldxToken = xTokens[salt]; if (oldxToken != address(0)) { - require(xTokenErc20(oldxToken).totalSupply() == 0, "can't delete old xToken"); delete originalTokens[oldxToken]; } xTokens[salt] = _xToken; @@ -81,6 +80,15 @@ contract xTokenIssuing is xTokenBridgeBase { emit IssuingERC20Updated(_originalChainId, _originalToken, _xToken, oldxToken); } + // transfer xToken ownership + function transferxTokenOwnership(address _xToken, address _newOwner) external onlyDao { + xTokenErc20(_xToken).transferOwnership(_newOwner); + } + + function acceptxTokenOwnership(address _xToken) external onlyDao { + xTokenErc20(_xToken).acceptOwnership(); + } + // receive issuing xToken message from remote backing contract function issuexToken( uint256 _remoteChainId, diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index dee72ef2..dfcb60fa 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -88,11 +88,13 @@ describe("xtoken tests", () => { }); const guardBackingContract = await ethers.getContractFactory("Guard"); - const backingGuard = await guardBackingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60, backing.address); + const backingGuard = await guardBackingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60); await backingGuard.deployed(); + await backingGuard.setDepositor(backing.address, true); const guardIssuingContract = await ethers.getContractFactory("Guard"); - const issuingGuard = await guardIssuingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60, issuing.address); + const issuingGuard = await guardIssuingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60); await issuingGuard.deployed(); + await issuingGuard.setDepositor(issuing.address, true); async function registerToken( originalTokenAddress, @@ -313,9 +315,9 @@ describe("xtoken tests", () => { ethUtil.keccak256( abi.rawEncode( ['bytes4', 'bytes'], - [abi.methodID('claim', [ 'uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), - abi.rawEncode(['uint256', 'uint256', 'address', 'address', 'uint256'], - [id, timestamp, token, recipient, amount]) + [abi.methodID('claim', ['address', 'uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), + abi.rawEncode(['address', 'uint256', 'uint256', 'address', 'address', 'uint256'], + [depositer, id, timestamp, token, recipient, amount]) ] ) ); @@ -332,7 +334,7 @@ describe("xtoken tests", () => { }); const balanceBackingBefore = await balanceOf(token, depositer); const balanceRecipientBefore = await balanceOf(token, recipient); - await guard.claim(id, timestamp, token, recipient, amount, signatures); + await guard.claim(depositer, id, timestamp, token, recipient, amount, signatures); const balanceBackingAfter = await balanceOf(token, depositer); const balanceRecipientAfter = await balanceOf(token, recipient); expect(balanceBackingBefore.sub(balanceBackingAfter)).to.equal(amount); From 1212491c8b0013adcf632c2e7544d787e734d665 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 13 Dec 2023 16:01:23 +0800 Subject: [PATCH 08/26] expire for never delivered msg --- .../contracts/interfaces/IMessager.sol | 2 +- .../v3/base/xTokenBridgeBase.sol | 2 +- .../contracts/messagers/MsglineMessager.sol | 23 +++++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/helix-contract/contracts/interfaces/IMessager.sol b/helix-contract/contracts/interfaces/IMessager.sol index 34d3aa12..dcdc830d 100644 --- a/helix-contract/contracts/interfaces/IMessager.sol +++ b/helix-contract/contracts/interfaces/IMessager.sol @@ -14,5 +14,5 @@ interface ILowLevelMessageReceiver { interface IMessageId { function latestSentMessageId() external view returns(bytes32); function latestRecvMessageId() external view returns(bytes32); - function messageDelivered(bytes32 messageId) external view returns(bool); + function messageDeliveredOrSlashed(bytes32 messageId) external view returns(bool); } diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol index 04cc8919..a7d0a499 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -92,7 +92,7 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim 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).messageDelivered(_transferId), "message not delivered"); + require(IMessageId(service.receiveService).messageDeliveredOrSlashed(_transferId), "message not delivered"); } // the latest received message id diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol index 4c73c228..20b3a11e 100644 --- a/helix-contract/contracts/messagers/MsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -5,6 +5,9 @@ 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 { @@ -21,6 +24,9 @@ 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); @@ -84,6 +90,10 @@ 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); @@ -94,6 +104,15 @@ contract MsglineMessager is Application, AccessController { emit CallResult(_srcAppChainId, transferId, success); } + function slashMessage(bytes32 transferId) external { + slashTransferIds[transferId] = block.timestamp; + } + + function _messageSlashed(bytes32 transferId) internal view returns(bool) { + uint256 slashTimestamp = slashTransferIds[transferId]; + return slashTimestamp > 0 && slashTimestamp + SLASH_EXPIRE_TIME < block.timestamp; + } + function latestSentMessageId() external view returns(bytes32) { return msgline.sentMessageId(); } @@ -102,8 +121,8 @@ contract MsglineMessager is Application, AccessController { return msgline.recvMessageId(); } - function messageDelivered(bytes32 messageId) external view returns(bool) { - return msgline.dones(messageId); + 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) { From a188c5f0099903e530e1c32d7a5bbfc4242f09a8 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 13 Dec 2023 16:03:31 +0800 Subject: [PATCH 09/26] slash msg --- helix-contract/contracts/messagers/MsglineMessager.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol index 20b3a11e..1fa7cba8 100644 --- a/helix-contract/contracts/messagers/MsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -105,6 +105,7 @@ contract MsglineMessager is Application, AccessController { } function slashMessage(bytes32 transferId) external { + require(slashTransferIds[transferId] == 0, "!slash"); slashTransferIds[transferId] = block.timestamp; } From ed1d109b91ca012e1300538fcf218304843ec5cf Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 13 Dec 2023 16:34:15 +0800 Subject: [PATCH 10/26] test message delivery expire --- .../contracts/messagers/mock/MockMsgline.sol | 14 ++++++- helix-contract/test/6_test_xtoken_v3.js | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/helix-contract/contracts/messagers/mock/MockMsgline.sol b/helix-contract/contracts/messagers/mock/MockMsgline.sol index b5f14ca8..fdcefa2a 100644 --- a/helix-contract/contracts/messagers/mock/MockMsgline.sol +++ b/helix-contract/contracts/messagers/mock/MockMsgline.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.17; contract MockMessageLine { mapping(bytes32 => bool) public dones; bool public failedFlag; + bool public neverDelivered; uint256 public sendNonce; uint256 public recvNonce; @@ -18,6 +19,10 @@ contract MockMessageLine { failedFlag = true; } + function setNeverDelivered() external { + neverDelivered = true; + } + function send( uint256 toChainId, address toDapp, @@ -26,16 +31,21 @@ contract MockMessageLine { ) external payable { require(msg.value >= 1 ether, "fee is not enough"); sendNonce += 1; - MockMessageLine(remote).recv(block.chainid, msg.sender, toDapp, message); + MockMessageLine(remote).recv(sendNonce, block.chainid, msg.sender, toDapp, message); } function recv( + uint256 sendNonce, uint256 sourceChainId, address sourceSender, address toDapp, bytes calldata message ) public { - recvNonce += 1; + if (neverDelivered) { + neverDelivered = false; + return; + } + recvNonce = sendNonce; dones[bytes32(recvNonce)] = true; if (failedFlag) { failedFlag = false; diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index dfcb60fa..9e88372f 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -534,6 +534,48 @@ describe("xtoken tests", () => { user.address, 20 )).to.be.revertedWith("Guard: Invalid id to claim"); + + // test message slashed + await mockIssuingMsgline.setNeverDelivered(); + // this message will be never delivered + 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 + ); }); }); From 79d6f4d646a5c31420596dca388326e00534eeb1 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 13 Dec 2023 17:12:01 +0800 Subject: [PATCH 11/26] repair unit test --- helix-contract/test/1_test_bscv2.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/helix-contract/test/1_test_bscv2.js b/helix-contract/test/1_test_bscv2.js index 0a24fcee..5d99aa02 100644 --- a/helix-contract/test/1_test_bscv2.js +++ b/helix-contract/test/1_test_bscv2.js @@ -411,8 +411,9 @@ describe("darwinia<>bsc mapping token tests", () => { }); console.log(wallets[0].address, wallets[1].address, wallets[2].address); const guardContract = await ethers.getContractFactory("Guard"); - const guard = await guardContract.deploy([wallets[0].address, wallets[1].address, wallets[2].address], 3, 60, owner.address); + const guard = await guardContract.deploy([wallets[0].address, wallets[1].address, wallets[2].address], 3, 60); await guard.deployed(); + await guard.setDepositor(owner.address, true); await originalToken.approve(guard.address, 1000); await guard.deposit(1, originalToken.address, wallets[1].address, 100); @@ -425,15 +426,16 @@ describe("darwinia<>bsc mapping token tests", () => { ethUtil.keccak256( abi.rawEncode( ['bytes4', 'bytes'], - [abi.methodID('claim', [ 'uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), - abi.rawEncode(['uint256', 'uint256', 'address', 'address', 'uint256'], - [1, timestamp01, originalToken.address, wallets[1].address, 100]) + [abi.methodID('claim', [ 'address', 'uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), + abi.rawEncode(['address', 'uint256', 'uint256', 'address', 'address', 'uint256'], + [owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100]) ] ) ); // cannot claim without signatures await expect(guard.claimByTimeout( + owner.address, 2, timestamp01, originalToken.address, @@ -443,6 +445,7 @@ describe("darwinia<>bsc mapping token tests", () => { await network.provider.send("evm_increaseTime", [3600]); await expect(guard.claimByTimeout( + owner.address, 2, timestamp01, originalToken.address, @@ -450,6 +453,7 @@ describe("darwinia<>bsc mapping token tests", () => { 100, false)).to.be.revertedWith("Guard: Invalid id to claim"); await expect(guard.claimByTimeout( + owner.address, 1, timestamp01, originalToken.address, @@ -457,6 +461,7 @@ describe("darwinia<>bsc mapping token tests", () => { 101, false)).to.be.revertedWith("Guard: Invalid id to claim"); await expect(guard.claimByTimeout( + owner.address, 1, timestamp01, originalToken.address, @@ -475,18 +480,19 @@ describe("darwinia<>bsc mapping token tests", () => { ); return ethers.utils.hexlify(signature); }); - await guard.claim(1, timestamp01, originalToken.address, wallets[1].address, 100, signatures); + await guard.claim(owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100, signatures); expect(await originalToken.balanceOf(wallets[1].address)).to.equal(100); // can't claim twice - await expect(guard.claim(1, timestamp01, originalToken.address, wallets[1].address, 100, signatures)).to.be.revertedWith("Guard: Invalid id to claim"); + await expect(guard.claim(owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100, signatures)).to.be.revertedWith("Guard: Invalid id to claim"); await expect(guard.claimByTimeout( + owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100, true)).to.be.revertedWith("Guard: Invalid id to claim"); - await expect(guard.claim(2, timestamp02, originalToken.address, wallets[2].address, 200, signatures)).to.be.revertedWith("Guard: Invalid guard provided"); + await expect(guard.claim(owner.address, 2, timestamp02, originalToken.address, wallets[2].address, 200, signatures)).to.be.revertedWith("Guard: Invalid guard provided"); }); it("test_gas", async function () { @@ -524,8 +530,9 @@ describe("darwinia<>bsc mapping token tests", () => { mtf.setMappingNativeWrappedToken(mappingTokenAddress); const guardContract = await ethers.getContractFactory("Guard"); - const guard = await guardContract.deploy([owner.address], 1, 60, mtf.address); + const guard = await guardContract.deploy([owner.address], 1, 60); await guard.deployed(); + await guard.setDepositor(mtf.address, true); await mtf.updateGuard(guard.address); From 6842f8cb12016334a4d9a141b1e7942c85fcc14c Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 13 Dec 2023 17:19:23 +0800 Subject: [PATCH 12/26] add event --- .../contracts/messagers/MsglineMessager.sol | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol index 1fa7cba8..5457222c 100644 --- a/helix-contract/contracts/messagers/MsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -29,6 +29,7 @@ contract MsglineMessager is Application, AccessController { 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"); @@ -104,14 +105,16 @@ contract MsglineMessager is Application, AccessController { emit CallResult(_srcAppChainId, transferId, success); } - function slashMessage(bytes32 transferId) external { - require(slashTransferIds[transferId] == 0, "!slash"); - slashTransferIds[transferId] = block.timestamp; + 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 + SLASH_EXPIRE_TIME < block.timestamp; + function _messageSlashed(bytes32 _transferId) internal view returns(bool) { + uint256 slashTimestamp = slashTransferIds[_transferId]; + return slashTimestamp > 0 && slashTimestamp < block.timestamp; } function latestSentMessageId() external view returns(bytes32) { @@ -122,8 +125,8 @@ contract MsglineMessager is Application, AccessController { return msgline.recvMessageId(); } - function messageDeliveredOrSlashed(bytes32 transferId) external view returns(bool) { - return msgline.dones(transferId) || _messageSlashed(transferId); + 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) { From 7bbe5cc39e99b4daa4458d2ef1895f9d806c0579 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Tue, 19 Dec 2023 09:07:57 +0800 Subject: [PATCH 13/26] repair version issue --- .../contracts/mapping-token/v2/Guard.sol | 44 ++++++++----------- .../mapping-token/v3/base/xTokenIssuing.sol | 2 + .../contracts/messagers/MsglineMessager.sol | 1 + helix-contract/test/6_test_xtoken_v3.js | 4 +- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/helix-contract/contracts/mapping-token/v2/Guard.sol b/helix-contract/contracts/mapping-token/v2/Guard.sol index e13cef69..1897df08 100644 --- a/helix-contract/contracts/mapping-token/v2/Guard.sol +++ b/helix-contract/contracts/mapping-token/v2/Guard.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity >=0.8.17; +pragma solidity >=0.8.10; import "@zeppelin-solidity/contracts/security/Pausable.sol"; import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; @@ -11,23 +11,24 @@ import "../interfaces/IWToken.sol"; contract Guard is GuardRegistry, Pausable { using SafeMath for uint256; - mapping(uint256 => bytes32) deposits; + mapping(uint256 => bytes32) depositors; uint256 public maxUnclaimableTime; - mapping(address => bool) depositors; + address public depositor; address public operator; - event TokenDeposit(address sender, uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); + event TokenDeposit(uint256 id, address token, address recipient, uint256 amount); event TokenClaimed(uint256 id); - constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime) { + constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime, address _depositor) { maxUnclaimableTime = _maxUnclaimableTime; + depositor = _depositor; operator = msg.sender; initialize(_guards, _threshold); } modifier onlyDepositor() { - require(depositors[msg.sender] == true, "Guard: Invalid depositor"); + require(msg.sender == depositor, "Guard: Invalid depositor"); _; } @@ -49,10 +50,6 @@ contract Guard is GuardRegistry, Pausable { operator = newOperator; } - function setDepositor(address depositor, bool enable) external onlyOperator { - depositors[depositor] = enable; - } - function setMaxUnclaimableTime(uint256 _maxUnclaimableTime) external onlyOperator { maxUnclaimableTime = _maxUnclaimableTime; } @@ -70,12 +67,11 @@ contract Guard is GuardRegistry, Pausable { address recipient, uint256 amount ) public onlyDepositor whenNotPaused { - deposits[id] = hash(abi.encodePacked(msg.sender, block.timestamp, token, recipient, amount)); - emit TokenDeposit(msg.sender, id, block.timestamp, token, recipient, amount); + depositors[id] = hash(abi.encodePacked(block.timestamp, token, recipient, amount)); + emit TokenDeposit(id, token, recipient, amount); } function claimById( - address from, uint256 id, uint256 timestamp, address token, @@ -83,18 +79,18 @@ contract Guard is GuardRegistry, Pausable { uint256 amount, bool isNative ) internal { - require(hash(abi.encodePacked(from, timestamp, token, recipient, amount)) == deposits[id], "Guard: Invalid id to claim"); + require(hash(abi.encodePacked(timestamp, token, recipient, amount)) == depositors[id], "Guard: Invalid id to claim"); require(amount > 0, "Guard: Invalid amount to claim"); if (isNative) { - require(IERC20(token).transferFrom(from, address(this), amount), "Guard: claim native token failed"); + require(IERC20(token).transferFrom(depositor, address(this), amount), "Guard: claim native token failed"); uint256 balanceBefore = address(this).balance; IWToken(token).withdraw(amount); require(address(this).balance == balanceBefore.add(amount), "Guard: token is not wrapped by native token"); payable(recipient).transfer(amount); } else { - require(IERC20(token).transferFrom(from, recipient, amount), "Guard: claim token failed"); + require(IERC20(token).transferFrom(depositor, recipient, amount), "Guard: claim token failed"); } - delete deposits[id]; + delete depositors[id]; emit TokenClaimed(id); } @@ -104,7 +100,6 @@ contract Guard is GuardRegistry, Pausable { * @param signatures the signatures of the guards which to claim tokens. */ function claim( - address from, uint256 id, uint256 timestamp, address token, @@ -112,8 +107,8 @@ contract Guard is GuardRegistry, Pausable { uint256 amount, bytes[] memory signatures ) public { - verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); - claimById(from, id, timestamp, token, recipient, amount, false); + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(id, timestamp, token, recipient, amount), signatures); + claimById(id, timestamp, token, recipient, amount, false); } /** @@ -122,7 +117,6 @@ contract Guard is GuardRegistry, Pausable { * @param signatures the signatures of the guards which to claim tokens. */ function claimNative( - address from, uint256 id, uint256 timestamp, address token, @@ -130,8 +124,8 @@ contract Guard is GuardRegistry, Pausable { uint256 amount, bytes[] memory signatures ) public { - verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); - claimById(from, id, timestamp, token, recipient, amount, true); + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(id, timestamp, token, recipient, amount), signatures); + claimById(id, timestamp, token, recipient, amount, true); } /** @@ -139,7 +133,6 @@ contract Guard is GuardRegistry, Pausable { * @param id the id to be claimed */ function claimByTimeout( - address from, uint256 id, uint256 timestamp, address token, @@ -148,11 +141,10 @@ contract Guard is GuardRegistry, Pausable { bool isNative ) public whenNotPaused { require(timestamp < block.timestamp && block.timestamp - timestamp > maxUnclaimableTime, "Guard: claim at invalid time"); - claimById(from, id, timestamp, token, recipient, amount, isNative); + claimById(id, timestamp, token, recipient, amount, isNative); } function hash(bytes memory value) public pure returns (bytes32) { return sha256(value); } } - diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol index 57c9db68..c0a024da 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -65,6 +65,8 @@ contract xTokenIssuing is xTokenBridgeBase { emit IssuingERC20Created(_originalChainId, _originalToken, xToken); } + // using this interface, the Issuing contract must be must be granted mint and burn authorities. + // warning: if the _xToken contract has no transferOwnership/acceptOwnership interface, then the authority cannot be transfered. function updatexToken( uint256 _originalChainId, address _originalToken, diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol index 5457222c..03dc5e36 100644 --- a/helix-contract/contracts/messagers/MsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -105,6 +105,7 @@ 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; diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index 9e88372f..5e180f5f 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -87,11 +87,11 @@ describe("xtoken tests", () => { return x.address.toLowerCase().localeCompare(y.address.toLowerCase()) }); - const guardBackingContract = await ethers.getContractFactory("Guard"); + const guardBackingContract = await ethers.getContractFactory("GuardV3"); const backingGuard = await guardBackingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60); await backingGuard.deployed(); await backingGuard.setDepositor(backing.address, true); - const guardIssuingContract = await ethers.getContractFactory("Guard"); + const guardIssuingContract = await ethers.getContractFactory("GuardV3"); const issuingGuard = await guardIssuingContract.deploy([guards[0].address, guards[1].address, guards[2].address], 2, 60); await issuingGuard.deployed(); await issuingGuard.setDepositor(issuing.address, true); From 2fbf6141c7abc24bae4fca9fe130dc19f2bc2f34 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Tue, 19 Dec 2023 17:09:09 +0800 Subject: [PATCH 14/26] use the fixed transfer(#46) --- .../contracts/interfaces/IMessager.sol | 1 - .../mapping-token/v3/base/xTokenBacking.sol | 117 ++++++------ .../v3/base/xTokenBridgeBase.sol | 47 +++-- .../mapping-token/v3/base/xTokenIssuing.sol | 106 ++++++----- .../v3/interfaces/IxTokenBacking.sol | 9 +- .../v3/interfaces/IxTokenIssuing.sol | 11 +- .../contracts/messagers/MsglineMessager.sol | 28 --- helix-contract/test/6_test_xtoken_v3.js | 167 +++++++----------- 8 files changed, 240 insertions(+), 246 deletions(-) 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 - ); }); }); From 4af638741992740c4bd9b25dfeeb7fa84c677e7c Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Tue, 19 Dec 2023 19:32:46 +0800 Subject: [PATCH 15/26] guard files --- .../mapping-token/v3/GuardRegistryV3.sol | 297 ++++++++++++++++++ .../contracts/mapping-token/v3/GuardV3.sol | 158 ++++++++++ 2 files changed, 455 insertions(+) create mode 100644 helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol create mode 100644 helix-contract/contracts/mapping-token/v3/GuardV3.sol diff --git a/helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol b/helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol new file mode 100644 index 00000000..83702dda --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.10; +pragma experimental ABIEncoderV2; + +import "@zeppelin-solidity/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @title Manages a set of guards and a threshold to double-check BEEFY commitment + * @dev Stores the guards and a threshold + * @author echo + */ +contract GuardRegistryV3 { + event AddedGuard(address guard); + event RemovedGuard(address guard); + event ChangedThreshold(uint256 threshold); + + // keccak256( + // "EIP712Domain(uint256 chainId,address verifyingContract)" + // ); + bytes32 internal constant DOMAIN_SEPARATOR_TYPEHASH = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; + + address internal constant SENTINEL_GUARDS = address(0x1); + + /** + * @dev Nonce to prevent replay of update operations + */ + uint256 public nonce; + /** + * @dev Store all guards in the linked list + */ + mapping(address => address) internal guards; + /** + * @dev Count of all guards + */ + uint256 internal guardCount; + /** + * @dev Number of required confirmations for update operations + */ + uint256 internal threshold; + + /** + * @dev Sets initial storage of contract. + * @param _guards List of Safe guards. + * @param _threshold Number of required confirmations for check commitment or change guards. + */ + function initialize(address[] memory _guards, uint256 _threshold) internal { + // Threshold can only be 0 at initialization. + // Check ensures that setup function can only be called once. + require(threshold == 0, "Guard: Guards have already been setup"); + // Validate that threshold is smaller than number of added guards. + require(_threshold <= _guards.length, "Guard: Threshold cannot exceed guard count"); + // There has to be at least one Safe guard. + require(_threshold >= 1, "Guard: Threshold needs to be greater than 0"); + // Initializing Safe guards. + address currentGuard = SENTINEL_GUARDS; + for (uint256 i = 0; i < _guards.length; i++) { + // Guard address cannot be null. + address guard = _guards[i]; + require(guard != address(0) && guard != SENTINEL_GUARDS && guard != address(this) && currentGuard != guard, "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[guard] == address(0), "Guard: Address is already an guard"); + guards[currentGuard] = guard; + currentGuard = guard; + emit AddedGuard(guard); + } + guards[currentGuard] = SENTINEL_GUARDS; + guardCount = _guards.length; + threshold = _threshold; + } + + /** + * @dev Allows to add a new guard to the registry and update the threshold at the same time. + * This can only be done via multi-sig. + * @notice Adds the guard `guard` to the registry and updates the threshold to `_threshold`. + * @param guard New guard address. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to add new guard and update the `threshold` . + */ + function addGuardWithThreshold( + address guard, + uint256 _threshold, + bytes[] memory signatures + ) public { + // Guard address cannot be null, the sentinel or the registry itself. + require(guard != address(0) && guard != SENTINEL_GUARDS && guard != address(this), "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[guard] == address(0), "Guard: Address is already an guard"); + verifyGuardSignatures(msg.sig, abi.encode(guard, _threshold), signatures); + guards[guard] = guards[SENTINEL_GUARDS]; + guards[SENTINEL_GUARDS] = guard; + guardCount++; + emit AddedGuard(guard); + // Change threshold if threshold was changed. + if (threshold != _threshold) _changeThreshold(_threshold); + } + + /** + * @dev Allows to remove an guard from the registry and update the threshold at the same time. + * This can only be done via multi-sig. + * @notice Removes the guard `guard` from the registry and updates the threshold to `_threshold`. + * @param prevGuard Guard that pointed to the guard to be removed in the linked list + * @param guard Guard address to be removed. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to remove a guard and update the `threshold` . + */ + function removeGuard( + address prevGuard, + address guard, + uint256 _threshold, + bytes[] memory signatures + ) public { + // Only allow to remove an guard, if threshold can still be reached. + require(guardCount - 1 >= _threshold, "Guard: Threshold cannot exceed guard count"); + // Validate guard address and check that it corresponds to guard index. + require(guard != address(0) && guard != SENTINEL_GUARDS, "Guard: Invalid guard address provided"); + require(guards[prevGuard] == guard, "Guard: Invalid prevGuard, guard pair provided"); + verifyGuardSignatures(msg.sig, abi.encode(prevGuard, guard, _threshold), signatures); + guards[prevGuard] = guards[guard]; + guards[guard] = address(0); + guardCount--; + emit RemovedGuard(guard); + // Change threshold if threshold was changed. + if (threshold != _threshold) _changeThreshold(_threshold); + } + + /** + * @dev Allows to swap/replace a guard from the registry with another address. + * This can only be done via multi-sig. + * @notice Replaces the guard `oldGuard` in the registry with `newGuard`. + * @param prevGuard guard that pointed to the guard to be replaced in the linked list + * @param oldGuard guard address to be replaced. + * @param newGuard New guard address. + * @param signatures The signatures of the guards which to swap/replace a guard and update the `threshold` . + */ + function swapGuard( + address prevGuard, + address oldGuard, + address newGuard, + bytes[] memory signatures + ) public { + // Guard address cannot be null, the sentinel or the registry itself. + require(newGuard != address(0) && newGuard != SENTINEL_GUARDS && newGuard != address(this), "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[newGuard] == address(0), "Guard: Address is already an guard"); + // Validate oldGuard address and check that it corresponds to guard index. + require(oldGuard != address(0) && oldGuard != SENTINEL_GUARDS, "Guard: Invalid guard address provided"); + require(guards[prevGuard] == oldGuard, "Guard: Invalid prevGuard, guard pair provided"); + verifyGuardSignatures(msg.sig, abi.encode(prevGuard, oldGuard, newGuard), signatures); + guards[newGuard] = guards[oldGuard]; + guards[prevGuard] = newGuard; + guards[oldGuard] = address(0); + emit RemovedGuard(oldGuard); + emit AddedGuard(newGuard); + } + + /** + * @dev Allows to update the number of required confirmations by guards. + * This can only be done via multi-sig. + * @notice Changes the threshold of the registry to `_threshold`. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to update the `threshold` . + */ + function changeThreshold(uint256 _threshold, bytes[] memory signatures) public { + verifyGuardSignatures(msg.sig, abi.encode(_threshold), signatures); + _changeThreshold(_threshold); + } + + function _changeThreshold(uint256 _threshold) internal { + // Validate that threshold is smaller than number of owners. + require(_threshold <= guardCount, "Guard: Threshold cannot exceed guard count"); + // There has to be at least one guard. + require(_threshold >= 1, "Guard: Threshold needs to be greater than 0"); + threshold = _threshold; + emit ChangedThreshold(threshold); + } + + function getThreshold() public view returns (uint256) { + return threshold; + } + + function isGuard(address guard) public view returns (bool) { + return guard != SENTINEL_GUARDS && guards[guard] != address(0); + } + + /** + * @dev Returns array of guards. + * @return Array of guards. + */ + function getGuards() public view returns (address[] memory) { + address[] memory array = new address[](guardCount); + + // populate return array + uint256 index = 0; + address currentGuard = guards[SENTINEL_GUARDS]; + while (currentGuard != SENTINEL_GUARDS) { + array[index] = currentGuard; + currentGuard = guards[currentGuard]; + index++; + } + return array; + } + + function verifyGuardSignatures( + bytes4 methodID, + bytes memory params, + bytes[] memory signatures + ) internal { + bytes32 structHash = + keccak256( + abi.encode( + methodID, + params, + nonce + ) + ); + checkGuardSignatures(structHash, signatures); + nonce++; + } + + function verifyGuardSignaturesWithoutNonce( + bytes4 methodID, + bytes memory params, + bytes[] memory signatures + ) view internal { + bytes32 structHash = + keccak256( + abi.encode( + methodID, + params + ) + ); + checkGuardSignatures(structHash, signatures); + } + + /** + * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. + * @param structHash The struct Hash of the data (could be either a message/commitment hash). + * @param signatures Signature data that should be verified. only ECDSA signature. + * Signers need to be sorted in ascending order + */ + function checkGuardSignatures( + bytes32 structHash, + bytes[] memory signatures + ) public view { + // Load threshold to avoid multiple storage loads + uint256 _threshold = threshold; + // Check that a threshold is set + require(_threshold > 0, "Guard: Threshold needs to be defined"); + bytes32 dataHash = encodeDataHash(structHash); + checkNSignatures(dataHash, signatures, _threshold); + } + + /** + * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. + * @param dataHash Hash of the data (could be either a message hash or transaction hash). + * @param signatures Signature data that should be verified. only ECDSA signature. + * Signers need to be sorted in ascending order + * @param requiredSignatures Amount of required valid signatures. + */ + function checkNSignatures( + bytes32 dataHash, + bytes[] memory signatures, + uint256 requiredSignatures + ) public view { + // Check that the provided signature data is not too short + require(signatures.length >= requiredSignatures, "GS020"); + // There cannot be an owner with address 0. + address lastGuard = address(0); + address currentGuard; + for (uint256 i = 0; i < requiredSignatures; i++) { + currentGuard = ECDSA.recover(dataHash, signatures[i]); + require(currentGuard > lastGuard && guards[currentGuard] != address(0) && currentGuard != SENTINEL_GUARDS, "Guard: Invalid guard provided"); + lastGuard = currentGuard; + } + } + + /** + * @dev Returns the chain id used by this contract. + */ + function getChainId() public view returns (uint256) { + uint256 id; + // solhint-disable-next-line no-inline-assembly + assembly { + id := chainid() + } + return id; + } + + function domainSeparator() public view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, getChainId(), address(this))); + } + + function encodeDataHash(bytes32 structHash) public view returns (bytes32) { + return keccak256(abi.encodePacked(hex"1901", domainSeparator(), structHash)); + } +} diff --git a/helix-contract/contracts/mapping-token/v3/GuardV3.sol b/helix-contract/contracts/mapping-token/v3/GuardV3.sol new file mode 100644 index 00000000..d0441f3a --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/GuardV3.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.17; + +import "@zeppelin-solidity/contracts/security/Pausable.sol"; +import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "@zeppelin-solidity/contracts/utils/math/SafeMath.sol"; +import "./GuardRegistryV3.sol"; +import "../interfaces/IWToken.sol"; + +contract GuardV3 is GuardRegistryV3, Pausable { + using SafeMath for uint256; + + mapping(uint256 => bytes32) deposits; + + uint256 public maxUnclaimableTime; + mapping(address => bool) depositors; + address public operator; + + event TokenDeposit(address sender, uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); + event TokenClaimed(uint256 id); + + constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime) { + maxUnclaimableTime = _maxUnclaimableTime; + operator = msg.sender; + initialize(_guards, _threshold); + } + + modifier onlyDepositor() { + require(depositors[msg.sender] == true, "Guard: Invalid depositor"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "Guard: Invalid operator"); + _; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } + + function setOperator(address newOperator, bytes[] memory signatures) external { + verifyGuardSignatures(msg.sig, abi.encode(newOperator), signatures); + operator = newOperator; + } + + function setDepositor(address depositor, bool enable) external onlyOperator { + depositors[depositor] = enable; + } + + function setMaxUnclaimableTime(uint256 _maxUnclaimableTime) external onlyOperator { + maxUnclaimableTime = _maxUnclaimableTime; + } + + /** + * @dev deposit token to guard, waiting to claim, only allowed depositor + * @param id the id of the operation, should be siged later by guards + * @param token the erc20 token address + * @param recipient the recipient of the token + * @param amount the amount of the token + */ + function deposit( + uint256 id, + address token, + address recipient, + uint256 amount + ) public onlyDepositor whenNotPaused { + deposits[id] = hash(abi.encodePacked(msg.sender, block.timestamp, token, recipient, amount)); + emit TokenDeposit(msg.sender, id, block.timestamp, token, recipient, amount); + } + + function claimById( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bool isNative + ) internal { + require(hash(abi.encodePacked(from, timestamp, token, recipient, amount)) == deposits[id], "Guard: Invalid id to claim"); + require(amount > 0, "Guard: Invalid amount to claim"); + if (isNative) { + require(IERC20(token).transferFrom(from, address(this), amount), "Guard: claim native token failed"); + uint256 balanceBefore = address(this).balance; + IWToken(token).withdraw(amount); + require(address(this).balance == balanceBefore.add(amount), "Guard: token is not wrapped by native token"); + payable(recipient).transfer(amount); + } else { + require(IERC20(token).transferFrom(from, recipient, amount), "Guard: claim token failed"); + } + delete deposits[id]; + emit TokenClaimed(id); + } + + /** + * @dev claim the tokens in the contract saved by deposit, this acquire signatures from guards + * @param id the id to be claimed + * @param signatures the signatures of the guards which to claim tokens. + */ + function claim( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bytes[] memory signatures + ) public { + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, false); + } + + /** + * @dev claimNative the tokens in the contract saved by deposit, this acquire signatures from guards + * @param id the id to be claimed + * @param signatures the signatures of the guards which to claim tokens. + */ + function claimNative( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bytes[] memory signatures + ) public { + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, true); + } + + /** + * @dev claim the tokens without signatures, this only allowed when timeout + * @param id the id to be claimed + */ + function claimByTimeout( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bool isNative + ) public whenNotPaused { + require(timestamp < block.timestamp && block.timestamp - timestamp > maxUnclaimableTime, "Guard: claim at invalid time"); + claimById(from, id, timestamp, token, recipient, amount, isNative); + } + + function hash(bytes memory value) public pure returns (bytes32) { + return sha256(value); + } +} + From 0c458d339c7f07e911711f68bfa1c70fb916a062 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 20 Dec 2023 16:08:32 +0800 Subject: [PATCH 16/26] base transfer & refund --- .../mapping-token/v3/base/xTokenBacking.sol | 12 +++----- .../v3/base/xTokenBridgeBase.sol | 28 +++++++++++++++++++ .../mapping-token/v3/base/xTokenIssuing.sol | 9 ++---- helix-contract/test/6_test_xtoken_v3.js | 4 +-- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol index 8f3366c3..67d0088f 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -54,6 +54,8 @@ contract xTokenBacking is xTokenBridgeBase { // We use nonce to ensure that messages are not duplicated // especially in reorg scenarios, the destination chain use nonce to filter out duplicate deliveries. + // nonce is user-defined, there is no requirement that it must not be repeated. + // But the transferId generated must not be repeated. function lockAndRemoteIssuing( uint256 _remoteChainId, address _originalToken, @@ -124,8 +126,7 @@ contract xTokenBacking is xTokenBridgeBase { expendDailyLimit(_originalToken, _amount); bytes32 transferId = getTransferId(_nonce, block.chainid, _originalToken, _originSender, _recipient, _amount); - require(filledTransfers[transferId] == TRANSFER_UNFILLED, "message has been accepted"); - filledTransfers[transferId] = TRANSFER_DELIVERED; + _handleTransfer(transferId); // native token do not use guard if (address(0) == _originalToken) { @@ -182,12 +183,7 @@ contract xTokenBacking is xTokenBridgeBase { ) 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 - uint256 filledTransfer = filledTransfers[transferId]; - require(filledTransfer != TRANSFER_DELIVERED, "success message can't refund for failed"); - if (filledTransfer != TRANSFER_REFUNDED) { - filledTransfers[transferId] = TRANSFER_REFUNDED; - } + _requestRefund(transferId); bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( _originalToken, _originalSender, diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol index ecc12823..b63b76bb 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -103,11 +103,18 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim messageId = IMessageId(service.sendService).latestSentMessageId(); } + // request a cross-chain transfer + // 1. lock and remote issue + // 2. burn and remote unlock + // save the transferId if not exist, else revert function _requestTransfer(bytes32 _transferId) internal { require(requestInfos[_transferId].isRequested == false, "request exist"); requestInfos[_transferId].isRequested = true; } + // receive a cross-chain refund request + // 1. request must be exist + // 2. can't repeat function _handleRefund(bytes32 _transferId) internal { RequestInfo memory requestInfo = requestInfos[_transferId]; require(requestInfo.isRequested == true, "request not exist"); @@ -115,6 +122,27 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim requestInfos[_transferId].hasRefundForFailed = true; } + // receive a cross-chain request + // must not filled + // fill the transfer with delivered transfer type + function _handleTransfer(bytes32 _transferId) internal { + require(filledTransfers[_transferId] == TRANSFER_UNFILLED, "!conflict"); + filledTransfers[_transferId] = TRANSFER_DELIVERED; + } + + // request a cross-chain refund + // 1. can retry + // 2. can't be filled by delivery + function _requestRefund(bytes32 _transferId) internal { + uint256 filledTransfer = filledTransfers[_transferId]; + // already fill by refund, retry request + if (filledTransfer == TRANSFER_REFUNDED) { + return; + } + require(filledTransfer == TRANSFER_UNFILLED, "!conflict"); + filledTransfers[_transferId] = TRANSFER_REFUNDED; + } + function getTransferId( uint256 _nonce, uint256 _targetChainId, diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol index c56b4fbf..5c3b1914 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -107,8 +107,7 @@ contract xTokenIssuing is xTokenBridgeBase { require(_amount > 0, "can not receive amount zero"); expendDailyLimit(xToken, _amount); - require(filledTransfers[transferId] == TRANSFER_UNFILLED, "message has been accepted"); - filledTransfers[transferId] = TRANSFER_DELIVERED; + _handleTransfer(transferId); address _guard = guard; if (_guard != address(0)) { @@ -182,11 +181,7 @@ contract xTokenIssuing is xTokenBridgeBase { ) external payable { 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; - } + _requestRefund(transferId); bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( _originalToken, _originalSender, diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index 43eaf14c..b7ebef6d 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -396,14 +396,14 @@ describe("xtoken tests", () => { nonce01, "1.1", true - )).to.be.revertedWith("success message can't refund for failed"); + )).to.be.revertedWith("!conflict"); await expect(requestRemoteIssuingForUnlockFailure( nativeTokenAddress, 100, nonce02, "1.1", true - )).to.be.revertedWith("success message can't refund for failed"); + )).to.be.revertedWith("!conflict"); // lock exceed daily limit const nonce03 = await lockAndRemoteIssuing( From 3760eea6aa69c6934d8ea2efdde7f90a31dfa8cc Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 20 Dec 2023 16:43:04 +0800 Subject: [PATCH 17/26] flatten files --- .../flatten/xtoken-v3/MsglineMessager.sol | 19 +- .../flatten/xtoken-v3/xTokenBacking.sol | 352 +++++++++----- .../flatten/xtoken-v3/xTokenErc20.sol | 152 ++---- .../flatten/xtoken-v3/xTokenIssuing.sol | 458 ++++++++++-------- 4 files changed, 505 insertions(+), 476 deletions(-) diff --git a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol index 69731772..05e6eaf9 100644 --- a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol +++ b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/8/2023 + * 12/20/2023 **/ pragma solidity ^0.8.17; @@ -113,8 +113,8 @@ contract MsglineMessager is Application, AccessController { mapping(bytes32=>address) public remoteAppReceivers; mapping(bytes32=>address) public remoteAppSenders; - event CallerUnMatched(uint256 srcAppChainId, address srcAppAddress); - event CallResult(uint256 srcAppChainId, bool result); + event CallerUnMatched(uint256 srcAppChainId, bytes32 transferId, address srcAppAddress); + event CallResult(uint256 srcAppChainId, bytes32 transferId, bool result); modifier onlyWhiteList() { require(whiteList[msg.sender], "msg.sender not in whitelist"); @@ -126,8 +126,6 @@ contract MsglineMessager is Application, AccessController { _; } - //event CallResult(string sourceChain, string srcAddress, bool successed); - constructor(address _dao, address _msgline) { _initialize(_dao); msgline = IMessageLine(_msgline); @@ -176,29 +174,26 @@ contract MsglineMessager is Application, AccessController { require(srcChainId == remoteMessager.msglineRemoteChainId, "invalid remote chainid"); require(remoteMessager.messager == _xmsgSender(), "invalid remote messager"); bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); + bytes32 transferId = latestRecvMessageId(); // check remote appSender if (_remoteAppAddress != remoteAppSenders[key]) { - emit CallerUnMatched(_srcAppChainId, _remoteAppAddress); + emit CallerUnMatched(_srcAppChainId, transferId, _remoteAppAddress); return; } (bool success,) = _localAppAddress.call(_message); // don't revert to prevent message block - emit CallResult(_srcAppChainId, success); + emit CallResult(_srcAppChainId, transferId, success); } function latestSentMessageId() external view returns(bytes32) { return msgline.sentMessageId(); } - function latestRecvMessageId() external view returns(bytes32) { + function latestRecvMessageId() public view returns(bytes32) { return msgline.recvMessageId(); } - function messageDelivered(bytes32 messageId) external view returns(bool) { - return msgline.dones(messageId); - } - 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/flatten/xtoken-v3/xTokenBacking.sol b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol index 8c764df9..67a502da 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenBacking.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/8/2023 + * 12/20/2023 **/ pragma solidity ^0.8.17; @@ -160,6 +160,29 @@ library TokenTransferHelper { } } +// File contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol +// License-Identifier: MIT + +interface IxTokenIssuing { + function handleIssuingForUnlockFailureFromRemote( + uint256 originalChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; + + function issuexToken( + uint256 remoteChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; +} + // File contracts/interfaces/IMessager.sol // License-Identifier: MIT @@ -176,7 +199,48 @@ interface ILowLevelMessageReceiver { interface IMessageId { function latestSentMessageId() external view returns(bytes32); function latestRecvMessageId() external view returns(bytes32); - function messageDelivered(bytes32 messageId) external view returns(bool); +} + +// File contracts/utils/AccessController.sol +// License-Identifier: MIT + +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; + } + + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; + } } // File contracts/utils/DailyLimit.sol @@ -262,48 +326,6 @@ contract DailyLimit { } } -// File contracts/utils/AccessController.sol -// License-Identifier: MIT - -/// @title AccessController -/// @notice AccessController is a contract to control the access permission -/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract -contract AccessController { - address public dao; - address public operator; - address public pendingDao; - - modifier onlyDao() { - require(msg.sender == dao, "!dao"); - _; - } - - modifier onlyOperator() { - require(msg.sender == operator, "!operator"); - _; - } - - function _initialize(address _dao) internal { - dao = _dao; - operator = _dao; - } - - function setOperator(address _operator) onlyDao external { - operator = _operator; - } - - function transferOwnership(address _dao) onlyDao external { - pendingDao = _dao; - } - - function acceptOwnership() external { - address newDao = msg.sender; - require(pendingDao == newDao, "!pendingDao"); - delete pendingDao; - dao = newDao; - } -} - // File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) @@ -801,20 +823,42 @@ abstract contract Initializable { +// The Base contract for xToken protocol +// 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 uint256 public protocolFee; + // the reserved protocol fee in the contract uint256 public protocolFeeReserved; address public guard; // remoteChainId => info mapping(uint256 => MessagerService) public messagers; - // common method + // 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; require(receiveService == msg.sender, "invalid messager"); @@ -859,13 +903,14 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim function _sendMessage( uint256 _remoteChainId, bytes memory _payload, - uint256 feePrepaid, + uint256 _feePrepaid, bytes memory _extParams ) internal whenNotPaused returns(bytes32 messageId) { MessagerService memory service = messagers[_remoteChainId]; require(service.sendService != address(0), "bridge not configured"); - protocolFeeReserved += protocolFee; - ILowLevelMessageSender(service.sendService).sendMessage{value: feePrepaid - protocolFee}( + uint256 _protocolFee = protocolFee; + protocolFeeReserved += _protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: _feePrepaid - _protocolFee}( _remoteChainId, _payload, _extParams @@ -873,16 +918,55 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim messageId = IMessageId(service.sendService).latestSentMessageId(); } - 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).messageDelivered(_transferId), "message not delivered"); + // request a cross-chain transfer + // 1. lock and remote issue + // 2. burn and remote unlock + // save the transferId if not exist, else revert + function _requestTransfer(bytes32 _transferId) internal { + require(requestInfos[_transferId].isRequested == false, "request exist"); + requestInfos[_transferId].isRequested = true; } - 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(); + // receive a cross-chain refund request + // 1. request must be exist + // 2. can't repeat + 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; + } + + // receive a cross-chain request + // must not filled + // fill the transfer with delivered transfer type + function _handleTransfer(bytes32 _transferId) internal { + require(filledTransfers[_transferId] == TRANSFER_UNFILLED, "!conflict"); + filledTransfers[_transferId] = TRANSFER_DELIVERED; + } + + // request a cross-chain refund + // 1. can retry + // 2. can't be filled by delivery + function _requestRefund(bytes32 _transferId) internal { + uint256 filledTransfer = filledTransfers[_transferId]; + // already fill by refund, retry request + if (filledTransfer == TRANSFER_REFUNDED) { + return; + } + require(filledTransfer == TRANSFER_UNFILLED, "!conflict"); + filledTransfers[_transferId] = TRANSFER_REFUNDED; + } + + 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 @@ -895,26 +979,6 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim } } -// File contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol -// License-Identifier: MIT - -interface IxTokenIssuing { - function handleIssuingForUnlockFailureFromRemote( - uint256 remoteChainId, - bytes32 transferId, - address originalToken, - address originalSender, - uint256 amount - ) external; - - function issuexToken( - uint256 remoteChainId, - address originalToken, - address recipient, - uint256 amount - ) external; -} - // File contracts/mapping-token/v3/base/xTokenBacking.sol // License-Identifier: MIT @@ -923,33 +987,39 @@ interface IxTokenIssuing { +// The contract implements the backing side of the Helix xToken protocol. +// 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 + // this is used to unlock token to guard function setwToken(address _wtoken) external onlyDao { wToken = _wtoken; } + // register token on source chain + // this is used to prevent the unregistered token's transfer + // and must be registered on the target chain before function registerOriginalToken( uint256 _remoteChainId, address _originalToken, @@ -961,16 +1031,24 @@ 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. + // nonce is user-defined, there is no requirement that it must not be repeated. + // But the transferId generated must not be repeated. 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) { @@ -988,46 +1066,46 @@ 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)); - 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 ); } - // in backing contract, it only know the original token info - // in issuing contract, it know the mapping relationship of original token and it's mapping token xToken - // we use original token info in messages - - // method for backing // receive unlock original token message from remote issuing contract 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); + _handleTransfer(transferId); // native token do not use guard if (address(0) == _originalToken) { @@ -1047,11 +1125,12 @@ contract xTokenBacking is xTokenBridgeBase { if (_guard == address(0)) { TokenTransferHelper.safeTransferNative(_recipient, _amount); } else { - IWToken(wToken).deposit{value: _amount}(); - // see https://github.com/helix-bridge/contracts/issues/18 - uint allowance = IERC20(wToken).allowance(address(this), _guard); - require(IERC20(wToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); - IGuard(_guard).deposit(uint256(_transferId), wToken, _recipient, _amount); + address _wToken = wToken; + // when use guard, we deposit native token to the wToken contract + IWToken(_wToken).deposit{value: _amount}(); + uint allowance = IERC20(_wToken).allowance(address(this), _guard); + require(IERC20(_wToken).approve(_guard, allowance + _amount), "approve token transfer to guard failed"); + IGuard(_guard).deposit(uint256(_transferId), _wToken, _recipient, _amount); } } @@ -1071,63 +1150,68 @@ contract xTokenBacking is xTokenBridgeBase { } } + // send message to Issuing when unlock failed function requestRemoteIssuingForUnlockFailure( - bytes32 _transferId, uint256 _remoteChainId, address _originalToken, address _originalSender, + address _recipient, uint256 _amount, + uint256 _nonce, bytes memory _extParams ) external payable { - // must not exist in successful issue list - require(unlockedTransferIds[_transferId] == false, "success message can't refund for failed"); - _assertMessageIsDelivered(_remoteChainId, _transferId); + require(_originalSender == msg.sender || _recipient == msg.sender || dao == msg.sender, "invalid msgSender"); + bytes32 transferId = getTransferId(_nonce, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); + _requestRefund(transferId); 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 ); } // when lock and issuing failed // receive unlock(refund) message from remote issuing contract // this will refund original token to original sender + // 1. the message is not refunded before + // 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); } } \ No newline at end of file diff --git a/helix-contract/flatten/xtoken-v3/xTokenErc20.sol b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol index 41299649..285502e8 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenErc20.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/8/2023 + * 12/20/2023 **/ pragma solidity ^0.8.17; @@ -330,135 +330,53 @@ library SafeMath { } } -// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// File contracts/mapping-token/v3/base/xTokenErc20.sol // License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) -/** - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -abstract contract Context { - function _msgSender() internal view virtual returns (address) { - return msg.sender; - } +contract xTokenErc20 is IERC20 { + using SafeMath for uint256; - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; - } -} + mapping (address => uint256) private _balances; + mapping (address => mapping (address => uint256)) private _allowances; -// File @zeppelin-solidity/contracts/access/Ownable.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) + uint256 private _totalSupply; + string public name; + string public symbol; + uint8 public decimals; -/** - * @dev Contract module which provides a basic access control mechanism, where - * there is an account (an owner) that can be granted exclusive access to - * specific functions. - * - * By default, the owner account will be the one that deploys the contract. This - * can later be changed with {transferOwnership}. - * - * This module is used through inheritance. It will make available the modifier - * `onlyOwner`, which can be applied to your functions to restrict their use to - * the owner. - */ -abstract contract Ownable is Context { - address private _owner; + address public owner; + address public pendingOwner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - /** - * @dev Initializes the contract setting the deployer as the initial owner. - */ - constructor() { - _transferOwnership(_msgSender()); - } - - /** - * @dev Throws if called by any account other than the owner. - */ modifier onlyOwner() { - _checkOwner(); + require(owner == msg.sender, "Ownable: caller is not the owner"); _; } - /** - * @dev Returns the address of the current owner. - */ - function owner() public view virtual returns (address) { - return _owner; - } - - /** - * @dev Throws if the sender is not the owner. - */ - function _checkOwner() internal view virtual { - require(owner() == _msgSender(), "Ownable: caller is not the owner"); - } - - /** - * @dev Leaves the contract without owner. It will not be possible to call - * `onlyOwner` functions anymore. Can only be called by the current owner. - * - * NOTE: Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. - */ - function renounceOwnership() public virtual onlyOwner { - _transferOwnership(address(0)); - } - - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) public virtual onlyOwner { - require(newOwner != address(0), "Ownable: new owner is the zero address"); - _transferOwnership(newOwner); + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + _transferOwnership(msg.sender); } - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Internal function without access restriction. - */ - function _transferOwnership(address newOwner) internal virtual { - address oldOwner = _owner; - _owner = newOwner; + function _transferOwnership(address newOwner) internal { + address oldOwner = owner; + owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } -} - -// File contracts/mapping-token/v3/base/xTokenErc20.sol -// License-Identifier: MIT - + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + } -contract xTokenErc20 is IERC20, Ownable { - using SafeMath for uint256; - - mapping (address => uint256) private _balances; - mapping (address => mapping (address => uint256)) private _allowances; - - uint256 private _totalSupply; - - string public name; - string public symbol; - uint8 public decimals; - - constructor(string memory _name, string memory _symbol, uint8 _decimals) { - name = _name; - symbol = _symbol; - decimals = _decimals; - _transferOwnership(_msgSender()); + function acceptOwnership() external { + require(pendingOwner == msg.sender, "invalid pending owner"); + _transferOwnership(pendingOwner); + pendingOwner = address(0); } function totalSupply() public view override returns (uint256) { @@ -474,8 +392,8 @@ contract xTokenErc20 is IERC20, Ownable { return true; } - function allowance(address owner, address spender) public view virtual override returns (uint256) { - return _allowances[owner][spender]; + function allowance(address account, address spender) public view virtual override returns (uint256) { + return _allowances[account][spender]; } function approve(address spender, uint256 amount) public virtual override returns (bool) { @@ -516,7 +434,7 @@ contract xTokenErc20 is IERC20, Ownable { } function burn(address account, uint256 amount) external { - if (account != msg.sender && owner() != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { + if (account != msg.sender && owner != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { _approve(account, msg.sender, _allowances[account][msg.sender].sub(amount, "ERC20: decreased allowance below zero")); } _burn(account, amount); @@ -542,12 +460,12 @@ contract xTokenErc20 is IERC20, Ownable { emit Transfer(account, address(0), amount); } - function _approve(address owner, address spender, uint256 amount) internal virtual { - require(owner != address(0), "ERC20: approve from the zero address"); + function _approve(address account, address spender, uint256 amount) internal virtual { + require(account != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address"); - _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); + _allowances[account][spender] = amount; + emit Approval(account, spender, amount); } function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } diff --git a/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol index 074a6f39..863e7237 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/8/2023 + * 12/20/2023 **/ pragma solidity ^0.8.17; @@ -151,6 +151,66 @@ interface IGuard { function deposit(uint256 id, address token, address recipient, uint256 amount) external; } +// File contracts/interfaces/IMessager.sol +// License-Identifier: MIT + +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; +} + +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +} + +interface IMessageId { + function latestSentMessageId() external view returns(bytes32); + function latestRecvMessageId() external view returns(bytes32); +} + +// File contracts/utils/AccessController.sol +// License-Identifier: MIT + +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; + + modifier onlyDao() { + require(msg.sender == dao, "!dao"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); + _; + } + + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; + } + + function setOperator(address _operator) onlyDao external { + operator = _operator; + } + + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; + } + + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; + } +} + // File contracts/utils/DailyLimit.sol // License-Identifier: MIT @@ -234,67 +294,6 @@ contract DailyLimit { } } -// File contracts/utils/AccessController.sol -// License-Identifier: MIT - -/// @title AccessController -/// @notice AccessController is a contract to control the access permission -/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract -contract AccessController { - address public dao; - address public operator; - address public pendingDao; - - modifier onlyDao() { - require(msg.sender == dao, "!dao"); - _; - } - - modifier onlyOperator() { - require(msg.sender == operator, "!operator"); - _; - } - - function _initialize(address _dao) internal { - dao = _dao; - operator = _dao; - } - - function setOperator(address _operator) onlyDao external { - operator = _operator; - } - - function transferOwnership(address _dao) onlyDao external { - pendingDao = _dao; - } - - function acceptOwnership() external { - address newDao = msg.sender; - require(pendingDao == newDao, "!pendingDao"); - delete pendingDao; - dao = newDao; - } -} - -// File contracts/interfaces/IMessager.sol -// License-Identifier: MIT - -interface ILowLevelMessageSender { - function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; - function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; -} - -interface ILowLevelMessageReceiver { - function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; - function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; -} - -interface IMessageId { - function latestSentMessageId() external view returns(bytes32); - function latestRecvMessageId() external view returns(bytes32); - function messageDelivered(bytes32 messageId) external view returns(bool); -} - // File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) @@ -792,20 +791,42 @@ abstract contract Initializable { +// The Base contract for xToken protocol +// 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 uint256 public protocolFee; + // the reserved protocol fee in the contract uint256 public protocolFeeReserved; address public guard; // remoteChainId => info mapping(uint256 => MessagerService) public messagers; - // common method + // 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; require(receiveService == msg.sender, "invalid messager"); @@ -850,13 +871,14 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim function _sendMessage( uint256 _remoteChainId, bytes memory _payload, - uint256 feePrepaid, + uint256 _feePrepaid, bytes memory _extParams ) internal whenNotPaused returns(bytes32 messageId) { MessagerService memory service = messagers[_remoteChainId]; require(service.sendService != address(0), "bridge not configured"); - protocolFeeReserved += protocolFee; - ILowLevelMessageSender(service.sendService).sendMessage{value: feePrepaid - protocolFee}( + uint256 _protocolFee = protocolFee; + protocolFeeReserved += _protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: _feePrepaid - _protocolFee}( _remoteChainId, _payload, _extParams @@ -864,16 +886,55 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim messageId = IMessageId(service.sendService).latestSentMessageId(); } - 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).messageDelivered(_transferId), "message not delivered"); + // request a cross-chain transfer + // 1. lock and remote issue + // 2. burn and remote unlock + // save the transferId if not exist, else revert + function _requestTransfer(bytes32 _transferId) internal { + require(requestInfos[_transferId].isRequested == false, "request exist"); + requestInfos[_transferId].isRequested = true; + } + + // receive a cross-chain refund request + // 1. request must be exist + // 2. can't repeat + 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; + } + + // receive a cross-chain request + // must not filled + // fill the transfer with delivered transfer type + function _handleTransfer(bytes32 _transferId) internal { + require(filledTransfers[_transferId] == TRANSFER_UNFILLED, "!conflict"); + filledTransfers[_transferId] = TRANSFER_DELIVERED; + } + + // request a cross-chain refund + // 1. can retry + // 2. can't be filled by delivery + function _requestRefund(bytes32 _transferId) internal { + uint256 filledTransfer = filledTransfers[_transferId]; + // already fill by refund, retry request + if (filledTransfer == TRANSFER_REFUNDED) { + return; + } + require(filledTransfer == TRANSFER_UNFILLED, "!conflict"); + filledTransfers[_transferId] = TRANSFER_REFUNDED; } - 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 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 @@ -886,88 +947,6 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim } } -// File @zeppelin-solidity/contracts/access/Ownable.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) - - -/** - * @dev Contract module which provides a basic access control mechanism, where - * there is an account (an owner) that can be granted exclusive access to - * specific functions. - * - * By default, the owner account will be the one that deploys the contract. This - * can later be changed with {transferOwnership}. - * - * This module is used through inheritance. It will make available the modifier - * `onlyOwner`, which can be applied to your functions to restrict their use to - * the owner. - */ -abstract contract Ownable is Context { - address private _owner; - - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - /** - * @dev Initializes the contract setting the deployer as the initial owner. - */ - constructor() { - _transferOwnership(_msgSender()); - } - - /** - * @dev Throws if called by any account other than the owner. - */ - modifier onlyOwner() { - _checkOwner(); - _; - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view virtual returns (address) { - return _owner; - } - - /** - * @dev Throws if the sender is not the owner. - */ - function _checkOwner() internal view virtual { - require(owner() == _msgSender(), "Ownable: caller is not the owner"); - } - - /** - * @dev Leaves the contract without owner. It will not be possible to call - * `onlyOwner` functions anymore. Can only be called by the current owner. - * - * NOTE: Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. - */ - function renounceOwnership() public virtual onlyOwner { - _transferOwnership(address(0)); - } - - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) public virtual onlyOwner { - require(newOwner != address(0), "Ownable: new owner is the zero address"); - _transferOwnership(newOwner); - } - - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Internal function without access restriction. - */ - function _transferOwnership(address newOwner) internal virtual { - address oldOwner = _owner; - _owner = newOwner; - emit OwnershipTransferred(oldOwner, newOwner); - } -} - // File @zeppelin-solidity/contracts/utils/math/SafeMath.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) @@ -1200,8 +1179,7 @@ library SafeMath { // License-Identifier: MIT - -contract xTokenErc20 is IERC20, Ownable { +contract xTokenErc20 is IERC20 { using SafeMath for uint256; mapping (address => uint256) private _balances; @@ -1213,11 +1191,37 @@ contract xTokenErc20 is IERC20, Ownable { string public symbol; uint8 public decimals; + address public owner; + address public pendingOwner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + modifier onlyOwner() { + require(owner == msg.sender, "Ownable: caller is not the owner"); + _; + } + constructor(string memory _name, string memory _symbol, uint8 _decimals) { name = _name; symbol = _symbol; decimals = _decimals; - _transferOwnership(_msgSender()); + _transferOwnership(msg.sender); + } + + function _transferOwnership(address newOwner) internal { + address oldOwner = owner; + owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } + + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + } + + function acceptOwnership() external { + require(pendingOwner == msg.sender, "invalid pending owner"); + _transferOwnership(pendingOwner); + pendingOwner = address(0); } function totalSupply() public view override returns (uint256) { @@ -1233,8 +1237,8 @@ contract xTokenErc20 is IERC20, Ownable { return true; } - function allowance(address owner, address spender) public view virtual override returns (uint256) { - return _allowances[owner][spender]; + function allowance(address account, address spender) public view virtual override returns (uint256) { + return _allowances[account][spender]; } function approve(address spender, uint256 amount) public virtual override returns (bool) { @@ -1275,7 +1279,7 @@ contract xTokenErc20 is IERC20, Ownable { } function burn(address account, uint256 amount) external { - if (account != msg.sender && owner() != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { + if (account != msg.sender && owner != msg.sender && _allowances[account][msg.sender] != type(uint256).max) { _approve(account, msg.sender, _allowances[account][msg.sender].sub(amount, "ERC20: decreased allowance below zero")); } _burn(account, amount); @@ -1301,12 +1305,12 @@ contract xTokenErc20 is IERC20, Ownable { emit Transfer(account, address(0), amount); } - function _approve(address owner, address spender, uint256 amount) internal virtual { - require(owner != address(0), "ERC20: approve from the zero address"); + function _approve(address account, address spender, uint256 amount) internal virtual { + require(account != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address"); - _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); + _allowances[account][spender] = amount; + emit Approval(account, spender, amount); } function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } @@ -1319,16 +1323,19 @@ 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; } @@ -1340,21 +1347,11 @@ interface IxTokenBacking { 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; @@ -1365,7 +1362,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( @@ -1397,6 +1404,8 @@ contract xTokenIssuing is xTokenBridgeBase { emit IssuingERC20Created(_originalChainId, _originalToken, xToken); } + // using this interface, the Issuing contract must be must be granted mint and burn authorities. + // warning: if the _xToken contract has no transferOwnership/acceptOwnership interface, then the authority cannot be transfered. function updatexToken( uint256 _originalChainId, address _originalToken, @@ -1405,7 +1414,6 @@ contract xTokenIssuing is xTokenBridgeBase { bytes32 salt = xTokenSalt(_originalChainId, _originalToken); address oldxToken = xTokens[salt]; if (oldxToken != address(0)) { - require(xTokenErc20(oldxToken).totalSupply() == 0, "can't delete old xToken"); delete originalTokens[oldxToken]; } xTokens[salt] = _xToken; @@ -1413,22 +1421,32 @@ contract xTokenIssuing is xTokenBridgeBase { emit IssuingERC20Updated(_originalChainId, _originalToken, _xToken, oldxToken); } + // transfer xToken ownership + function transferxTokenOwnership(address _xToken, address _newOwner) external onlyDao { + xTokenErc20(_xToken).transferOwnership(_newOwner); + } + + function acceptxTokenOwnership(address _xToken) external onlyDao { + xTokenErc20(_xToken).acceptOwnership(); + } + // receive issuing xToken message from remote backing contract 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; + _handleTransfer(transferId); address _guard = guard; if (_guard != address(0)) { @@ -1446,100 +1464,114 @@ 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); + _requestRefund(transferId); 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 ); } // when burn and unlock failed // receive reIssue(refund) message from remote backing contract // this will refund xToken to original sender + // 1. the transfer not refund before + // 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( From b312ae7da62c1dd8f95289beb2b58db7c853c4ab Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Fri, 22 Dec 2023 13:08:03 +0800 Subject: [PATCH 18/26] remove messageId --- .../contracts/interfaces/IMessageLine.sol | 3 --- .../contracts/interfaces/IMessager.sol | 4 ---- .../mapping-token/v3/base/xTokenBacking.sol | 11 +++++------ .../mapping-token/v3/base/xTokenBridgeBase.sol | 3 +-- .../mapping-token/v3/base/xTokenIssuing.sol | 12 +++++------- .../contracts/messagers/MsglineMessager.sol | 17 ++++------------- helix-contract/test/6_test_xtoken_v3.js | 2 +- 7 files changed, 16 insertions(+), 36 deletions(-) diff --git a/helix-contract/contracts/interfaces/IMessageLine.sol b/helix-contract/contracts/interfaces/IMessageLine.sol index 33440856..d56739a7 100644 --- a/helix-contract/contracts/interfaces/IMessageLine.sol +++ b/helix-contract/contracts/interfaces/IMessageLine.sol @@ -4,9 +4,6 @@ pragma solidity >=0.8.17; interface IMessageLine { function send(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external payable; function fee(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external view returns (uint256); - function sentMessageId() external view returns(bytes32); - function recvMessageId() external view returns(bytes32); - function dones(bytes32) external view returns(bool); } abstract contract Application { diff --git a/helix-contract/contracts/interfaces/IMessager.sol b/helix-contract/contracts/interfaces/IMessager.sol index 59f07d2e..01cdd6c5 100644 --- a/helix-contract/contracts/interfaces/IMessager.sol +++ b/helix-contract/contracts/interfaces/IMessager.sol @@ -11,7 +11,3 @@ interface ILowLevelMessageReceiver { function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; } -interface IMessageId { - function latestSentMessageId() external view returns(bytes32); - function latestRecvMessageId() external view returns(bytes32); -} diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol index 67d0088f..37488352 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -19,7 +19,6 @@ contract xTokenBacking is xTokenBridgeBase { event TokenLocked( bytes32 transferId, - bytes32 messageId, uint256 nonce, uint256 remoteChainId, address token, @@ -29,7 +28,7 @@ contract xTokenBacking is xTokenBridgeBase { uint256 fee ); event TokenUnlocked(bytes32 transferId, uint256 remoteChainId, address token, address recipient, uint256 amount); - event RemoteIssuingFailure(bytes32 transferId, bytes32 messageId, address xToken, address originalSender, uint256 amount, uint256 fee); + event RemoteIssuingFailure(bytes32 transferId, 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 @@ -92,8 +91,8 @@ contract xTokenBacking is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); - emit TokenLocked(transferId, messageId, _nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); + _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); + emit TokenLocked(transferId, _nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); } function encodeIssuexToken( @@ -191,8 +190,8 @@ contract xTokenBacking is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); - emit RemoteIssuingFailure(transferId, messageId, _originalToken, _originalSender, _amount, msg.value); + _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); + emit RemoteIssuingFailure(transferId, _originalToken, _originalSender, _amount, msg.value); } function encodeIssuingForUnlockFailureFromRemote( diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol index b63b76bb..d70e3be7 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -90,7 +90,7 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim bytes memory _payload, uint256 _feePrepaid, bytes memory _extParams - ) internal whenNotPaused returns(bytes32 messageId) { + ) internal whenNotPaused { MessagerService memory service = messagers[_remoteChainId]; require(service.sendService != address(0), "bridge not configured"); uint256 _protocolFee = protocolFee; @@ -100,7 +100,6 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim _payload, _extParams ); - messageId = IMessageId(service.sendService).latestSentMessageId(); } // request a cross-chain transfer diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol index 5c3b1914..b9a2c224 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -21,11 +21,10 @@ contract xTokenIssuing is xTokenBridgeBase { event IssuingERC20Created(uint256 originalChainId, address originalToken, address xToken); 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 RemoteUnlockForIssuingFailureRequested(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, - bytes32 messageId, uint256 nonce, uint256 remoteChainId, address sender, @@ -143,9 +142,8 @@ contract xTokenIssuing is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); - - emit BurnAndRemoteUnlocked(transferId, messageId, _nonce, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _amount, msg.value); + _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); + emit BurnAndRemoteUnlocked(transferId, _nonce, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _amount, msg.value); } function encodeUnlockFromRemote( @@ -189,8 +187,8 @@ contract xTokenIssuing is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); - emit RemoteUnlockForIssuingFailureRequested(transferId, messageId, _originalToken, _originalSender, _amount, msg.value); + _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); + emit RemoteUnlockForIssuingFailureRequested(transferId, _originalToken, _originalSender, _amount, msg.value); } function encodeUnlockForIssuingFailureFromRemote( diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol index fcf7ac2d..08d10ec5 100644 --- a/helix-contract/contracts/messagers/MsglineMessager.sol +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -21,8 +21,8 @@ contract MsglineMessager is Application, AccessController { mapping(bytes32=>address) public remoteAppReceivers; mapping(bytes32=>address) public remoteAppSenders; - event CallerUnMatched(uint256 srcAppChainId, bytes32 transferId, address srcAppAddress); - event CallResult(uint256 srcAppChainId, bytes32 transferId, bool result); + event CallerUnMatched(uint256 srcAppChainId, address srcAppAddress); + event CallResult(uint256 srcAppChainId, bool result); modifier onlyWhiteList() { require(whiteList[msg.sender], "msg.sender not in whitelist"); @@ -82,24 +82,15 @@ contract MsglineMessager is Application, AccessController { require(srcChainId == remoteMessager.msglineRemoteChainId, "invalid remote chainid"); require(remoteMessager.messager == _xmsgSender(), "invalid remote messager"); bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); - bytes32 transferId = latestRecvMessageId(); // check remote appSender if (_remoteAppAddress != remoteAppSenders[key]) { - emit CallerUnMatched(_srcAppChainId, transferId, _remoteAppAddress); + emit CallerUnMatched(_srcAppChainId, _remoteAppAddress); return; } (bool success,) = _localAppAddress.call(_message); // don't revert to prevent message block - emit CallResult(_srcAppChainId, transferId, success); - } - - function latestSentMessageId() external view returns(bytes32) { - return msgline.sentMessageId(); - } - - function latestRecvMessageId() public view returns(bytes32) { - return msgline.recvMessageId(); + emit CallResult(_srcAppChainId, success); } function messagePayload(address _from, address _to, bytes memory _message) public view returns(bytes memory) { diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index b7ebef6d..cf8b5e50 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -527,7 +527,7 @@ describe("xtoken tests", () => { await expect(guardClaim( backingGuard, backing.address, - await backingMessager.latestRecvMessageId(), + transferId06, await getBlockTimestamp(), [guards[0], guards[1]], weth.address, From 67961ed8673cf686581352568ba41073f8a842c8 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Mon, 25 Dec 2023 09:13:28 +0800 Subject: [PATCH 19/26] redeloy xtoken on testnet --- helix-contract/address/xtoken-dev.json | 12 +- .../deploy/deploy_msgline_messager.js | 4 +- helix-contract/deploy/deploy_xtoken_config.js | 24 +-- helix-contract/deploy/deploy_xtoken_proxy.js | 8 +- helix-contract/deploy/deploy_xtoken_test.js | 158 ++++++++++++++++++ 5 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 helix-contract/deploy/deploy_xtoken_test.js diff --git a/helix-contract/address/xtoken-dev.json b/helix-contract/address/xtoken-dev.json index e9675ddc..4883da39 100644 --- a/helix-contract/address/xtoken-dev.json +++ b/helix-contract/address/xtoken-dev.json @@ -1,22 +1,22 @@ { "messagers": { "crab": { - "msglineMessager": "0xCAb1f69C671f1548fd3dE5d63852E9B9181a0D0E" + "msglineMessager": "0xf85638B61E0425D6BB91981190B73246e3AF3CA9" }, "sepolia": { - "msglineMessager": "0x527B67a61C6E1344C359Af2e241aAFeb0c3a9DE9" + "msglineMessager": "0xc876D0873e4060472334E297b2db200Ca10cc806" } }, "backingProxy": { - "crab": "0xb137BDf1Ad5392027832f54a4409685Ef52Aa9dA" + "crab": "0x27F58339CbB8c5A6f58d5D05Bfc1B3fd121F489C" }, "backingLogic": { - "crab": "0x0cDc94088C40B461C3c9cF44DD38B328BDca95e9" + "crab": "0xF2274a80e93075ccD5E2Af8F07beF370Bb6dfac7" }, "issuingProxy": { - "sepolia": "0x44A001aF6AcD2d5f5cB82FCB14Af3d497D56faB4" + "sepolia": "0xFF3bc7372A8011CFaD43D240464ef2fe74C59b86" }, "issuingLogic": { - "sepolia": "0x97cd4227eFC7AEd96CD027994dE2e3E9ACc1b394" + "sepolia": "0x1c955644c527BAA5a28255bCf9F7D3635B7ad99b" } } diff --git a/helix-contract/deploy/deploy_msgline_messager.js b/helix-contract/deploy/deploy_msgline_messager.js index b0e07834..140ac62a 100644 --- a/helix-contract/deploy/deploy_msgline_messager.js +++ b/helix-contract/deploy/deploy_msgline_messager.js @@ -12,7 +12,7 @@ const crabNetwork = { url: "https://crab-rpc.darwinia.network", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", - msgline: "0x000000000EFcBAdA3793cC59c62D79b9f56Ae48F", + msgline: "0x0000000000D2de3e2444926c4577b0A59F1DD8BC", }; const sepoliaNetwork = { @@ -20,7 +20,7 @@ const sepoliaNetwork = { url: "https://rpc-sepolia.rockx.com", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", - msgline: "0x000000000EFcBAdA3793cC59c62D79b9f56Ae48F", + msgline: "0x0000000000D2de3e2444926c4577b0A59F1DD8BC", }; function wallet(url) { diff --git a/helix-contract/deploy/deploy_xtoken_config.js b/helix-contract/deploy/deploy_xtoken_config.js index 5ad953d2..e0b07264 100644 --- a/helix-contract/deploy/deploy_xtoken_config.js +++ b/helix-contract/deploy/deploy_xtoken_config.js @@ -11,8 +11,8 @@ const crabNetwork = { name: "crab", url: "https://crab-rpc.darwinia.network", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", - messager: "0xCAb1f69C671f1548fd3dE5d63852E9B9181a0D0E", - backing: "0xb137BDf1Ad5392027832f54a4409685Ef52Aa9dA", + messager: "0xf85638B61E0425D6BB91981190B73246e3AF3CA9", + backing: "0x27F58339CbB8c5A6f58d5D05Bfc1B3fd121F489C", chainid: 44 }; @@ -20,8 +20,8 @@ const sepoliaNetwork = { name: "sepolia", url: "https://rpc-sepolia.rockx.com", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", - messager: "0x527B67a61C6E1344C359Af2e241aAFeb0c3a9DE9", - issuing: "0x44A001aF6AcD2d5f5cB82FCB14Af3d497D56faB4", + messager: "0xc876D0873e4060472334E297b2db200Ca10cc806", + issuing: "0xFF3bc7372A8011CFaD43D240464ef2fe74C59b86", chainid: 11155111 }; @@ -40,20 +40,20 @@ async function deploy() { // connect messager const backingMessager = await ethers.getContractAt("MsglineMessager", backingNetwork.messager, walletBacking); const issuingMessager = await ethers.getContractAt("MsglineMessager", issuingNetwork.messager, walletIssuing); - await backingMessager.setRemoteMessager(issuingNetwork.chainid, issuingNetwork.chainid, issuingMessager.address); - await issuingMessager.setRemoteMessager(backingNetwork.chainid, backingNetwork.chainid, backingMessager.address); + await backingMessager.setRemoteMessager(issuingNetwork.chainid, issuingNetwork.chainid, issuingMessager.address, {gasLimit: 2000000}); + await issuingMessager.setRemoteMessager(backingNetwork.chainid, backingNetwork.chainid, backingMessager.address, {gasLimit: 2000000}); console.log("connect messager successed"); // xTokenBridge <> messager authorize const backing = await ethers.getContractAt("xTokenBacking", backingNetwork.backing, walletBacking); const issuing = await ethers.getContractAt("xTokenIssuing", issuingNetwork.issuing, walletIssuing); - await backingMessager.setWhiteList(backing.address, true); - await issuingMessager.setWhiteList(issuing.address, true); + await backingMessager.setWhiteList(backing.address, true, {gasLimit: 2000000}); + await issuingMessager.setWhiteList(issuing.address, true, {gasLimit: 2000000}); console.log("messager authorize xtoken bridge successed"); - await backing.setSendService(issuingNetwork.chainid, issuing.address, backingMessager.address); - await backing.setReceiveService(issuingNetwork.chainid, issuing.address, backingMessager.address); - await issuing.setSendService(backingNetwork.chainid, backing.address, issuingMessager.address); - await issuing.setReceiveService(backingNetwork.chainid, backing.address, issuingMessager.address); + await backing.setSendService(issuingNetwork.chainid, issuing.address, backingMessager.address, {gasLimit: 2000000}); + await backing.setReceiveService(issuingNetwork.chainid, issuing.address, backingMessager.address, {gasLimit: 2000000}); + await issuing.setSendService(backingNetwork.chainid, backing.address, issuingMessager.address, {gasLimit: 2000000}); + await issuing.setReceiveService(backingNetwork.chainid, backing.address, issuingMessager.address, {gasLimit: 2000000}); console.log("xtoken bridge connect remote successed"); } diff --git a/helix-contract/deploy/deploy_xtoken_proxy.js b/helix-contract/deploy/deploy_xtoken_proxy.js index d031ec2a..ea341514 100644 --- a/helix-contract/deploy/deploy_xtoken_proxy.js +++ b/helix-contract/deploy/deploy_xtoken_proxy.js @@ -45,12 +45,12 @@ async function deployxTokenProxy(wallet, salt, dao, proxyAdminAddress, logicAddr async function deploy() { const walletCrab = wallet(crabNetwork.url); - const backingLogic = "0x0cDc94088C40B461C3c9cF44DD38B328BDca95e9"; - await deployxTokenProxy(walletCrab, "xtoken-backing-1.0.0", crabNetwork.dao, crabNetwork.proxyAdmin, backingLogic, crabNetwork.deployer); + const backingLogic = "0xF2274a80e93075ccD5E2Af8F07beF370Bb6dfac7"; + await deployxTokenProxy(walletCrab, "xtoken-backing-1.0.1", crabNetwork.dao, crabNetwork.proxyAdmin, backingLogic, crabNetwork.deployer); const walletSepolia = wallet(sepoliaNetwork.url); - const issuingLogic = "0x97cd4227eFC7AEd96CD027994dE2e3E9ACc1b394"; - await deployxTokenProxy(walletSepolia, "xtoken-issuing-1.0.0", sepoliaNetwork.dao, sepoliaNetwork.proxyAdmin, issuingLogic, sepoliaNetwork.deployer); + const issuingLogic = "0x1c955644c527BAA5a28255bCf9F7D3635B7ad99b"; + await deployxTokenProxy(walletSepolia, "xtoken-issuing-1.0.1", sepoliaNetwork.dao, sepoliaNetwork.proxyAdmin, issuingLogic, sepoliaNetwork.deployer); } async function main() { diff --git a/helix-contract/deploy/deploy_xtoken_test.js b/helix-contract/deploy/deploy_xtoken_test.js new file mode 100644 index 00000000..1ddcb6fd --- /dev/null +++ b/helix-contract/deploy/deploy_xtoken_test.js @@ -0,0 +1,158 @@ +const ethUtil = require('ethereumjs-util'); +const abi = require('ethereumjs-abi'); +const secp256k1 = require('secp256k1'); +const fs = require("fs"); + +var ProxyDeployer = require("./proxy.js"); + +const privateKey = process.env.PRIKEY + +const crabNetwork = { + name: "crab", + url: "https://crab-rpc.darwinia.network", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + backing: "0x27F58339CbB8c5A6f58d5D05Bfc1B3fd121F489C", + chainid: 44 +}; + +const sepoliaNetwork = { + name: "sepolia", + url: "https://rpc-sepolia.rockx.com", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + issuing: "0xFF3bc7372A8011CFaD43D240464ef2fe74C59b86", + chainid: 11155111 +}; + +function wallet(url) { + const provider = new ethers.providers.JsonRpcProvider(url); + const wallet = new ethers.Wallet(privateKey, provider); + return wallet; +} + +async function registerIssuing() { + const issuingNetwork = sepoliaNetwork; + const walletIssuing = wallet(sepoliaNetwork.url); + + const issuing = await ethers.getContractAt("xTokenIssuing", issuingNetwork.issuing, walletIssuing); + + await issuing.registerxToken( + 44, + "0x0000000000000000000000000000000000000000", + "crab", + "crab native token", + "CRAB", + 18, + "0x56bc75e2d63100000", + { gasLimit: 1000000 } + ); +} + +async function registerBacking() { + const backingNetwork = crabNetwork; + const walletBacking = wallet(crabNetwork.url); + + const backing = await ethers.getContractAt("xTokenBacking", backingNetwork.backing, walletBacking); + const xToken = "0x4828B88240166B8bB79A833Fd0dD38EaeADAAB1a"; + + await backing.registerOriginalToken( + 11155111, + "0x0000000000000000000000000000000000000000", + xToken, + "0x56bc75e2d63100000" + ); +} + +async function lockAndRemoteIssuing() { + const backingNetwork = crabNetwork; + const walletBacking = wallet(crabNetwork.url); + + const backing = await ethers.getContractAt("xTokenBacking", backingNetwork.backing, walletBacking); + + //const tx = await backing.callStatic.lockAndRemoteIssuing( + await backing.lockAndRemoteIssuing( + 11155111, + "0x0000000000000000000000000000000000000000", + walletBacking.address, + ethers.utils.parseEther("100"), + 1703247763001, + "0x000000000000000000000000000000000000000000000000000000000005f02200000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + { value: ethers.utils.parseEther("105.4") } + ); +} + +async function burnAndRemoteUnlock() { + const issuingNetwork = sepoliaNetwork; + const walletIssuing = wallet(sepoliaNetwork.url); + + const issuing = await ethers.getContractAt("xTokenIssuing", issuingNetwork.issuing, walletIssuing); + + const xTokenAddress = "0x4828B88240166B8bB79A833Fd0dD38EaeADAAB1a"; + const xToken = await ethers.getContractAt("xTokenErc20", xTokenAddress, walletIssuing); + await xToken.approve(issuing.address, ethers.utils.parseEther("10000000"), {gasLimit: 500000}); + await issuing.burnAndRemoteUnlock( + xTokenAddress, + walletIssuing.address, + ethers.utils.parseEther("5"), + 1703248419043, + "0x000000000000000000000000000000000000000000000000000000000006493c00000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + { + value: ethers.utils.parseEther("0.0000007"), + gasLimit: 1000000, + } + ); +} + +async function requestRemoteUnlockForIssuingFailure() { + const issuingNetwork = sepoliaNetwork; + const walletIssuing = wallet(sepoliaNetwork.url); + + const issuing = await ethers.getContractAt("xTokenIssuing", issuingNetwork.issuing, walletIssuing); + + await issuing.requestRemoteUnlockForIssuingFailure( + 44, + "0x0000000000000000000000000000000000000000", + walletIssuing.address, + walletIssuing.address, + ethers.utils.parseEther("100"), + 1703247763000, + "0x000000000000000000000000000000000000000000000000000000000006493c00000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + { + value: ethers.utils.parseEther("0.0000007"), + gasLimit: 1000000, + } + ); +} + +async function requestRemoteIssuingForUnlockFailure() { + const backingNetwork = crabNetwork; + const walletBacking = wallet(crabNetwork.url); + + const backing = await ethers.getContractAt("xTokenBacking", backingNetwork.backing, walletBacking); + + await backing.requestRemoteIssuingForUnlockFailure( + "0xab4f619082b61cac56f60611a661e2b16d8052ab2531be791d95821f8fb232ce", + 11155111, + "0x0000000000000000000000000000000000000000", + walletBacking.address, + ethers.utils.parseEther("100"), + "0x000000000000000000000000000000000000000000000000000000000005f02200000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + { value: ethers.utils.parseEther("5.4") } + ); +} + +async function main() { + //await registerIssuing(); + //await registerBacking(); + //await lockAndRemoteIssuing(); + //await burnAndRemoteUnlock(); + await requestRemoteUnlockForIssuingFailure(); + //await requestRemoteIssuingForUnlockFailure(); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); + From acc39a852256eff29b48ce3fa025b7fb4668c5fd Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Mon, 25 Dec 2023 13:37:52 +0800 Subject: [PATCH 20/26] add src chainid to transferId --- helix-contract/address/xtoken-dev.json | 8 ++++---- .../contracts/mapping-token/v3/base/xTokenBacking.sol | 8 ++++---- .../contracts/mapping-token/v3/base/xTokenBridgeBase.sol | 3 ++- .../contracts/mapping-token/v3/base/xTokenIssuing.sol | 8 ++++---- helix-contract/deploy/deploy_xtoken_config.js | 4 ++-- helix-contract/deploy/deploy_xtoken_logic.js | 4 ++-- helix-contract/deploy/deploy_xtoken_proxy.js | 8 ++++---- 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/helix-contract/address/xtoken-dev.json b/helix-contract/address/xtoken-dev.json index 4883da39..b298f0e0 100644 --- a/helix-contract/address/xtoken-dev.json +++ b/helix-contract/address/xtoken-dev.json @@ -8,15 +8,15 @@ } }, "backingProxy": { - "crab": "0x27F58339CbB8c5A6f58d5D05Bfc1B3fd121F489C" + "crab": "0xbdC7bbF408931C5d666b4F0520E0D9E9A0B04e99" }, "backingLogic": { - "crab": "0xF2274a80e93075ccD5E2Af8F07beF370Bb6dfac7" + "crab": "0x01F53415adC20a2D058DfF14e295Ab955CafD6d6" }, "issuingProxy": { - "sepolia": "0xFF3bc7372A8011CFaD43D240464ef2fe74C59b86" + "sepolia": "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1" }, "issuingLogic": { - "sepolia": "0x1c955644c527BAA5a28255bCf9F7D3635B7ad99b" + "sepolia": "0xCD1c1C799f3914ECFC5e3653D3Cc846355d3dFC9" } } diff --git a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol index 37488352..412f3747 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -66,7 +66,7 @@ contract xTokenBacking is xTokenBridgeBase { 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); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, msg.sender, _recipient, _amount); _requestTransfer(transferId); uint256 prepaid = msg.value; @@ -124,7 +124,7 @@ contract xTokenBacking is xTokenBridgeBase { ) external calledByMessager(_remoteChainId) whenNotPaused { expendDailyLimit(_originalToken, _amount); - bytes32 transferId = getTransferId(_nonce, block.chainid, _originalToken, _originSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originSender, _recipient, _amount); _handleTransfer(transferId); // native token do not use guard @@ -181,7 +181,7 @@ contract xTokenBacking is xTokenBridgeBase { 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); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); _requestRefund(transferId); bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( _originalToken, @@ -225,7 +225,7 @@ contract xTokenBacking is xTokenBridgeBase { uint256 _amount, uint256 _nonce ) external calledByMessager(_remoteChainId) whenNotPaused { - bytes32 transferId = keccak256(abi.encodePacked(_nonce, _remoteChainId, _originalToken, _originalSender, _recipient, _amount)); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); _handleRefund(transferId); if (_originalToken == address(0)) { TokenTransferHelper.safeTransferNative(_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 d70e3be7..c87333e7 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -144,13 +144,14 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim function getTransferId( uint256 _nonce, + uint256 _sourceChainId, uint256 _targetChainId, address _originalToken, address _originalSender, address _recipient, uint256 _amount ) public pure returns(bytes32) { - return keccak256(abi.encodePacked(_nonce, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); + return keccak256(abi.encodePacked(_nonce, _sourceChainId, _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 b9a2c224..81304948 100644 --- a/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -99,7 +99,7 @@ contract xTokenIssuing is xTokenBridgeBase { uint256 _amount, uint256 _nonce ) external calledByMessager(_remoteChainId) whenNotPaused { - bytes32 transferId = getTransferId(_nonce, block.chainid, _originalToken, _originalSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, _remoteChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); bytes32 salt = xTokenSalt(_remoteChainId, _originalToken); address xToken = xTokens[salt]; require(xToken != address(0), "xToken not exist"); @@ -129,7 +129,7 @@ contract xTokenIssuing is xTokenBridgeBase { ) 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); + bytes32 transferId = getTransferId(_nonce, originalInfo.chainId, block.chainid, originalInfo.token, msg.sender, _recipient, _amount); _requestTransfer(transferId); // transfer to this and then burn TokenTransferHelper.safeTransferFrom(_xToken, msg.sender, address(this), _amount); @@ -178,7 +178,7 @@ contract xTokenIssuing is xTokenBridgeBase { bytes memory _extParams ) external payable { require(_originalSender == msg.sender || _recipient == msg.sender || dao == msg.sender, "invalid msgSender"); - bytes32 transferId = getTransferId(_nonce, _originalChainId, _originalToken, _originalSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); _requestRefund(transferId); bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( _originalToken, @@ -222,7 +222,7 @@ contract xTokenIssuing is xTokenBridgeBase { uint256 _amount, uint256 _nonce ) external calledByMessager(_originalChainId) whenNotPaused { - bytes32 transferId = getTransferId(_nonce, _originalChainId, _originalToken, _originalSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); _handleRefund(transferId); bytes32 salt = xTokenSalt(_originalChainId, _originalToken); diff --git a/helix-contract/deploy/deploy_xtoken_config.js b/helix-contract/deploy/deploy_xtoken_config.js index e0b07264..0a1d4d31 100644 --- a/helix-contract/deploy/deploy_xtoken_config.js +++ b/helix-contract/deploy/deploy_xtoken_config.js @@ -12,7 +12,7 @@ const crabNetwork = { url: "https://crab-rpc.darwinia.network", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", messager: "0xf85638B61E0425D6BB91981190B73246e3AF3CA9", - backing: "0x27F58339CbB8c5A6f58d5D05Bfc1B3fd121F489C", + backing: "0xbdC7bbF408931C5d666b4F0520E0D9E9A0B04e99", chainid: 44 }; @@ -21,7 +21,7 @@ const sepoliaNetwork = { url: "https://rpc-sepolia.rockx.com", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", messager: "0xc876D0873e4060472334E297b2db200Ca10cc806", - issuing: "0xFF3bc7372A8011CFaD43D240464ef2fe74C59b86", + issuing: "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1", chainid: 11155111 }; diff --git a/helix-contract/deploy/deploy_xtoken_logic.js b/helix-contract/deploy/deploy_xtoken_logic.js index a74ad4c7..92308bd0 100644 --- a/helix-contract/deploy/deploy_xtoken_logic.js +++ b/helix-contract/deploy/deploy_xtoken_logic.js @@ -42,11 +42,11 @@ async function deployxTokenIssuing(wallet, deployerAddress, salt) { async function main() { // deploy backing on crab const walletCrab = wallet(crabNetwork.url); - const backingLogic = await deployxTokenBacking(walletCrab, crabNetwork.deployer, "xTokenBacking-logic-v1.0.1"); + const backingLogic = await deployxTokenBacking(walletCrab, crabNetwork.deployer, "xTokenBacking-logic-v1.0.3"); // deploy issuing on sepolia const walletSpeolia = wallet(sepoliaNetwork.url); - const issuingLogic = await deployxTokenIssuing(walletSpeolia, sepoliaNetwork.deployer, "xTokenIssuing-logic-v1.0.0"); + const issuingLogic = await deployxTokenIssuing(walletSpeolia, sepoliaNetwork.deployer, "xTokenIssuing-logic-v1.0.3"); } main() diff --git a/helix-contract/deploy/deploy_xtoken_proxy.js b/helix-contract/deploy/deploy_xtoken_proxy.js index ea341514..fadbe60c 100644 --- a/helix-contract/deploy/deploy_xtoken_proxy.js +++ b/helix-contract/deploy/deploy_xtoken_proxy.js @@ -45,12 +45,12 @@ async function deployxTokenProxy(wallet, salt, dao, proxyAdminAddress, logicAddr async function deploy() { const walletCrab = wallet(crabNetwork.url); - const backingLogic = "0xF2274a80e93075ccD5E2Af8F07beF370Bb6dfac7"; - await deployxTokenProxy(walletCrab, "xtoken-backing-1.0.1", crabNetwork.dao, crabNetwork.proxyAdmin, backingLogic, crabNetwork.deployer); + const backingLogic = "0x22E50D0511538B78D4E3b94d4D51AFDa924286D0"; + await deployxTokenProxy(walletCrab, "xtoken-backing-1.0.4", crabNetwork.dao, crabNetwork.proxyAdmin, backingLogic, crabNetwork.deployer); const walletSepolia = wallet(sepoliaNetwork.url); - const issuingLogic = "0x1c955644c527BAA5a28255bCf9F7D3635B7ad99b"; - await deployxTokenProxy(walletSepolia, "xtoken-issuing-1.0.1", sepoliaNetwork.dao, sepoliaNetwork.proxyAdmin, issuingLogic, sepoliaNetwork.deployer); + const issuingLogic = "0xCD1c1C799f3914ECFC5e3653D3Cc846355d3dFC9"; + await deployxTokenProxy(walletSepolia, "xtoken-issuing-1.0.4", sepoliaNetwork.dao, sepoliaNetwork.proxyAdmin, issuingLogic, sepoliaNetwork.deployer); } async function main() { From 5948118ec742b2810c6cabc1300f2b6a0772d7cb Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Mon, 25 Dec 2023 16:08:48 +0800 Subject: [PATCH 21/26] flatten files --- .../flatten/xtoken-v3/MsglineMessager.sol | 22 +- .../flatten/xtoken-v3/xTokenBacking.sol | 360 +++++++++--------- .../flatten/xtoken-v3/xTokenErc20.sol | 2 +- .../flatten/xtoken-v3/xTokenIssuing.sol | 59 ++- helix-contract/test/6_test_xtoken_v3.js | 10 +- 5 files changed, 214 insertions(+), 239 deletions(-) diff --git a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol index 05e6eaf9..65c80028 100644 --- a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol +++ b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/20/2023 + * 12/25/2023 **/ pragma solidity ^0.8.17; @@ -67,9 +67,6 @@ contract AccessController { interface IMessageLine { function send(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external payable; function fee(uint256 toChainId, address toDapp, bytes calldata message, bytes calldata params) external view returns (uint256); - function sentMessageId() external view returns(bytes32); - function recvMessageId() external view returns(bytes32); - function dones(bytes32) external view returns(bool); } abstract contract Application { @@ -113,8 +110,8 @@ contract MsglineMessager is Application, AccessController { mapping(bytes32=>address) public remoteAppReceivers; mapping(bytes32=>address) public remoteAppSenders; - event CallerUnMatched(uint256 srcAppChainId, bytes32 transferId, address srcAppAddress); - event CallResult(uint256 srcAppChainId, bytes32 transferId, bool result); + event CallerUnMatched(uint256 srcAppChainId, address srcAppAddress); + event CallResult(uint256 srcAppChainId, bool result); modifier onlyWhiteList() { require(whiteList[msg.sender], "msg.sender not in whitelist"); @@ -174,24 +171,15 @@ contract MsglineMessager is Application, AccessController { require(srcChainId == remoteMessager.msglineRemoteChainId, "invalid remote chainid"); require(remoteMessager.messager == _xmsgSender(), "invalid remote messager"); bytes32 key = keccak256(abi.encodePacked(srcChainId, _localAppAddress)); - bytes32 transferId = latestRecvMessageId(); // check remote appSender if (_remoteAppAddress != remoteAppSenders[key]) { - emit CallerUnMatched(_srcAppChainId, transferId, _remoteAppAddress); + emit CallerUnMatched(_srcAppChainId, _remoteAppAddress); return; } (bool success,) = _localAppAddress.call(_message); // don't revert to prevent message block - emit CallResult(_srcAppChainId, transferId, success); - } - - function latestSentMessageId() external view returns(bytes32) { - return msgline.sentMessageId(); - } - - function latestRecvMessageId() public view returns(bytes32) { - return msgline.recvMessageId(); + emit CallResult(_srcAppChainId, success); } function messagePayload(address _from, address _to, bytes memory _message) public view returns(bytes memory) { diff --git a/helix-contract/flatten/xtoken-v3/xTokenBacking.sol b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol index 67a502da..872ba049 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenBacking.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol @@ -14,28 +14,11 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/20/2023 + * 12/25/2023 **/ pragma solidity ^0.8.17; -// File contracts/mapping-token/interfaces/IGuard.sol -// License-Identifier: MIT - - -interface IGuard { - function deposit(uint256 id, address token, address recipient, uint256 amount) external; -} - -// File contracts/mapping-token/interfaces/IWToken.sol -// License-Identifier: MIT - - -interface IWToken { - function deposit() external payable; - function withdraw(uint wad) external; -} - // File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) @@ -160,27 +143,21 @@ library TokenTransferHelper { } } -// File contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol +// File contracts/mapping-token/interfaces/IGuard.sol // License-Identifier: MIT -interface IxTokenIssuing { - function handleIssuingForUnlockFailureFromRemote( - uint256 originalChainId, - address originalToken, - address originalSender, - address recipient, - uint256 amount, - uint256 nonce - ) external; - function issuexToken( - uint256 remoteChainId, - address originalToken, - address originalSender, - address recipient, - uint256 amount, - uint256 nonce - ) external; +interface IGuard { + function deposit(uint256 id, address token, address recipient, uint256 amount) external; +} + +// File contracts/mapping-token/interfaces/IWToken.sol +// License-Identifier: MIT + + +interface IWToken { + function deposit() external payable; + function withdraw(uint wad) external; } // File contracts/interfaces/IMessager.sol @@ -196,11 +173,6 @@ interface ILowLevelMessageReceiver { function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; } -interface IMessageId { - function latestSentMessageId() external view returns(bytes32); - function latestRecvMessageId() external view returns(bytes32); -} - // File contracts/utils/AccessController.sol // License-Identifier: MIT @@ -326,135 +298,6 @@ contract DailyLimit { } } -// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) - - -/** - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -abstract contract Context { - function _msgSender() internal view virtual returns (address) { - return msg.sender; - } - - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; - } -} - -// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) - - -/** - * @dev Contract module which allows children to implement an emergency stop - * mechanism that can be triggered by an authorized account. - * - * This module is used through inheritance. It will make available the - * modifiers `whenNotPaused` and `whenPaused`, which can be applied to - * the functions of your contract. Note that they will not be pausable by - * simply including this module, only once the modifiers are put in place. - */ -abstract contract Pausable is Context { - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - bool private _paused; - - /** - * @dev Initializes the contract in unpaused state. - */ - constructor() { - _paused = false; - } - - /** - * @dev Modifier to make a function callable only when the contract is not paused. - * - * Requirements: - * - * - The contract must not be paused. - */ - modifier whenNotPaused() { - _requireNotPaused(); - _; - } - - /** - * @dev Modifier to make a function callable only when the contract is paused. - * - * Requirements: - * - * - The contract must be paused. - */ - modifier whenPaused() { - _requirePaused(); - _; - } - - /** - * @dev Returns true if the contract is paused, and false otherwise. - */ - function paused() public view virtual returns (bool) { - return _paused; - } - - /** - * @dev Throws if the contract is paused. - */ - function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); - } - - /** - * @dev Throws if the contract is not paused. - */ - function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); - } - - /** - * @dev Triggers stopped state. - * - * Requirements: - * - * - The contract must not be paused. - */ - function _pause() internal virtual whenNotPaused { - _paused = true; - emit Paused(_msgSender()); - } - - /** - * @dev Returns to normal state. - * - * Requirements: - * - * - The contract must be paused. - */ - function _unpause() internal virtual whenPaused { - _paused = false; - emit Unpaused(_msgSender()); - } -} - // File @zeppelin-solidity/contracts/utils/Address.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol) @@ -815,6 +658,135 @@ abstract contract Initializable { } } +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + // File contracts/mapping-token/v3/base/xTokenBridgeBase.sol // License-Identifier: MIT @@ -905,7 +877,7 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim bytes memory _payload, uint256 _feePrepaid, bytes memory _extParams - ) internal whenNotPaused returns(bytes32 messageId) { + ) internal whenNotPaused { MessagerService memory service = messagers[_remoteChainId]; require(service.sendService != address(0), "bridge not configured"); uint256 _protocolFee = protocolFee; @@ -915,7 +887,6 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim _payload, _extParams ); - messageId = IMessageId(service.sendService).latestSentMessageId(); } // request a cross-chain transfer @@ -960,13 +931,14 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim function getTransferId( uint256 _nonce, + uint256 _sourceChainId, uint256 _targetChainId, address _originalToken, address _originalSender, address _recipient, uint256 _amount ) public pure returns(bytes32) { - return keccak256(abi.encodePacked(_nonce, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); + return keccak256(abi.encodePacked(_nonce, _sourceChainId, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); } // settings @@ -979,6 +951,29 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim } } +// File contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol +// License-Identifier: MIT + +interface IxTokenIssuing { + function handleIssuingForUnlockFailureFromRemote( + uint256 originalChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; + + function issuexToken( + uint256 remoteChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; +} + // File contracts/mapping-token/v3/base/xTokenBacking.sol // License-Identifier: MIT @@ -998,7 +993,6 @@ contract xTokenBacking is xTokenBridgeBase { event TokenLocked( bytes32 transferId, - bytes32 messageId, uint256 nonce, uint256 remoteChainId, address token, @@ -1008,7 +1002,7 @@ contract xTokenBacking is xTokenBridgeBase { uint256 fee ); event TokenUnlocked(bytes32 transferId, uint256 remoteChainId, address token, address recipient, uint256 amount); - event RemoteIssuingFailure(bytes32 transferId, bytes32 messageId, address xToken, address originalSender, uint256 amount, uint256 fee); + event RemoteIssuingFailure(bytes32 transferId, 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 @@ -1046,7 +1040,7 @@ contract xTokenBacking is xTokenBridgeBase { 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); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, msg.sender, _recipient, _amount); _requestTransfer(transferId); uint256 prepaid = msg.value; @@ -1071,8 +1065,8 @@ contract xTokenBacking is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); - emit TokenLocked(transferId, messageId, _nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); + _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); + emit TokenLocked(transferId, _nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); } function encodeIssuexToken( @@ -1104,7 +1098,7 @@ contract xTokenBacking is xTokenBridgeBase { ) external calledByMessager(_remoteChainId) whenNotPaused { expendDailyLimit(_originalToken, _amount); - bytes32 transferId = getTransferId(_nonce, block.chainid, _originalToken, _originSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originSender, _recipient, _amount); _handleTransfer(transferId); // native token do not use guard @@ -1161,7 +1155,7 @@ contract xTokenBacking is xTokenBridgeBase { 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); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); _requestRefund(transferId); bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( _originalToken, @@ -1170,8 +1164,8 @@ contract xTokenBacking is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); - emit RemoteIssuingFailure(transferId, messageId, _originalToken, _originalSender, _amount, msg.value); + _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); + emit RemoteIssuingFailure(transferId, _originalToken, _originalSender, _amount, msg.value); } function encodeIssuingForUnlockFailureFromRemote( @@ -1205,7 +1199,7 @@ contract xTokenBacking is xTokenBridgeBase { uint256 _amount, uint256 _nonce ) external calledByMessager(_remoteChainId) whenNotPaused { - bytes32 transferId = keccak256(abi.encodePacked(_nonce, _remoteChainId, _originalToken, _originalSender, _recipient, _amount)); + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); _handleRefund(transferId); if (_originalToken == address(0)) { TokenTransferHelper.safeTransferNative(_originalSender, _amount); diff --git a/helix-contract/flatten/xtoken-v3/xTokenErc20.sol b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol index 285502e8..c62b02e6 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenErc20.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/20/2023 + * 12/25/2023 **/ pragma solidity ^0.8.17; diff --git a/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol index 863e7237..9ee81cdc 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/20/2023 + * 12/25/2023 **/ pragma solidity ^0.8.17; @@ -151,24 +151,6 @@ interface IGuard { function deposit(uint256 id, address token, address recipient, uint256 amount) external; } -// File contracts/interfaces/IMessager.sol -// License-Identifier: MIT - -interface ILowLevelMessageSender { - function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; - function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; -} - -interface ILowLevelMessageReceiver { - function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; - function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; -} - -interface IMessageId { - function latestSentMessageId() external view returns(bytes32); - function latestRecvMessageId() external view returns(bytes32); -} - // File contracts/utils/AccessController.sol // License-Identifier: MIT @@ -211,6 +193,19 @@ contract AccessController { } } +// File contracts/interfaces/IMessager.sol +// License-Identifier: MIT + +interface ILowLevelMessageSender { + function registerRemoteReceiver(uint256 remoteChainId, address remoteBridge) external; + function sendMessage(uint256 remoteChainId, bytes memory message, bytes memory params) external payable; +} + +interface ILowLevelMessageReceiver { + function registerRemoteSender(uint256 remoteChainId, address remoteBridge) external; + function recvMessage(address remoteSender, address localReceiver, bytes memory payload) external; +} + // File contracts/utils/DailyLimit.sol // License-Identifier: MIT @@ -873,7 +868,7 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim bytes memory _payload, uint256 _feePrepaid, bytes memory _extParams - ) internal whenNotPaused returns(bytes32 messageId) { + ) internal whenNotPaused { MessagerService memory service = messagers[_remoteChainId]; require(service.sendService != address(0), "bridge not configured"); uint256 _protocolFee = protocolFee; @@ -883,7 +878,6 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim _payload, _extParams ); - messageId = IMessageId(service.sendService).latestSentMessageId(); } // request a cross-chain transfer @@ -928,13 +922,14 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim function getTransferId( uint256 _nonce, + uint256 _sourceChainId, uint256 _targetChainId, address _originalToken, address _originalSender, address _recipient, uint256 _amount ) public pure returns(bytes32) { - return keccak256(abi.encodePacked(_nonce, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); + return keccak256(abi.encodePacked(_nonce, _sourceChainId, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); } // settings @@ -1360,11 +1355,10 @@ contract xTokenIssuing is xTokenBridgeBase { event IssuingERC20Created(uint256 originalChainId, address originalToken, address xToken); 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 RemoteUnlockForIssuingFailureRequested(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, - bytes32 messageId, uint256 nonce, uint256 remoteChainId, address sender, @@ -1439,7 +1433,7 @@ contract xTokenIssuing is xTokenBridgeBase { uint256 _amount, uint256 _nonce ) external calledByMessager(_remoteChainId) whenNotPaused { - bytes32 transferId = getTransferId(_nonce, block.chainid, _originalToken, _originalSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, _remoteChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); bytes32 salt = xTokenSalt(_remoteChainId, _originalToken); address xToken = xTokens[salt]; require(xToken != address(0), "xToken not exist"); @@ -1469,7 +1463,7 @@ contract xTokenIssuing is xTokenBridgeBase { ) 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); + bytes32 transferId = getTransferId(_nonce, originalInfo.chainId, block.chainid, originalInfo.token, msg.sender, _recipient, _amount); _requestTransfer(transferId); // transfer to this and then burn TokenTransferHelper.safeTransferFrom(_xToken, msg.sender, address(this), _amount); @@ -1482,9 +1476,8 @@ contract xTokenIssuing is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); - - emit BurnAndRemoteUnlocked(transferId, messageId, _nonce, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _amount, msg.value); + _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); + emit BurnAndRemoteUnlocked(transferId, _nonce, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _amount, msg.value); } function encodeUnlockFromRemote( @@ -1519,7 +1512,7 @@ contract xTokenIssuing is xTokenBridgeBase { bytes memory _extParams ) external payable { require(_originalSender == msg.sender || _recipient == msg.sender || dao == msg.sender, "invalid msgSender"); - bytes32 transferId = getTransferId(_nonce, _originalChainId, _originalToken, _originalSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); _requestRefund(transferId); bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( _originalToken, @@ -1528,8 +1521,8 @@ contract xTokenIssuing is xTokenBridgeBase { _amount, _nonce ); - bytes32 messageId = _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); - emit RemoteUnlockForIssuingFailureRequested(transferId, messageId, _originalToken, _originalSender, _amount, msg.value); + _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); + emit RemoteUnlockForIssuingFailureRequested(transferId, _originalToken, _originalSender, _amount, msg.value); } function encodeUnlockForIssuingFailureFromRemote( @@ -1563,7 +1556,7 @@ contract xTokenIssuing is xTokenBridgeBase { uint256 _amount, uint256 _nonce ) external calledByMessager(_originalChainId) whenNotPaused { - bytes32 transferId = getTransferId(_nonce, _originalChainId, _originalToken, _originalSender, _recipient, _amount); + bytes32 transferId = getTransferId(_nonce, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); _handleRefund(transferId); bytes32 salt = xTokenSalt(_originalChainId, _originalToken); diff --git a/helix-contract/test/6_test_xtoken_v3.js b/helix-contract/test/6_test_xtoken_v3.js index cf8b5e50..6baa648e 100644 --- a/helix-contract/test/6_test_xtoken_v3.js +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -175,7 +175,7 @@ describe("xtoken tests", () => { const balanceRecipientAfter = await balanceOf(xTokenAddress, recipient); const balanceBackingAfter = await balanceOf(originalAddress, backing.address); - const transferId = await backing.getTransferId(nonce, issuingChainId, originalAddress, user01.address, recipient, amount); + const transferId = await backing.getTransferId(nonce, backingChainId, issuingChainId, originalAddress, user01.address, recipient, amount); const requestInfo = await backing.requestInfos(transferId); expect(requestInfo.isRequested).to.equal(true); expect(requestInfo.hasRefundForFailed).to.equal(false); @@ -219,7 +219,7 @@ describe("xtoken tests", () => { const balanceBackingAfter = await balanceOf(originalAddress, backing.address); const balanceUserAfter = await balanceOf(xTokenAddress, user02.address); - const transferId = await backing.getTransferId(nonce, backingChainId, originalAddress, user02.address, recipient, amount); + const transferId = await backing.getTransferId(nonce, backingChainId, issuingChainId, originalAddress, user02.address, recipient, amount); const requestInfo = await issuing.requestInfos(transferId); expect(requestInfo.isRequested).to.equal(true); expect(requestInfo.hasRefundForFailed).to.equal(false); @@ -270,7 +270,7 @@ describe("xtoken tests", () => { let receipt = await transaction.wait(); let gasFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice); - const transferId = await backing.getTransferId(nonce, issuingChainId, originalToken, originalSender, recipient, amount); + const transferId = await backing.getTransferId(nonce, backingChainId, issuingChainId, originalToken, originalSender, recipient, amount); const requestInfo = await backing.requestInfos(transferId); if (result) { expect(balanceSenderAfter.sub(balanceSenderBefore)).to.be.equal(amount); @@ -492,7 +492,7 @@ describe("xtoken tests", () => { true,//using guard true ); - const transferId = await backing.getTransferId(nonce05, issuingChainId, nativeTokenAddress, user01.address, user02.address, 10); + const transferId = await backing.getTransferId(nonce05, backingChainId, issuingChainId, nativeTokenAddress, user01.address, user02.address, 10); await guardClaim( issuingGuard, issuing.address, @@ -511,7 +511,7 @@ describe("xtoken tests", () => { true, //using guard true ); - const transferId06 = await backing.getTransferId(nonce06, backingChainId, nativeTokenAddress, user02.address, user01.address, 20); + const transferId06 = await backing.getTransferId(nonce06, backingChainId, issuingChainId, nativeTokenAddress, user02.address, user01.address, 20); await guardClaim( backingGuard, backing.address, From 410f724372a342e15304b72e9e5548e31e05d795 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Mon, 25 Dec 2023 16:36:40 +0800 Subject: [PATCH 22/26] repair test --- helix-contract/test/1_test_bscv2.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/helix-contract/test/1_test_bscv2.js b/helix-contract/test/1_test_bscv2.js index 5d99aa02..215aead9 100644 --- a/helix-contract/test/1_test_bscv2.js +++ b/helix-contract/test/1_test_bscv2.js @@ -411,9 +411,8 @@ describe("darwinia<>bsc mapping token tests", () => { }); console.log(wallets[0].address, wallets[1].address, wallets[2].address); const guardContract = await ethers.getContractFactory("Guard"); - const guard = await guardContract.deploy([wallets[0].address, wallets[1].address, wallets[2].address], 3, 60); + const guard = await guardContract.deploy([wallets[0].address, wallets[1].address, wallets[2].address], 3, 60, owner.address); await guard.deployed(); - await guard.setDepositor(owner.address, true); await originalToken.approve(guard.address, 1000); await guard.deposit(1, originalToken.address, wallets[1].address, 100); @@ -426,16 +425,15 @@ describe("darwinia<>bsc mapping token tests", () => { ethUtil.keccak256( abi.rawEncode( ['bytes4', 'bytes'], - [abi.methodID('claim', [ 'address', 'uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), - abi.rawEncode(['address', 'uint256', 'uint256', 'address', 'address', 'uint256'], - [owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100]) + [abi.methodID('claim', ['uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), + abi.rawEncode(['uint256', 'uint256', 'address', 'address', 'uint256'], + [1, timestamp01, originalToken.address, wallets[1].address, 100]) ] ) ); // cannot claim without signatures await expect(guard.claimByTimeout( - owner.address, 2, timestamp01, originalToken.address, @@ -445,7 +443,6 @@ describe("darwinia<>bsc mapping token tests", () => { await network.provider.send("evm_increaseTime", [3600]); await expect(guard.claimByTimeout( - owner.address, 2, timestamp01, originalToken.address, @@ -453,7 +450,6 @@ describe("darwinia<>bsc mapping token tests", () => { 100, false)).to.be.revertedWith("Guard: Invalid id to claim"); await expect(guard.claimByTimeout( - owner.address, 1, timestamp01, originalToken.address, @@ -461,7 +457,6 @@ describe("darwinia<>bsc mapping token tests", () => { 101, false)).to.be.revertedWith("Guard: Invalid id to claim"); await expect(guard.claimByTimeout( - owner.address, 1, timestamp01, originalToken.address, @@ -480,19 +475,18 @@ describe("darwinia<>bsc mapping token tests", () => { ); return ethers.utils.hexlify(signature); }); - await guard.claim(owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100, signatures); + await guard.claim(1, timestamp01, originalToken.address, wallets[1].address, 100, signatures); expect(await originalToken.balanceOf(wallets[1].address)).to.equal(100); // can't claim twice - await expect(guard.claim(owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100, signatures)).to.be.revertedWith("Guard: Invalid id to claim"); + await expect(guard.claim(1, timestamp01, originalToken.address, wallets[1].address, 100, signatures)).to.be.revertedWith("Guard: Invalid id to claim"); await expect(guard.claimByTimeout( - owner.address, 1, timestamp01, originalToken.address, wallets[1].address, 100, true)).to.be.revertedWith("Guard: Invalid id to claim"); - await expect(guard.claim(owner.address, 2, timestamp02, originalToken.address, wallets[2].address, 200, signatures)).to.be.revertedWith("Guard: Invalid guard provided"); + await expect(guard.claim(2, timestamp02, originalToken.address, wallets[2].address, 200, signatures)).to.be.revertedWith("Guard: Invalid guard provided"); }); it("test_gas", async function () { @@ -530,9 +524,8 @@ describe("darwinia<>bsc mapping token tests", () => { mtf.setMappingNativeWrappedToken(mappingTokenAddress); const guardContract = await ethers.getContractFactory("Guard"); - const guard = await guardContract.deploy([owner.address], 1, 60); + const guard = await guardContract.deploy([owner.address], 1, 60, mtf.address); await guard.deployed(); - await guard.setDepositor(mtf.address, true); await mtf.updateGuard(guard.address); From 810ccf04882f3e0693d1b5a072a672c0894ce331 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Mon, 25 Dec 2023 16:59:59 +0800 Subject: [PATCH 23/26] test --- helix-contract/deploy/deploy_xtoken_test.js | 29 +++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/helix-contract/deploy/deploy_xtoken_test.js b/helix-contract/deploy/deploy_xtoken_test.js index 1ddcb6fd..b2a9a153 100644 --- a/helix-contract/deploy/deploy_xtoken_test.js +++ b/helix-contract/deploy/deploy_xtoken_test.js @@ -11,7 +11,7 @@ const crabNetwork = { name: "crab", url: "https://crab-rpc.darwinia.network", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", - backing: "0x27F58339CbB8c5A6f58d5D05Bfc1B3fd121F489C", + backing: "0xbdC7bbF408931C5d666b4F0520E0D9E9A0B04e99", chainid: 44 }; @@ -19,7 +19,7 @@ const sepoliaNetwork = { name: "sepolia", url: "https://rpc-sepolia.rockx.com", dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", - issuing: "0xFF3bc7372A8011CFaD43D240464ef2fe74C59b86", + issuing: "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1", chainid: 11155111 }; @@ -52,7 +52,7 @@ async function registerBacking() { const walletBacking = wallet(crabNetwork.url); const backing = await ethers.getContractAt("xTokenBacking", backingNetwork.backing, walletBacking); - const xToken = "0x4828B88240166B8bB79A833Fd0dD38EaeADAAB1a"; + const xToken = "0x9Da7E18441f26515CC713290BE846E726d41781d"; await backing.registerOriginalToken( 11155111, @@ -73,10 +73,10 @@ async function lockAndRemoteIssuing() { 11155111, "0x0000000000000000000000000000000000000000", walletBacking.address, - ethers.utils.parseEther("100"), - 1703247763001, + ethers.utils.parseEther("10"), + 1703247763002, "0x000000000000000000000000000000000000000000000000000000000005f02200000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", - { value: ethers.utils.parseEther("105.4") } + { value: ethers.utils.parseEther("15.4") } ); } @@ -86,14 +86,14 @@ async function burnAndRemoteUnlock() { const issuing = await ethers.getContractAt("xTokenIssuing", issuingNetwork.issuing, walletIssuing); - const xTokenAddress = "0x4828B88240166B8bB79A833Fd0dD38EaeADAAB1a"; + const xTokenAddress = "0x9Da7E18441f26515CC713290BE846E726d41781d"; const xToken = await ethers.getContractAt("xTokenErc20", xTokenAddress, walletIssuing); await xToken.approve(issuing.address, ethers.utils.parseEther("10000000"), {gasLimit: 500000}); await issuing.burnAndRemoteUnlock( xTokenAddress, walletIssuing.address, ethers.utils.parseEther("5"), - 1703248419043, + 1703248419044, "0x000000000000000000000000000000000000000000000000000000000006493c00000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", { value: ethers.utils.parseEther("0.0000007"), @@ -113,8 +113,8 @@ async function requestRemoteUnlockForIssuingFailure() { "0x0000000000000000000000000000000000000000", walletIssuing.address, walletIssuing.address, - ethers.utils.parseEther("100"), - 1703247763000, + ethers.utils.parseEther("91"), + 1703247763001, "0x000000000000000000000000000000000000000000000000000000000006493c00000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", { value: ethers.utils.parseEther("0.0000007"), @@ -130,11 +130,12 @@ async function requestRemoteIssuingForUnlockFailure() { const backing = await ethers.getContractAt("xTokenBacking", backingNetwork.backing, walletBacking); await backing.requestRemoteIssuingForUnlockFailure( - "0xab4f619082b61cac56f60611a661e2b16d8052ab2531be791d95821f8fb232ce", 11155111, "0x0000000000000000000000000000000000000000", walletBacking.address, - ethers.utils.parseEther("100"), + walletBacking.address, + ethers.utils.parseEther("5"), + 1703248419044, "0x000000000000000000000000000000000000000000000000000000000005f02200000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", { value: ethers.utils.parseEther("5.4") } ); @@ -145,8 +146,8 @@ async function main() { //await registerBacking(); //await lockAndRemoteIssuing(); //await burnAndRemoteUnlock(); - await requestRemoteUnlockForIssuingFailure(); - //await requestRemoteIssuingForUnlockFailure(); + //await requestRemoteUnlockForIssuingFailure(); + await requestRemoteIssuingForUnlockFailure(); } main() From 5161375530a223e95cf4b93a1fa46a7652f04937 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Tue, 26 Dec 2023 11:29:12 +0800 Subject: [PATCH 24/26] deploy on tron testnet --- helix-contract/address/xtoken-dev.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/helix-contract/address/xtoken-dev.json b/helix-contract/address/xtoken-dev.json index b298f0e0..7f81ce8c 100644 --- a/helix-contract/address/xtoken-dev.json +++ b/helix-contract/address/xtoken-dev.json @@ -5,6 +5,9 @@ }, "sepolia": { "msglineMessager": "0xc876D0873e4060472334E297b2db200Ca10cc806" + }, + "tron": { + "msglineMessager": "TR3nibHkcXovd1nsuNrLWigQboj4uduhKT" } }, "backingProxy": { @@ -14,9 +17,17 @@ "crab": "0x01F53415adC20a2D058DfF14e295Ab955CafD6d6" }, "issuingProxy": { - "sepolia": "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1" + "sepolia": "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1", + "tron": "TJK57bJTvnaNRGFHwbihg1bXtgnyed6sKa" }, "issuingLogic": { - "sepolia": "0xCD1c1C799f3914ECFC5e3653D3Cc846355d3dFC9" + "sepolia": "0xCD1c1C799f3914ECFC5e3653D3Cc846355d3dFC9", + "tron": "TD7VoAQnJDKsWsgZZTeeHvBKY5fogRDVc2" + }, + "proxyAdmin": { + "tron": "TQuYHJyHkE6wS5uyhWVDAibosNkvMYgyVF" + }, + "xToken": { + "tron": "TRXTkfGxTL8CjuGgz55BYkTAyVyiEMFY6F" } } From 320d11e27c78405ceb05522a2324b281f923680e Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Tue, 26 Dec 2023 13:33:46 +0800 Subject: [PATCH 25/26] add guard --- helix-contract/address/xtoken-dev.json | 3 + helix-contract/deploy/deploy_bytescode.js | 11 +- helix-contract/deploy/flatten-xtoken.sh | 2 +- helix-contract/flatten/xtoken-v3/GuardV3.sol | 1214 ++++++++++++++++++ 4 files changed, 1228 insertions(+), 2 deletions(-) create mode 100644 helix-contract/flatten/xtoken-v3/GuardV3.sol diff --git a/helix-contract/address/xtoken-dev.json b/helix-contract/address/xtoken-dev.json index 7f81ce8c..520fe86b 100644 --- a/helix-contract/address/xtoken-dev.json +++ b/helix-contract/address/xtoken-dev.json @@ -29,5 +29,8 @@ }, "xToken": { "tron": "TRXTkfGxTL8CjuGgz55BYkTAyVyiEMFY6F" + }, + "guard": { + "sepolia": "0x8F207f0e9Ed3CC1487C5C8981213AD4482d4a972" } } diff --git a/helix-contract/deploy/deploy_bytescode.js b/helix-contract/deploy/deploy_bytescode.js index f2af3f20..86598ef5 100644 --- a/helix-contract/deploy/deploy_bytescode.js +++ b/helix-contract/deploy/deploy_bytescode.js @@ -43,6 +43,7 @@ async function getDefaultBridgeBytecode(networkUrl, version) { async function getLnProxyBridgeBytecode(w, version, logicFactory, logicAddress, proxyAdminAddress, args) { const salt = ethers.utils.hexZeroPad(ethers.utils.hexlify(ethers.utils.toUtf8Bytes(version)), 32); const calldata = ProxyDeployer.getInitializerData(logicFactory.interface, args, "initialize"); + console.log(calldata); const proxyContract = await ethers.getContractFactory("TransparentUpgradeableProxy", w); const bytecode = Create2.getDeployedBytecode(proxyContract, ["address", "address", "bytes"], [logicAddress, proxyAdminAddress, calldata]); console.log(`get helix proxy bridge bytecode, salt ${salt}, bytecode ${bytecode}`); @@ -63,13 +64,21 @@ async function getLnOppositeProxyBridgeBytecode(networkUrl, version, logicAddres return; } +async function getxTokenIssuingProxyBridgeBytecode(networkUrl, version, logicAddress, proxyAdminAddress) { + const w = wallet(networkUrl); + const xTokenBaseFactory = await ethers.getContractFactory("xTokenBridgeBase", w); + await getLnProxyBridgeBytecode(w, version, xTokenBaseFactory, logicAddress, proxyAdminAddress, [w.address, version]); + return; +} + // 2. deploy mapping token factory async function main() { //await getHelixProxyAdminBytecode('https://rpc.ankr.com/eth_goerli', 'v1.0.0'); //await getOppositeBridgeBytecode('https://rpc.ankr.com/eth_goerli', 'v1.0.0'); //await getDefaultBridgeBytecode('https://rpc.ankr.com/eth_goerli', 'v1.0.0'); - await getLnDefaultProxyBridgeBytecode('https://rpc.ankr.com/eth_goerli', 'v1.0.0', '0x8af688056c6614acb5A78c62e1f9f49022C0452f', '0x601dE3B81c7cE04BecE3b29e5cEe4F3251d250dB'); + //await getLnDefaultProxyBridgeBytecode('https://rpc.ankr.com/eth_goerli', 'v1.0.0', '0x8af688056c6614acb5A78c62e1f9f49022C0452f', '0x601dE3B81c7cE04BecE3b29e5cEe4F3251d250dB'); //await getLnOppositeProxyBridgeBytecode('https://rpc.ankr.com/eth_goerli', 'v1.0.0', '0x90873fa1bbd028F22277567530A22E05f7721D37', '0x601dE3B81c7cE04BecE3b29e5cEe4F3251d250dB'); + await getxTokenIssuingProxyBridgeBytecode('https://rpc.ankr.com/eth_goerli', 'v1.0.0', "0x2279B98741D66ccbB1a9e8c80A571378a29afCf0", "0xa3D85134B8f8dB225D54AA4C5E4A25Bda3bD50eA"); } main() diff --git a/helix-contract/deploy/flatten-xtoken.sh b/helix-contract/deploy/flatten-xtoken.sh index 06e08da0..bd09b6aa 100644 --- a/helix-contract/deploy/flatten-xtoken.sh +++ b/helix-contract/deploy/flatten-xtoken.sh @@ -3,5 +3,5 @@ mkdir -p $path yarn flat contracts/mapping-token/v3/base/xTokenBacking.sol --output $path/xTokenBacking.sol yarn flat contracts/mapping-token/v3/base/xTokenIssuing.sol --output $path/xTokenIssuing.sol yarn flat contracts/mapping-token/v3/base/xTokenErc20.sol --output $path/xTokenErc20.sol -yarn flat contracts/mapping-token/v2/Guard.sol --output $path/Guard.sol +yarn flat contracts/mapping-token/v3/GuardV3.sol --output $path/GuardV3.sol yarn flat contracts/messagers/MsglineMessager.sol --output $path/MsglineMessager.sol diff --git a/helix-contract/flatten/xtoken-v3/GuardV3.sol b/helix-contract/flatten/xtoken-v3/GuardV3.sol new file mode 100644 index 00000000..5f6d580a --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/GuardV3.sol @@ -0,0 +1,1214 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/26/2023 + **/ + +pragma solidity ^0.8.17; + +// File contracts/mapping-token/interfaces/IWToken.sol +// License-Identifier: MIT + + +interface IWToken { + function deposit() external payable; + function withdraw(uint wad) external; +} + +// File @zeppelin-solidity/contracts/utils/Strings.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol) + + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } +} + +// File @zeppelin-solidity/contracts/utils/cryptography/ECDSA.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.3) (utils/cryptography/ECDSA.sol) + + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS, + InvalidSignatureV + } + + function _throwError(RecoverError error) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert("ECDSA: invalid signature"); + } else if (error == RecoverError.InvalidSignatureLength) { + revert("ECDSA: invalid signature length"); + } else if (error == RecoverError.InvalidSignatureS) { + revert("ECDSA: invalid signature 's' value"); + } else if (error == RecoverError.InvalidSignatureV) { + revert("ECDSA: invalid signature 'v' value"); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature` or error string. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, signature); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + * + * _Available since v4.3._ + */ + function tryRecover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address, RecoverError) { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + * + * _Available since v4.2._ + */ + function recover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, r, vs); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + * + * _Available since v4.3._ + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS); + } + if (v != 27 && v != 28) { + return (address(0), RecoverError.InvalidSignatureV); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature); + } + + return (signer, RecoverError.NoError); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, v, r, s); + _throwError(error); + return recovered; + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } + + /** + * @dev Returns an Ethereum Signed Message, created from `s`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s)); + } + + /** + * @dev Returns an Ethereum Signed Typed Data, created from a + * `domainSeparator` and a `structHash`. This produces hash corresponding + * to the one signed with the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] + * JSON-RPC method as part of EIP-712. + * + * See {recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } +} + +// File contracts/mapping-token/v3/GuardRegistryV3.sol +// License-Identifier: Apache-2.0 + +pragma experimental ABIEncoderV2; + +/** + * @title Manages a set of guards and a threshold to double-check BEEFY commitment + * @dev Stores the guards and a threshold + * @author echo + */ +contract GuardRegistryV3 { + event AddedGuard(address guard); + event RemovedGuard(address guard); + event ChangedThreshold(uint256 threshold); + + // keccak256( + // "EIP712Domain(uint256 chainId,address verifyingContract)" + // ); + bytes32 internal constant DOMAIN_SEPARATOR_TYPEHASH = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; + + address internal constant SENTINEL_GUARDS = address(0x1); + + /** + * @dev Nonce to prevent replay of update operations + */ + uint256 public nonce; + /** + * @dev Store all guards in the linked list + */ + mapping(address => address) internal guards; + /** + * @dev Count of all guards + */ + uint256 internal guardCount; + /** + * @dev Number of required confirmations for update operations + */ + uint256 internal threshold; + + /** + * @dev Sets initial storage of contract. + * @param _guards List of Safe guards. + * @param _threshold Number of required confirmations for check commitment or change guards. + */ + function initialize(address[] memory _guards, uint256 _threshold) internal { + // Threshold can only be 0 at initialization. + // Check ensures that setup function can only be called once. + require(threshold == 0, "Guard: Guards have already been setup"); + // Validate that threshold is smaller than number of added guards. + require(_threshold <= _guards.length, "Guard: Threshold cannot exceed guard count"); + // There has to be at least one Safe guard. + require(_threshold >= 1, "Guard: Threshold needs to be greater than 0"); + // Initializing Safe guards. + address currentGuard = SENTINEL_GUARDS; + for (uint256 i = 0; i < _guards.length; i++) { + // Guard address cannot be null. + address guard = _guards[i]; + require(guard != address(0) && guard != SENTINEL_GUARDS && guard != address(this) && currentGuard != guard, "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[guard] == address(0), "Guard: Address is already an guard"); + guards[currentGuard] = guard; + currentGuard = guard; + emit AddedGuard(guard); + } + guards[currentGuard] = SENTINEL_GUARDS; + guardCount = _guards.length; + threshold = _threshold; + } + + /** + * @dev Allows to add a new guard to the registry and update the threshold at the same time. + * This can only be done via multi-sig. + * @notice Adds the guard `guard` to the registry and updates the threshold to `_threshold`. + * @param guard New guard address. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to add new guard and update the `threshold` . + */ + function addGuardWithThreshold( + address guard, + uint256 _threshold, + bytes[] memory signatures + ) public { + // Guard address cannot be null, the sentinel or the registry itself. + require(guard != address(0) && guard != SENTINEL_GUARDS && guard != address(this), "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[guard] == address(0), "Guard: Address is already an guard"); + verifyGuardSignatures(msg.sig, abi.encode(guard, _threshold), signatures); + guards[guard] = guards[SENTINEL_GUARDS]; + guards[SENTINEL_GUARDS] = guard; + guardCount++; + emit AddedGuard(guard); + // Change threshold if threshold was changed. + if (threshold != _threshold) _changeThreshold(_threshold); + } + + /** + * @dev Allows to remove an guard from the registry and update the threshold at the same time. + * This can only be done via multi-sig. + * @notice Removes the guard `guard` from the registry and updates the threshold to `_threshold`. + * @param prevGuard Guard that pointed to the guard to be removed in the linked list + * @param guard Guard address to be removed. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to remove a guard and update the `threshold` . + */ + function removeGuard( + address prevGuard, + address guard, + uint256 _threshold, + bytes[] memory signatures + ) public { + // Only allow to remove an guard, if threshold can still be reached. + require(guardCount - 1 >= _threshold, "Guard: Threshold cannot exceed guard count"); + // Validate guard address and check that it corresponds to guard index. + require(guard != address(0) && guard != SENTINEL_GUARDS, "Guard: Invalid guard address provided"); + require(guards[prevGuard] == guard, "Guard: Invalid prevGuard, guard pair provided"); + verifyGuardSignatures(msg.sig, abi.encode(prevGuard, guard, _threshold), signatures); + guards[prevGuard] = guards[guard]; + guards[guard] = address(0); + guardCount--; + emit RemovedGuard(guard); + // Change threshold if threshold was changed. + if (threshold != _threshold) _changeThreshold(_threshold); + } + + /** + * @dev Allows to swap/replace a guard from the registry with another address. + * This can only be done via multi-sig. + * @notice Replaces the guard `oldGuard` in the registry with `newGuard`. + * @param prevGuard guard that pointed to the guard to be replaced in the linked list + * @param oldGuard guard address to be replaced. + * @param newGuard New guard address. + * @param signatures The signatures of the guards which to swap/replace a guard and update the `threshold` . + */ + function swapGuard( + address prevGuard, + address oldGuard, + address newGuard, + bytes[] memory signatures + ) public { + // Guard address cannot be null, the sentinel or the registry itself. + require(newGuard != address(0) && newGuard != SENTINEL_GUARDS && newGuard != address(this), "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[newGuard] == address(0), "Guard: Address is already an guard"); + // Validate oldGuard address and check that it corresponds to guard index. + require(oldGuard != address(0) && oldGuard != SENTINEL_GUARDS, "Guard: Invalid guard address provided"); + require(guards[prevGuard] == oldGuard, "Guard: Invalid prevGuard, guard pair provided"); + verifyGuardSignatures(msg.sig, abi.encode(prevGuard, oldGuard, newGuard), signatures); + guards[newGuard] = guards[oldGuard]; + guards[prevGuard] = newGuard; + guards[oldGuard] = address(0); + emit RemovedGuard(oldGuard); + emit AddedGuard(newGuard); + } + + /** + * @dev Allows to update the number of required confirmations by guards. + * This can only be done via multi-sig. + * @notice Changes the threshold of the registry to `_threshold`. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to update the `threshold` . + */ + function changeThreshold(uint256 _threshold, bytes[] memory signatures) public { + verifyGuardSignatures(msg.sig, abi.encode(_threshold), signatures); + _changeThreshold(_threshold); + } + + function _changeThreshold(uint256 _threshold) internal { + // Validate that threshold is smaller than number of owners. + require(_threshold <= guardCount, "Guard: Threshold cannot exceed guard count"); + // There has to be at least one guard. + require(_threshold >= 1, "Guard: Threshold needs to be greater than 0"); + threshold = _threshold; + emit ChangedThreshold(threshold); + } + + function getThreshold() public view returns (uint256) { + return threshold; + } + + function isGuard(address guard) public view returns (bool) { + return guard != SENTINEL_GUARDS && guards[guard] != address(0); + } + + /** + * @dev Returns array of guards. + * @return Array of guards. + */ + function getGuards() public view returns (address[] memory) { + address[] memory array = new address[](guardCount); + + // populate return array + uint256 index = 0; + address currentGuard = guards[SENTINEL_GUARDS]; + while (currentGuard != SENTINEL_GUARDS) { + array[index] = currentGuard; + currentGuard = guards[currentGuard]; + index++; + } + return array; + } + + function verifyGuardSignatures( + bytes4 methodID, + bytes memory params, + bytes[] memory signatures + ) internal { + bytes32 structHash = + keccak256( + abi.encode( + methodID, + params, + nonce + ) + ); + checkGuardSignatures(structHash, signatures); + nonce++; + } + + function verifyGuardSignaturesWithoutNonce( + bytes4 methodID, + bytes memory params, + bytes[] memory signatures + ) view internal { + bytes32 structHash = + keccak256( + abi.encode( + methodID, + params + ) + ); + checkGuardSignatures(structHash, signatures); + } + + /** + * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. + * @param structHash The struct Hash of the data (could be either a message/commitment hash). + * @param signatures Signature data that should be verified. only ECDSA signature. + * Signers need to be sorted in ascending order + */ + function checkGuardSignatures( + bytes32 structHash, + bytes[] memory signatures + ) public view { + // Load threshold to avoid multiple storage loads + uint256 _threshold = threshold; + // Check that a threshold is set + require(_threshold > 0, "Guard: Threshold needs to be defined"); + bytes32 dataHash = encodeDataHash(structHash); + checkNSignatures(dataHash, signatures, _threshold); + } + + /** + * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. + * @param dataHash Hash of the data (could be either a message hash or transaction hash). + * @param signatures Signature data that should be verified. only ECDSA signature. + * Signers need to be sorted in ascending order + * @param requiredSignatures Amount of required valid signatures. + */ + function checkNSignatures( + bytes32 dataHash, + bytes[] memory signatures, + uint256 requiredSignatures + ) public view { + // Check that the provided signature data is not too short + require(signatures.length >= requiredSignatures, "GS020"); + // There cannot be an owner with address 0. + address lastGuard = address(0); + address currentGuard; + for (uint256 i = 0; i < requiredSignatures; i++) { + currentGuard = ECDSA.recover(dataHash, signatures[i]); + require(currentGuard > lastGuard && guards[currentGuard] != address(0) && currentGuard != SENTINEL_GUARDS, "Guard: Invalid guard provided"); + lastGuard = currentGuard; + } + } + + /** + * @dev Returns the chain id used by this contract. + */ + function getChainId() public view returns (uint256) { + uint256 id; + // solhint-disable-next-line no-inline-assembly + assembly { + id := chainid() + } + return id; + } + + function domainSeparator() public view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, getChainId(), address(this))); + } + + function encodeDataHash(bytes32 structHash) public view returns (bytes32) { + return keccak256(abi.encodePacked(hex"1901", domainSeparator(), structHash)); + } +} + +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + +// File @zeppelin-solidity/contracts/utils/math/SafeMath.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) + + +// CAUTION +// This version of SafeMath should only be used with Solidity 0.8 or later, +// because it relies on the compiler's built in overflow checks. + +/** + * @dev Wrappers over Solidity's arithmetic operations. + * + * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler + * now has built in overflow checking. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return a % b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {trySub}. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b <= a, errorMessage); + return a - b; + } + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a / b; + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting with custom message when dividing by zero. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {tryMod}. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a % b; + } + } +} + +// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +// File contracts/mapping-token/v3/GuardV3.sol +// License-Identifier: Apache-2.0 + + + + + + +contract GuardV3 is GuardRegistryV3, Pausable { + using SafeMath for uint256; + + mapping(uint256 => bytes32) deposits; + + uint256 public maxUnclaimableTime; + mapping(address => bool) depositors; + address public operator; + + event TokenDeposit(address sender, uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); + event TokenClaimed(uint256 id); + + constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime) { + maxUnclaimableTime = _maxUnclaimableTime; + operator = msg.sender; + initialize(_guards, _threshold); + } + + modifier onlyDepositor() { + require(depositors[msg.sender] == true, "Guard: Invalid depositor"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "Guard: Invalid operator"); + _; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } + + function setOperator(address newOperator, bytes[] memory signatures) external { + verifyGuardSignatures(msg.sig, abi.encode(newOperator), signatures); + operator = newOperator; + } + + function setDepositor(address depositor, bool enable) external onlyOperator { + depositors[depositor] = enable; + } + + function setMaxUnclaimableTime(uint256 _maxUnclaimableTime) external onlyOperator { + maxUnclaimableTime = _maxUnclaimableTime; + } + + /** + * @dev deposit token to guard, waiting to claim, only allowed depositor + * @param id the id of the operation, should be siged later by guards + * @param token the erc20 token address + * @param recipient the recipient of the token + * @param amount the amount of the token + */ + function deposit( + uint256 id, + address token, + address recipient, + uint256 amount + ) public onlyDepositor whenNotPaused { + deposits[id] = hash(abi.encodePacked(msg.sender, block.timestamp, token, recipient, amount)); + emit TokenDeposit(msg.sender, id, block.timestamp, token, recipient, amount); + } + + function claimById( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bool isNative + ) internal { + require(hash(abi.encodePacked(from, timestamp, token, recipient, amount)) == deposits[id], "Guard: Invalid id to claim"); + require(amount > 0, "Guard: Invalid amount to claim"); + if (isNative) { + require(IERC20(token).transferFrom(from, address(this), amount), "Guard: claim native token failed"); + uint256 balanceBefore = address(this).balance; + IWToken(token).withdraw(amount); + require(address(this).balance == balanceBefore.add(amount), "Guard: token is not wrapped by native token"); + payable(recipient).transfer(amount); + } else { + require(IERC20(token).transferFrom(from, recipient, amount), "Guard: claim token failed"); + } + delete deposits[id]; + emit TokenClaimed(id); + } + + /** + * @dev claim the tokens in the contract saved by deposit, this acquire signatures from guards + * @param id the id to be claimed + * @param signatures the signatures of the guards which to claim tokens. + */ + function claim( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bytes[] memory signatures + ) public { + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, false); + } + + /** + * @dev claimNative the tokens in the contract saved by deposit, this acquire signatures from guards + * @param id the id to be claimed + * @param signatures the signatures of the guards which to claim tokens. + */ + function claimNative( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bytes[] memory signatures + ) public { + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, true); + } + + /** + * @dev claim the tokens without signatures, this only allowed when timeout + * @param id the id to be claimed + */ + function claimByTimeout( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bool isNative + ) public whenNotPaused { + require(timestamp < block.timestamp && block.timestamp - timestamp > maxUnclaimableTime, "Guard: claim at invalid time"); + claimById(from, id, timestamp, token, recipient, amount, isNative); + } + + function hash(bytes memory value) public pure returns (bytes32) { + return sha256(value); + } +} \ No newline at end of file From d305154129441e9df5a7e13acadf44f39d953e30 Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Wed, 27 Dec 2023 15:18:31 +0800 Subject: [PATCH 26/26] GuardV3 public member --- .../contracts/mapping-token/v3/GuardV3.sol | 4 +- helix-contract/flatten/xtoken-v3/GuardV3.sol | 188 +++++------ .../flatten/xtoken-v3/MsglineMessager.sol | 2 +- .../flatten/xtoken-v3/xTokenBacking.sol | 306 ++++++++--------- .../flatten/xtoken-v3/xTokenErc20.sol | 2 +- .../flatten/xtoken-v3/xTokenIssuing.sol | 308 +++++++++--------- 6 files changed, 405 insertions(+), 405 deletions(-) diff --git a/helix-contract/contracts/mapping-token/v3/GuardV3.sol b/helix-contract/contracts/mapping-token/v3/GuardV3.sol index d0441f3a..2715ab6e 100644 --- a/helix-contract/contracts/mapping-token/v3/GuardV3.sol +++ b/helix-contract/contracts/mapping-token/v3/GuardV3.sol @@ -11,10 +11,10 @@ import "../interfaces/IWToken.sol"; contract GuardV3 is GuardRegistryV3, Pausable { using SafeMath for uint256; - mapping(uint256 => bytes32) deposits; + mapping(uint256 => bytes32) public deposits; uint256 public maxUnclaimableTime; - mapping(address => bool) depositors; + mapping(address => bool) public depositors; address public operator; event TokenDeposit(address sender, uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); diff --git a/helix-contract/flatten/xtoken-v3/GuardV3.sol b/helix-contract/flatten/xtoken-v3/GuardV3.sol index 5f6d580a..497e2586 100644 --- a/helix-contract/flatten/xtoken-v3/GuardV3.sol +++ b/helix-contract/flatten/xtoken-v3/GuardV3.sol @@ -19,15 +19,6 @@ pragma solidity ^0.8.17; -// File contracts/mapping-token/interfaces/IWToken.sol -// License-Identifier: MIT - - -interface IWToken { - function deposit() external payable; - function withdraw(uint wad) external; -} - // File @zeppelin-solidity/contracts/utils/Strings.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol) @@ -617,6 +608,98 @@ contract GuardRegistryV3 { } } +// File contracts/mapping-token/interfaces/IWToken.sol +// License-Identifier: MIT + + +interface IWToken { + function deposit() external payable; + function withdraw(uint wad) external; +} + +// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + // File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) @@ -974,89 +1057,6 @@ library SafeMath { } } -// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) - - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. - */ -interface IERC20 { - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); - - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `to`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address to, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `from` to `to` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom( - address from, - address to, - uint256 amount - ) external returns (bool); -} - // File contracts/mapping-token/v3/GuardV3.sol // License-Identifier: Apache-2.0 @@ -1068,10 +1068,10 @@ interface IERC20 { contract GuardV3 is GuardRegistryV3, Pausable { using SafeMath for uint256; - mapping(uint256 => bytes32) deposits; + mapping(uint256 => bytes32) public deposits; uint256 public maxUnclaimableTime; - mapping(address => bool) depositors; + mapping(address => bool) public depositors; address public operator; event TokenDeposit(address sender, uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); diff --git a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol index 65c80028..eab164b2 100644 --- a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol +++ b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/25/2023 + * 12/26/2023 **/ pragma solidity ^0.8.17; diff --git a/helix-contract/flatten/xtoken-v3/xTokenBacking.sol b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol index 872ba049..851ce085 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenBacking.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/25/2023 + * 12/26/2023 **/ pragma solidity ^0.8.17; @@ -160,6 +160,29 @@ interface IWToken { function withdraw(uint wad) external; } +// File contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol +// License-Identifier: MIT + +interface IxTokenIssuing { + function handleIssuingForUnlockFailureFromRemote( + uint256 originalChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; + + function issuexToken( + uint256 remoteChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; +} + // File contracts/interfaces/IMessager.sol // License-Identifier: MIT @@ -298,6 +321,135 @@ contract DailyLimit { } } +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + // File @zeppelin-solidity/contracts/utils/Address.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol) @@ -658,135 +810,6 @@ abstract contract Initializable { } } -// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) - - -/** - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -abstract contract Context { - function _msgSender() internal view virtual returns (address) { - return msg.sender; - } - - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; - } -} - -// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) - - -/** - * @dev Contract module which allows children to implement an emergency stop - * mechanism that can be triggered by an authorized account. - * - * This module is used through inheritance. It will make available the - * modifiers `whenNotPaused` and `whenPaused`, which can be applied to - * the functions of your contract. Note that they will not be pausable by - * simply including this module, only once the modifiers are put in place. - */ -abstract contract Pausable is Context { - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - bool private _paused; - - /** - * @dev Initializes the contract in unpaused state. - */ - constructor() { - _paused = false; - } - - /** - * @dev Modifier to make a function callable only when the contract is not paused. - * - * Requirements: - * - * - The contract must not be paused. - */ - modifier whenNotPaused() { - _requireNotPaused(); - _; - } - - /** - * @dev Modifier to make a function callable only when the contract is paused. - * - * Requirements: - * - * - The contract must be paused. - */ - modifier whenPaused() { - _requirePaused(); - _; - } - - /** - * @dev Returns true if the contract is paused, and false otherwise. - */ - function paused() public view virtual returns (bool) { - return _paused; - } - - /** - * @dev Throws if the contract is paused. - */ - function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); - } - - /** - * @dev Throws if the contract is not paused. - */ - function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); - } - - /** - * @dev Triggers stopped state. - * - * Requirements: - * - * - The contract must not be paused. - */ - function _pause() internal virtual whenNotPaused { - _paused = true; - emit Paused(_msgSender()); - } - - /** - * @dev Returns to normal state. - * - * Requirements: - * - * - The contract must be paused. - */ - function _unpause() internal virtual whenPaused { - _paused = false; - emit Unpaused(_msgSender()); - } -} - // File contracts/mapping-token/v3/base/xTokenBridgeBase.sol // License-Identifier: MIT @@ -951,29 +974,6 @@ contract xTokenBridgeBase is Initializable, Pausable, AccessController, DailyLim } } -// File contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol -// License-Identifier: MIT - -interface IxTokenIssuing { - function handleIssuingForUnlockFailureFromRemote( - uint256 originalChainId, - address originalToken, - address originalSender, - address recipient, - uint256 amount, - uint256 nonce - ) external; - - function issuexToken( - uint256 remoteChainId, - address originalToken, - address originalSender, - address recipient, - uint256 amount, - uint256 nonce - ) external; -} - // File contracts/mapping-token/v3/base/xTokenBacking.sol // License-Identifier: MIT diff --git a/helix-contract/flatten/xtoken-v3/xTokenErc20.sol b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol index c62b02e6..ae6d4a3f 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenErc20.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/25/2023 + * 12/26/2023 **/ pragma solidity ^0.8.17; diff --git a/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol index 9ee81cdc..4487af73 100644 --- a/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol +++ b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 12/25/2023 + * 12/26/2023 **/ pragma solidity ^0.8.17; @@ -151,48 +151,6 @@ interface IGuard { function deposit(uint256 id, address token, address recipient, uint256 amount) external; } -// File contracts/utils/AccessController.sol -// License-Identifier: MIT - -/// @title AccessController -/// @notice AccessController is a contract to control the access permission -/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract -contract AccessController { - address public dao; - address public operator; - address public pendingDao; - - modifier onlyDao() { - require(msg.sender == dao, "!dao"); - _; - } - - modifier onlyOperator() { - require(msg.sender == operator, "!operator"); - _; - } - - function _initialize(address _dao) internal { - dao = _dao; - operator = _dao; - } - - function setOperator(address _operator) onlyDao external { - operator = _operator; - } - - function transferOwnership(address _dao) onlyDao external { - pendingDao = _dao; - } - - function acceptOwnership() external { - address newDao = msg.sender; - require(pendingDao == newDao, "!pendingDao"); - delete pendingDao; - dao = newDao; - } -} - // File contracts/interfaces/IMessager.sol // License-Identifier: MIT @@ -289,132 +247,45 @@ contract DailyLimit { } } -// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 -// License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) - - -/** - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -abstract contract Context { - function _msgSender() internal view virtual returns (address) { - return msg.sender; - } - - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; - } -} - -// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// File contracts/utils/AccessController.sol // License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) - -/** - * @dev Contract module which allows children to implement an emergency stop - * mechanism that can be triggered by an authorized account. - * - * This module is used through inheritance. It will make available the - * modifiers `whenNotPaused` and `whenPaused`, which can be applied to - * the functions of your contract. Note that they will not be pausable by - * simply including this module, only once the modifiers are put in place. - */ -abstract contract Pausable is Context { - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - bool private _paused; - - /** - * @dev Initializes the contract in unpaused state. - */ - constructor() { - _paused = false; - } +/// @title AccessController +/// @notice AccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract AccessController { + address public dao; + address public operator; + address public pendingDao; - /** - * @dev Modifier to make a function callable only when the contract is not paused. - * - * Requirements: - * - * - The contract must not be paused. - */ - modifier whenNotPaused() { - _requireNotPaused(); + modifier onlyDao() { + require(msg.sender == dao, "!dao"); _; } - /** - * @dev Modifier to make a function callable only when the contract is paused. - * - * Requirements: - * - * - The contract must be paused. - */ - modifier whenPaused() { - _requirePaused(); + modifier onlyOperator() { + require(msg.sender == operator, "!operator"); _; } - /** - * @dev Returns true if the contract is paused, and false otherwise. - */ - function paused() public view virtual returns (bool) { - return _paused; - } - - /** - * @dev Throws if the contract is paused. - */ - function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); + function _initialize(address _dao) internal { + dao = _dao; + operator = _dao; } - /** - * @dev Throws if the contract is not paused. - */ - function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); + function setOperator(address _operator) onlyDao external { + operator = _operator; } - /** - * @dev Triggers stopped state. - * - * Requirements: - * - * - The contract must not be paused. - */ - function _pause() internal virtual whenNotPaused { - _paused = true; - emit Paused(_msgSender()); + function transferOwnership(address _dao) onlyDao external { + pendingDao = _dao; } - /** - * @dev Returns to normal state. - * - * Requirements: - * - * - The contract must be paused. - */ - function _unpause() internal virtual whenPaused { - _paused = false; - emit Unpaused(_msgSender()); + function acceptOwnership() external { + address newDao = msg.sender; + require(pendingDao == newDao, "!pendingDao"); + delete pendingDao; + dao = newDao; } } @@ -778,6 +649,135 @@ abstract contract Initializable { } } +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract Pausable is Context { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + constructor() { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } +} + // File contracts/mapping-token/v3/base/xTokenBridgeBase.sol // License-Identifier: MIT