diff --git a/helix-contract/address/xtoken-product.json b/helix-contract/address/xtoken-product.json index b80f0f1..cd6610a 100644 --- a/helix-contract/address/xtoken-product.json +++ b/helix-contract/address/xtoken-product.json @@ -31,9 +31,12 @@ "darwinia-dvm(to ethereum)": "0x092e19c46c9daab7824393f1cd9c22f5bea13560", "crab-dvm(to darwinia)": "0x004D0dE211BC148c3Ce696C51Cbc85BD421727E9" }, - "xtokenConvertor": { + "xRINGConvertor": { "ethereum": "0xc29dCb1F12a1618262eF9FBA673b77140adc02D6" }, + "xKTONConvertor": { + "ethereum": "0x3217F36AE34aCA2CE60d218af8F47d29101204a8" + }, "guard": { "ethereum": "0x4CA75992d2750BEC270731A72DfDedE6b9E71cC7", "darwinia-dvm": "0x4ca75992d2750bec270731a72dfdede6b9e71cc7" @@ -45,6 +48,7 @@ "xtoken": { "darwinia-dvm(xWCRAB)": "0x656567Eb75b765FC320783cc6EDd86bD854b2305", "crab-dvm(xWRING)": "0x273131F7CB50ac002BDd08cA721988731F7e1092", - "ethereum(xRING)": "0x81e32d4652Be82AE225DEdd1bD0bf3BCba8FEE07 " + "ethereum(xRING)": "0x81e32d4652Be82AE225DEdd1bD0bf3BCba8FEE07 ", + "ethereum(xKTON)": "0x35f15275041B53324dF461d5ccC952EE19D4a982" } } diff --git a/helix-contract/contracts/xtoken/v3/templates/XKTONConvertor.sol b/helix-contract/contracts/xtoken/v3/templates/XKTONConvertor.sol new file mode 100644 index 0000000..3c5510f --- /dev/null +++ b/helix-contract/contracts/xtoken/v3/templates/XKTONConvertor.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.17; + +import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "@zeppelin-solidity/contracts/utils/introspection/ERC165.sol"; +import "./interfaces/IXKTONLockBox.sol"; +import "../interfaces/IXTokenIssuing.sol"; +import "../interfaces/IXTokenCallback.sol"; + +contract XKTONConvertor is IXTokenCallback, IXTokenRollbackCallback, ERC165 { + IXKTONLockBox public lockBox; + IXTokenIssuing public xTokenIssuing; + address public immutable KTON; + address public immutable XKTON; + + mapping(uint256=>address) public senders; + + event IssueKTON(uint256 transferId, address recipient, uint256 amount); + event RollbackBurn(uint256 transferId, address originalSender, uint256 amount); + event BurnAndXUnlock(uint256 transferId, address sender, address recipient, uint256 amount); + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) { + return + interfaceId == type(IXTokenCallback).interfaceId || + interfaceId == type(IXTokenRollbackCallback).interfaceId || + super.supportsInterface(interfaceId); + } + + receive() external payable {} + + modifier onlyXTokenIssuing() { + require(address(xTokenIssuing) == msg.sender, "invalid sender"); + _; + } + + modifier onlyXTokenIssuingAuthorized() { + require(address(xTokenIssuing) == msg.sender || xTokenIssuing.guard() == msg.sender, "invalid sender"); + _; + } + + constructor(address _xKTON, address _kton, address _xTokenIssuing, address _lockBox) { + KTON = _kton; + XKTON = _xKTON; + lockBox = IXKTONLockBox(_lockBox); + xTokenIssuing = IXTokenIssuing(_xTokenIssuing); + IERC20(_kton).approve(_lockBox, type(uint256).max); + IERC20(_xKTON).approve(_lockBox, type(uint256).max); + IERC20(_xKTON).approve(_xTokenIssuing, type(uint256).max); + } + + function xTokenCallback( + uint256 _transferId, + address _xToken, + uint256 _amount, + bytes calldata extData + ) onlyXTokenIssuingAuthorized external { + address recipient = address(bytes20(extData)); + require(_xToken == XKTON, "invalid xtoken"); + lockBox.depositFor(recipient, _amount); + emit IssueKTON(_transferId, recipient, _amount); + } + + function xTokenRollbackCallback( + uint256 _transferId, + address _xToken, + uint256 _amount + ) onlyXTokenIssuing external { + require(_xToken == XKTON, "invalid xtoken"); + address originalSender = senders[_transferId]; + lockBox.depositFor(originalSender, _amount); + emit RollbackBurn(_transferId, originalSender, _amount); + } + + function burnAndXUnlock( + address _recipient, + address _rollbackAccount, + uint256 _amount, + uint256 _nonce, + bytes calldata _extData, + bytes memory _extParams + ) payable external { + IERC20(KTON).transferFrom(msg.sender, address(this), _amount); + lockBox.withdraw(_amount); + bytes32 transferId = xTokenIssuing.burnAndXUnlock{value: msg.value}(XKTON, _recipient, _rollbackAccount, _amount, _nonce, _extData, _extParams); + uint256 id = uint256(transferId); + senders[id] = msg.sender; + emit BurnAndXUnlock(id, msg.sender, _recipient, _amount); + } +} + diff --git a/helix-contract/contracts/xtoken/v3/templates/interfaces/IXKTONLockBox.sol b/helix-contract/contracts/xtoken/v3/templates/interfaces/IXKTONLockBox.sol new file mode 100644 index 0000000..99030dc --- /dev/null +++ b/helix-contract/contracts/xtoken/v3/templates/interfaces/IXKTONLockBox.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.17; + +interface IXKTONLockBox { + function depositFor(address to, uint256 amount) external; + function withdraw(uint256 amount) external; +} diff --git a/helix-contract/flatten/xtoken-v3/XKTONConvertor.sol b/helix-contract/flatten/xtoken-v3/XKTONConvertor.sol new file mode 100644 index 0000000..c81c267 --- /dev/null +++ b/helix-contract/flatten/xtoken-v3/XKTONConvertor.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: MIT + +/** + * .----------------. .----------------. .----------------. .----------------. .----------------. + * | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | + * | | ____ ____ | || | _________ | || | _____ | || | _____ | || | ____ ____ | | + * | | |_ || _| | || | |_ ___ | | || | |_ _| | || | |_ _| | || | |_ _||_ _| | | + * | | | |__| | | || | | |_ \_| | || | | | | || | | | | || | \ \ / / | | + * | | | __ | | || | | _| _ | || | | | _ | || | | | | || | > `' < | | + * | | _| | | |_ | || | _| |___/ | | || | _| |__/ | | || | _| |_ | || | _/ /'`\ \_ | | + * | | |____||____| | || | |_________| | || | |________| | || | |_____| | || | |____||____| | | + * | | | || | | || | | || | | || | | | + * | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | + * '----------------' '----------------' '----------------' '----------------' '----------------' ' + * + * + * 2/6/2025 + **/ + +pragma solidity ^0.8.17; + +// File contracts/xtoken/v3/interfaces/IXTokenIssuing.sol +// License-Identifier: MIT + +interface IXTokenIssuing { + function burnAndXUnlock( + address xToken, + address recipient, + address rollbackAccount, + uint256 amount, + uint256 nonce, + bytes calldata extData, + bytes memory extParams + ) external payable returns(bytes32); + + function rollbackBurnAndXUnlock( + uint256 originalChainId, + address originalToken, + address originalSender, + address recipient, + address rollbackAccount, + uint256 amount, + uint256 nonce + ) external; + + function issue( + uint256 remoteChainId, + address originalToken, + address originalSender, + address recipient, + address rollbackAccount, + uint256 amount, + uint256 nonce, + bytes calldata extData + ) external; + + function guard() external returns(address); +} + +// File contracts/xtoken/v3/interfaces/IXTokenCallback.sol +// License-Identifier: MIT + +interface IXTokenCallback { + function xTokenCallback( + uint256 transferId, + address xToken, + uint256 amount, + bytes calldata extData + ) external; +} + +interface IXTokenRollbackCallback { + function xTokenRollbackCallback( + uint256 transferId, + address token, + uint256 amount + ) external; +} + +// File contracts/xtoken/v3/templates/interfaces/IXKTONLockBox.sol +// License-Identifier: MIT + +interface IXKTONLockBox { + function depositFor(address to, uint256 amount) external; + function withdraw(uint256 amount) external; +} + +// File @zeppelin-solidity/contracts/utils/introspection/IERC165.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) + + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +// File @zeppelin-solidity/contracts/utils/introspection/ERC165.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) + + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + * + * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. + */ +abstract contract ERC165 is IERC165 { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} + +// 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/xtoken/v3/templates/XKTONConvertor.sol +// License-Identifier: Apache-2.0 + + + + + + +contract XKTONConvertor is IXTokenCallback, IXTokenRollbackCallback, ERC165 { + IXKTONLockBox public lockBox; + IXTokenIssuing public xTokenIssuing; + address public immutable KTON; + address public immutable XKTON; + + mapping(uint256=>address) public senders; + + event IssueKTON(uint256 transferId, address recipient, uint256 amount); + event RollbackBurn(uint256 transferId, address originalSender, uint256 amount); + event BurnAndXUnlock(uint256 transferId, address sender, address recipient, uint256 amount); + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) { + return + interfaceId == type(IXTokenCallback).interfaceId || + interfaceId == type(IXTokenRollbackCallback).interfaceId || + super.supportsInterface(interfaceId); + } + + receive() external payable {} + + modifier onlyXTokenIssuing() { + require(address(xTokenIssuing) == msg.sender, "invalid sender"); + _; + } + + modifier onlyXTokenIssuingAuthorized() { + require(address(xTokenIssuing) == msg.sender || xTokenIssuing.guard() == msg.sender, "invalid sender"); + _; + } + + constructor(address _xKTON, address _kton, address _xTokenIssuing, address _lockBox) { + KTON = _kton; + XKTON = _xKTON; + lockBox = IXKTONLockBox(_lockBox); + xTokenIssuing = IXTokenIssuing(_xTokenIssuing); + IERC20(_kton).approve(_lockBox, type(uint256).max); + IERC20(_xKTON).approve(_lockBox, type(uint256).max); + IERC20(_xKTON).approve(_xTokenIssuing, type(uint256).max); + } + + function xTokenCallback( + uint256 _transferId, + address _xToken, + uint256 _amount, + bytes calldata extData + ) onlyXTokenIssuingAuthorized external { + address recipient = address(bytes20(extData)); + require(_xToken == XKTON, "invalid xtoken"); + lockBox.depositFor(recipient, _amount); + emit IssueKTON(_transferId, recipient, _amount); + } + + function xTokenRollbackCallback( + uint256 _transferId, + address _xToken, + uint256 _amount + ) onlyXTokenIssuing external { + require(_xToken == XKTON, "invalid xtoken"); + address originalSender = senders[_transferId]; + lockBox.depositFor(originalSender, _amount); + emit RollbackBurn(_transferId, originalSender, _amount); + } + + function burnAndXUnlock( + address _recipient, + address _rollbackAccount, + uint256 _amount, + uint256 _nonce, + bytes calldata _extData, + bytes memory _extParams + ) payable external { + IERC20(KTON).transferFrom(msg.sender, address(this), _amount); + lockBox.withdraw(_amount); + bytes32 transferId = xTokenIssuing.burnAndXUnlock{value: msg.value}(XKTON, _recipient, _rollbackAccount, _amount, _nonce, _extData, _extParams); + uint256 id = uint256(transferId); + senders[id] = msg.sender; + emit BurnAndXUnlock(id, msg.sender, _recipient, _amount); + } +} \ No newline at end of file