diff --git a/helix-contract/address/xtoken-dev.json b/helix-contract/address/xtoken-dev.json new file mode 100644 index 00000000..520fe86b --- /dev/null +++ b/helix-contract/address/xtoken-dev.json @@ -0,0 +1,36 @@ +{ + "messagers": { + "crab": { + "msglineMessager": "0xf85638B61E0425D6BB91981190B73246e3AF3CA9" + }, + "sepolia": { + "msglineMessager": "0xc876D0873e4060472334E297b2db200Ca10cc806" + }, + "tron": { + "msglineMessager": "TR3nibHkcXovd1nsuNrLWigQboj4uduhKT" + } + }, + "backingProxy": { + "crab": "0xbdC7bbF408931C5d666b4F0520E0D9E9A0B04e99" + }, + "backingLogic": { + "crab": "0x01F53415adC20a2D058DfF14e295Ab955CafD6d6" + }, + "issuingProxy": { + "sepolia": "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1", + "tron": "TJK57bJTvnaNRGFHwbihg1bXtgnyed6sKa" + }, + "issuingLogic": { + "sepolia": "0xCD1c1C799f3914ECFC5e3653D3Cc846355d3dFC9", + "tron": "TD7VoAQnJDKsWsgZZTeeHvBKY5fogRDVc2" + }, + "proxyAdmin": { + "tron": "TQuYHJyHkE6wS5uyhWVDAibosNkvMYgyVF" + }, + "xToken": { + "tron": "TRXTkfGxTL8CjuGgz55BYkTAyVyiEMFY6F" + }, + "guard": { + "sepolia": "0x8F207f0e9Ed3CC1487C5C8981213AD4482d4a972" + } +} diff --git a/helix-contract/contracts/interfaces/IMessageLine.sol b/helix-contract/contracts/interfaces/IMessageLine.sol new file mode 100644 index 00000000..d56739a7 --- /dev/null +++ b/helix-contract/contracts/interfaces/IMessageLine.sol @@ -0,0 +1,27 @@ +// 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); +} + +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..01cdd6c5 --- /dev/null +++ b/helix-contract/contracts/interfaces/IMessager.sol @@ -0,0 +1,13 @@ +// 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; +} + diff --git a/helix-contract/contracts/mapping-token/v2/Guard.sol b/helix-contract/contracts/mapping-token/v2/Guard.sol index 767c0668..1897df08 100644 --- a/helix-contract/contracts/mapping-token/v2/Guard.sol +++ b/helix-contract/contracts/mapping-token/v2/Guard.sol @@ -148,4 +148,3 @@ contract Guard is GuardRegistry, Pausable { return sha256(value); } } - 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..2715ab6e --- /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) public deposits; + + uint256 public maxUnclaimableTime; + mapping(address => bool) public 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); + } +} + 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..412f3747 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBacking.sol @@ -0,0 +1,238 @@ +// 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"; + +// 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 { + address public wToken; + + // save original token => xToken to prevent unregistered token lock + mapping(bytes32 => address) public originalToken2xTokens; + + event TokenLocked( + bytes32 transferId, + 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 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 + // 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, + address _xToken, + uint256 _dailyLimit + ) external onlyDao { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); + originalToken2xTokens[key] = _xToken; + _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, block.chainid, _remoteChainId, _originalToken, msg.sender, _recipient, _amount); + _requestTransfer(transferId); + + 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, + msg.sender, + _recipient, + _amount, + _nonce + ); + _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); + emit TokenLocked(transferId, _nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); + } + + function encodeIssuexToken( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.issuexToken.selector, + block.chainid, + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + } + + // receive unlock original token message from remote issuing contract + function unlockFromRemote( + uint256 _remoteChainId, + address _originalToken, + address _originSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) external calledByMessager(_remoteChainId) whenNotPaused { + expendDailyLimit(_originalToken, _amount); + + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originSender, _recipient, _amount); + _handleTransfer(transferId); + + // 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 { + 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); + } + } + + 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); + } + } + + // send message to Issuing when unlock failed + function requestRemoteIssuingForUnlockFailure( + 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, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); + _requestRefund(transferId); + bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); + emit RemoteIssuingFailure(transferId, _originalToken, _originalSender, _amount, msg.value); + } + + function encodeIssuingForUnlockFailureFromRemote( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.handleIssuingForUnlockFailureFromRemote.selector, + block.chainid, + _originalToken, + _originalSender, + _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, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) external calledByMessager(_remoteChainId) whenNotPaused { + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); + _handleRefund(transferId); + if (_originalToken == address(0)) { + TokenTransferHelper.safeTransferNative(_originalSender, _amount); + } else { + TokenTransferHelper.safeTransfer(_originalToken, _originalSender, _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 new file mode 100644 index 00000000..c87333e7 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenBridgeBase.sol @@ -0,0 +1,166 @@ +// 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"; + +// 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; + + // 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"); + _; + } + + 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 { + MessagerService memory service = messagers[_remoteChainId]; + require(service.sendService != address(0), "bridge not configured"); + uint256 _protocolFee = protocolFee; + protocolFeeReserved += _protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: _feePrepaid - _protocolFee}( + _remoteChainId, + _payload, + _extParams + ); + } + + // 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 getTransferId( + uint256 _nonce, + uint256 _sourceChainId, + uint256 _targetChainId, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount + ) public pure returns(bytes32) { + return keccak256(abi.encodePacked(_nonce, _sourceChainId, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); + } + + // 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..f3cbe3dc --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenErc20.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "@zeppelin-solidity/contracts/utils/math/SafeMath.sol"; + +contract xTokenErc20 is IERC20 { + 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; + + 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(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) { + 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 account, address spender) public view virtual override returns (uint256) { + return _allowances[account][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 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[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 new file mode 100644 index 00000000..81304948 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/base/xTokenIssuing.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./xTokenBridgeBase.sol"; +import "./xTokenErc20.sol"; +import "../interfaces/IxTokenBacking.sol"; +import "../../interfaces/IGuard.sol"; +import "../../../utils/TokenTransferHelper.sol"; + +contract xTokenIssuing is xTokenBridgeBase { + struct OriginalTokenInfo { + uint256 chainId; + address token; + } + + // 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 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 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( + 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); + } + + // 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, + address _xToken + ) external onlyDao { + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); + address oldxToken = xTokens[salt]; + if (oldxToken != address(0)) { + delete originalTokens[oldxToken]; + } + xTokens[salt] = _xToken; + originalTokens[_xToken] = OriginalTokenInfo(_originalChainId, _originalToken); + 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 _nonce + ) external calledByMessager(_remoteChainId) whenNotPaused { + 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"); + require(_amount > 0, "can not receive amount zero"); + expendDailyLimit(xToken, _amount); + + _handleTransfer(transferId); + + 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, + 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, block.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, + _nonce + ); + _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); + emit BurnAndRemoteUnlocked(transferId, _nonce, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _amount, msg.value); + } + + function encodeUnlockFromRemote( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.unlockFromRemote.selector, + block.chainid, + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + } + + // send unlock message when issuing failed + // 1. message has been delivered + // 2. xtoken not issued + // this method can retry + function requestRemoteUnlockForIssuingFailure( + uint256 _originalChainId, + 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, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); + _requestRefund(transferId); + bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); + emit RemoteUnlockForIssuingFailureRequested(transferId, _originalToken, _originalSender, _amount, msg.value); + } + + function encodeUnlockForIssuingFailureFromRemote( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.handleUnlockForIssuingFailureFromRemote.selector, + block.chainid, + _originalToken, + _originalSender, + _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, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) external calledByMessager(_originalChainId) whenNotPaused { + bytes32 transferId = getTransferId(_nonce, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); + _handleRefund(transferId); + + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); + address xToken = xTokens[salt]; + require(xToken != address(0), "xToken not exist"); + + 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)); + } +} 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..a190f1ad --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenBacking.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +interface IxTokenBacking { + function unlockFromRemote( + uint256 remoteChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; + + function handleUnlockForIssuingFailureFromRemote( + uint256 remoteChainId, + address originalToken, + address originalSender, + 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 new file mode 100644 index 00000000..00f59685 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/interfaces/IxTokenIssuing.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +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; +} diff --git a/helix-contract/contracts/messagers/MsglineMessager.sol b/helix-contract/contracts/messagers/MsglineMessager.sol new file mode 100644 index 00000000..08d10ec5 --- /dev/null +++ b/helix-contract/contracts/messagers/MsglineMessager.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "../utils/AccessController.sol"; +import "../interfaces/IMessageLine.sol"; + +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"); + _; + } + + 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 messagePayload(address _from, address _to, bytes memory _message) public view returns(bytes memory) { + return abi.encodeWithSelector( + MsglineMessager.receiveMessage.selector, + block.chainid, + _from, + _to, + _message + ); + } +} + diff --git a/helix-contract/contracts/messagers/mock/MockMsgline.sol b/helix-contract/contracts/messagers/mock/MockMsgline.sol new file mode 100644 index 00000000..fdcefa2a --- /dev/null +++ b/helix-contract/contracts/messagers/mock/MockMsgline.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +contract MockMessageLine { + mapping(bytes32 => bool) public dones; + bool public failedFlag; + bool public neverDelivered; + + uint256 public sendNonce; + uint256 public recvNonce; + + address public remote; + + function setRemote(address _remote) external { + remote = _remote; + } + + function setRecvFailed() external { + failedFlag = true; + } + + function setNeverDelivered() external { + neverDelivered = 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(sendNonce, block.chainid, msg.sender, toDapp, message); + } + + function recv( + uint256 sendNonce, + uint256 sourceChainId, + address sourceSender, + address toDapp, + bytes calldata message + ) public { + if (neverDelivered) { + neverDelivered = false; + return; + } + recvNonce = sendNonce; + 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/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..2751f8ea --- /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))), "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"); + } +} + 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/deploy_msgline_messager.js b/helix-contract/deploy/deploy_msgline_messager.js new file mode 100644 index 00000000..140ac62a --- /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: "0x0000000000D2de3e2444926c4577b0A59F1DD8BC", +}; + +const sepoliaNetwork = { + name: "sepolia", + url: "https://rpc-sepolia.rockx.com", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + deployer: "0xbe6b2860d3c17a719be0A4911EA0EE689e8357f3", + msgline: "0x0000000000D2de3e2444926c4577b0A59F1DD8BC", +}; + +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..0a1d4d31 --- /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: "0xf85638B61E0425D6BB91981190B73246e3AF3CA9", + backing: "0xbdC7bbF408931C5d666b4F0520E0D9E9A0B04e99", + chainid: 44 +}; + +const sepoliaNetwork = { + name: "sepolia", + url: "https://rpc-sepolia.rockx.com", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + messager: "0xc876D0873e4060472334E297b2db200Ca10cc806", + issuing: "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1", + 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, {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, {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, {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"); +} + +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..92308bd0 --- /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.3"); + + // deploy issuing on sepolia + const walletSpeolia = wallet(sepoliaNetwork.url); + const issuingLogic = await deployxTokenIssuing(walletSpeolia, sepoliaNetwork.deployer, "xTokenIssuing-logic-v1.0.3"); +} + +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..fadbe60c --- /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 = "0x22E50D0511538B78D4E3b94d4D51AFDa924286D0"; + await deployxTokenProxy(walletCrab, "xtoken-backing-1.0.4", crabNetwork.dao, crabNetwork.proxyAdmin, backingLogic, crabNetwork.deployer); + + const walletSepolia = wallet(sepoliaNetwork.url); + const issuingLogic = "0xCD1c1C799f3914ECFC5e3653D3Cc846355d3dFC9"; + await deployxTokenProxy(walletSepolia, "xtoken-issuing-1.0.4", 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/deploy_xtoken_test.js b/helix-contract/deploy/deploy_xtoken_test.js new file mode 100644 index 00000000..b2a9a153 --- /dev/null +++ b/helix-contract/deploy/deploy_xtoken_test.js @@ -0,0 +1,159 @@ +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: "0xbdC7bbF408931C5d666b4F0520E0D9E9A0B04e99", + chainid: 44 +}; + +const sepoliaNetwork = { + name: "sepolia", + url: "https://rpc-sepolia.rockx.com", + dao: "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", + issuing: "0xf22D0bb66b39745Ae6e3fEa3E5859d7f0b367Fd1", + 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 = "0x9Da7E18441f26515CC713290BE846E726d41781d"; + + 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("10"), + 1703247763002, + "0x000000000000000000000000000000000000000000000000000000000005f02200000000000000000000000088a39b052d477cfde47600a7c9950a441ce61cb400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + { value: ethers.utils.parseEther("15.4") } + ); +} + +async function burnAndRemoteUnlock() { + const issuingNetwork = sepoliaNetwork; + const walletIssuing = wallet(sepoliaNetwork.url); + + const issuing = await ethers.getContractAt("xTokenIssuing", issuingNetwork.issuing, walletIssuing); + + 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"), + 1703248419044, + "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("91"), + 1703247763001, + "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( + 11155111, + "0x0000000000000000000000000000000000000000", + walletBacking.address, + walletBacking.address, + ethers.utils.parseEther("5"), + 1703248419044, + "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); + }); + diff --git a/helix-contract/deploy/flatten-xtoken.sh b/helix-contract/deploy/flatten-xtoken.sh new file mode 100644 index 00000000..bd09b6aa --- /dev/null +++ b/helix-contract/deploy/flatten-xtoken.sh @@ -0,0 +1,7 @@ +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/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..497e2586 --- /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 @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 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) + + +/** + * @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 contracts/mapping-token/v3/GuardV3.sol +// License-Identifier: Apache-2.0 + + + + + + +contract GuardV3 is GuardRegistryV3, Pausable { + using SafeMath for uint256; + + mapping(uint256 => bytes32) public deposits; + + uint256 public maxUnclaimableTime; + mapping(address => bool) public 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 diff --git a/helix-contract/flatten/xtoken-v3/MsglineMessager.sol b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol new file mode 100644 index 00000000..eab164b2 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/MsglineMessager.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/26/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); +} + +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"); + _; + } + + 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 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..851ce085 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/xTokenBacking.sol @@ -0,0 +1,1211 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/26/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/mapping-token/interfaces/IWToken.sol +// License-Identifier: MIT + + +interface IWToken { + function deposit() external payable; + 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 + +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/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 + + +/// @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 @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 + + + + + + +// 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; + + // 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"); + _; + } + + 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 { + MessagerService memory service = messagers[_remoteChainId]; + require(service.sendService != address(0), "bridge not configured"); + uint256 _protocolFee = protocolFee; + protocolFeeReserved += _protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: _feePrepaid - _protocolFee}( + _remoteChainId, + _payload, + _extParams + ); + } + + // 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 getTransferId( + uint256 _nonce, + uint256 _sourceChainId, + uint256 _targetChainId, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount + ) public pure returns(bytes32) { + return keccak256(abi.encodePacked(_nonce, _sourceChainId, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); + } + + // 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/base/xTokenBacking.sol +// License-Identifier: MIT + + + + + + +// 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 { + address public wToken; + + // save original token => xToken to prevent unregistered token lock + mapping(bytes32 => address) public originalToken2xTokens; + + event TokenLocked( + bytes32 transferId, + 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 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 + // 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, + address _xToken, + uint256 _dailyLimit + ) external onlyDao { + bytes32 key = keccak256(abi.encodePacked(_remoteChainId, _originalToken)); + originalToken2xTokens[key] = _xToken; + _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, block.chainid, _remoteChainId, _originalToken, msg.sender, _recipient, _amount); + _requestTransfer(transferId); + + 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, + msg.sender, + _recipient, + _amount, + _nonce + ); + _sendMessage(_remoteChainId, issuxToken, prepaid, _extParams); + emit TokenLocked(transferId, _nonce, _remoteChainId, _originalToken, msg.sender, _recipient, _amount, prepaid); + } + + function encodeIssuexToken( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.issuexToken.selector, + block.chainid, + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + } + + // receive unlock original token message from remote issuing contract + function unlockFromRemote( + uint256 _remoteChainId, + address _originalToken, + address _originSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) external calledByMessager(_remoteChainId) whenNotPaused { + expendDailyLimit(_originalToken, _amount); + + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originSender, _recipient, _amount); + _handleTransfer(transferId); + + // 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 { + 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); + } + } + + 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); + } + } + + // send message to Issuing when unlock failed + function requestRemoteIssuingForUnlockFailure( + 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, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); + _requestRefund(transferId); + bytes memory unlockForFailed = encodeIssuingForUnlockFailureFromRemote( + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + _sendMessage(_remoteChainId, unlockForFailed, msg.value, _extParams); + emit RemoteIssuingFailure(transferId, _originalToken, _originalSender, _amount, msg.value); + } + + function encodeIssuingForUnlockFailureFromRemote( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenIssuing.handleIssuingForUnlockFailureFromRemote.selector, + block.chainid, + _originalToken, + _originalSender, + _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, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) external calledByMessager(_remoteChainId) whenNotPaused { + bytes32 transferId = getTransferId(_nonce, block.chainid, _remoteChainId, _originalToken, _originalSender, _recipient, _amount); + _handleRefund(transferId); + if (_originalToken == address(0)) { + TokenTransferHelper.safeTransferNative(_originalSender, _amount); + } else { + TokenTransferHelper.safeTransfer(_originalToken, _originalSender, _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 new file mode 100644 index 00000000..ae6d4a3f --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/xTokenErc20.sol @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/26/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 contracts/mapping-token/v3/base/xTokenErc20.sol +// License-Identifier: MIT + + +contract xTokenErc20 is IERC20 { + 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; + + 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(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) { + 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 account, address spender) public view virtual override returns (uint256) { + return _allowances[account][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 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[account][spender] = amount; + emit Approval(account, 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..4487af73 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/xTokenIssuing.sol @@ -0,0 +1,1576 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 12/26/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/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 + + +/// @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/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 @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 + + + + + + +// 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; + + // 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"); + _; + } + + 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 { + MessagerService memory service = messagers[_remoteChainId]; + require(service.sendService != address(0), "bridge not configured"); + uint256 _protocolFee = protocolFee; + protocolFeeReserved += _protocolFee; + ILowLevelMessageSender(service.sendService).sendMessage{value: _feePrepaid - _protocolFee}( + _remoteChainId, + _payload, + _extParams + ); + } + + // 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 getTransferId( + uint256 _nonce, + uint256 _sourceChainId, + uint256 _targetChainId, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount + ) public pure returns(bytes32) { + return keccak256(abi.encodePacked(_nonce, _sourceChainId, _targetChainId, _originalToken, _originalSender, _recipient, _amount)); + } + + // settings + function updateGuard(address _guard) external onlyDao { + guard = _guard; + } + + function setDailyLimit(address _token, uint256 _dailyLimit) external onlyDao { + _setDailyLimit(_token, _dailyLimit); + } +} + +// 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 { + 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; + + 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(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) { + 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 account, address spender) public view virtual override returns (uint256) { + return _allowances[account][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 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[account][spender] = amount; + emit Approval(account, 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 originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; + + function handleUnlockForIssuingFailureFromRemote( + uint256 remoteChainId, + address originalToken, + address originalSender, + address recipient, + uint256 amount, + uint256 nonce + ) external; +} + +// File contracts/mapping-token/v3/base/xTokenIssuing.sol +// License-Identifier: MIT + + + + + +contract xTokenIssuing is xTokenBridgeBase { + struct OriginalTokenInfo { + uint256 chainId; + address token; + } + + // 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 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 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( + 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); + } + + // 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, + address _xToken + ) external onlyDao { + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); + address oldxToken = xTokens[salt]; + if (oldxToken != address(0)) { + delete originalTokens[oldxToken]; + } + xTokens[salt] = _xToken; + originalTokens[_xToken] = OriginalTokenInfo(_originalChainId, _originalToken); + 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 _nonce + ) external calledByMessager(_remoteChainId) whenNotPaused { + 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"); + require(_amount > 0, "can not receive amount zero"); + expendDailyLimit(xToken, _amount); + + _handleTransfer(transferId); + + 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, + 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, block.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, + _nonce + ); + _sendMessage(originalInfo.chainId, remoteUnlockCall, msg.value, _extParams); + emit BurnAndRemoteUnlocked(transferId, _nonce, originalInfo.chainId, msg.sender, _recipient, originalInfo.token, _amount, msg.value); + } + + function encodeUnlockFromRemote( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.unlockFromRemote.selector, + block.chainid, + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + } + + // send unlock message when issuing failed + // 1. message has been delivered + // 2. xtoken not issued + // this method can retry + function requestRemoteUnlockForIssuingFailure( + uint256 _originalChainId, + 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, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); + _requestRefund(transferId); + bytes memory handleUnlockForFailed = encodeUnlockForIssuingFailureFromRemote( + _originalToken, + _originalSender, + _recipient, + _amount, + _nonce + ); + _sendMessage(_originalChainId, handleUnlockForFailed, msg.value, _extParams); + emit RemoteUnlockForIssuingFailureRequested(transferId, _originalToken, _originalSender, _amount, msg.value); + } + + function encodeUnlockForIssuingFailureFromRemote( + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) public view returns(bytes memory) { + return abi.encodeWithSelector( + IxTokenBacking.handleUnlockForIssuingFailureFromRemote.selector, + block.chainid, + _originalToken, + _originalSender, + _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, + address _originalToken, + address _originalSender, + address _recipient, + uint256 _amount, + uint256 _nonce + ) external calledByMessager(_originalChainId) whenNotPaused { + bytes32 transferId = getTransferId(_nonce, _originalChainId, block.chainid, _originalToken, _originalSender, _recipient, _amount); + _handleRefund(transferId); + + bytes32 salt = xTokenSalt(_originalChainId, _originalToken); + address xToken = xTokens[salt]; + require(xToken != address(0), "xToken not exist"); + + 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 diff --git a/helix-contract/test/1_test_bscv2.js b/helix-contract/test/1_test_bscv2.js index 0a24fcee..215aead9 100644 --- a/helix-contract/test/1_test_bscv2.js +++ b/helix-contract/test/1_test_bscv2.js @@ -425,7 +425,7 @@ describe("darwinia<>bsc mapping token tests", () => { ethUtil.keccak256( abi.rawEncode( ['bytes4', 'bytes'], - [abi.methodID('claim', [ 'uint256', 'uint256', 'address', 'address', 'uint256', '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]) ] 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..6baa648e --- /dev/null +++ b/helix-contract/test/6_test_xtoken_v3.js @@ -0,0 +1,550 @@ +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, user01, user02] = await ethers.getSigners(); + const dao = owner.address; + const backingChainId = 31337; + const issuingChainId = 31337; + const nativeTokenAddress = "0x0000000000000000000000000000000000000000"; + let globalNonce = 10001; + + 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); + + 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("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("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); + + function generateNonce() { + globalNonce += 1; + return globalNonce; + } + + 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.connect(user02).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, + 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.connect(user01).lockAndRemoteIssuing( + issuingChainId, + originalAddress, + recipient, + amount, + nonce, + 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 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); + if (result == true && !usingGuard) { + 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); + } + return nonce; + } + + async function burnAndRemoteUnlock( + originalAddress, + amount, + fee, + usingGuard, + result + ) { + const recipient = user01.address; + const nonce = generateNonce(); + const xTokenAddress = xTokens[originalAddress]; + + const balanceUserBefore = await balanceOf(xTokenAddress, user02.address); + const balanceRecipientBefore = await balanceOf(originalAddress, recipient); + const balanceBackingBefore = await balanceOf(originalAddress, backing.address); + + const transaction = await issuing.connect(user02).burnAndRemoteUnlock( + xTokenAddress, + recipient, + amount, + nonce, + 0, + {value: ethers.utils.parseEther(fee)} + ); + const receipt = await transaction.wait(); + console.log("burnAndRemoteUnlock gasUsed: ", receipt.cumulativeGasUsed); + + const balanceRecipientAfter = await balanceOf(originalAddress, recipient); + const balanceBackingAfter = await balanceOf(originalAddress, backing.address); + const balanceUserAfter = await balanceOf(xTokenAddress, user02.address); + + 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); + expect(balanceUserBefore.sub(balanceUserAfter)).to.equal(amount); + + if (result && !usingGuard) { + expect(balanceRecipientAfter.sub(balanceRecipientBefore)).to.equal(amount); + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.equal(amount); + } else { + // 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); + } + return nonce; + } + + async function requestRemoteUnlockForIssuingFailure( + originalToken, + 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 transaction = await issuing.requestRemoteUnlockForIssuingFailure( + backingChainId, + originalToken, + originalSender, + recipient, + amount, + nonce, + 0, + { + value: ethers.utils.parseEther(fee), + gasPrice: 10000000000, + } + ); + const balanceSenderAfter = await balanceOf(originalToken, originalSender); + const balanceBackingAfter = await balanceOf(originalToken, backing.address); + + let receipt = await transaction.wait(); + let gasFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice); + + 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); + expect(balanceBackingBefore.sub(balanceBackingAfter)).to.be.equal(amount); + 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); + } + } + + async function requestRemoteIssuingForUnlockFailure( + originalToken, + 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( + issuingChainId, + originalToken, + originalSender, + recipient, + amount, + nonce, + 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); + } + } + + async function guardClaim( + guard, + depositer, + id, + timestamp, + wallets, + token, + recipient, + amount + ) { + // encode value + const structHash = + ethUtil.keccak256( + abi.rawEncode( + ['bytes4', 'bytes'], + [abi.methodID('claim', ['address', 'uint256', 'uint256', 'address', 'address', 'uint256', 'bytes[]' ]), + abi.rawEncode(['address', 'uint256', 'uint256', 'address', 'address', 'uint256'], + [depositer, 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(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); + expect(balanceRecipientAfter.sub(balanceRecipientBefore)).to.equal(amount); + } + + await registerToken( + nativeTokenAddress, + "ethereum", + "native token", + "eth", + 18, + 1000 + ); + + await expect(lockAndRemoteIssuing( + nativeTokenAddress, + 100, + "0.9", + false, + true + )).to.be.revertedWith("fee is not enough"); + + // success lock and remote xtoken + const nonce01 = await lockAndRemoteIssuing( + nativeTokenAddress, + 500, + "1.1", + false, + true + ); + // success burn and remote unlock + const nonce02 = await burnAndRemoteUnlock( + nativeTokenAddress, + 100, + "1.1", + false, + true + ); + + // test refund failed if the message has been successed + await expect(requestRemoteUnlockForIssuingFailure( + nativeTokenAddress, + 500, + nonce01, + "1.1", + true + )).to.be.revertedWith("!conflict"); + await expect(requestRemoteIssuingForUnlockFailure( + nativeTokenAddress, + 100, + nonce02, + "1.1", + true + )).to.be.revertedWith("!conflict"); + + // lock exceed daily limit + const nonce03 = await lockAndRemoteIssuing( + nativeTokenAddress, + 501, + "1.1", + false, + false + ); + // refund (when isssuing failed) + await requestRemoteUnlockForIssuingFailure( + nativeTokenAddress, + 501, + nonce03, + "1.1", + true + ); + // the params not right + // 1. amount + await requestRemoteUnlockForIssuingFailure( + nativeTokenAddress, + 500, + nonce03, + "1.1", + false + ); + // receiver + await requestRemoteUnlockForIssuingFailure( + nativeTokenAddress, + 501, + nonce03, + "1.1", + false + ); + // refund twice + await requestRemoteUnlockForIssuingFailure( + nativeTokenAddress, + 501, + nonce03, + "1.1", + false + ); + // burn failed + await mockBackingMsgline.setRecvFailed(); + const nonce04 = await burnAndRemoteUnlock( + nativeTokenAddress, + 100, + "1.1", + false, + false + ); + // invalid args + await requestRemoteIssuingForUnlockFailure( + nativeTokenAddress, + 101, + nonce04, + "1.1", + false + ); + // refund (when unlock failed) + await requestRemoteIssuingForUnlockFailure( + nativeTokenAddress, + 100, + nonce04, + "1.1", + true + ); + // refund twice + await requestRemoteIssuingForUnlockFailure( + nativeTokenAddress, + 100, + nonce04, + "1.1", + false + ); + + // using guard + await backing.updateGuard(backingGuard.address); + await issuing.updateGuard(issuingGuard.address); + + // lock -> issuing using guard + const nonce05 = await lockAndRemoteIssuing( + nativeTokenAddress, + 10, + "1.1", + true,//using guard + true + ); + const transferId = await backing.getTransferId(nonce05, backingChainId, issuingChainId, nativeTokenAddress, user01.address, user02.address, 10); + await guardClaim( + issuingGuard, + issuing.address, + transferId, + await getBlockTimestamp(), + [guards[0], guards[1]], + xTokens[nativeTokenAddress], + user02.address, + 10 + ); + // burn -> unlock using guard (native token) + const nonce06 = await burnAndRemoteUnlock( + nativeTokenAddress, + 20, + "1.1", + true, //using guard + true + ); + const transferId06 = await backing.getTransferId(nonce06, backingChainId, issuingChainId, nativeTokenAddress, user02.address, user01.address, 20); + await guardClaim( + backingGuard, + backing.address, + transferId06, + await getBlockTimestamp(), + [guards[0], guards[1]], + // native token must be claimed by wtoken + weth.address, + user01.address, + 20 + ); + // claim twice + await expect(guardClaim( + backingGuard, + backing.address, + transferId06, + await getBlockTimestamp(), + [guards[0], guards[1]], + weth.address, + user01.address, + 20 + )).to.be.revertedWith("Guard: Invalid id to claim"); + + // test message slashed + await mockIssuingMsgline.setNeverDelivered(); + // this message will be never delivered + const nonce07 = await lockAndRemoteIssuing( + nativeTokenAddress, + 10, + "1.1", + true, + false + ); + }); +}); +