diff --git a/.gitmodules b/.gitmodules index a6260fb..899b5ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "lib/create3-deploy"] path = lib/create3-deploy url = https://github.com/darwinia-network/create3-deploy +[submodule "lib/xapi-contracts"] + path = lib/xapi-contracts + url = https://github.com/ringecosystem/xapi-contracts +[submodule "lib/chainlink"] + path = lib/chainlink + url = https://github.com/smartcontractkit/chainlink diff --git a/lib/chainlink b/lib/chainlink new file mode 160000 index 0000000..fb7d6e8 --- /dev/null +++ b/lib/chainlink @@ -0,0 +1 @@ +Subproject commit fb7d6e88ea3471909a4f9aa29992ec080bba9057 diff --git a/lib/xapi-contracts b/lib/xapi-contracts new file mode 160000 index 0000000..92466ba --- /dev/null +++ b/lib/xapi-contracts @@ -0,0 +1 @@ +Subproject commit 92466ba49ce1bf1c0b8ef4f846d20923cd70183c diff --git a/remappings.txt b/remappings.txt index c7d2aae..25fb4ec 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,6 @@ forge-std/=lib/forge-std/src/ ds-test/=lib/forge-std/lib/ds-test/src/ create3-deploy/=lib/create3-deploy/ +xapi/=lib/xapi-contracts/xapi/ +xapi-consumer/=lib/xapi-contracts/xapi-consumer/contracts/ +@chainlink/=lib/chainlink/ diff --git a/src/eco/XAPIOracle.sol b/src/eco/XAPIOracle.sol new file mode 100644 index 0000000..111ea0f --- /dev/null +++ b/src/eco/XAPIOracle.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../Verifier.sol"; +import "../interfaces/IORMP.sol"; +import "xapi-consumer/interfaces/IXAPIConsumer.sol"; +import "xapi/contracts/lib/XAPIBuilder.sol"; + +contract XAPIOracle is Verifier, IXAPIConsumer { + using XAPIBuilder for XAPIBuilder.Request; + + event SetFee(uint256 indexed chainId, uint256 fee); + event SetApproved(address operator, bool approve); + event Withdrawal(address indexed to, uint256 amt); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event XAPIRequestMade(uint256 indexed requestId, XAPIBuilder.Request requestData); + event XAPIConsumeResult(uint256 indexed requestId, bytes responseData, uint16 errorCode); + event XAPIConsumeError(uint256 indexed requestId, uint16 errorCode); + + address public immutable PROTOCOL; + address public immutable XAPI; + address public immutable EXAGGREGATOR; + + address public owner; + // chainId => price + mapping(uint256 => uint256) public feeOf; + // operator => isApproved + mapping(address => bool) public approvedOf; + + struct DataSource { + string name; + string url; + string method; + string resultPath; + } + + uint256 requestId; + + modifier onlyOwner() { + require(msg.sender == owner, "!owner"); + _; + } + + modifier onlyXAPI() { + require(msg.sender == XAPI, "!xapi"); + _; + } + + modifier onlyApproved() { + require(isApproved(msg.sender), "!approve"); + _; + } + + constructor(address dao, address ormp, address xapi, address exagg) { + PROTOCOL = ormp; + owner = dao; + XAPI = xapi; + EXAGGREGATOR = exagg; + } + + receive() external payable {} + + function version() public pure returns (string memory) { + return "1.0.0"; + } + + function _buildRequest(uint256 chainId, address channel, uint256 msgIndex) + internal + view + returns (XAPIBuilder.Request memory) + { + XAPIBuilder.Request memory requestData; + requestData._initialize(EXAGGREGATOR, this.xapiCallback.selector); + requestData._addParamUint("_dataSources", chainId); + requestData._startNestedParam("*"); + { + requestData._startNestedParam("variables"); + { + requestData._addParamUint("chainId", chainId); + requestData._addParam("channel", toHexString(channel)); + requestData._addParamUint("msgIndex", msgIndex); + } + requestData._endNestedParam(); + } + requestData._endNestedParam(); + return requestData; + } + + /// @dev Only could be called by approved address. + /// @param chainId The request source chain id. + /// @param channel The request message channel. + /// @param msgIndex The request message index. + function makeRequestForMessageHash(uint256 chainId, address channel, uint256 msgIndex) + external + payable + onlyApproved + { + XAPIBuilder.Request memory requestData = _buildRequest(chainId, channel, msgIndex); + uint256 fee_ = IXAPI(XAPI).fee(EXAGGREGATOR); + require(msg.value == fee_, "!fee"); + requestId = IXAPI(XAPI).makeRequest{value: fee_}(requestData); + emit XAPIRequestMade(requestId, requestData); + } + + function xapiCallback(uint256 requestId_, ResponseData memory response) external onlyXAPI { + require(requestId_ == requestId, "!requestId"); + if (response.errorCode != 0) { + (uint256 chainId, address channel, uint256 msgIndex, bytes32 msgHash) = + abi.decode(response.result, (uint256, address, uint256, bytes32)); + IORMP(PROTOCOL).importHash(chainId, channel, msgIndex, msgHash); + emit XAPIConsumeResult(requestId, response.result, response.errorCode); + } else { + emit XAPIConsumeError(requestId, response.errorCode); + } + } + + function hashOf(uint256 chainId, address channel, uint256 msgIndex) public view override returns (bytes32) { + return IORMP(PROTOCOL).hashLookup(address(this), keccak256(abi.encode(chainId, channel, msgIndex))); + } + + function changeOwner(address newOwner) external onlyOwner { + address oldOwner = owner; + owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } + + function setApproved(address operator, bool approve) external onlyOwner { + approvedOf[operator] = approve; + emit SetApproved(operator, approve); + } + + function isApproved(address operator) public view returns (bool) { + return approvedOf[operator]; + } + + function withdraw(address to, uint256 amount) external onlyApproved { + (bool success,) = to.call{value: amount}(""); + require(success, "!withdraw"); + emit Withdrawal(to, amount); + } + + function setFee(uint256 chainId, uint256 fee_) external onlyApproved { + feeOf[chainId] = fee_; + emit SetFee(chainId, fee_); + } + + function fee(uint256 toChainId, address /*ua*/ ) public view returns (uint256) { + uint256 f = feeOf[toChainId]; + require(f != 0, "!fee"); + return f; + } + + // Inspired from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.6/contracts/utils/Strings.sol + bytes16 private constant _SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + 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] = _SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } +}