├── .gitignore ├── README.md ├── core ├── README.md ├── evm │ ├── .gitignore │ ├── README.md │ ├── config.json │ ├── contracts │ │ ├── DemoApp.sol │ │ ├── Omnic.sol │ │ ├── OmnicBase.sol │ │ ├── OmnicFeeManager.sol │ │ ├── interfaces │ │ │ ├── IOmnic.sol │ │ │ ├── IOmnicFeeManager.sol │ │ │ └── IOmnicReciver.sol │ │ ├── libs │ │ │ ├── Merkle.sol │ │ │ ├── Queue.sol │ │ │ └── Types.sol │ │ ├── upgrade │ │ │ ├── UpgradeBeacon.sol │ │ │ ├── UpgradeBeaconController.sol │ │ │ └── UpgradeBeaconProxy.sol │ │ └── utils │ │ │ ├── QueueManager.sol │ │ │ └── Utils.sol │ ├── hardhat.config.ts │ ├── package-lock.json │ ├── package.json │ ├── scripts │ │ ├── deploy-demo.ts │ │ ├── deploy-feemanager.ts │ │ ├── deploy-impl.ts │ │ ├── deploy-proxy.ts │ │ ├── helpers.ts │ │ ├── send-msg.ts │ │ ├── set-demo-omnic-addr.ts │ │ ├── set-fee-whitelist.ts │ │ └── upgrade-impl.ts │ ├── test │ │ └── Omnic.ts │ ├── tsconfig.json │ └── yarn.lock └── ic │ ├── .gitignore │ ├── Cargo.toml │ ├── accumulator │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── error.rs │ │ ├── full.rs │ │ ├── lib.rs │ │ ├── light.rs │ │ ├── proof.rs │ │ ├── tree.rs │ │ ├── utils.rs │ │ └── wasm.rs │ ├── canister_ids.json │ ├── dfx.json │ ├── readme.md │ ├── src │ ├── call.rs │ ├── canisters │ │ ├── demo.did │ │ ├── demo.rs │ │ ├── gateway.did │ │ ├── gateway.old.did │ │ ├── gateway.rs │ │ ├── proxy.did │ │ ├── proxy.old.did │ │ └── proxy.rs │ ├── chain_state.rs │ ├── chains │ │ ├── evm.rs │ │ ├── mod.rs │ │ └── omnic.abi │ ├── config.rs │ ├── consts.rs │ ├── error.rs │ ├── lib.rs │ ├── state.rs │ ├── traits │ │ ├── chain.rs │ │ └── mod.rs │ ├── types │ │ ├── message.rs │ │ └── mod.rs │ └── utils.rs │ └── start.sh ├── examples └── placeholder ├── pics └── arch.jpg └── spec.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | cache/ 3 | artifacts/ 4 | coverage/ 5 | coverage.json 6 | .env 7 | src.ts/ 8 | dist/ 9 | typechain-types 10 | Cargo.lock 11 | target/ 12 | .dfx 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Omnic Crosschain Messaging Protocol 2 | 3 | Omnic is a crosschain messaging protocol, powered by the Internet Computer's threshold ECDSA and outbound http call features, we can build programmability into the message passing process. 4 | 5 | Currently support EVM compatible chains. 6 | 7 | ## 1. Architecture 8 | 9 | ![](./pics/arch.jpg) 10 | 11 | Core components: 12 | 13 | * Gateway contracts living on EVM chains: 14 | * receive crosschain message requests from application contracts on local chain, messages are organized into a merkle tree; 15 | * receive crosschain messages from external chains and notify recipient contracts on the local chain. 16 | * Omnic proxy canister living on the IC: 17 | * verify messages from offchain relayer(using the merkle roots fetched by gateway canisters), forward crosschain messages: 18 | * if message destination is IC, proxy canister notify the recipient canister on IC 19 | * if message destination is another chain, proxy canister create and sign a tx to the Omnic gateway contract on the destination chain, gateway contract will then notify the recipient contract 20 | * Omnic gateway canisters living on the IC: 21 | * each chain has a dedicated gateway canister, controlled by the Omnic proxy canister 22 | * responsible for periodically fetching message merkle roots from external chains, for later message verification use 23 | 24 | * Omnic offchain relayer: 25 | * fetch crosschain messages from supported chains; fetch known merkle roots from Omnic proxy canister 26 | * generate a merkle proof for each message then send the proof along with the message to the proxy canister 27 | * Why offchain: Proof generation can be computational intense & fetch messages via http calls in canister is expensive. If cost is low, relayer can also be a canister onchain. 28 | * Applications 29 | * Application canisters living on IC 30 | * Application contracts living on external EVM chains 31 | 32 | 33 | 34 | ## 2. TODOs 35 | 36 | * Fix todos in code 37 | * Add more chains 38 | * Add examples 39 | * ... 40 | 41 | 42 | 43 | ## 3. Deployment 44 | 45 | Omnic test version is live on IC and 2 evm testnets: 46 | 47 | Omnic proxy canister on IC mainnet: y3lks-laaaa-aaaam-aat7q-cai 48 | 49 | Omnic gateway contract on EVM chains: 50 | 51 | * Goerli: 0xc7D718dC3C9248c91813A98dCbFEC6CF57619520 52 | * Mumbai: 0x2F711bEbA7a30242f4ba24544eA3869815c41413 53 | 54 | 55 | 56 | ## 4. Code 57 | 58 | 1. [core](./core): Omnic message passing protocol core implementation 59 | 2. [examples](./examples): Example apps built on Omnic messaging protocol 60 | 3. [omnic-bridge](https://github.com/rocklabs-io/omnic-bridge): A demo bridge app based on the Omnic messaging protocol 61 | 4. [omnic-relayer](https://github.com/rocklabs-io/omnic-relayer): Omnic offchain relayer implementation 62 | 63 | 64 | 65 | **Note: This project is unaudited and still in very early experimental stage!** 66 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | ## Omnic Crosschain Messaging Protocol 2 | 3 | * [evm](./evm): Solidity contracts on EVM chains 4 | * [ic](./ic): Omnic proxy canister on the IC 5 | 6 | 7 | 8 | Ref: https://github.com/nomad-xyz/monorepo 9 | -------------------------------------------------------------------------------- /core/evm/.gitignore: -------------------------------------------------------------------------------- 1 | src/ 2 | node_modules 3 | -------------------------------------------------------------------------------- /core/evm/README.md: -------------------------------------------------------------------------------- 1 | ## Omnic EVM side contracts 2 | 3 | 4 | ### Build & deploy 5 | install deps: 6 | ``` 7 | yarn install 8 | ``` 9 | 10 | compile: 11 | ``` 12 | npx hardhat compile 13 | ``` 14 | 15 | put your infura key & test private key to .env: 16 | ``` 17 | TEST_PRIV_KEY=0x... 18 | TEST_ADDR=0x.. 19 | INFURA_API_KEY=... 20 | ETHERSCAN_API_KEY=... 21 | ``` 22 | 23 | deployment process: 24 | 1. deploy-impl.ts 25 | 2. deploy-proxy.ts 26 | 27 | upgrade process: 28 | 1. upgrade-impl.ts -------------------------------------------------------------------------------- /core/evm/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "OmnicCanisterId": "rrkah-fqaaa-aaaaa-aaaaq-cai", 3 | "OmnicCanisterAddr": "0x8b6da33954f9a2f9cb9f97a0449f5200be8b3905", 4 | "ChainIds": { 5 | "ic": 0, 6 | "goerli": 5, 7 | "mumbai": 80001, 8 | "arbi_goerli": 421613, 9 | "op_goerli": 430 10 | }, 11 | "networks": { 12 | "goerli": { 13 | "UpgradeBeaconController": "0x45C5FEe389Fe5D4405d710057a79678102524EC7", 14 | "UpgradeBeacon": "0x7679f2F18d456D1e1119Cf759F1C08bc605938ba", 15 | "UpgradeBeaconProxy": "0xc7D718dC3C9248c91813A98dCbFEC6CF57619520", 16 | "Implementation": "0x8C43E5977Fc5152f7eFA0283EdDECdE6764D2395", 17 | "Demo": "", 18 | "Bridge": "0xf3A2370F59703B0948b6454a1b6446cfa3C186eF", 19 | "USDT": "0xf972a2E180aB1ee74161b28d173b83987dEF1Af6", 20 | "OmnicFeeManager": "0xcDb431Ce81268767DD05416527ae2076c2b8E693" 21 | }, 22 | "localhost": { 23 | "UpgradeBeaconController": "0x5b1869D9A4C187F2EAa108f3062412ecf0526b24", 24 | "UpgradeBeacon": "0xCfEB869F69431e42cdB54A4F4f105C19C080A601", 25 | "UpgradeBeaconProxy": "0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B", 26 | "Implementation": "0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab", 27 | "Demo": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" 28 | }, 29 | "mumbai": { 30 | "UpgradeBeaconController": "0xd03dEb5d8d0a661Fd956DC484Db1F98C791Ab5d4", 31 | "UpgradeBeacon": "0xbddF29608215D04Ce2631EA9dd248B15CE53d290", 32 | "UpgradeBeaconProxy": "0x2F711bEbA7a30242f4ba24544eA3869815c41413", 33 | "Implementation": "0xf972a2E180aB1ee74161b28d173b83987dEF1Af6", 34 | "Demo": "", 35 | "Bridge": "0xD52F83162324422547489699cEF8479B4F548E85", 36 | "USDT": "0x4a22D4DEdbF85Ee9F1fbC4aBfD1349D56C905fDC", 37 | "OmnicFeeManager": "0xb297C8A1af6981C992De84272BBaBFde3EB1F458" 38 | }, 39 | "arbi_goerli": { 40 | "UpgradeBeaconController": "0x0fA355bEEA41d190CAE64F24a58F70ff2912D7df", 41 | "UpgradeBeacon": "0x32459C1d39bFdd95A8b7F167FBF2a4ab5A78A41E", 42 | "UpgradeBeaconProxy": "0x0AA014fB917b93f17E2B98A7ee15D24fE9aD40C8", 43 | "Implementation": "0x430365a65eAC8626AFf329e5B3a549f6C13D0c26", 44 | "Demo": "" 45 | }, 46 | "op_goerli": { 47 | "UpgradeBeaconController": "0x0fA355bEEA41d190CAE64F24a58F70ff2912D7df", 48 | "UpgradeBeacon": "0x32459C1d39bFdd95A8b7F167FBF2a4ab5A78A41E", 49 | "UpgradeBeaconProxy": "0x0AA014fB917b93f17E2B98A7ee15D24fE9aD40C8", 50 | "Implementation": "0x430365a65eAC8626AFf329e5B3a549f6C13D0c26", 51 | "Demo": "" 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /core/evm/contracts/DemoApp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | //internal 5 | import {QueueLib} from "./libs/Queue.sol"; 6 | import {QueueManager} from "./utils/QueueManager.sol"; 7 | import {MerkleLib} from "./libs/Merkle.sol"; 8 | import {Types} from "./libs/Types.sol"; 9 | import {TypeCasts} from "./utils/Utils.sol"; 10 | import {IOmnicReciver} from "./interfaces/IOmnicReciver.sol"; 11 | 12 | import {Omnic} from "./Omnic.sol"; 13 | 14 | contract DemoApp { 15 | 16 | address owner; 17 | address omnicAddr; 18 | 19 | // ============ Events ============ 20 | event ReceivedMessage( 21 | uint32 indexed srcChainId, 22 | bytes32 srcSender, 23 | uint32 nonce, 24 | bytes payload 25 | ); 26 | 27 | modifier onlyOmnicContract() { 28 | require(msg.sender == omnicAddr, "!omnicContract"); 29 | _; 30 | } 31 | 32 | modifier onlyOwner() { 33 | require(msg.sender == owner, "!owner"); 34 | _; 35 | } 36 | 37 | constructor(address omnic) { 38 | owner = msg.sender; 39 | omnicAddr = omnic; 40 | } 41 | 42 | function setOmnicContractAddr(address _newAddr) 43 | public 44 | onlyOwner 45 | { 46 | omnicAddr = _newAddr; 47 | } 48 | 49 | function sendMessage( 50 | uint32 _dstChainId, 51 | bytes32 _recipientAddress, 52 | bytes memory _payload 53 | ) public payable { 54 | // send message to dst chain, call omnic contract 55 | Omnic(omnicAddr).sendMessage( 56 | _dstChainId, 57 | _recipientAddress, 58 | _payload, 59 | payable(msg.sender), 60 | address(this) 61 | ); 62 | } 63 | 64 | // only omnic canister can call this func 65 | function handleMessage( 66 | uint32 srcChainId, 67 | bytes32 srcSender, 68 | uint32 nonce, 69 | bytes memory payload 70 | ) 71 | public 72 | onlyOmnicContract 73 | returns (bool success) 74 | { 75 | // emit event when received message 76 | emit ReceivedMessage(srcChainId, srcSender, nonce, payload); 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /core/evm/contracts/Omnic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 7 | 8 | //internal 9 | import {QueueLib} from "./libs/Queue.sol"; 10 | import {QueueManager} from "./utils/QueueManager.sol"; 11 | import {MerkleLib} from "./libs/Merkle.sol"; 12 | import {Types} from "./libs/Types.sol"; 13 | import {TypeCasts} from "./utils/Utils.sol"; 14 | import {IOmnicReciver} from "./interfaces/IOmnicReciver.sol"; 15 | import {IOmnicFeeManager} from "./interfaces/IOmnicFeeManager.sol"; 16 | import {OmnicBase} from "./OmnicBase.sol"; 17 | 18 | 19 | 20 | // Omnic Crosschain message passing protocol core contract 21 | contract Omnic is QueueManager, OmnicBase { 22 | using QueueLib for QueueLib.Queue; 23 | using MerkleLib for MerkleLib.Tree; 24 | using SafeMath for uint256; 25 | using SafeERC20 for IERC20; 26 | MerkleLib.Tree public tree; 27 | 28 | // re-entrancy 29 | uint8 internal entered; 30 | 31 | // ic canister which is responsible for message management, verification and update 32 | address public omnicProxyCanisterAddr; 33 | 34 | // Maximum bytes per message = 2 KiB 35 | // (somewhat arbitrarily set to begin) 36 | uint256 public constant MAX_MESSAGE_BODY_BYTES = 2 * 2**10; 37 | 38 | // chainId => next available nonce for the chainId 39 | mapping(uint32 => uint32) public nonces; 40 | 41 | // Token and Contracts 42 | IERC20 public erc20FeeToken; // choose a ERC20 token as fee token specified by omnic owner 43 | IOmnicFeeManager public omnicFeeManager; 44 | 45 | uint256 public nativeFees; 46 | // ERC20 token => fee amount 47 | mapping(address => uint256) public erc20Fees; // record different ERC20 fee amount 48 | 49 | mapping(address => bool) public whitelisted; // whitelisted addresses can send msg for free 50 | 51 | // gap for upgrade safety 52 | uint256[49] private __GAP; 53 | 54 | // ============ modifiers ============ 55 | 56 | modifier onlyProxyCanister() { 57 | require(msg.sender == omnicProxyCanisterAddr, "!proxyCanisterAddress"); 58 | _; 59 | } 60 | 61 | // ============ Events ============ 62 | event SendMessage( 63 | bytes32 indexed messageHash, 64 | uint256 indexed leafIndex, 65 | uint32 indexed dstChainId, 66 | uint32 nonce, 67 | bytes message 68 | ); 69 | 70 | event ProcessMessage( 71 | bytes32 indexed messageHash, 72 | bytes indexed returnData, 73 | bool success 74 | ); 75 | 76 | event UpdateProxyCanister( 77 | address oldProxyCanisterAddr, 78 | address newProxyCanisterAddr 79 | ); 80 | 81 | // ============== Start =============== 82 | constructor() { 83 | entered = 1; 84 | } 85 | 86 | function initialize(address proxyCanisterAddr, address feeManagerAddr) public initializer { 87 | __QueueManager_initialize(); 88 | __OmnicBase_initialize(); 89 | entered = 1; 90 | omnicProxyCanisterAddr = proxyCanisterAddr; 91 | omnicFeeManager = IOmnicFeeManager(feeManagerAddr); 92 | erc20FeeToken = IERC20(omnicFeeManager.getERC20FeeToken()); 93 | } 94 | 95 | function sendMessage( 96 | uint32 _dstChainId, 97 | bytes32 _recipientAddress, 98 | bytes memory _payload, 99 | address payable _refundAddress, 100 | address _erc20PaymentAddress 101 | ) public payable { 102 | require(_payload.length <= MAX_MESSAGE_BODY_BYTES, "msg too long"); 103 | // compute all the fees 104 | uint256 nativeProtocolFee = _handleProtocolFee( 105 | msg.sender, 106 | _erc20PaymentAddress, 107 | _payload.length 108 | ); 109 | 110 | // assert whether the user has enough native token amount 111 | require( 112 | nativeProtocolFee <= msg.value, 113 | "Omnic: not enough value for fees" 114 | ); 115 | // refund if sent too much 116 | uint256 amount = msg.value.sub(nativeProtocolFee); 117 | if (amount > 0) { 118 | (bool success, ) = _refundAddress.call{value: amount}(""); 119 | require(success, "Omnic: failed to refund"); 120 | } 121 | 122 | // get the next nonce for the destination domain, then increment it 123 | uint32 _nonce = nonces[_dstChainId]; 124 | nonces[_dstChainId] = _nonce + 1; 125 | 126 | bytes memory _message = Types.formatMessage( 127 | chainId, 128 | TypeCasts.addressToBytes32(msg.sender), 129 | _nonce, 130 | _dstChainId, 131 | _recipientAddress, 132 | _payload 133 | ); 134 | bytes32 _messageHash = keccak256(_message); 135 | tree.insert(_messageHash); 136 | // enqueue the new Merkle root after inserting the message 137 | queue.enqueue(tree.root()); 138 | 139 | emit SendMessage( 140 | _messageHash, 141 | tree.count - 1, 142 | chainId, 143 | _nonce, 144 | _message 145 | ); 146 | } 147 | 148 | // fee free function for whitelisted contracts 149 | // for omnic bridge's CreatePool, Add/RemoveLiquidity operations 150 | function sendMessageFree( 151 | uint32 _dstChainId, 152 | bytes32 _recipientAddress, 153 | bytes memory _payload 154 | ) public { 155 | require(whitelisted[msg.sender], "not whitelisted caller"); 156 | require(_payload.length <= MAX_MESSAGE_BODY_BYTES, "msg too long"); 157 | // get the next nonce for the destination domain, then increment it 158 | uint32 _nonce = nonces[_dstChainId]; 159 | nonces[_dstChainId] = _nonce + 1; 160 | 161 | bytes memory _message = Types.formatMessage( 162 | chainId, 163 | TypeCasts.addressToBytes32(msg.sender), 164 | _nonce, 165 | _dstChainId, 166 | _recipientAddress, 167 | _payload 168 | ); 169 | bytes32 _messageHash = keccak256(_message); 170 | tree.insert(_messageHash); 171 | // enqueue the new Merkle root after inserting the message 172 | queue.enqueue(tree.root()); 173 | 174 | emit SendMessage( 175 | _messageHash, 176 | tree.count - 1, 177 | chainId, 178 | _nonce, 179 | _message 180 | ); 181 | } 182 | 183 | // only omnic canister can call this func 184 | function processMessage(bytes memory _message) 185 | public 186 | onlyProxyCanister 187 | returns (bool success) 188 | { 189 | // decode message 190 | ( 191 | uint32 _srcChainId, 192 | bytes32 _srcSenderAddress, 193 | uint32 _nonce, 194 | uint32 _dstChainId, 195 | bytes32 _recipientAddress, 196 | bytes memory _payload 197 | ) = abi.decode( 198 | _message, 199 | (uint32, bytes32, uint32, uint32, bytes32, bytes) 200 | ); 201 | bytes32 _messageHash = keccak256(_message); 202 | require(_dstChainId == chainId, "!destination"); 203 | // check re-entrancy guard 204 | require(entered == 1, "!reentrant"); 205 | entered = 0; 206 | 207 | // call handle function 208 | IOmnicReciver(TypeCasts.bytes32ToAddress(_recipientAddress)) 209 | .handleMessage(_srcChainId, _srcSenderAddress, _nonce, _payload); 210 | // emit process results 211 | emit ProcessMessage(_messageHash, "", true); 212 | // reset re-entrancy guard 213 | entered = 1; 214 | // return true 215 | return true; 216 | } 217 | 218 | function _handleProtocolFee( 219 | address _srcSenderAddress, 220 | address _erc20PaymentAddress, 221 | uint256 _msgLength 222 | ) internal returns (uint256 protocolNativeFee) { 223 | // asset fee pay with native token or ERC20 token 224 | bool payInNative = _erc20PaymentAddress == address(0x0); 225 | uint256 protocolFee = omnicFeeManager.getFees(!payInNative, _msgLength); 226 | 227 | if (protocolFee > 0) { 228 | if (payInNative) { 229 | nativeFees = nativeFees.add(protocolFee); 230 | protocolNativeFee = protocolFee; 231 | } else { 232 | require( 233 | _erc20PaymentAddress == _srcSenderAddress || 234 | _erc20PaymentAddress == tx.origin, 235 | "Omnic: must be paid by sender or origin" 236 | ); 237 | if(protocolFee == 0) { 238 | return protocolNativeFee; 239 | } 240 | // transfer the fee with ERC20 token 241 | erc20FeeToken.safeTransferFrom( 242 | _erc20PaymentAddress, 243 | address(this), 244 | protocolFee 245 | ); 246 | 247 | address erc20TokenAddr = address(erc20FeeToken); 248 | erc20Fees[erc20TokenAddr] = erc20Fees[erc20TokenAddr].add( 249 | protocolFee 250 | ); 251 | } 252 | } 253 | } 254 | 255 | function setWhitelist(address addr, bool value) external onlyOwner { 256 | whitelisted[addr] = value; 257 | } 258 | 259 | // withdraw ERC20 token function 260 | function withdrawERC20Fee( 261 | address _to, 262 | address _erc20FeeToken, 263 | uint256 _amount 264 | ) external onlyOwner { 265 | require( 266 | erc20Fees[_erc20FeeToken] >= _amount, 267 | "Omnic: insufficient ERC20 amount" 268 | ); 269 | erc20Fees[_erc20FeeToken] = erc20Fees[_erc20FeeToken].sub(_amount); 270 | IERC20(_erc20FeeToken).safeTransfer(_to, _amount); 271 | } 272 | 273 | // withdraw native token function. 274 | function withdrawNativeFee(address payable _to, uint256 _amount) 275 | external 276 | onlyOwner 277 | { 278 | require(_to != address(0x0), "Omnic: _to cannot be zero address"); 279 | nativeFees = nativeFees.sub(_amount); 280 | 281 | (bool success, ) = _to.call{value: _amount}(""); 282 | require(success, "Omnic: withdraw failed"); 283 | } 284 | 285 | // ============ onlyOwner Set Functions ============ 286 | function setOmnicCanisterAddr(address _newProxyCanisterAddr) 287 | public 288 | onlyOwner 289 | { 290 | address _oldProxyCanisterAddr = omnicProxyCanisterAddr; 291 | omnicProxyCanisterAddr = _newProxyCanisterAddr; 292 | emit UpdateProxyCanister(_oldProxyCanisterAddr, _newProxyCanisterAddr); 293 | } 294 | 295 | // ============ Public Functions ============ 296 | function getLatestRoot() public view returns (bytes32) { 297 | require(queue.length() != 0, "no item in queue"); 298 | return queue.lastItem(); 299 | } 300 | 301 | function rootExists(bytes32 _root) public view returns (bool) { 302 | return queue.contains(_root); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /core/evm/contracts/OmnicBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | //internal 5 | import {QueueLib} from "./libs/Queue.sol"; 6 | import {QueueManager} from "./utils/QueueManager.sol"; 7 | import {MerkleLib} from "./libs/Merkle.sol"; 8 | import {Types} from "./libs/Types.sol"; 9 | import {TypeCasts} from "./utils/Utils.sol"; 10 | import {IOmnicReciver} from "./interfaces/IOmnicReciver.sol"; 11 | 12 | //external 13 | import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 14 | import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 15 | 16 | 17 | contract OmnicBase is Initializable, OwnableUpgradeable { 18 | 19 | // ============ Constants ============ 20 | uint32 public chainId; 21 | 22 | // gap for upgrade safety 23 | uint256[49] private __GAP; 24 | 25 | 26 | // ============== Start =============== 27 | constructor() { 28 | chainId = uint32(block.chainid); 29 | } 30 | 31 | function __OmnicBase_initialize() internal initializer { 32 | __Ownable_init(); 33 | chainId = uint32(block.chainid); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/evm/contracts/OmnicFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | // external 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 8 | 9 | // internal 10 | import "./interfaces/IOmnicFeeManager.sol"; 11 | 12 | contract OmnicFeeManager is IOmnicFeeManager, Ownable { 13 | using SafeMath for uint256; 14 | 15 | uint256 public nativeFeeBase; 16 | uint256 public nativeFeeForPerByte; 17 | 18 | address public erc20FeeToken; 19 | uint256 public erc20FeeBase; 20 | uint256 public erc20FeeForPerByte; 21 | 22 | bool public feeEnabled; 23 | bool public erc20Enabled; 24 | 25 | event NativeFeeBase(uint256 nativeFeeBase); 26 | event NativeFeeForPerByte(uint256 nativeFeeForPerByte); 27 | event ERC20FeeBase(uint256 erc20FeeBase); 28 | event ERC20FeeForPerByte(uint256 erc20FeeForPerByte); 29 | event FeeEnabled(bool feeEnabled); 30 | event ERC20Enabled(bool erc20Enabled); 31 | 32 | constructor( 33 | bool _feeEnabled, 34 | bool _erc20Enabled, 35 | address _erc20FeeToken, 36 | uint256 _nativeFeeBase, 37 | uint256 _nativeFeePerByte 38 | ) { 39 | feeEnabled = _feeEnabled; 40 | erc20Enabled = _erc20Enabled; 41 | erc20FeeToken = _erc20FeeToken; 42 | 43 | nativeFeeBase = _nativeFeeBase; 44 | nativeFeeForPerByte = _nativeFeePerByte; 45 | erc20FeeBase = 0; 46 | erc20FeeForPerByte = 0; 47 | } 48 | 49 | function getFees(bool payInERC20, uint256 msgLength) external view override returns (uint256) { 50 | if (feeEnabled) { 51 | if (payInERC20) { 52 | return erc20FeeBase.add(erc20FeeForPerByte.mul(msgLength)); 53 | } else { 54 | return nativeFeeBase.add(nativeFeeForPerByte.mul(msgLength)); 55 | } 56 | } 57 | return 0; 58 | } 59 | 60 | function getERC20FeeToken() external view override returns (address) { 61 | return erc20FeeToken; 62 | } 63 | 64 | function setFeeEnabled(bool _feeEnabled) external onlyOwner { 65 | feeEnabled = _feeEnabled; 66 | emit FeeEnabled(feeEnabled); 67 | } 68 | 69 | function setERC20Enabled(bool _erc20Enabled) external onlyOwner { 70 | erc20Enabled = _erc20Enabled; 71 | emit ERC20Enabled(erc20Enabled); 72 | } 73 | 74 | function setNativeFeeBase(uint256 _nativeFeeBase) external onlyOwner { 75 | nativeFeeBase = _nativeFeeBase; 76 | emit NativeFeeBase(nativeFeeBase); 77 | } 78 | 79 | function setERC20FeeBase(uint256 _erc20FeeBase) external onlyOwner { 80 | erc20FeeBase = _erc20FeeBase; 81 | emit ERC20FeeBase(erc20FeeBase); 82 | } 83 | 84 | function setNativeFeeForPerByte(uint256 _nativeFeeForPerByte) external onlyOwner { 85 | nativeFeeForPerByte = _nativeFeeForPerByte; 86 | emit NativeFeeBase(nativeFeeForPerByte); 87 | } 88 | 89 | function setERC20FeeForPerByte(uint256 _erc20FeeForPerByte) external onlyOwner { 90 | erc20FeeForPerByte = _erc20FeeForPerByte; 91 | emit ERC20FeeForPerByte(erc20FeeForPerByte); 92 | } 93 | } -------------------------------------------------------------------------------- /core/evm/contracts/interfaces/IOmnic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | interface IOmnic { 6 | function sendMessage( 7 | uint32 _dstChainId, 8 | bytes32 _recipientAddress, 9 | bytes memory _payload, 10 | address payable _refundAddress, 11 | address _erc20PaymentAddress 12 | ) external payable; 13 | 14 | function sendMessageFree( 15 | uint32 _dstChainId, 16 | bytes32 _recipientAddress, 17 | bytes memory _payload 18 | ) external; 19 | 20 | function handleMessage( 21 | uint32 _srcChainId, 22 | bytes32 _srcSenderAddress, 23 | uint32 _nonce, 24 | bytes calldata payload 25 | ) external; 26 | } 27 | -------------------------------------------------------------------------------- /core/evm/contracts/interfaces/IOmnicFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | interface IOmnicFeeManager { 6 | /** 7 | * @notice call it to get protocol fee 8 | * @param payInERC20 if using ERC20 for fee 9 | * @param msgLength message length 10 | */ 11 | function getFees(bool payInERC20, uint256 msgLength) 12 | external 13 | view 14 | returns (uint256); 15 | 16 | function getERC20FeeToken() 17 | external 18 | view 19 | returns (address); 20 | } -------------------------------------------------------------------------------- /core/evm/contracts/interfaces/IOmnicReciver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | // Omnic Crosschain APP should implement the following interface 5 | 6 | interface IOmnicReciver { 7 | function handleMessage( 8 | uint32 _srcChainId, 9 | bytes32 _srcSenderAddress, 10 | uint32 _nonce, 11 | bytes calldata payload 12 | ) external; 13 | } -------------------------------------------------------------------------------- /core/evm/contracts/libs/Merkle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.9; 3 | 4 | // refrence: https://github.com/nomad-xyz/monorepo/blob/main/packages/contracts-core/contracts/libs/Merkle.sol 5 | // work based on eth2 deposit contract, which is used under CC0-1.0 6 | 7 | /** 8 | * @title MerkleLib 9 | * @author Illusory Systems Inc. 10 | * @notice An incremental merkle tree modeled on the eth2 deposit contract. 11 | **/ 12 | library MerkleLib { 13 | uint256 internal constant TREE_DEPTH = 32; 14 | uint256 internal constant MAX_LEAVES = 2**TREE_DEPTH - 1; 15 | 16 | /** 17 | * @notice Struct representing incremental merkle tree. Contains current 18 | * branch and the number of inserted leaves in the tree. 19 | **/ 20 | struct Tree { 21 | bytes32[TREE_DEPTH] branch; 22 | uint256 count; 23 | } 24 | 25 | /** 26 | * @notice Inserts `_node` into merkle tree 27 | * @dev Reverts if tree is full 28 | * @param _node Element to insert into tree 29 | **/ 30 | function insert(Tree storage _tree, bytes32 _node) internal { 31 | require(_tree.count < MAX_LEAVES, "merkle tree full"); 32 | 33 | _tree.count += 1; 34 | uint256 size = _tree.count; 35 | for (uint256 i = 0; i < TREE_DEPTH; i++) { 36 | if ((size & 1) == 1) { 37 | _tree.branch[i] = _node; 38 | return; 39 | } 40 | _node = keccak256(abi.encodePacked(_tree.branch[i], _node)); 41 | size /= 2; 42 | } 43 | // As the loop should always end prematurely with the `return` statement, 44 | // this code should be unreachable. We assert `false` just to be safe. 45 | assert(false); 46 | } 47 | 48 | /** 49 | * @notice Calculates and returns`_tree`'s current root given array of zero 50 | * hashes 51 | * @param _zeroes Array of zero hashes 52 | * @return _current Calculated root of `_tree` 53 | **/ 54 | function rootWithCtx(Tree storage _tree, bytes32[TREE_DEPTH] memory _zeroes) 55 | internal 56 | view 57 | returns (bytes32 _current) 58 | { 59 | uint256 _index = _tree.count; 60 | 61 | for (uint256 i = 0; i < TREE_DEPTH; i++) { 62 | uint256 _ithBit = (_index >> i) & 0x01; 63 | bytes32 _next = _tree.branch[i]; 64 | if (_ithBit == 1) { 65 | _current = keccak256(abi.encodePacked(_next, _current)); 66 | } else { 67 | _current = keccak256(abi.encodePacked(_current, _zeroes[i])); 68 | } 69 | } 70 | } 71 | 72 | /// @notice Calculates and returns`_tree`'s current root 73 | function root(Tree storage _tree) internal view returns (bytes32) { 74 | return rootWithCtx(_tree, zeroHashes()); 75 | } 76 | 77 | /// @notice Returns array of TREE_DEPTH zero hashes 78 | /// @return _zeroes Array of TREE_DEPTH zero hashes 79 | function zeroHashes() 80 | internal 81 | pure 82 | returns (bytes32[TREE_DEPTH] memory _zeroes) 83 | { 84 | _zeroes[0] = Z_0; 85 | _zeroes[1] = Z_1; 86 | _zeroes[2] = Z_2; 87 | _zeroes[3] = Z_3; 88 | _zeroes[4] = Z_4; 89 | _zeroes[5] = Z_5; 90 | _zeroes[6] = Z_6; 91 | _zeroes[7] = Z_7; 92 | _zeroes[8] = Z_8; 93 | _zeroes[9] = Z_9; 94 | _zeroes[10] = Z_10; 95 | _zeroes[11] = Z_11; 96 | _zeroes[12] = Z_12; 97 | _zeroes[13] = Z_13; 98 | _zeroes[14] = Z_14; 99 | _zeroes[15] = Z_15; 100 | _zeroes[16] = Z_16; 101 | _zeroes[17] = Z_17; 102 | _zeroes[18] = Z_18; 103 | _zeroes[19] = Z_19; 104 | _zeroes[20] = Z_20; 105 | _zeroes[21] = Z_21; 106 | _zeroes[22] = Z_22; 107 | _zeroes[23] = Z_23; 108 | _zeroes[24] = Z_24; 109 | _zeroes[25] = Z_25; 110 | _zeroes[26] = Z_26; 111 | _zeroes[27] = Z_27; 112 | _zeroes[28] = Z_28; 113 | _zeroes[29] = Z_29; 114 | _zeroes[30] = Z_30; 115 | _zeroes[31] = Z_31; 116 | } 117 | 118 | /** 119 | * @notice Calculates and returns the merkle root for the given leaf 120 | * `_item`, a merkle branch, and the index of `_item` in the tree. 121 | * @param _item Merkle leaf 122 | * @param _branch Merkle proof 123 | * @param _index Index of `_item` in tree 124 | * @return _current Calculated merkle root 125 | **/ 126 | function branchRoot( 127 | bytes32 _item, 128 | bytes32[TREE_DEPTH] memory _branch, 129 | uint256 _index 130 | ) internal pure returns (bytes32 _current) { 131 | _current = _item; 132 | 133 | for (uint256 i = 0; i < TREE_DEPTH; i++) { 134 | uint256 _ithBit = (_index >> i) & 0x01; 135 | bytes32 _next = _branch[i]; 136 | if (_ithBit == 1) { 137 | _current = keccak256(abi.encodePacked(_next, _current)); 138 | } else { 139 | _current = keccak256(abi.encodePacked(_current, _next)); 140 | } 141 | } 142 | } 143 | 144 | // keccak256 zero hashes 145 | bytes32 internal constant Z_0 = 146 | hex"0000000000000000000000000000000000000000000000000000000000000000"; 147 | bytes32 internal constant Z_1 = 148 | hex"ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5"; 149 | bytes32 internal constant Z_2 = 150 | hex"b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30"; 151 | bytes32 internal constant Z_3 = 152 | hex"21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85"; 153 | bytes32 internal constant Z_4 = 154 | hex"e58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344"; 155 | bytes32 internal constant Z_5 = 156 | hex"0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d"; 157 | bytes32 internal constant Z_6 = 158 | hex"887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968"; 159 | bytes32 internal constant Z_7 = 160 | hex"ffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f83"; 161 | bytes32 internal constant Z_8 = 162 | hex"9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af"; 163 | bytes32 internal constant Z_9 = 164 | hex"cefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0"; 165 | bytes32 internal constant Z_10 = 166 | hex"f9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5"; 167 | bytes32 internal constant Z_11 = 168 | hex"f8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf892"; 169 | bytes32 internal constant Z_12 = 170 | hex"3490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99c"; 171 | bytes32 internal constant Z_13 = 172 | hex"c1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb"; 173 | bytes32 internal constant Z_14 = 174 | hex"5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc"; 175 | bytes32 internal constant Z_15 = 176 | hex"da7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d2"; 177 | bytes32 internal constant Z_16 = 178 | hex"2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f"; 179 | bytes32 internal constant Z_17 = 180 | hex"e1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a"; 181 | bytes32 internal constant Z_18 = 182 | hex"5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0"; 183 | bytes32 internal constant Z_19 = 184 | hex"b46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0"; 185 | bytes32 internal constant Z_20 = 186 | hex"c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2"; 187 | bytes32 internal constant Z_21 = 188 | hex"f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9"; 189 | bytes32 internal constant Z_22 = 190 | hex"5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377"; 191 | bytes32 internal constant Z_23 = 192 | hex"4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652"; 193 | bytes32 internal constant Z_24 = 194 | hex"cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef"; 195 | bytes32 internal constant Z_25 = 196 | hex"0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d"; 197 | bytes32 internal constant Z_26 = 198 | hex"b8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0"; 199 | bytes32 internal constant Z_27 = 200 | hex"838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e"; 201 | bytes32 internal constant Z_28 = 202 | hex"662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e"; 203 | bytes32 internal constant Z_29 = 204 | hex"388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322"; 205 | bytes32 internal constant Z_30 = 206 | hex"93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735"; 207 | bytes32 internal constant Z_31 = 208 | hex"8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9"; 209 | } 210 | -------------------------------------------------------------------------------- /core/evm/contracts/libs/Queue.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.9; 3 | 4 | /** 5 | * @title QueueLib 6 | * @author Illusory Systems Inc. 7 | * @notice Library containing queue struct and operations for queue used by 8 | * Home and Replica. 9 | **/ 10 | library QueueLib { 11 | /** 12 | * @notice Queue struct 13 | * @dev Internally keeps track of the `first` and `last` elements through 14 | * indices and a mapping of indices to enqueued elements. 15 | **/ 16 | struct Queue { 17 | uint128 first; 18 | uint128 last; 19 | mapping(uint256 => bytes32) queue; 20 | } 21 | 22 | /** 23 | * @notice Initializes the queue 24 | * @dev Empty state denoted by _q.first > q._last. Queue initialized 25 | * with _q.first = 1 and _q.last = 0. 26 | **/ 27 | function initialize(Queue storage _q) internal { 28 | if (_q.first == 0) { 29 | _q.first = 1; 30 | } 31 | } 32 | 33 | /** 34 | * @notice Enqueues a single new element 35 | * @param _item New element to be enqueued 36 | * @return _last Index of newly enqueued element 37 | **/ 38 | function enqueue(Queue storage _q, bytes32 _item) 39 | internal 40 | returns (uint128 _last) 41 | { 42 | _last = _q.last + 1; 43 | _q.last = _last; 44 | if (_item != bytes32(0)) { 45 | // saves gas if we're queueing 0 46 | _q.queue[_last] = _item; 47 | } 48 | } 49 | 50 | /** 51 | * @notice Dequeues element at front of queue 52 | * @dev Removes dequeued element from storage 53 | * @return _item Dequeued element 54 | **/ 55 | function dequeue(Queue storage _q) internal returns (bytes32 _item) { 56 | uint128 _last = _q.last; 57 | uint128 _first = _q.first; 58 | require(_length(_last, _first) != 0, "Empty"); 59 | _item = _q.queue[_first]; 60 | if (_item != bytes32(0)) { 61 | // saves gas if we're dequeuing 0 62 | delete _q.queue[_first]; 63 | } 64 | _q.first = _first + 1; 65 | } 66 | 67 | /** 68 | * @notice Batch enqueues several elements 69 | * @param _items Array of elements to be enqueued 70 | * @return _last Index of last enqueued element 71 | **/ 72 | function enqueue(Queue storage _q, bytes32[] memory _items) 73 | internal 74 | returns (uint128 _last) 75 | { 76 | _last = _q.last; 77 | for (uint256 i = 0; i < _items.length; i += 1) { 78 | _last += 1; 79 | bytes32 _item = _items[i]; 80 | if (_item != bytes32(0)) { 81 | _q.queue[_last] = _item; 82 | } 83 | } 84 | _q.last = _last; 85 | } 86 | 87 | /** 88 | * @notice Batch dequeues `_number` elements 89 | * @dev Reverts if `_number` > queue length 90 | * @param _number Number of elements to dequeue 91 | * @return Array of dequeued elements 92 | **/ 93 | function dequeue(Queue storage _q, uint256 _number) 94 | internal 95 | returns (bytes32[] memory) 96 | { 97 | uint128 _last = _q.last; 98 | uint128 _first = _q.first; 99 | // Cannot underflow unless state is corrupted 100 | require(_length(_last, _first) >= _number, "Insufficient"); 101 | 102 | bytes32[] memory _items = new bytes32[](_number); 103 | 104 | for (uint256 i = 0; i < _number; i++) { 105 | _items[i] = _q.queue[_first]; 106 | delete _q.queue[_first]; 107 | _first++; 108 | } 109 | _q.first = _first; 110 | return _items; 111 | } 112 | 113 | /** 114 | * @notice Returns true if `_item` is in the queue and false if otherwise 115 | * @dev Linearly scans from _q.first to _q.last looking for `_item` 116 | * @param _item Item being searched for in queue 117 | * @return True if `_item` currently exists in queue, false if otherwise 118 | **/ 119 | function contains(Queue storage _q, bytes32 _item) 120 | internal 121 | view 122 | returns (bool) 123 | { 124 | for (uint256 i = _q.first; i <= _q.last; i++) { 125 | if (_q.queue[i] == _item) { 126 | return true; 127 | } 128 | } 129 | return false; 130 | } 131 | 132 | /// @notice Returns last item in queue 133 | /// @dev Returns bytes32(0) if queue empty 134 | function lastItem(Queue storage _q) internal view returns (bytes32) { 135 | return _q.queue[_q.last]; 136 | } 137 | 138 | /// @notice Returns element at front of queue without removing element 139 | /// @dev Reverts if queue is empty 140 | function peek(Queue storage _q) internal view returns (bytes32 _item) { 141 | require(!isEmpty(_q), "Empty"); 142 | _item = _q.queue[_q.first]; 143 | } 144 | 145 | /// @notice Returns true if queue is empty and false if otherwise 146 | function isEmpty(Queue storage _q) internal view returns (bool) { 147 | return _q.last < _q.first; 148 | } 149 | 150 | /// @notice Returns number of elements in queue 151 | function length(Queue storage _q) internal view returns (uint256) { 152 | uint128 _last = _q.last; 153 | uint128 _first = _q.first; 154 | // Cannot underflow unless state is corrupted 155 | return _length(_last, _first); 156 | } 157 | 158 | /// @notice Returns number of elements between `_last` and `_first` (used internally) 159 | function _length(uint128 _last, uint128 _first) 160 | internal 161 | pure 162 | returns (uint256) 163 | { 164 | return uint256(_last + 1 - _first); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /core/evm/contracts/libs/Types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.9; 3 | 4 | /** 5 | * @title Types Library 6 | **/ 7 | library Types { 8 | 9 | /** 10 | * @notice Returns formatted (packed) message with provided fields 11 | * @param _srcChainId source chain id 12 | * @param _srcSenderAddress Address of sender, address length/format may vary by chains 13 | * @param _nonce Destination-specific nonce 14 | * @param _dstChainId destination chain id 15 | * @param _recipientAddress Address of recipient on destination chain 16 | * @param _payload Raw bytes of message body 17 | * @return Formatted message 18 | **/ 19 | function formatMessage( 20 | uint32 _srcChainId, 21 | bytes32 _srcSenderAddress, 22 | uint32 _nonce, 23 | uint32 _dstChainId, 24 | bytes32 _recipientAddress, 25 | bytes memory _payload 26 | ) internal pure returns (bytes memory) { 27 | return 28 | abi.encode( 29 | _srcChainId, 30 | _srcSenderAddress, 31 | _nonce, 32 | _dstChainId, 33 | _recipientAddress, 34 | _payload 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/evm/contracts/upgrade/UpgradeBeacon.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.9; 3 | 4 | // ============ External Imports ============ 5 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 6 | 7 | /** 8 | * @title UpgradeBeacon 9 | * @notice Stores the address of an implementation contract 10 | * and allows a controller to upgrade the implementation address 11 | * @dev This implementation combines the gas savings of having no function selectors 12 | * found in 0age's implementation: 13 | * https://github.com/dharma-eng/dharma-smart-wallet/blob/master/contracts/proxies/smart-wallet/UpgradeBeaconProxyV1.sol 14 | * With the added niceties of a safety check that each implementation is a contract 15 | * and an Upgrade event emitted each time the implementation is changed 16 | * found in OpenZeppelin's implementation: 17 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/BeaconProxy.sol 18 | */ 19 | contract UpgradeBeacon { 20 | // ============ Immutables ============ 21 | 22 | // The controller is capable of modifying the implementation address 23 | address private immutable controller; 24 | 25 | // ============ Private Storage Variables ============ 26 | 27 | // The implementation address is held in storage slot zero. 28 | address private implementation; 29 | 30 | // ============ Events ============ 31 | 32 | // Upgrade event is emitted each time the implementation address is set 33 | // (including deployment) 34 | event Upgrade(address indexed implementation); 35 | 36 | // ============ Constructor ============ 37 | 38 | /** 39 | * @notice Validate the initial implementation and store it. 40 | * Store the controller immutably. 41 | * @param _initialImplementation Address of the initial implementation contract 42 | * @param _controller Address of the controller who can upgrade the implementation 43 | */ 44 | constructor(address _initialImplementation, address _controller) { 45 | _setImplementation(_initialImplementation); 46 | controller = _controller; 47 | } 48 | 49 | // ============ External Functions ============ 50 | 51 | /** 52 | * @notice For all callers except the controller, return the current implementation address. 53 | * If called by the Controller, update the implementation address 54 | * to the address passed in the calldata. 55 | * Note: this requires inline assembly because Solidity fallback functions 56 | * do not natively take arguments or return values. 57 | */ 58 | fallback() external payable { 59 | if (msg.sender != controller) { 60 | // if not called by the controller, 61 | // load implementation address from storage slot zero 62 | // and return it. 63 | assembly { 64 | mstore(0, sload(0)) 65 | return(0, 32) 66 | } 67 | } else { 68 | // if called by the controller, 69 | // load new implementation address from the first word of the calldata 70 | address _newImplementation; 71 | assembly { 72 | _newImplementation := calldataload(0) 73 | } 74 | // set the new implementation 75 | _setImplementation(_newImplementation); 76 | } 77 | } 78 | 79 | // ============ Private Functions ============ 80 | 81 | /** 82 | * @notice Perform checks on the new implementation address 83 | * then upgrade the stored implementation. 84 | * @param _newImplementation Address of the new implementation contract which will replace the old one 85 | */ 86 | function _setImplementation(address _newImplementation) private { 87 | // Require that the new implementation is different from the current one 88 | require(implementation != _newImplementation, "!upgrade"); 89 | // Require that the new implementation is a contract 90 | require( 91 | Address.isContract(_newImplementation), 92 | "implementation !contract" 93 | ); 94 | // set the new implementation 95 | implementation = _newImplementation; 96 | emit Upgrade(_newImplementation); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /core/evm/contracts/upgrade/UpgradeBeaconController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | // pragma solidity 0.7.6; 3 | pragma solidity ^0.8.9; 4 | 5 | // ============ Internal Imports ============ 6 | import {UpgradeBeacon} from "./UpgradeBeacon.sol"; 7 | // ============ External Imports ============ 8 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 9 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 10 | 11 | /** 12 | * @title UpgradeBeaconController 13 | * @notice Set as the controller of UpgradeBeacon contract(s), 14 | * capable of changing their stored implementation address. 15 | * @dev This implementation is a minimal version inspired by 0age's implementation: 16 | * https://github.com/dharma-eng/dharma-smart-wallet/blob/master/contracts/upgradeability/DharmaUpgradeBeaconController.sol 17 | */ 18 | contract UpgradeBeaconController is Ownable { 19 | // ============ Events ============ 20 | 21 | event BeaconUpgraded(address indexed beacon, address implementation); 22 | 23 | // ============ External Functions ============ 24 | 25 | /** 26 | * @notice Modify the implementation stored in the UpgradeBeacon, 27 | * which will upgrade the implementation used by all 28 | * Proxy contracts using that UpgradeBeacon 29 | * @param _beacon Address of the UpgradeBeacon which will be updated 30 | * @param _implementation Address of the Implementation contract to upgrade the Beacon to 31 | */ 32 | function upgrade(address _beacon, address _implementation) 33 | external 34 | onlyOwner 35 | { 36 | // Require that the beacon is a contract 37 | require(Address.isContract(_beacon), "beacon !contract"); 38 | // Call into beacon and supply address of new implementation to update it. 39 | (bool _success, ) = _beacon.call(abi.encode(_implementation)); 40 | // Revert with message on failure (i.e. if the beacon is somehow incorrect). 41 | if (!_success) { 42 | assembly { 43 | returndatacopy(0, 0, returndatasize()) 44 | revert(0, returndatasize()) 45 | } 46 | } 47 | emit BeaconUpgraded(_beacon, _implementation); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/evm/contracts/upgrade/UpgradeBeaconProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // pragma solidity 0.7.6; 3 | pragma solidity ^0.8.9; 4 | 5 | // ============ External Imports ============ 6 | import {Address} from "@openzeppelin/contracts/utils/Address.sol"; 7 | 8 | /** 9 | * @title UpgradeBeaconProxy 10 | * @notice 11 | * Proxy contract which delegates all logic, including initialization, 12 | * to an implementation contract. 13 | * The implementation contract is stored within an Upgrade Beacon contract; 14 | * the implementation contract can be changed by performing an upgrade on the Upgrade Beacon contract. 15 | * The Upgrade Beacon contract for this Proxy is immutably specified at deployment. 16 | * @dev This implementation combines the gas savings of keeping the UpgradeBeacon address outside of contract storage 17 | * found in 0age's implementation: 18 | * https://github.com/dharma-eng/dharma-smart-wallet/blob/master/contracts/proxies/smart-wallet/UpgradeBeaconProxyV1.sol 19 | * With the added safety checks that the UpgradeBeacon and implementation are contracts at time of deployment 20 | * found in OpenZeppelin's implementation: 21 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/BeaconProxy.sol 22 | */ 23 | contract UpgradeBeaconProxy { 24 | // ============ Immutables ============ 25 | 26 | // Upgrade Beacon address is immutable (therefore not kept in contract storage) 27 | address private immutable upgradeBeacon; 28 | 29 | // ============ Constructor ============ 30 | 31 | /** 32 | * @notice Validate that the Upgrade Beacon is a contract, then set its 33 | * address immutably within this contract. 34 | * Validate that the implementation is also a contract, 35 | * Then call the initialization function defined at the implementation. 36 | * The deployment will revert and pass along the 37 | * revert reason if the initialization function reverts. 38 | * @param _upgradeBeacon Address of the Upgrade Beacon to be stored immutably in the contract 39 | * @param _initializationCalldata Calldata supplied when calling the initialization function 40 | */ 41 | constructor(address _upgradeBeacon, bytes memory _initializationCalldata) 42 | payable 43 | { 44 | // Validate the Upgrade Beacon is a contract 45 | require(Address.isContract(_upgradeBeacon), "beacon !contract"); 46 | // set the Upgrade Beacon 47 | upgradeBeacon = _upgradeBeacon; 48 | // Validate the implementation is a contract 49 | address _implementation = _getImplementation(_upgradeBeacon); 50 | require( 51 | Address.isContract(_implementation), 52 | "beacon implementation !contract" 53 | ); 54 | // Call the initialization function on the implementation 55 | if (_initializationCalldata.length > 0) { 56 | _initialize(_implementation, _initializationCalldata); 57 | } 58 | } 59 | 60 | // ============ External Functions ============ 61 | 62 | /** 63 | * @notice Forwards all calls with data to _fallback() 64 | * No public functions are declared on the contract, so all calls hit fallback 65 | */ 66 | fallback() external payable { 67 | _fallback(); 68 | } 69 | 70 | /** 71 | * @notice Forwards all calls with no data to _fallback() 72 | */ 73 | receive() external payable { 74 | _fallback(); 75 | } 76 | 77 | // ============ Private Functions ============ 78 | 79 | /** 80 | * @notice Call the initialization function on the implementation 81 | * Used at deployment to initialize the proxy 82 | * based on the logic for initialization defined at the implementation 83 | * @param _implementation - Contract to which the initalization is delegated 84 | * @param _initializationCalldata - Calldata supplied when calling the initialization function 85 | */ 86 | function _initialize( 87 | address _implementation, 88 | bytes memory _initializationCalldata 89 | ) private { 90 | // Delegatecall into the implementation, supplying initialization calldata. 91 | (bool _ok, ) = _implementation.delegatecall(_initializationCalldata); 92 | // Revert and include revert data if delegatecall to implementation reverts. 93 | if (!_ok) { 94 | assembly { 95 | returndatacopy(0, 0, returndatasize()) 96 | revert(0, returndatasize()) 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * @notice Delegates function calls to the implementation contract returned by the Upgrade Beacon 103 | */ 104 | function _fallback() private { 105 | _delegate(_getImplementation()); 106 | } 107 | 108 | /** 109 | * @notice Delegate function execution to the implementation contract 110 | * @dev This is a low level function that doesn't return to its internal 111 | * call site. It will return whatever is returned by the implementation to the 112 | * external caller, reverting and returning the revert data if implementation 113 | * reverts. 114 | * @param _implementation - Address to which the function execution is delegated 115 | */ 116 | function _delegate(address _implementation) private { 117 | assembly { 118 | // Copy msg.data. We take full control of memory in this inline assembly 119 | // block because it will not return to Solidity code. We overwrite the 120 | // Solidity scratch pad at memory position 0. 121 | calldatacopy(0, 0, calldatasize()) 122 | // Delegatecall to the implementation, supplying calldata and gas. 123 | // Out and outsize are set to zero - instead, use the return buffer. 124 | let result := delegatecall( 125 | gas(), 126 | _implementation, 127 | 0, 128 | calldatasize(), 129 | 0, 130 | 0 131 | ) 132 | // Copy the returned data from the return buffer. 133 | returndatacopy(0, 0, returndatasize()) 134 | switch result 135 | // Delegatecall returns 0 on error. 136 | case 0 { 137 | revert(0, returndatasize()) 138 | } 139 | default { 140 | return(0, returndatasize()) 141 | } 142 | } 143 | } 144 | 145 | /** 146 | * @notice Call the Upgrade Beacon to get the current implementation contract address 147 | * @return _implementation Address of the current implementation. 148 | */ 149 | function _getImplementation() 150 | private 151 | view 152 | returns (address _implementation) 153 | { 154 | _implementation = _getImplementation(upgradeBeacon); 155 | } 156 | 157 | /** 158 | * @notice Call the Upgrade Beacon to get the current implementation contract address 159 | * @dev _upgradeBeacon is passed as a parameter so that 160 | * we can also use this function in the constructor, 161 | * where we can't access immutable variables. 162 | * @param _upgradeBeacon Address of the UpgradeBeacon storing the current implementation 163 | * @return _implementation Address of the current implementation. 164 | */ 165 | function _getImplementation(address _upgradeBeacon) 166 | private 167 | view 168 | returns (address _implementation) 169 | { 170 | // Get the current implementation address from the upgrade beacon. 171 | (bool _ok, bytes memory _returnData) = _upgradeBeacon.staticcall(""); 172 | // Revert and pass along revert message if call to upgrade beacon reverts. 173 | require(_ok, string(_returnData)); 174 | // Set the implementation to the address returned from the upgrade beacon. 175 | _implementation = abi.decode(_returnData, (address)); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /core/evm/contracts/utils/QueueManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.9; 3 | 4 | // ============ Internal Imports ============ 5 | import {QueueLib} from "../libs/Queue.sol"; 6 | // ============ External Imports ============ 7 | import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 8 | 9 | /** 10 | * @title QueueManager 11 | * @author Illusory Systems Inc. 12 | * @notice Contains a queue instance and 13 | * exposes view functions for the queue. 14 | **/ 15 | contract QueueManager is Initializable { 16 | // ============ Libraries ============ 17 | 18 | using QueueLib for QueueLib.Queue; 19 | QueueLib.Queue internal queue; 20 | 21 | // ============ Upgrade Gap ============ 22 | 23 | // gap for upgrade safety 24 | uint256[49] private __GAP; 25 | 26 | // ============ Initializer ============ 27 | 28 | function __QueueManager_initialize() internal initializer { 29 | queue.initialize(); 30 | } 31 | 32 | // ============ Public Functions ============ 33 | 34 | /** 35 | * @notice Returns number of elements in queue 36 | */ 37 | function queueLength() external view returns (uint256) { 38 | return queue.length(); 39 | } 40 | 41 | /** 42 | * @notice Returns TRUE iff `_item` is in the queue 43 | */ 44 | function queueContains(bytes32 _item) external view returns (bool) { 45 | return queue.contains(_item); 46 | } 47 | 48 | /** 49 | * @notice Returns last item enqueued to the queue 50 | */ 51 | function queueEnd() external view returns (bytes32) { 52 | return queue.lastItem(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/evm/contracts/utils/Utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity ^0.8.9; 3 | 4 | library TypeCasts { 5 | 6 | // alignment preserving cast 7 | function addressToBytes32(address _addr) internal pure returns (bytes32) { 8 | return bytes32(uint256(uint160(_addr))); 9 | } 10 | 11 | // alignment preserving cast 12 | function bytes32ToAddress(bytes32 _buf) internal pure returns (address) { 13 | return address(uint160(uint256(_buf))); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/evm/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import "hardhat-gas-reporter" 4 | 5 | import * as dotenv from "dotenv"; 6 | dotenv.config(); 7 | 8 | const etherscanKey = process.env.ETHERSCAN_API_KEY; 9 | const infuraKey = process.env.INFURA_API_KEY; 10 | const alchemyKey = process.env.ALCHEMY_KEY; 11 | const testPrivKey = process.env.TEST_PRIV_KEY; 12 | 13 | module.exports = { 14 | solidity: { 15 | version: "0.8.9", 16 | settings: { 17 | optimizer: { 18 | enabled: true, 19 | runs: 999999, 20 | }, 21 | metadata: { 22 | bytecodeHash: "none", 23 | }, 24 | }, 25 | }, 26 | 27 | gasReporter: { 28 | enabled: true, 29 | gasPrice: 21000000000, 30 | currency: "USD", 31 | }, 32 | 33 | networks: { 34 | localhost: { 35 | url: "http://localhost:8545", 36 | accounts: [testPrivKey], 37 | }, 38 | kovan: { 39 | url: `https://kovan.infura.io/v3/${infuraKey}`, 40 | accounts: [testPrivKey] 41 | }, 42 | rinkeby: { 43 | url: `https://rinkeby.infura.io/v3/${infuraKey}`, 44 | accounts: [testPrivKey] 45 | }, 46 | goerli: { 47 | url: `https://goerli.infura.io/v3/${infuraKey}`, 48 | // url: `https://eth-goerli.g.alchemy.com/v2/${alchemyKey}`, 49 | accounts: [testPrivKey] 50 | }, 51 | mumbai: { // polygon mumbai testnet 52 | url: `https://polygon-mumbai.g.alchemy.com/v2/${alchemyKey}`, 53 | accounts: [testPrivKey], 54 | // gas: 100000, 55 | // gasPrice: 10, 56 | }, 57 | arbi_goerli: { 58 | url: `https://arb-goerli.g.alchemy.com/v2/${alchemyKey}`, 59 | accounts: [testPrivKey], 60 | }, 61 | op_goerli: { 62 | url: `https://opt-goerli.g.alchemy.com/v2/${alchemyKey}`, 63 | accounts: [testPrivKey], 64 | }, 65 | mainnet: { 66 | url: `https://mainnet.infura.io/v3/${infuraKey}`, 67 | }, 68 | }, 69 | 70 | typechain: { 71 | outDir: "./src", 72 | target: "ethers-v5", 73 | alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads? 74 | }, 75 | 76 | etherscan: { 77 | apiKey: etherscanKey, 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /core/evm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evm", 3 | "version": "1.0.0", 4 | "description": "EVM side omnic gateway contract", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@dfinity/principal": "^0.13.1", 14 | "@openzeppelin/contracts": "^4.7.3", 15 | "@openzeppelin/contracts-upgradeable": "^4.8.0-rc.1", 16 | "dotenv": "^16.0.1", 17 | "hardhat": "^2.11.2" 18 | }, 19 | "devDependencies": { 20 | "@ethersproject/abi": "^5.4.7", 21 | "@ethersproject/providers": "^5.4.7", 22 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", 23 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 24 | "@nomicfoundation/hardhat-toolbox": "^1.0.1", 25 | "@nomiclabs/hardhat-ethers": "^2.1.0", 26 | "@nomiclabs/hardhat-etherscan": "^3.0.0", 27 | "@openzeppelin/hardhat-upgrades": "^1.21.0", 28 | "@typechain/ethers-v5": "^10.1.0", 29 | "@typechain/hardhat": "^6.1.2", 30 | "@types/chai": "^4.2.0", 31 | "@types/mocha": "^9.1.0", 32 | "@types/node": ">=12.0.0", 33 | "chai": "^4.2.0", 34 | "ethers": "^5.6.9", 35 | "hardhat-gas-reporter": "^1.0.9", 36 | "solidity-coverage": "^0.7.21", 37 | "ts-node": ">=8.0.0", 38 | "typechain": "^8.1.0", 39 | "typescript": ">=4.5.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/evm/scripts/deploy-demo.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { getContractAddr, updateConfig } from "./helpers"; 3 | import fs from 'fs'; 4 | const hre = require("hardhat"); 5 | 6 | // deploy Omnic implementataion contract 7 | // and set the implementation address to UpgradeBeacon by calling UpgradeBeaconController 8 | 9 | export const deployDemo = async function (chain: string) { 10 | const DemoApp = await ethers.getContractFactory("DemoApp"); 11 | 12 | const omnicAddr = getContractAddr(chain, "UpgradeBeaconProxy"); 13 | const demoAddr = getContractAddr(chain, "Demo"); 14 | let demo; 15 | if(demoAddr == null) { 16 | console.log("deploying Demo App..."); 17 | demo = await DemoApp.deploy(omnicAddr); 18 | 19 | await demo.deployed(); 20 | console.log("chain: ", chain, "DemoApp deployed to:", demo.address); 21 | updateConfig(chain, "Demo", demo.address); 22 | } else { 23 | console.log("found deployed DempApp:", demoAddr); 24 | demo = await ethers.getContractAt("DemoApp", demoAddr); 25 | } 26 | return demo; 27 | } 28 | 29 | const main = async function () { 30 | let chain = hre.network.name; 31 | await deployDemo(chain); 32 | } 33 | 34 | // We recommend this pattern to be able to use async/await everywhere 35 | // and properly handle errors. 36 | main().catch((error) => { 37 | console.error(error); 38 | process.exitCode = 1; 39 | }); 40 | -------------------------------------------------------------------------------- /core/evm/scripts/deploy-feemanager.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { getContractAddr, updateConfig } from "./helpers"; 3 | import fs from 'fs'; 4 | const hre = require("hardhat"); 5 | 6 | // deploy Omnic implementataion contract 7 | // and set the implementation address to UpgradeBeacon by calling UpgradeBeaconController 8 | 9 | export const deployFeeManager = async function (chain: string) { 10 | const FeeManager = await ethers.getContractFactory("OmnicFeeManager"); 11 | 12 | const feeAddr = getContractAddr(chain, "OmnicFeeManager"); 13 | let feeManager; 14 | let nativeBaseFee = 1 * 10**15; // 0.001 eth 15 | let nativeFeePerByte = 0; 16 | let erc20FeeToken = getContractAddr(chain, 'USDT'); 17 | let erc20BaseFee = 0; 18 | let erc20FeePerByte = 0; 19 | if(feeAddr == null) { 20 | console.log("deploying OmnicFeeManager..."); 21 | feeManager = await FeeManager.deploy( 22 | true, 23 | true, 24 | erc20FeeToken, 25 | nativeBaseFee, 26 | nativeFeePerByte, 27 | ); 28 | 29 | await feeManager.deployed(); 30 | console.log("chain: ", chain, "OmnicFeeManager deployed to:", feeManager.address); 31 | updateConfig(chain, "OmnicFeeManager", feeManager.address); 32 | } else { 33 | console.log("found deployed OmnicFeeManager:", feeAddr); 34 | feeManager = await ethers.getContractAt("OmnicFeeManager", feeAddr); 35 | } 36 | return feeManager; 37 | } 38 | 39 | const main = async function () { 40 | let chain = hre.network.name; 41 | await deployFeeManager(chain); 42 | } 43 | 44 | // We recommend this pattern to be able to use async/await everywhere 45 | // and properly handle errors. 46 | main().catch((error) => { 47 | console.error(error); 48 | process.exitCode = 1; 49 | }); 50 | -------------------------------------------------------------------------------- /core/evm/scripts/deploy-impl.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { getContractAddr, updateConfig } from "./helpers"; 3 | import fs from 'fs'; 4 | const hre = require("hardhat"); 5 | 6 | // deploy Omnic implementataion contract 7 | // and set the implementation address to UpgradeBeacon by calling UpgradeBeaconController 8 | 9 | export const deployOmnicImpl = async function (chain: string, upgrade: boolean) { 10 | const Omnic = await ethers.getContractFactory("Omnic"); 11 | 12 | const omnicAddr = getContractAddr(chain, "Implementation"); 13 | let omnic; 14 | // if it is upgrade, redeploy the implementation even though we found an existing implemenataion 15 | // otherwise, just return the deployed one 16 | if(omnicAddr == null || upgrade == true) { 17 | console.log("deploying Omnic implemenataion..."); 18 | omnic = await Omnic.deploy(); 19 | 20 | await omnic.deployed(); 21 | console.log("chain: ", chain, "Omnic implementation deployed to:", omnic.address); 22 | } else { 23 | console.log("found deployed Omnic implementation:", omnicAddr); 24 | omnic = await ethers.getContractAt("Omnic", omnicAddr); 25 | } 26 | // first deployment 27 | if(upgrade == false) { 28 | // recording omnic contract address 29 | updateConfig(chain, "Implementation", omnic.address); 30 | } 31 | return omnic; 32 | } 33 | 34 | const main = async function () { 35 | let chain = hre.network.name; 36 | const upgrade = false; 37 | await deployOmnicImpl(chain, upgrade); 38 | } 39 | 40 | // We recommend this pattern to be able to use async/await everywhere 41 | // and properly handle errors. 42 | main().catch((error) => { 43 | console.error(error); 44 | process.exitCode = 1; 45 | }); 46 | -------------------------------------------------------------------------------- /core/evm/scripts/deploy-proxy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { getContractAddr, updateConfig, getProxyCanisterAddr, encodeCalldata } from "./helpers"; 3 | const hre = require("hardhat"); 4 | 5 | // deploy upgrade related contracts: 6 | // 1. UpgradeBeaconController: controls UpgradeBeacon 7 | // 2. UpgradeBeacon: stores the implementation address 8 | // 3. UpgradeBeaconProxy: the proxy contract that users interact with 9 | 10 | const deployProxy = async function (chain: string) { 11 | const UpgradeBeaconController = await ethers.getContractFactory("UpgradeBeaconController"); 12 | const UpgradeBeacon = await ethers.getContractFactory("UpgradeBeacon"); 13 | const UpgradeBeaconProxy = await ethers.getContractFactory("UpgradeBeaconProxy"); 14 | 15 | // deploy UpgradeBeaconController 16 | const controllerAddr = getContractAddr(chain, "UpgradeBeaconController"); 17 | let controller; 18 | if(controllerAddr == null) { 19 | console.log("deploying UpgradeBeaconController..."); 20 | controller = await UpgradeBeaconController.deploy(); 21 | 22 | await controller.deployed(); 23 | console.log("chain: ", chain, "UpgradeBeaconController deployed to:", controller.address); 24 | // recording omnic contract address 25 | updateConfig(chain, "UpgradeBeaconController", controller.address); 26 | } else { 27 | console.log("found deployed UpgradeBeaconController:", controllerAddr); 28 | controller = await ethers.getContractAt("UpgradeBeaconController", controllerAddr); 29 | } 30 | 31 | const implAddr = getContractAddr(chain, "Implementation"); 32 | if(implAddr == null) { 33 | console.log("Implementation not found! Please deploy an initial implemetation first!"); 34 | return null; 35 | } 36 | 37 | // deploy UpgradeBeacon 38 | const beaconAddr = getContractAddr(chain, "UpgradeBeacon"); 39 | let beacon; 40 | if(beaconAddr == null) { 41 | console.log("deploying UpgradeBeacon..."); 42 | beacon = await UpgradeBeacon.deploy(implAddr, controller.address); 43 | 44 | await beacon.deployed(); 45 | console.log("chain: ", chain, "UpgradeBeacon deployed to:", beacon.address); 46 | // recording omnic contract address 47 | updateConfig(chain, "UpgradeBeacon", beacon.address); 48 | } else { 49 | console.log("found deployed UpgradeBeacon:", beaconAddr); 50 | beacon = await ethers.getContractAt("UpgradeBeacon", beaconAddr); 51 | } 52 | 53 | // deploy UpgradeBeaconProxy 54 | // deploy UpgradeBeacon 55 | const proxyAddr = getContractAddr(chain, "UpgradeBeaconProxy"); 56 | let proxy; 57 | if(proxyAddr == null) { 58 | console.log("deploying UpgradeBeaconProxy..."); 59 | // initializer call data 60 | const initCalldata = encodeCalldata(getProxyCanisterAddr(), getContractAddr(chain, "OmnicFeeManager")); 61 | proxy = await UpgradeBeaconProxy.deploy(beacon.address, initCalldata); 62 | 63 | await proxy.deployed(); 64 | console.log("chain: ", chain, "UpgradeBeaconProxy deployed to:", proxy.address); 65 | // recording omnic contract address 66 | updateConfig(chain, "UpgradeBeaconProxy", proxy.address); 67 | } else { 68 | console.log("found deployed UpgradeBeaconProxy:", proxyAddr); 69 | proxy = ethers.getContractAt("UpgradeBeaconProxy", proxyAddr); 70 | } 71 | } 72 | 73 | const main = async function() { 74 | let chain = hre.network.name; 75 | await deployProxy(chain); 76 | console.log("All done!") 77 | } 78 | 79 | // We recommend this pattern to be able to use async/await everywhere 80 | // and properly handle errors. 81 | main().catch((error) => { 82 | console.error(error); 83 | process.exitCode = 1; 84 | }); 85 | -------------------------------------------------------------------------------- /core/evm/scripts/helpers.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { ethers } from "hardhat"; 3 | 4 | export const getChainId = function(network: string | number) { 5 | if(typeof network == "number") { 6 | return network; 7 | } 8 | let config = JSON.parse(fs.readFileSync('./config.json', 'utf-8')); 9 | return config.ChainIds[network]; 10 | } 11 | 12 | export const updateConfig = function (network: string, contract: string, addr: string) { 13 | let config = JSON.parse(fs.readFileSync('./config.json', 'utf-8')); 14 | if(config.networks[network] == undefined) { 15 | config.networks[network] = { 16 | "UpgradeBeaconController": "", 17 | "UpgradeBeacon": "", 18 | "UpgradeBeaconProxy": "", 19 | "Implementation": "", 20 | "Demo": "", 21 | }; 22 | } 23 | config.networks[network][contract] = addr; 24 | fs.writeFileSync("config.json", JSON.stringify(config, null, "\t")); 25 | console.log(network, ":", config.networks[network]); 26 | } 27 | 28 | export const getContractAddr = function(network: string, contract: string) { 29 | let config = JSON.parse(fs.readFileSync('./config.json', 'utf-8')); 30 | if(config.networks[network] == undefined) { 31 | return null; 32 | } 33 | if(config.networks[network][contract] == undefined) { 34 | return null; 35 | } 36 | let res = config.networks[network][contract]; 37 | if(res == "") { 38 | return null; 39 | } 40 | return res; 41 | } 42 | 43 | export const getProxyCanisterAddr = function() { 44 | let config = JSON.parse(fs.readFileSync('./config.json', 'utf-8')); 45 | return config.OmnicCanisterAddr; 46 | } 47 | 48 | export const abi_encode = function(abi: Array, func: string, args: Array) { 49 | let iface = new ethers.utils.Interface(abi); 50 | return iface.encodeFunctionData(func, args); 51 | } 52 | 53 | export const encodeCalldata = function(addr: string, addr1: string) { 54 | let abi = [ 55 | "function initialize(address proxyCanisterAddr, address feeManagerAddr)" 56 | ]; 57 | return abi_encode(abi, "initialize", [addr, addr1]); 58 | } 59 | -------------------------------------------------------------------------------- /core/evm/scripts/send-msg.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import fs from "fs"; 3 | import { getContractAddr, getChainId } from "./helpers"; 4 | const hre = require("hardhat"); 5 | 6 | const send_msg = async function(chain: string, dst_chain: string, recipient: string, data: string) { 7 | const Omnic = await ethers.getContractFactory("Omnic"); 8 | const beaconProxyAddr = getContractAddr(chain, "UpgradeBeaconProxy"); 9 | const omnic = await Omnic.attach(beaconProxyAddr); 10 | 11 | const addrs = await ethers.getSigners(); 12 | let caller = addrs[0].address; 13 | 14 | console.log(`sending message from ${chain} to ${dst_chain}, recipient: ${recipient}, data: ${data}`); 15 | let tx = await omnic.sendMessage( 16 | getChainId(dst_chain), recipient, data, caller, caller, { 17 | value: ethers.utils.parseEther("0.001") 18 | } 19 | ); 20 | console.log("txhash:", tx.hash); 21 | console.log("new merkle root:", await omnic.getLatestRoot()); 22 | } 23 | 24 | async function main() { 25 | const chain = hre.network.name; 26 | let dst_chain = "mumbai"; 27 | let recipient_addr = "0xcD5330aCf97E53489E3093Da52844e4D57b6Eae8"; 28 | let recipient = ethers.utils.hexZeroPad(recipient_addr, 32); 29 | let data = ethers.utils.hexlify(ethers.utils.toUtf8Bytes("hello omnic demo app on polygon!")); 30 | await send_msg(chain, dst_chain, recipient, data); 31 | } 32 | 33 | // We recommend this pattern to be able to use async/await everywhere 34 | // and properly handle errors. 35 | main().catch((error) => { 36 | console.error(error); 37 | process.exitCode = 1; 38 | }); 39 | -------------------------------------------------------------------------------- /core/evm/scripts/set-demo-omnic-addr.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { getContractAddr, updateConfig } from "./helpers"; 3 | import fs from 'fs'; 4 | const hre = require("hardhat"); 5 | 6 | // deploy Omnic implementataion contract 7 | // and set the implementation address to UpgradeBeacon by calling UpgradeBeaconController 8 | 9 | export const setDemoOmnicAddr = async function (chain: string) { 10 | const DemoApp = await ethers.getContractFactory("DemoApp"); 11 | 12 | const omnicAddr = getContractAddr(chain, "UpgradeBeaconProxy"); 13 | const demoAddr = getContractAddr(chain, "Demo"); 14 | let demo; 15 | if(demoAddr == null) { 16 | console.log("deploying Demo App..."); 17 | demo = await DemoApp.deploy(omnicAddr); 18 | 19 | await demo.deployed(); 20 | console.log("chain: ", chain, "DemoApp deployed to:", demo.address); 21 | updateConfig(chain, "Demo", demo.address); 22 | } else { 23 | console.log("found deployed DempApp:", demoAddr); 24 | demo = await ethers.getContractAt("DemoApp", demoAddr); 25 | } 26 | let tx = await demo.setOmnicContractAddr(omnicAddr); 27 | console.log("txhash:", tx.hash); 28 | } 29 | 30 | const main = async function () { 31 | let chain = hre.network.name; 32 | await setDemoOmnicAddr(chain); 33 | } 34 | 35 | // We recommend this pattern to be able to use async/await everywhere 36 | // and properly handle errors. 37 | main().catch((error) => { 38 | console.error(error); 39 | process.exitCode = 1; 40 | }); 41 | -------------------------------------------------------------------------------- /core/evm/scripts/set-fee-whitelist.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { getContractAddr, updateConfig, getProxyCanisterAddr, encodeCalldata } from "./helpers"; 3 | const hre = require("hardhat"); 4 | 5 | // deploy upgrade related contracts: 6 | // 1. UpgradeBeaconController: controls UpgradeBeacon 7 | // 2. UpgradeBeacon: stores the implementation address 8 | // 3. UpgradeBeaconProxy: the proxy contract that users interact with 9 | 10 | const setWhitelist = async function (chain: string) { 11 | const UpgradeBeaconProxy = await ethers.getContractFactory("UpgradeBeaconProxy"); 12 | 13 | // deploy UpgradeBeaconProxy 14 | // deploy UpgradeBeacon 15 | const proxyAddr = getContractAddr(chain, "UpgradeBeaconProxy"); 16 | console.log("found deployed UpgradeBeaconProxy:", proxyAddr); 17 | let omnic = await ethers.getContractAt("Omnic", proxyAddr); 18 | 19 | // set fee whitelist 20 | let bridgeAddr = getContractAddr(chain, "Bridge"); 21 | let tx = await omnic.setWhitelist(bridgeAddr, true); 22 | console.log("setWhitelist tx:", tx); 23 | } 24 | 25 | const main = async function() { 26 | let chain = hre.network.name; 27 | await setWhitelist(chain); 28 | console.log("All done!") 29 | } 30 | 31 | // We recommend this pattern to be able to use async/await everywhere 32 | // and properly handle errors. 33 | main().catch((error) => { 34 | console.error(error); 35 | process.exitCode = 1; 36 | }); 37 | -------------------------------------------------------------------------------- /core/evm/scripts/upgrade-impl.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { getContractAddr, updateConfig } from "./helpers"; 3 | import fs from 'fs'; 4 | const hre = require("hardhat"); 5 | import { deployOmnicImpl } from "./deploy-impl"; 6 | 7 | // deploy Omnic implementataion contract 8 | // and set the implementation address to UpgradeBeacon by calling UpgradeBeaconController 9 | 10 | export const setOmnicImpl = async function (chain: string, implAddr: string) { 11 | let controller = await ethers.getContractAt( 12 | "UpgradeBeaconController", 13 | getContractAddr(chain, "UpgradeBeaconController") 14 | ); 15 | 16 | console.log("call UpgradeBeaconController.upgrade..."); 17 | let tx = await controller.upgrade( 18 | getContractAddr(chain, "UpgradeBeacon"), 19 | implAddr 20 | ); 21 | console.log("txhash:", tx.hash); 22 | 23 | console.log("saving implementation to config..."); 24 | updateConfig(chain, "Implementation", implAddr); 25 | } 26 | 27 | const main = async function () { 28 | let chain = hre.network.name; 29 | const upgrade = true; 30 | const impl = await deployOmnicImpl(chain, upgrade); 31 | await setOmnicImpl(chain, impl.address); 32 | } 33 | 34 | // We recommend this pattern to be able to use async/await everywhere 35 | // and properly handle errors. 36 | main().catch((error) => { 37 | console.error(error); 38 | process.exitCode = 1; 39 | }); 40 | -------------------------------------------------------------------------------- /core/evm/test/Omnic.ts: -------------------------------------------------------------------------------- 1 | import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 2 | import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; 3 | import { expect } from "chai"; 4 | import { ethers } from "hardhat"; 5 | 6 | describe("Omnic", function () { 7 | // We define a fixture to reuse the same setup in every test. 8 | // We use loadFixture to run this setup once, snapshot that state, 9 | // and reset Hardhat Network to that snapshopt in every test. 10 | 11 | // describe("Deployment", function () { 12 | // it("Should set the right unlockTime", async function () { 13 | // const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); 14 | 15 | // expect(await lock.unlockTime()).to.equal(unlockTime); 16 | // }); 17 | // }); 18 | describe("SendMessage", function() { 19 | it("Should send msg", async function () { 20 | const Omnic = await ethers.getContractFactory("Omnic"); 21 | const omnic = await Omnic.deploy(); 22 | 23 | let recipient_addr = "0xcD5330aCf97E53489E3093Da52844e4D57b6Eae8"; 24 | let recipient = ethers.utils.hexZeroPad(recipient_addr, 32); 25 | let data = ethers.utils.hexlify(ethers.utils.toUtf8Bytes("hello omnic demo app on polygon!")); 26 | const tx = await omnic.sendMessage(0, recipient, data); 27 | //const tx1 = await omnic.processMessage(recipient + data.slice(2)) 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /core/evm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/ic/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target/ 3 | .idea/ -------------------------------------------------------------------------------- /core/ic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "omnic" 3 | version = "0.1.0" 4 | description = "Omnic Core" 5 | homepage = "https://github.com/rocklabs-io/omnic" 6 | repository = "https://github.com/rocklabs-io/omnic" 7 | license = "MIT" 8 | keywords = ["dfinity", "icp", "web3", "ethereum", "crosschain"] 9 | authors = ["Rocklabs "] 10 | readme = "README.md" 11 | edition = "2018" 12 | 13 | [[bin]] 14 | name = "proxy" 15 | path = "src/canisters/proxy.rs" 16 | 17 | [[bin]] 18 | name = "gateway" 19 | path = "src/canisters/gateway.rs" 20 | 21 | [[bin]] 22 | name = "demo" 23 | path = "src/canisters/demo.rs" 24 | 25 | [dependencies] 26 | accumulator = { path = "./accumulator" } 27 | candid = "0.8.0" 28 | ic-cdk = "=0.6.8" 29 | ic-cdk-macros = "=0.6.8" 30 | serde = { version = "1.0.137", features = ["derive"]} 31 | serde_json = "1.0.41" 32 | serde_bytes = "0.11.5" 33 | num-traits = "0.2.15" 34 | sha3 = "0.10.1" 35 | hex = "0.4.3" 36 | rand = "0.8.5" 37 | tiny-keccak = { version = "2.0.1", features = ["keccak"] } 38 | thiserror = "*" 39 | async-trait = { version = "0.1.42", default-features = false } 40 | ic-web3 = { git = "https://github.com/rocklabs-io/ic-web3", rev = "cedffa9764c22b7ae35014825aa49314feb8e1c9"} 41 | # ic-web3 = { path = "../../../../ic-web3" } 42 | #ic-web3 = "0.1.6" 43 | #ic-cron = "0.7.0" 44 | ic-cron = { git = "https://github.com/rocklabs-io/ic-cron" } -------------------------------------------------------------------------------- /core/ic/accumulator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accumulator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["James Prestwich ", "The Nomad Developers "] 6 | description = "Nomad sparse merle tree" 7 | repository = "https://github.com/nomad-xyz/rust" 8 | license = "MIT OR Apache-2.0" 9 | exclude = [ 10 | "*.sh", 11 | ".git*" 12 | ] 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | ic-web3 = { git = "https://github.com/rocklabs-io/ic-web3" } 21 | sha3 = "0.9.1" 22 | thiserror = "1.0.30" 23 | serde = {version = "1.0", features = ["derive"]} 24 | affix = "0.1.2" 25 | once_cell = "1.8.0" 26 | 27 | [target.'cfg(target_arch = "wasm32")'.dependencies] 28 | wee_alloc = "0.4.5" 29 | js-sys = "0.3.56" 30 | wasm-bindgen = { version = "0.2.79", features = ["serde-serialize"] } 31 | affix = "0.1.2" -------------------------------------------------------------------------------- /core/ic/accumulator/README.md: -------------------------------------------------------------------------------- 1 | Merkle tree module 2 | 3 | Modified from: https://github.com/nomad-xyz/rust/accumulator -------------------------------------------------------------------------------- /core/ic/accumulator/src/error.rs: -------------------------------------------------------------------------------- 1 | // use ethers::prelude::H256; 2 | use ic_web3::types::H256; 3 | use thiserror; 4 | 5 | /// Tree Errors 6 | #[derive(Debug, thiserror::Error, Clone, Copy)] 7 | pub enum ProvingError { 8 | /// Index is above tree max size 9 | #[error("Requested proof for index above u32::MAX: {0}")] 10 | IndexTooHigh(usize), 11 | /// Requested proof for a zero element 12 | #[error("Requested proof for a zero element. Requested: {index}. Tree has: {count}")] 13 | ZeroProof { 14 | /// The index requested 15 | index: usize, 16 | /// The number of leaves 17 | count: usize, 18 | }, 19 | } 20 | 21 | /// Tree Errors 22 | #[derive(Debug, thiserror::Error, Clone, Copy)] 23 | pub enum VerifyingError { 24 | /// Failed proof verification 25 | #[error("Proof verification failed. Root is {expected}, produced is {actual}")] 26 | #[allow(dead_code)] 27 | VerificationFailed { 28 | /// The expected root (this tree's current root) 29 | expected: H256, 30 | /// The root produced by branch evaluation 31 | actual: H256, 32 | }, 33 | } 34 | 35 | /// Error type for merkle tree ops. 36 | #[derive(Debug, PartialEq, Clone, Copy, thiserror::Error)] 37 | pub enum IngestionError { 38 | /// Trying to push in a leaf 39 | #[error("Trying to push in a leaf")] 40 | LeafReached, 41 | /// No more space in the MerkleTree 42 | #[error("No more space in the MerkleTree")] 43 | MerkleTreeFull, 44 | /// MerkleTree is invalid 45 | #[error("MerkleTree is invalid")] 46 | Invalid, 47 | /// Incorrect Depth provided 48 | #[error("Incorrect Depth provided")] 49 | DepthTooSmall, 50 | } 51 | -------------------------------------------------------------------------------- /core/ic/accumulator/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![warn(missing_docs)] 3 | #![warn(missing_debug_implementations)] 4 | #![warn(missing_copy_implementations)] 5 | 6 | /// A full incremental merkle. Suitable for running off-chain. 7 | pub mod full; 8 | 9 | /// Hashing utils 10 | pub mod utils; 11 | 12 | /// Common error types for the merkle trees. 13 | pub mod error; 14 | 15 | /// A lightweight incremental merkle, suitable for running on-chain. Stores O 16 | /// (1) data 17 | pub mod light; 18 | /// Merkle Proof struct 19 | pub mod proof; 20 | 21 | /// A full incremental merkle tree. Suitable for proving. 22 | pub mod tree; 23 | 24 | // #[cfg(target_arch = "wasm32")] 25 | // /// Wasm bindings for common operations 26 | // pub mod wasm; 27 | 28 | #[cfg(target_arch = "wasm32")] 29 | #[cfg_attr(target_arch = "wasm32", global_allocator)] 30 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 31 | 32 | // use ethers::{core::types::H256, prelude::U256}; 33 | use ic_web3::types::{H256, U256}; 34 | use once_cell::sync::Lazy; 35 | 36 | /// Tree depth 37 | pub const TREE_DEPTH: usize = 32; 38 | 39 | 40 | const EMPTY_SLICE: &[H256] = &[]; 41 | 42 | pub use error::*; 43 | pub use full::*; 44 | pub use light::*; 45 | pub use proof::*; 46 | pub use tree::*; 47 | 48 | pub use utils::*; 49 | 50 | /// A cache of the zero hashes for each layer of the tree. 51 | pub static ZERO_HASHES: Lazy<[H256; TREE_DEPTH + 1]> = Lazy::new(|| { 52 | let mut hashes = [H256::zero(); TREE_DEPTH + 1]; 53 | for i in 0..TREE_DEPTH { 54 | hashes[i + 1] = hash_concat(hashes[i], hashes[i]); 55 | } 56 | hashes 57 | }); 58 | 59 | /// A merkle proof 60 | pub trait MerkleProof { 61 | /// Calculate the merkle root of this proof's branch 62 | fn root(&self) -> H256; 63 | } 64 | 65 | /// A simple trait for merkle-based accumulators 66 | pub trait Merkle: std::fmt::Debug + Default { 67 | /// A proof of some leaf in this tree 68 | type Proof: MerkleProof; 69 | 70 | /// The maximum number of elements the tree can ingest 71 | fn max_elements() -> U256; 72 | 73 | /// The number of elements currently in the tree 74 | fn count(&self) -> usize; 75 | 76 | /// Calculate the root hash of this Merkle tree. 77 | fn root(&self) -> H256; 78 | 79 | /// Get the tree's depth. 80 | fn depth(&self) -> usize; 81 | 82 | /// Push a leaf to the tree 83 | fn ingest(&mut self, element: H256) -> Result; 84 | 85 | /// Verify a proof against this tree's root. 86 | fn verify(&self, proof: &Self::Proof) -> Result<(), VerifyingError> { 87 | let actual = proof.root(); 88 | let expected = self.root(); 89 | if expected == actual { 90 | Ok(()) 91 | } else { 92 | Err(VerifyingError::VerificationFailed { expected, actual }) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/ic/accumulator/src/light.rs: -------------------------------------------------------------------------------- 1 | // use ethers::{core::types::H256, prelude::U256}; 2 | use ic_web3::types::{H256, U256}; 3 | 4 | use crate::{ 5 | error::IngestionError, utils::hash_concat, Merkle, MerkleProof, Proof, TREE_DEPTH, ZERO_HASHES, 6 | }; 7 | 8 | #[derive(Debug, Clone, Copy)] 9 | /// An incremental merkle tree, modeled on the eth2 deposit contract 10 | pub struct LightMerkle { 11 | branch: [H256; N], 12 | count: usize, 13 | } 14 | 15 | impl Default for LightMerkle { 16 | fn default() -> Self { 17 | let mut branch: [H256; N] = [Default::default(); N]; 18 | branch 19 | .iter_mut() 20 | .enumerate() 21 | .for_each(|(i, elem)| *elem = ZERO_HASHES[i]); 22 | Self { branch, count: 0 } 23 | } 24 | } 25 | 26 | impl Merkle for LightMerkle { 27 | type Proof = Proof; 28 | 29 | /// Return the maximum number of leaves in this tree 30 | fn max_elements() -> U256 { 31 | crate::utils::max_leaves(N) 32 | } 33 | 34 | fn count(&self) -> usize { 35 | self.count 36 | } 37 | 38 | fn root(&self) -> H256 { 39 | let mut node: H256 = Default::default(); 40 | let mut size = self.count; 41 | 42 | self.branch.iter().enumerate().for_each(|(i, elem)| { 43 | node = if (size & 1) == 1 { 44 | crate::utils::hash_concat(elem, node) 45 | } else { 46 | crate::utils::hash_concat(node, ZERO_HASHES[i]) 47 | }; 48 | size /= 2; 49 | }); 50 | 51 | node 52 | } 53 | 54 | fn depth(&self) -> usize { 55 | N 56 | } 57 | 58 | fn ingest(&mut self, element: H256) -> Result { 59 | let mut node = element; 60 | if Self::max_leaves() <= self.count.into() { 61 | return Err(IngestionError::MerkleTreeFull); 62 | } 63 | assert!(self.count < u32::MAX as usize); 64 | self.count += 1; 65 | let mut size = self.count; 66 | for i in 0..TREE_DEPTH { 67 | if (size & 1) == 1 { 68 | self.branch[i] = node; 69 | return Ok(self.root()); 70 | } 71 | node = hash_concat(self.branch[i], node); 72 | size /= 2; 73 | } 74 | unreachable!() 75 | } 76 | } 77 | 78 | impl LightMerkle { 79 | /// Return the maximum number of leaves in this tree 80 | pub fn max_leaves() -> U256 { 81 | crate::utils::max_leaves(N) 82 | } 83 | 84 | /// Instantiate a new tree with a known depth and a starting leaf-set 85 | pub fn from_leaves(leaves: &[H256]) -> Self { 86 | let mut tree = Self::default(); 87 | 88 | for leaf in leaves.iter() { 89 | tree.ingest(*leaf).unwrap(); 90 | } 91 | 92 | tree 93 | } 94 | 95 | /// Calculate the initital root of a tree of this depth 96 | pub fn initial_root() -> H256 { 97 | LightMerkle::::default().root() 98 | } 99 | /// Get the leading-edge branch. 100 | pub fn branch(&self) -> &[H256; N] { 101 | &self.branch 102 | } 103 | 104 | /// Verify a incremental merkle proof of inclusion 105 | pub fn verify(&self, proof: &Proof) -> bool { 106 | proof.root() == self.root() 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod test { 112 | use super::*; 113 | #[test] 114 | fn it_calculates_the_initial_root() { 115 | assert_eq!( 116 | LightMerkle::<32>::initial_root(), 117 | "0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757" 118 | .parse() 119 | .unwrap() 120 | ); 121 | } 122 | 123 | // use ethers::utils::hash_message; 124 | // use super::*; 125 | // use crate::test_utils; 126 | // #[test] 127 | // fn it_computes_branch_roots() { 128 | // let test_cases = test_utils::load_merkle_test_json(); 129 | // for test_case in test_cases.iter() { 130 | // let mut tree = IncrementalMerkle::default(); 131 | // // insert the leaves 132 | // for leaf in test_case.leaves.iter() { 133 | // let hashed_leaf = hash_message(leaf); 134 | // tree.ingest(hashed_leaf); 135 | // } 136 | // // assert the tree has the proper leaf count 137 | // assert_eq!(tree.count(), test_case.leaves.len()); 138 | // // assert the tree generates the proper root 139 | // let root = tree.root(); // root is type H256 140 | // assert_eq!(root, test_case.expected_root); 141 | // for n in 0..test_case.leaves.len() { 142 | // // check that the tree can verify the proof for this leaf 143 | // assert!(tree.verify(&test_case.proofs[n])); 144 | // } 145 | // } 146 | // } 147 | } 148 | -------------------------------------------------------------------------------- /core/ic/accumulator/src/proof.rs: -------------------------------------------------------------------------------- 1 | use crate::{merkle_root_from_branch, MerkleProof}; 2 | // use ethers::prelude::H256; 3 | use ic_web3::types::H256; 4 | 5 | /// A merkle proof object. The leaf, its path to the root, and its index in the 6 | /// tree. 7 | #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Proof { 10 | /// The leaf 11 | pub leaf: H256, 12 | /// The index 13 | pub index: usize, 14 | /// The merkle branch 15 | #[serde(with = "const_array_serde")] 16 | pub path: [H256; N], 17 | } 18 | 19 | mod const_array_serde { 20 | use super::H256; 21 | use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serializer}; 22 | 23 | pub fn serialize(item: &[H256; N], serializer: S) -> Result 24 | where 25 | S: Serializer, 26 | { 27 | let mut seq = serializer.serialize_seq(Some(N))?; 28 | for i in item { 29 | seq.serialize_element(i)?; 30 | } 31 | seq.end() 32 | } 33 | 34 | pub fn deserialize<'de, D, const N: usize>(d: D) -> Result<[H256; N], D::Error> 35 | where 36 | D: Deserializer<'de>, 37 | { 38 | let v: Vec = Deserialize::deserialize(d)?; 39 | if v.len() != N { 40 | Err(serde::de::Error::custom(format!( 41 | "Expected a sequence with {} elements. Got {} elements", 42 | N, 43 | v.len() 44 | ))) 45 | } else { 46 | let mut h: [H256; N] = [Default::default(); N]; 47 | h.copy_from_slice(&v[..N]); 48 | Ok(h) 49 | } 50 | } 51 | } 52 | 53 | impl MerkleProof for Proof { 54 | /// Calculate the merkle root produced by evaluating the proof 55 | fn root(&self) -> H256 { 56 | merkle_root_from_branch(self.leaf, self.path.as_ref(), N, self.index) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/ic/accumulator/src/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::{full::MerkleTree, IngestionError, LightMerkle, Merkle, Proof, ProvingError}; 2 | // use ethers::{core::types::H256, prelude::U256}; 3 | use ic_web3::types::{H256, U256}; 4 | 5 | /// A simplified interface for a full sparse merkle tree 6 | #[derive(Debug, PartialEq, Clone)] 7 | pub struct Tree { 8 | count: usize, 9 | tree: Box, 10 | } 11 | 12 | impl Default for Tree { 13 | fn default() -> Self { 14 | Self::from_leaves(&[]) 15 | } 16 | } 17 | 18 | impl Merkle for Tree { 19 | type Proof = Proof; 20 | 21 | /// Return the maximum number of leaves in this tree 22 | fn max_elements() -> U256 { 23 | crate::utils::max_leaves(N) 24 | } 25 | 26 | fn count(&self) -> usize { 27 | self.count 28 | } 29 | 30 | fn root(&self) -> H256 { 31 | self.tree.hash() 32 | } 33 | 34 | fn depth(&self) -> usize { 35 | N 36 | } 37 | 38 | fn ingest(&mut self, element: H256) -> Result { 39 | self.count += 1; 40 | self.tree.push_leaf(element, N)?; 41 | Ok(self.tree.hash()) 42 | } 43 | } 44 | 45 | impl Tree { 46 | /// Instantiate a new tree with a known depth and a starting leaf-set 47 | pub fn from_leaves(leaves: &[H256]) -> Self { 48 | Self { 49 | count: leaves.len(), 50 | tree: Box::new(MerkleTree::create(leaves, N)), 51 | } 52 | } 53 | 54 | /// Calculate the initital root of a tree of this depth 55 | pub fn initial_root() -> H256 { 56 | LightMerkle::::default().root() 57 | } 58 | 59 | /// Return the leaf at `index` and a Merkle proof of its inclusion. 60 | /// 61 | /// The Merkle proof is in "bottom-up" order, starting with a leaf node 62 | /// and moving up the tree. Its length will be exactly equal to `depth`. 63 | pub fn prove(&self, index: usize) -> Result, ProvingError> { 64 | if index > 2usize.pow(N.try_into().unwrap()) - 1 { 65 | return Err(ProvingError::IndexTooHigh(index)); 66 | } 67 | 68 | let count = self.count(); 69 | if index >= count { 70 | return Err(ProvingError::ZeroProof { index, count }); 71 | } 72 | 73 | let (leaf, nodes) = self.tree.generate_proof(index, N); 74 | debug_assert_eq!(nodes.len(), N); 75 | let mut path = [H256::default(); N]; 76 | path.copy_from_slice(&nodes[..N]); 77 | Ok(Proof { leaf, index, path }) 78 | } 79 | } 80 | 81 | impl From for Tree 82 | where 83 | T: AsRef<[H256]>, 84 | { 85 | fn from(t: T) -> Self { 86 | Self::from_leaves(t.as_ref()) 87 | } 88 | } 89 | 90 | impl std::iter::FromIterator for Tree { 91 | /// Will panic if the tree fills 92 | fn from_iter>(iter: I) -> Self { 93 | let mut prover = Self::default(); 94 | prover.extend(iter); 95 | prover 96 | } 97 | } 98 | 99 | impl std::iter::Extend for Tree { 100 | /// Will panic if the tree fills 101 | fn extend>(&mut self, iter: I) { 102 | for i in iter { 103 | self.ingest(i).expect("!tree full"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /core/ic/accumulator/src/utils.rs: -------------------------------------------------------------------------------- 1 | use sha3::{Digest, Keccak256}; 2 | 3 | // use ethers::core::types::{H256, U256}; 4 | use ic_web3::types::{H256, U256}; 5 | 6 | /// Return the keccak256 digest of the preimage 7 | pub fn hash(preimage: impl AsRef<[u8]>) -> H256 { 8 | H256::from_slice(Keccak256::digest(preimage.as_ref()).as_slice()) 9 | } 10 | 11 | /// Return the keccak256 disgest of the concatenation of the arguments 12 | pub fn hash_concat(left: impl AsRef<[u8]>, right: impl AsRef<[u8]>) -> H256 { 13 | H256::from_slice( 14 | Keccak256::new() 15 | .chain(left.as_ref()) 16 | .chain(right.as_ref()) 17 | .finalize() 18 | .as_slice(), 19 | ) 20 | } 21 | 22 | /// Max number of leaves in a tree 23 | pub(crate) fn max_leaves(n: usize) -> U256 { 24 | U256::from(2).pow(n.into()) - 1 25 | } 26 | -------------------------------------------------------------------------------- /core/ic/accumulator/src/wasm.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_copy_implementations)] 2 | #![allow(clippy::unused_unit)] 3 | 4 | macro_rules! export_tree { 5 | ($depth:literal) => { 6 | affix::paste! { 7 | mod [] { 8 | use wasm_bindgen::prelude::*; 9 | use crate::{Merkle, MerkleProof}; 10 | 11 | #[wasm_bindgen(inspectable)] 12 | #[derive(Debug, Default, PartialEq)] 13 | #[doc = "A sparse merkle tree of depth " $depth] 14 | pub struct [](pub(crate) crate::Tree<$depth>); 15 | 16 | #[wasm_bindgen(inspectable)] 17 | #[derive(Debug, Clone, PartialEq)] 18 | #[doc = "A merkle proof of depth " $depth] 19 | pub struct [](pub(crate) crate::Proof<$depth>); 20 | 21 | type Internal = crate::Tree<$depth>; 22 | type InternalProof = crate::Proof<$depth>; 23 | 24 | impl From for []{ 25 | fn from(p: InternalProof) -> []{ 26 | [](p) 27 | } 28 | } 29 | 30 | impl From for [] { 31 | fn from(p: Internal) -> [] { 32 | [](p) 33 | } 34 | } 35 | 36 | #[wasm_bindgen] 37 | impl [] { 38 | #[wasm_bindgen(constructor)] 39 | #[doc = "Instantiate a new sparse merkle tree of depth " $depth] 40 | pub fn new() -> [] { 41 | Self(Default::default()) 42 | } 43 | 44 | #[wasm_bindgen(getter)] 45 | /// Get the current count of leaves in the tree 46 | pub fn count(&self) -> usize { 47 | self.0.count() 48 | } 49 | 50 | #[wasm_bindgen(js_name = "initalRoot")] 51 | #[doc = "Calculate the root of an empty sparse merkle tree of depth " $depth] 52 | pub fn initial_root() -> String { 53 | format!("{:?}", Internal::initial_root()) 54 | } 55 | 56 | #[wasm_bindgen] 57 | /// Push a leaf to the tree. Appends it to the first unoccupied slot 58 | /// 59 | /// This will fail if the underlying tree is full. 60 | pub fn ingest(&mut self, element: &str) -> Result { 61 | let h: ic_web3::types::H256 = element 62 | .parse() 63 | .map_err(|e| JsValue::from(format!("Unable to parse element as H256: {}", e)))?; 64 | self.0 65 | .ingest(h) 66 | .map(|root| format!("{:?}", root)) 67 | .map_err(|e| format!("Unable to ingest element: {}", e).into()) 68 | } 69 | 70 | #[wasm_bindgen] 71 | /// Retrieve the root hash of this Merkle tree. 72 | pub fn root(&self) -> String { 73 | format!("{:?}", self.0.root()) 74 | } 75 | 76 | #[wasm_bindgen(getter)] 77 | /// Get the tree's depth 78 | pub fn depth(&self) -> usize { 79 | self.0.depth() 80 | } 81 | 82 | #[wasm_bindgen] 83 | /// Return the leaf at `index` and a Merkle proof of its inclusion. 84 | /// 85 | /// The Merkle proof is in "bottom-up" order, starting with a leaf node 86 | /// and moving up the tree. Its length will be exactly equal to `depth`. 87 | pub fn prove(&self, index: usize) -> Result<[], JsValue> { 88 | self.0 89 | .prove(index) 90 | .map(Into::into) 91 | .map_err(|e| JsValue::from(format!("Unable to get proof for index {}: {}", index, e))) 92 | } 93 | 94 | #[wasm_bindgen] 95 | /// Verify a proof against this tree's root. 96 | pub fn verify(&self, proof: []) -> Result<(), JsValue> { 97 | self.0 98 | .verify(&proof.0) 99 | .map_err(|e| JsValue::from(format!("Proof verification failed: {}", e))) 100 | } 101 | } 102 | 103 | #[wasm_bindgen] 104 | impl [] { 105 | #[wasm_bindgen] 106 | /// Retrieve the root hash of this Merkle proof. 107 | pub fn root(&self) -> String { 108 | format!("{:?}", self.0.root()) 109 | } 110 | 111 | #[wasm_bindgen(getter)] 112 | /// Retrieve the leaf hash of this merkle proof. 113 | pub fn leaf(&self) -> String { 114 | format!("{:?}", self.0.leaf) 115 | } 116 | 117 | #[wasm_bindgen(getter)] 118 | /// Retrieve the leaf index of this merkle proof. 119 | pub fn index(&self) -> usize { 120 | self.0.index 121 | } 122 | 123 | #[wasm_bindgen(getter)] 124 | /// Get the depth of the tree associated with this proof. 125 | pub fn depth(&self) -> usize { 126 | self.0.path.len() 127 | } 128 | 129 | #[wasm_bindgen(getter)] 130 | /// Retrieve the intermediate nodes of this merkle proof. 131 | pub fn path(&self) -> js_sys::Array { 132 | self.0 133 | .path 134 | .iter() 135 | .map(|hash| format!("{:?}", hash)) 136 | .map(JsValue::from) 137 | .collect() 138 | } 139 | } 140 | } 141 | } 142 | }; 143 | } 144 | 145 | export_tree!(2); 146 | export_tree!(4); 147 | export_tree!(8); 148 | export_tree!(16); 149 | export_tree!(32); 150 | -------------------------------------------------------------------------------- /core/ic/canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "goerli-gateway": { 3 | "ic": "hsrxk-riaaa-aaaam-aa2eq-cai" 4 | }, 5 | "mumbai-gateway": { 6 | "ic": "h3s4w-haaaa-aaaam-aa2fa-cai" 7 | }, 8 | "proxy": { 9 | "ic": "y3lks-laaaa-aaaam-aat7q-cai" 10 | } 11 | } -------------------------------------------------------------------------------- /core/ic/dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "canisters": { 3 | "proxy": { 4 | "candid": "src/canisters/proxy.did", 5 | "type": "custom", 6 | "wasm": "target/wasm32-unknown-unknown/release/proxy_opt.wasm", 7 | "build": [ 8 | "cargo build --target wasm32-unknown-unknown --bin proxy --release", 9 | "ic-cdk-optimizer target/wasm32-unknown-unknown/release/proxy.wasm -o target/wasm32-unknown-unknown/release/proxy_opt.wasm" 10 | ] 11 | }, 12 | "goerli-gateway": { 13 | "candid": "src/canisters/gateway.did", 14 | "type": "custom", 15 | "wasm": "target/wasm32-unknown-unknown/release/gateway_opt.wasm", 16 | "build": [ 17 | "cargo build --target wasm32-unknown-unknown --bin gateway --release", 18 | "ic-cdk-optimizer target/wasm32-unknown-unknown/release/gateway.wasm -o target/wasm32-unknown-unknown/release/gateway_opt.wasm" 19 | ] 20 | }, 21 | "mumbai-gateway": { 22 | "candid": "src/canisters/gateway.did", 23 | "type": "custom", 24 | "wasm": "target/wasm32-unknown-unknown/release/gateway_opt.wasm", 25 | "build": [ 26 | "cargo build --target wasm32-unknown-unknown --bin gateway --release", 27 | "ic-cdk-optimizer target/wasm32-unknown-unknown/release/gateway.wasm -o target/wasm32-unknown-unknown/release/gateway_opt.wasm" 28 | ] 29 | }, 30 | "demo": { 31 | "candid": "src/canisters/demo.did", 32 | "type": "custom", 33 | "wasm": "target/wasm32-unknown-unknown/release/demo_opt.wasm", 34 | "build": [ 35 | "cargo build --target wasm32-unknown-unknown --bin demo --release", 36 | "ic-cdk-optimizer target/wasm32-unknown-unknown/release/demo.wasm -o target/wasm32-unknown-unknown/release/demo_opt.wasm" 37 | ] 38 | } 39 | }, 40 | "defaults": { 41 | "build": { 42 | "args": "", 43 | "packtool": "" 44 | } 45 | }, 46 | "networks": { 47 | "local": { 48 | "bind": "127.0.0.1:8000", 49 | "type": "ephemeral" 50 | } 51 | }, 52 | "version": 1 53 | } 54 | -------------------------------------------------------------------------------- /core/ic/readme.md: -------------------------------------------------------------------------------- 1 | ## IC side omnic proxy canister 2 | -------------------------------------------------------------------------------- /core/ic/src/call.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use ic_cdk::api::call::{call, CallResult}; 3 | 4 | use crate::consts::{MAX_RESP_BYTES, CYCLES_PER_CALL}; 5 | use crate::types::Message; 6 | use crate::chains::EVMChainClient; 7 | use crate::traits::chain::HomeContract; 8 | 9 | pub async fn call_to_canister(recipient: Principal, msg_hash: Vec, m: &Message) -> Result { 10 | // call ic recipient canister 11 | let ret: CallResult<(Result,)> = 12 | call(recipient, "handle_message", (msg_hash, m.origin, m.sender.as_bytes(), m.nonce, m.body.clone(), )).await; 13 | match ret { 14 | Ok((res, )) => { 15 | match res { 16 | Ok(r) => { 17 | ic_cdk::println!("handle_message success!"); 18 | return Ok(r); 19 | }, 20 | Err(err) => { 21 | ic_cdk::println!("handle_message failed: {:?}", err); 22 | return Err(format!("handle_message failed: {:?}", err)); 23 | } 24 | } 25 | // message delivered 26 | // Ok(true) 27 | }, 28 | Err((_code, msg)) => { 29 | ic_cdk::println!("call app canister failed: {:?}", (_code, msg.clone())); 30 | // message delivery failed 31 | Err(format!("call app canister failed: {:?}", (_code, msg))) 32 | } 33 | } 34 | } 35 | 36 | pub async fn call_to_chain( 37 | caller: String, 38 | omnic_addr: String, 39 | rpc: String, 40 | dst_chain: u32, 41 | msg_bytes: Vec 42 | ) -> Result { 43 | let client = EVMChainClient::new(rpc.clone(), omnic_addr.clone(), MAX_RESP_BYTES, CYCLES_PER_CALL) 44 | .map_err(|e| format!("init EVMChainClient failed: {:?}", e))?; 45 | client 46 | .dispatch_message(caller, dst_chain, msg_bytes) 47 | .await 48 | .map(|txhash| { 49 | // ic_cdk::println!("dispatch_message txhash: {:?}", hex::encode(txhash)); 50 | // true 51 | hex::encode(txhash) 52 | }) 53 | .map_err(|e| format!("dispatch_message failed: {:?}", e)) 54 | } 55 | -------------------------------------------------------------------------------- /core/ic/src/canisters/demo.did: -------------------------------------------------------------------------------- 1 | type Result = variant { Ok : bool; Err : text }; 2 | service : { 3 | handle_message : (nat32, nat32, vec nat8, vec nat8) -> (Result); 4 | hex_pid : (principal) -> () query; 5 | } -------------------------------------------------------------------------------- /core/ic/src/canisters/demo.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use ic_cdk_macros::{query, update}; 3 | use ic_cdk::export::candid::{candid_method}; 4 | use ic_cdk::export::Principal; 5 | 6 | 7 | #[update(name = "handle_message")] 8 | #[candid_method(update, rename = "handle_message")] 9 | fn handle_message(origin: u32, nonce: u32, sender: Vec, body: Vec) -> Result { 10 | ic_cdk::println!("demo app got message: {:?}", (origin, nonce, hex::encode(&sender), str::from_utf8(&body))); 11 | Ok(true) 12 | } 13 | 14 | #[query(name = "hex_pid")] 15 | #[candid_method(query, rename = "hex_pid")] 16 | fn hex_pid(pid: Principal) { 17 | ic_cdk::println!("pid len: {}", pid.clone().as_slice().len()); 18 | ic_cdk::println!("hex: {:?}", hex::encode(pid.as_slice())); 19 | } 20 | 21 | 22 | fn main() { 23 | candid::export_service!(); 24 | std::print!("{}", __export_service()); 25 | } -------------------------------------------------------------------------------- /core/ic/src/canisters/gateway.did: -------------------------------------------------------------------------------- 1 | type ChainConfig = record { 2 | rpc_urls : vec text; 3 | gateway_addr : principal; 4 | omnic_addr : text; 5 | chain_id : nat32; 6 | chain_type : ChainType; 7 | omnic_start_block : nat64; 8 | }; 9 | type ChainState = record { 10 | next_index : nat32; 11 | canister_addr : text; 12 | config : ChainConfig; 13 | roots : vec vec nat8; 14 | }; 15 | type ChainType = variant { Evm; Solana; Cosmos }; 16 | type Result = variant { Ok : bool; Err : text }; 17 | type Result_1 = variant { Ok : text; Err : text }; 18 | type Result_2 = variant { Ok : ChainState; Err : text }; 19 | type Result_3 = variant { Ok : nat64; Err : text }; 20 | type Result_4 = variant { Ok : StateInfo; Err : text }; 21 | type Result_5 = variant { Ok : nat32; Err : text }; 22 | type StateInfo = record { 23 | fetch_root_period : nat64; 24 | owners : vec principal; 25 | fetch_roots_period : nat64; 26 | query_rpc_number : nat64; 27 | }; 28 | service : () -> { 29 | add_chain : (nat32, vec text, text, nat64) -> (Result); 30 | add_owner : (principal) -> (); 31 | add_urls : (vec text) -> (Result); 32 | fetch_root : (nat64) -> (Result_1); 33 | get_chain : () -> (Result_2) query; 34 | get_gas_price : () -> (Result_3); 35 | get_info : () -> (Result_4) query; 36 | get_latest_root : () -> (text) query; 37 | get_logs : () -> (vec text) query; 38 | get_next_index : () -> (Result_5) query; 39 | get_tx_count : (text) -> (Result_3); 40 | is_valid : (vec nat8, vec vec nat8, nat32) -> (Result) query; 41 | remove_owner : (principal) -> (); 42 | set_fetch_period : (nat64, nat64) -> (Result); 43 | set_next_index : (nat32) -> (Result); 44 | set_rpc_number : (nat64) -> (Result); 45 | } -------------------------------------------------------------------------------- /core/ic/src/canisters/gateway.old.did: -------------------------------------------------------------------------------- 1 | type ChainConfig = record { 2 | rpc_urls : vec text; 3 | omnic_addr : text; 4 | chain_id : nat32; 5 | chain_type : ChainType; 6 | omnic_start_block : nat64; 7 | }; 8 | type ChainState = record { 9 | next_index : nat32; 10 | canister_addr : text; 11 | config : ChainConfig; 12 | roots : vec vec nat8; 13 | }; 14 | type ChainType = variant { Evm; Solana; Cosmos }; 15 | type Result = variant { Ok : bool; Err : text }; 16 | type Result_1 = variant { Ok : text; Err : text }; 17 | type Result_2 = variant { Ok : ChainState; Err : text }; 18 | type Result_3 = variant { Ok : nat64; Err : text }; 19 | type Result_4 = variant { Ok : nat32; Err : text }; 20 | service : () -> { 21 | add_chain : (nat32, vec text, text, nat64) -> (Result); 22 | add_owner : (principal) -> (); 23 | fetch_root : (nat64) -> (Result_1); 24 | get_chain : () -> (Result_2) query; 25 | get_gas_price : () -> (Result_3); 26 | get_latest_root : () -> (Result_1) query; 27 | get_logs : () -> (vec text) query; 28 | get_next_index : () -> (Result_4) query; 29 | get_tx_count : (text) -> (Result_3); 30 | is_valid : (vec nat8, vec vec nat8, nat32) -> (Result) query; 31 | remove_owner : (principal) -> (); 32 | set_fetch_period : (nat64, nat64) -> (Result); 33 | set_next_index : (nat32) -> (Result); 34 | } -------------------------------------------------------------------------------- /core/ic/src/canisters/gateway.rs: -------------------------------------------------------------------------------- 1 | /* 2 | omnic proxy canister: 3 | fetch_root: fetch merkel roots from all supported chains and insert to chain state 4 | */ 5 | 6 | use std::cell::{RefCell}; 7 | use std::collections::{HashMap, VecDeque}; 8 | use std::convert::TryInto; 9 | 10 | use rand::{rngs::StdRng, SeedableRng}; 11 | use rand::seq::SliceRandom; 12 | 13 | use ic_cdk::api::management_canister::http_request::{HttpResponse, TransformArgs}; 14 | use ic_cron::task_scheduler::TaskScheduler; 15 | use ic_web3::types::H256; 16 | 17 | use ic_cdk_macros::{init, post_upgrade, pre_upgrade, query, update, heartbeat}; 18 | use ic_cdk::export::candid::{candid_method, CandidType, Deserialize}; 19 | use candid::types::principal::Principal; 20 | 21 | use ic_cron::types::Iterations; 22 | 23 | use accumulator::{TREE_DEPTH, merkle_root_from_branch}; 24 | use omnic::{Message, chains::EVMChainClient, ChainConfig, ChainState, ChainType}; 25 | use omnic::HomeContract; 26 | use omnic::consts::{MAX_RESP_BYTES, CYCLES_PER_CALL, CYCLES_PER_BYTE}; 27 | use omnic::state::{State, StateMachine, StateMachineStable, StateInfo}; 28 | use omnic::utils::check_roots_result; 29 | 30 | ic_cron::implement_cron!(); 31 | 32 | #[derive(CandidType, Deserialize, Clone)] 33 | enum Task { 34 | FetchRoots, 35 | FetchRoot 36 | } 37 | 38 | thread_local! { 39 | static STATE_INFO: RefCell = RefCell::new(StateInfo::default()); 40 | static CHAINS: RefCell = RefCell::new(ChainState::default()); 41 | static STATE_MACHINE: RefCell = RefCell::new(StateMachine::default()); 42 | static LOGS: RefCell> = RefCell::new(VecDeque::default()); 43 | } 44 | 45 | #[query] 46 | async fn transform(raw: TransformArgs) -> HttpResponse { 47 | let mut t = raw.response; 48 | t.headers = vec![]; 49 | t 50 | } 51 | 52 | #[query] 53 | #[candid_method(query)] 54 | fn get_logs() -> Vec { 55 | LOGS.with(|l| { 56 | l.borrow().clone().into() 57 | }) 58 | } 59 | 60 | fn get_fetch_root_period() -> u64 { 61 | STATE_INFO.with(|s| s.borrow().fetch_root_period) 62 | } 63 | 64 | fn get_fetch_roots_period() -> u64 { 65 | STATE_INFO.with(|s| s.borrow().fetch_roots_period) 66 | } 67 | 68 | fn get_query_rpc_number() -> u64 { 69 | STATE_INFO.with(|s| s.borrow().query_rpc_number) 70 | } 71 | 72 | #[init] 73 | #[candid_method(init)] 74 | fn init() { 75 | let caller = ic_cdk::api::caller(); 76 | STATE_INFO.with(|info| { 77 | let mut info = info.borrow_mut(); 78 | info.add_owner(caller); 79 | }); 80 | 81 | // set up cron job 82 | cron_enqueue( 83 | Task::FetchRoots, 84 | ic_cron::types::SchedulingOptions { 85 | delay_nano: get_fetch_roots_period(), 86 | interval_nano: get_fetch_roots_period(), 87 | iterations: Iterations::Infinite, 88 | }, 89 | ).unwrap(); 90 | } 91 | 92 | #[update(guard = "is_authorized")] 93 | #[candid_method(update, rename = "set_fetch_period")] 94 | async fn set_fetch_period(fetch_root_period: u64, fetch_roots_period: u64) -> Result { 95 | STATE_INFO.with(|s| { 96 | let mut s = s.borrow_mut(); 97 | s.set_fetch_period(fetch_root_period, fetch_roots_period); 98 | }); 99 | Ok(true) 100 | } 101 | 102 | #[update(guard = "is_authorized")] 103 | #[candid_method(update, rename = "set_rpc_number")] 104 | async fn set_rpc_number(query_rpc_number: u64) -> Result { 105 | let rpc_url_count = CHAINS.with(|c| { 106 | let chain = c.borrow(); 107 | chain.config.rpc_urls.len() 108 | }); 109 | if query_rpc_number <= 0 || query_rpc_number > rpc_url_count as u64 { 110 | return Err("Invalid rpc number".to_string()); 111 | } 112 | STATE_INFO.with(|s| { 113 | let mut s = s.borrow_mut(); 114 | s.set_rpc_number(query_rpc_number); 115 | }); 116 | Ok(true) 117 | } 118 | 119 | #[update(guard = "is_authorized")] 120 | #[candid_method(update, rename = "add_chain")] 121 | fn add_chain( 122 | chain_id: u32, 123 | urls: Vec, 124 | omnic_addr: String, 125 | start_block: u64 126 | ) -> Result { 127 | // set chain config 128 | CHAINS.with(|c| { 129 | c.replace(ChainState::new( 130 | ChainConfig::new( 131 | ChainType::Evm, 132 | chain_id, 133 | urls.clone(), 134 | ic_cdk::id(), 135 | omnic_addr.clone(), 136 | start_block, 137 | ) 138 | )); 139 | }); 140 | STATE_MACHINE.with(|s| { 141 | let mut state_machine = s.borrow_mut(); 142 | state_machine.set_chain_id(chain_id); 143 | state_machine.set_rpc_urls(urls.clone()); 144 | state_machine.set_omnic_addr(omnic_addr.clone()); 145 | }); 146 | Ok(true) 147 | } 148 | 149 | #[update(guard = "is_authorized")] 150 | #[candid_method(update, rename = "add_urls")] 151 | fn add_urls( 152 | urls: Vec 153 | ) -> Result { 154 | // set chain config 155 | CHAINS.with(|c| { 156 | let mut c = c.borrow_mut(); 157 | c.add_urls(urls); 158 | }); 159 | Ok(true) 160 | } 161 | 162 | // set next index 163 | #[update(guard = "is_authorized")] 164 | #[candid_method(update, rename = "set_next_index")] 165 | fn set_next_index( 166 | next_index: u32 167 | ) -> Result { 168 | CHAINS.with(|chain| { 169 | let mut chain = chain.borrow_mut(); 170 | chain.next_index = next_index; 171 | }); 172 | Ok(true) 173 | } 174 | 175 | #[query(name = "get_chain")] 176 | #[candid_method(query, rename = "get_chain")] 177 | fn get_chain() -> Result { 178 | CHAINS.with(|chain| { 179 | let chain = chain.borrow(); 180 | Ok(chain.clone()) 181 | }) 182 | } 183 | 184 | #[query(name = "get_info", guard = "is_authorized")] 185 | #[candid_method(query, rename = "get_info")] 186 | fn get_info() -> Result { 187 | STATE_INFO.with(|info| { 188 | let info = info.borrow(); 189 | Ok(info.clone()) 190 | }) 191 | } 192 | 193 | #[update(name = "fetch_root")] 194 | #[candid_method(update, rename = "fetch_root")] 195 | async fn fetch(height: u64) -> Result { 196 | let (_, omnic_addr, rpc) = CHAINS.with(|chain| { 197 | let chain = chain.borrow(); 198 | (chain.canister_addr.clone(), chain.config.omnic_addr.clone(), chain.config.rpc_urls[0].clone()) 199 | }); 200 | 201 | let client = EVMChainClient::new(rpc, omnic_addr, MAX_RESP_BYTES, CYCLES_PER_CALL) 202 | .map_err(|e| format!("init client failed: {:?}", e))?; 203 | client.get_latest_root(Some(height)) 204 | .await 205 | .map(|v| hex::encode(v)) 206 | .map_err(|e| format!("get root err: {:?}", e)) 207 | } 208 | 209 | #[update(name = "get_tx_count")] 210 | #[candid_method(update, rename = "get_tx_count")] 211 | async fn get_tx_count(addr: String) -> Result { 212 | // check cycles 213 | let available = ic_cdk::api::call::msg_cycles_available(); 214 | let need_cycles = 10u64 * CYCLES_PER_BYTE; 215 | if available < need_cycles { 216 | return Err(format!("Insufficient cycles: require {} cycles. Received {}.", need_cycles, available)); 217 | } 218 | // accept cycles 219 | let _accepted = ic_cdk::api::call::msg_cycles_accept(need_cycles); 220 | 221 | // get tx count 222 | let (chain_type, rpc_url, omnic_addr) = CHAINS.with(|c| { 223 | let chain = c.borrow(); 224 | (chain.chain_type(), chain.config.rpc_urls[0].clone(), chain.config.omnic_addr.clone()) 225 | }); 226 | match chain_type { 227 | ChainType::Evm => {}, 228 | _ => return Err("chain type not supported yet".into()), 229 | } 230 | 231 | let client = EVMChainClient::new(rpc_url, omnic_addr, MAX_RESP_BYTES, CYCLES_PER_CALL) 232 | .map_err(|e| format!("init client failed: {:?}", e))?; 233 | 234 | client.get_tx_count(addr) 235 | .await 236 | .map_err(|e| format!("{:?}", e)) 237 | } 238 | 239 | #[update(name = "get_gas_price")] 240 | #[candid_method(update, rename = "get_gas_price")] 241 | async fn get_gas_price() -> Result { 242 | // check cycles 243 | let available = ic_cdk::api::call::msg_cycles_available(); 244 | let need_cycles = 10u64 * CYCLES_PER_BYTE; 245 | if available < need_cycles { 246 | return Err(format!("Insufficient cycles: require {} cycles. Received {}.", need_cycles, available)); 247 | } 248 | // accept cycles 249 | let _accepted = ic_cdk::api::call::msg_cycles_accept(need_cycles); 250 | 251 | // get gas price 252 | let (chain_type, rpc_url, omnic_addr) = CHAINS.with(|c| { 253 | let chain = c.borrow(); 254 | (chain.chain_type(), chain.config.rpc_urls[0].clone(), chain.config.omnic_addr.clone()) 255 | }); 256 | match chain_type { 257 | ChainType::Evm => {}, 258 | _ => return Err("chain type not supported yet".into()), 259 | } 260 | 261 | let client = EVMChainClient::new(rpc_url, omnic_addr, MAX_RESP_BYTES, CYCLES_PER_CALL) 262 | .map_err(|e| format!("init client failed: {:?}", e))?; 263 | 264 | client.get_gas_price() 265 | .await 266 | .map_err(|e| format!("{:?}", e)) 267 | } 268 | 269 | // relayer canister call this to check if a message is valid before process_message 270 | #[query(name = "is_valid")] 271 | #[candid_method(query, rename = "is_valid")] 272 | fn is_valid(message: Vec, proof: Vec>, leaf_index: u32) -> Result { 273 | // verify message proof: use proof, message to calculate the merkle root, 274 | // check if the merkle root exists in corresponding chain state 275 | let m = Message::from_raw(message.clone()).map_err(|e| { 276 | format!("parse message from bytes failed: {:?}", e) 277 | })?; 278 | let h = m.to_leaf(); 279 | let p_h256: Vec = proof.iter().map(|v| H256::from_slice(&v)).collect(); 280 | let p: [H256; TREE_DEPTH] = p_h256.try_into().map_err(|e| format!("parse proof failed: {:?}", e))?; 281 | // calculate root with leaf hash & proof 282 | let root = merkle_root_from_branch(h, &p, TREE_DEPTH, leaf_index as usize); 283 | // do not add optimistic yet 284 | CHAINS.with(|c| { 285 | let chain = c.borrow(); 286 | Ok(chain.is_root_exist(root)) 287 | }) 288 | } 289 | 290 | #[query(name = "get_latest_root")] 291 | #[candid_method(query, rename = "get_latest_root")] 292 | fn get_latest_root() -> String { 293 | CHAINS.with(|c| { 294 | let chain = c.borrow(); 295 | format!("{:x}", chain.latest_root()) 296 | }) 297 | } 298 | 299 | #[query(name = "get_next_index")] 300 | #[candid_method(query, rename = "get_next_index")] 301 | fn get_next_index() -> Result { 302 | CHAINS.with(|c| { 303 | let chain = c.borrow(); 304 | Ok(chain.next_index()) 305 | }) 306 | } 307 | 308 | #[update(name = "add_owner", guard = "is_authorized")] 309 | #[candid_method(update, rename = "add_owner")] 310 | async fn add_owner(owner: Principal) { 311 | STATE_INFO.with(|s| { 312 | s.borrow_mut().add_owner(owner); 313 | }); 314 | } 315 | 316 | #[update(name = "remove_owner", guard = "is_authorized")] 317 | #[candid_method(update, rename = "remove_owner")] 318 | async fn remove_owner(owner: Principal) { 319 | STATE_INFO.with(|s| { 320 | s.borrow_mut().delete_owner(owner); 321 | }); 322 | } 323 | 324 | async fn fetch_root() { 325 | // query omnic contract.getLatestRoot, 326 | // fetch from multiple rpc providers and aggregrate results, should be exact match 327 | let state = STATE_MACHINE.with(|s| { 328 | s.borrow().clone() 329 | }); 330 | 331 | let next_state = match state.sub_state { 332 | State::Init => { 333 | match EVMChainClient::new(state.rpc_urls[0].clone(), state.omnic_addr.clone(), MAX_RESP_BYTES, CYCLES_PER_CALL) { 334 | Ok(client) => { 335 | match client.get_block_number().await { 336 | Ok(h) => { 337 | STATE_MACHINE.with(|s| { 338 | let mut state = s.borrow_mut(); 339 | state.block_height = h; 340 | state.roots = HashMap::default(); // reset roots in this round 341 | }); 342 | State::Fetching(0) 343 | }, 344 | Err(e) => { 345 | add_log(format!("init contract failed: {}", e)); 346 | State::Fail 347 | }, 348 | } 349 | }, 350 | Err(_e) => { 351 | State::Fail 352 | }, 353 | } 354 | }, 355 | State::Fetching(idx) => { 356 | // query root in block height 357 | match EVMChainClient::new(state.rpc_urls[0].clone(), state.omnic_addr.clone(), MAX_RESP_BYTES, CYCLES_PER_CALL) { 358 | Ok(client) => { 359 | let root = client.get_latest_root(Some(state.block_height)).await; 360 | add_log(format!("root from {:?}: {:?}", state.rpc_urls[idx], root)); 361 | match root { 362 | Ok(r) => { 363 | incr_state_root(r); 364 | }, 365 | Err(e) => { 366 | add_log(format!("query root failed: {}", e)); 367 | incr_state_root(H256::zero()); 368 | }, 369 | }; 370 | STATE_MACHINE.with(|s| { 371 | let s = s.borrow(); 372 | let (check_result, _) = check_roots_result(&s.roots, s.rpc_count()); 373 | s.get_fetching_next_sub_state(check_result) 374 | }) 375 | }, 376 | Err(e) => { 377 | add_log(format!("init evm chain client failed: {}", e)); 378 | State::Fail 379 | } 380 | } 381 | }, 382 | State::End | State::Fail => { 383 | return 384 | }, 385 | }; 386 | 387 | // update sub state 388 | STATE_MACHINE.with(|s| { 389 | s.borrow_mut().sub_state = next_state; 390 | }); 391 | 392 | if next_state != State::End && next_state != State::Fail { 393 | cron_enqueue( 394 | Task::FetchRoot, 395 | ic_cron::types::SchedulingOptions { 396 | delay_nano: get_fetch_root_period(), 397 | interval_nano: get_fetch_root_period(), 398 | iterations: Iterations::Exact(1), 399 | }, 400 | ).unwrap(); 401 | } 402 | } 403 | 404 | // this is done in heart_beat 405 | async fn fetch_roots() { 406 | let state = STATE_MACHINE.with(|s| { 407 | s.borrow().clone() 408 | }); 409 | 410 | match state.state { 411 | State::Init => { 412 | // get chain ids 413 | let chain_id = CHAINS.with(|c| { 414 | c.borrow().config.chain_id 415 | }); 416 | // when chain is set, start fetching 417 | if chain_id != 0 { 418 | STATE_MACHINE.with(|s| { 419 | let mut state = s.borrow_mut(); 420 | state.state = State::Fetching(0); 421 | }); 422 | } 423 | } 424 | State::Fetching(_) => { 425 | match state.sub_state { 426 | State::Init => { 427 | // randomly select rpc url to fetch 428 | // call IC raw rand to get random seed 429 | let seed_res = ic_cdk::api::management_canister::main::raw_rand().await; 430 | match seed_res { 431 | Ok((seed, )) => { 432 | let mut rpc_urls = CHAINS.with(|c| { 433 | c.borrow().config.rpc_urls.clone() 434 | }); 435 | // shuffle 436 | let seed: [u8; 32] = seed.as_slice().try_into().expect("convert vector to array error"); 437 | let mut rng: StdRng = SeedableRng::from_seed(seed); 438 | rpc_urls.shuffle(&mut rng); 439 | let random_urls = rpc_urls[..get_query_rpc_number() as usize].to_vec(); 440 | // set random urls for this round 441 | STATE_MACHINE.with(|s| { 442 | s.borrow_mut().set_rpc_urls(random_urls.clone()); 443 | }); 444 | add_log(format!("start fetching, random rpc urls: {:?}", random_urls)); 445 | add_log(format!("start_cycles: {:?}, start_time: {:?}", ic_cdk::api::canister_balance(), ic_cdk::api::time())); 446 | cron_enqueue( 447 | Task::FetchRoot, 448 | ic_cron::types::SchedulingOptions { 449 | delay_nano: get_fetch_root_period(), 450 | interval_nano: get_fetch_root_period(), 451 | iterations: Iterations::Exact(1), 452 | }, 453 | ).unwrap(); 454 | }, 455 | Err((_code, msg)) => { 456 | // error, do nothing 457 | add_log(format!("Error getting raw rand: {}", msg)); 458 | }, 459 | } 460 | } 461 | State::Fetching(_) => {}, 462 | State::End => { 463 | add_log(format!("end_cycles: {:?}, end_time: {:?}", ic_cdk::api::canister_balance(), ic_cdk::api::time())); 464 | // update root 465 | CHAINS.with(|c| { 466 | let mut chain = c.borrow_mut(); 467 | let (check_result, root) = check_roots_result(&state.roots, state.rpc_count()); 468 | if check_result { 469 | chain.insert_root(root); 470 | } else { 471 | add_log(format!("invalid roots: {:?}", state.roots)) 472 | } 473 | }); 474 | // update state 475 | STATE_MACHINE.with(|s| { 476 | let mut state = s.borrow_mut(); 477 | (state.state, state.sub_state) = state.get_fetching_next_state(); 478 | }); 479 | }, 480 | State::Fail => { 481 | // update state 482 | STATE_MACHINE.with(|s| { 483 | let mut state = s.borrow_mut(); 484 | (state.state, state.sub_state) = state.get_fetching_next_state(); 485 | }); 486 | }, 487 | } 488 | }, 489 | _ => { panic!("can't reach here")}, 490 | } 491 | } 492 | 493 | #[heartbeat] 494 | fn heart_beat() { 495 | for task in cron_ready_tasks() { 496 | let kind = task.get_payload::().expect("Serialization error"); 497 | match kind { 498 | Task::FetchRoots => { 499 | ic_cdk::spawn(fetch_roots()); 500 | }, 501 | Task::FetchRoot => { 502 | ic_cdk::spawn(fetch_root()); 503 | }, 504 | } 505 | } 506 | } 507 | 508 | #[pre_upgrade] 509 | fn pre_upgrade() { 510 | let chains = CHAINS.with(|c| { 511 | c.replace(ChainState::default()) 512 | }); 513 | let state_info = STATE_INFO.with(|s| { 514 | s.replace(StateInfo::default()) 515 | }); 516 | let state_machine = STATE_MACHINE.with(|s| { 517 | s.replace(StateMachine::default()) 518 | }); 519 | ic_cdk::storage::stable_save((chains, state_info, StateMachineStable::from(state_machine), _take_cron_state())).expect("pre upgrade error"); 520 | } 521 | 522 | #[post_upgrade] 523 | fn post_upgrade() { 524 | let (chains, 525 | state_info, 526 | state_machine, 527 | cron_state 528 | ): (ChainState, 529 | StateInfo, 530 | StateMachineStable, 531 | Option 532 | ) = ic_cdk::storage::stable_restore().expect("post upgrade error"); 533 | 534 | CHAINS.with(|c| { 535 | c.replace(chains); 536 | }); 537 | STATE_INFO.with(|s| { 538 | s.replace(state_info); 539 | }); 540 | STATE_MACHINE.with(|s| { 541 | s.replace(state_machine.into()); 542 | }); 543 | _put_cron_state(cron_state); 544 | } 545 | 546 | /// get the unix timestamp in second 547 | // fn get_time() -> u64 { 548 | // ic_cdk::api::time() / 1000000000 549 | // } 550 | 551 | fn is_authorized() -> Result<(), String> { 552 | let user = ic_cdk::api::caller(); 553 | STATE_INFO.with(|info| { 554 | let info = info.borrow(); 555 | if !info.is_owner(user) { 556 | Err("unauthorized!".into()) 557 | } else { 558 | Ok(()) 559 | } 560 | }) 561 | } 562 | 563 | fn incr_state_root(root: H256) { 564 | STATE_MACHINE.with(|s| { 565 | let mut state = s.borrow_mut(); 566 | state 567 | .roots 568 | .entry(root) 569 | .and_modify(|c| *c += 1) 570 | .or_insert(1); 571 | }) 572 | } 573 | 574 | fn add_log(log: String) { 575 | LOGS.with(|l| { 576 | let mut logs = l.borrow_mut(); 577 | if logs.len() == 1000 { 578 | logs.pop_front(); 579 | } 580 | logs.push_back(log); 581 | }); 582 | } 583 | 584 | #[cfg(not(any(target_arch = "wasm32", test)))] 585 | fn main() { 586 | // The line below generates did types and service definition from the 587 | // methods annotated with `candid_method` above. The definition is then 588 | // obtained with `__export_service()`. 589 | candid::export_service!(); 590 | std::print!("{}", __export_service()); 591 | } 592 | 593 | #[cfg(any(target_arch = "wasm32", test))] 594 | fn main() {} -------------------------------------------------------------------------------- /core/ic/src/canisters/proxy.did: -------------------------------------------------------------------------------- 1 | type ChainConfig = record { 2 | rpc_urls : vec text; 3 | gateway_addr : principal; 4 | omnic_addr : text; 5 | chain_id : nat32; 6 | chain_type : ChainType; 7 | omnic_start_block : nat64; 8 | }; 9 | type ChainState = record { 10 | next_index : nat32; 11 | canister_addr : text; 12 | config : ChainConfig; 13 | roots : vec vec nat8; 14 | }; 15 | type ChainType = variant { Evm; Solana; Cosmos }; 16 | type DetailValue = variant { 17 | I64 : int64; 18 | U64 : nat64; 19 | Vec : vec DetailValue; 20 | Slice : vec nat8; 21 | Text : text; 22 | True; 23 | False; 24 | Float : float64; 25 | Principal : principal; 26 | }; 27 | type Record = record { 28 | id : nat64; 29 | operation : text; 30 | timestamp : nat64; 31 | details : vec record { text; DetailValue }; 32 | caller : principal; 33 | }; 34 | type Result = variant { Ok : bool; Err : text }; 35 | type Result_1 = variant { Ok : record { text; nat64; nat64 }; Err : text }; 36 | type Result_2 = variant { Ok : text; Err : text }; 37 | type Result_3 = variant { Ok : vec ChainState; Err : text }; 38 | type Result_4 = variant { Ok : nat64; Err : text }; 39 | type Result_5 = variant { Ok : record { text; nat64 }; Err : text }; 40 | type Result_6 = variant { Ok : vec nat8; Err : text }; 41 | service : () -> { 42 | add_chain : (nat32, vec text, principal, text, nat64) -> (Result); 43 | add_owner : (principal) -> (); 44 | delete_chain : (nat32) -> (Result); 45 | fetch_root : (nat32, nat64) -> (Result_1); 46 | get_canister_addr : (ChainType) -> (Result_2); 47 | get_chains : () -> (Result_3) query; 48 | get_gas_price : (nat32) -> (Result_4); 49 | get_latest_root : (nat32) -> (Result_2) query; 50 | get_logs : () -> (vec text) query; 51 | get_record : (nat64) -> (opt Record) query; 52 | get_record_size : (opt text) -> (nat64) query; 53 | get_records : (opt record { nat64; nat64 }, opt text) -> (vec Record) query; 54 | get_tx_count : (nat32, text) -> (Result_4); 55 | is_valid : (vec nat8, vec vec nat8, nat32) -> (Result) query; 56 | process_message : (vec nat8, vec vec nat8, nat32) -> (Result_5); 57 | remove_owner : (principal) -> (); 58 | send_raw_tx : (nat32, vec nat8) -> (Result_6); 59 | set_canister_addrs : () -> (Result); 60 | set_fetch_period : (nat64, nat64) -> (Result); 61 | set_next_index : (nat32, nat32) -> (Result); 62 | update_chain : (nat32, vec text, principal, text, nat64) -> (Result); 63 | } -------------------------------------------------------------------------------- /core/ic/src/canisters/proxy.old.did: -------------------------------------------------------------------------------- 1 | type ChainConfig = record { 2 | rpc_urls : vec text; 3 | omnic_addr : text; 4 | chain_id : nat32; 5 | chain_type : ChainType; 6 | omnic_start_block : nat64; 7 | }; 8 | type ChainState = record { 9 | next_index : nat32; 10 | canister_addr : text; 11 | config : ChainConfig; 12 | roots : vec vec nat8; 13 | }; 14 | type ChainType = variant { Evm; Solana; Cosmos }; 15 | type Result = variant { Ok : bool; Err : text }; 16 | type Result_1 = variant { Ok : text; Err : text }; 17 | type Result_2 = variant { Ok : vec ChainState; Err : text }; 18 | type Result_3 = variant { Ok : nat32; Err : text }; 19 | service : () -> { 20 | add_chain : (nat32, vec text, text, nat64) -> (Result); 21 | add_owner : (principal) -> (); 22 | get_canister_addr : (ChainType) -> (Result_1); 23 | get_chains : () -> (Result_2) query; 24 | get_latest_root : (nat32) -> (Result_1) query; 25 | get_next_index : (nat32) -> (Result_3) query; 26 | is_valid : (vec nat8, vec vec nat8, nat32) -> (Result) query; 27 | process_message : (vec nat8, vec vec nat8, nat32) -> (Result); 28 | remove_owner : (principal) -> (); 29 | set_canister_addrs : () -> (Result); 30 | set_next_index : (nat32, nat32) -> (Result); 31 | update_chain : (nat32, vec text, text, nat64) -> (Result); 32 | } -------------------------------------------------------------------------------- /core/ic/src/canisters/proxy.rs: -------------------------------------------------------------------------------- 1 | /* 2 | omnic proxy canister: 3 | fetch_root: fetch merkel roots from all supported chains and insert to chain state 4 | */ 5 | 6 | use std::cell::{RefCell}; 7 | use std::collections::{HashMap, VecDeque}; 8 | 9 | use ic_web3::ic::get_eth_addr; 10 | 11 | use ic_cdk_macros::{init, post_upgrade, pre_upgrade, query, update}; 12 | use ic_cdk::export::candid::{candid_method}; 13 | use ic_cdk::api::management_canister::http_request::{HttpResponse, TransformArgs}; 14 | use candid::types::principal::Principal; 15 | 16 | use omnic::utils::DetailsBuilder; 17 | use omnic::{Message, chains::EVMChainClient, ChainConfig, ChainState, ChainType}; 18 | use omnic::{HomeContract, DetailValue, Record}; 19 | use omnic::consts::{KEY_NAME, MAX_RESP_BYTES, CYCLES_PER_CALL, CYCLES_PER_BYTE}; 20 | use omnic::state::{StateInfo, RecordDB}; 21 | use omnic::call::{call_to_canister, call_to_chain}; 22 | 23 | thread_local! { 24 | static STATE_INFO: RefCell = RefCell::new(StateInfo::default()); 25 | static CHAINS: RefCell> = RefCell::new(HashMap::new()); 26 | static LOGS: RefCell> = RefCell::new(VecDeque::default()); 27 | static RECORDS: RefCell = RefCell::new(RecordDB::new()); 28 | } 29 | 30 | #[query] 31 | #[candid_method(query)] 32 | fn get_logs() -> Vec { 33 | LOGS.with(|l| { 34 | l.borrow().clone().into() 35 | }) 36 | } 37 | 38 | #[init] 39 | #[candid_method(init)] 40 | fn init() { 41 | let caller = ic_cdk::api::caller(); 42 | STATE_INFO.with(|info| { 43 | let mut info = info.borrow_mut(); 44 | info.add_owner(caller); 45 | }); 46 | } 47 | 48 | #[query] 49 | async fn transform(raw: TransformArgs) -> HttpResponse { 50 | let mut t = raw.response; 51 | t.headers = vec![]; 52 | t 53 | } 54 | 55 | // get canister's evm address 56 | #[update(name = "get_canister_addr")] 57 | #[candid_method(update, rename = "get_canister_addr")] 58 | async fn get_canister_addr(chain_type: ChainType) -> Result { 59 | let cid = ic_cdk::id(); 60 | let derivation_path = vec![cid.clone().as_slice().to_vec()]; 61 | match chain_type { 62 | ChainType::Evm => match get_eth_addr(Some(cid), Some(derivation_path), KEY_NAME.to_string()).await { 63 | Ok(addr) => { Ok(hex::encode(addr)) }, 64 | Err(e) => { Err(e) }, 65 | }, 66 | _ => Err("chain type not supported yet!".into()), 67 | } 68 | } 69 | 70 | #[update(guard = "is_authorized")] 71 | #[candid_method(update, rename = "set_canister_addrs")] 72 | async fn set_canister_addrs() -> Result { 73 | let cid = ic_cdk::id(); 74 | let derivation_path = vec![cid.clone().as_slice().to_vec()]; 75 | let evm_addr = get_eth_addr(Some(cid), Some(derivation_path), KEY_NAME.to_string()) 76 | .await 77 | .map(|v| hex::encode(v)) 78 | .map_err(|e| format!("calc evm address failed: {:?}", e))?; 79 | CHAINS.with(|chains| { 80 | let mut chains = chains.borrow_mut(); 81 | for (_id, chain) in chains.iter_mut() { 82 | match chain.chain_type() { 83 | ChainType::Evm => chain.set_canister_addr(evm_addr.clone()), 84 | _ => { 85 | add_log("chain type not supported yet!".to_string()); 86 | } 87 | } 88 | } 89 | }); 90 | Ok(true) 91 | } 92 | 93 | #[update(guard = "is_authorized")] 94 | #[candid_method(update, rename = "set_fetch_period")] 95 | async fn set_fetch_period(fetch_root_period: u64, fetch_roots_period: u64) -> Result { 96 | STATE_INFO.with(|s| { 97 | let mut s = s.borrow_mut(); 98 | s.set_fetch_period(fetch_root_period, fetch_roots_period); 99 | }); 100 | Ok(true) 101 | } 102 | 103 | #[update(guard = "is_authorized")] 104 | #[candid_method(update, rename = "add_chain")] 105 | fn add_chain( 106 | chain_id: u32, 107 | urls: Vec, 108 | gateway_canister_addr: Principal, 109 | omnic_addr: String, 110 | start_block: u64 111 | ) -> Result { 112 | // add chain config 113 | // need to deploy gateway canister manually 114 | // provide the gateway canister principal, as the WASM size will exceed if include the gateway canister bytes 115 | CHAINS.with(|chains| { 116 | let mut chains = chains.borrow_mut(); 117 | if !chains.contains_key(&chain_id) { 118 | chains.insert(chain_id, ChainState::new( 119 | ChainConfig::new( 120 | ChainType::Evm, 121 | chain_id, 122 | urls.clone(), 123 | gateway_canister_addr, 124 | omnic_addr.clone(), 125 | start_block, 126 | ) 127 | )); 128 | } 129 | }); 130 | // add record 131 | add_record( 132 | ic_cdk::caller(), 133 | "add_chain".to_string(), 134 | DetailsBuilder::new() 135 | .insert("chain_id", DetailValue::U64(chain_id as u64)) 136 | .insert("urls", DetailValue::Text(urls.join(","))) 137 | .insert("gatewat_addr", DetailValue::Principal(gateway_canister_addr)) 138 | .insert("omnic_addr", DetailValue::Text(omnic_addr)) 139 | .insert("start_block", DetailValue::U64(start_block)) 140 | ); 141 | Ok(true) 142 | } 143 | 144 | #[update(name = "delete_chain", guard = "is_authorized")] 145 | #[candid_method(update, rename = "delete_chain")] 146 | fn delete_chain(chain_id: u32) -> Result { 147 | match CHAINS.with(|c| { 148 | let mut chains = c.borrow_mut(); 149 | chains.remove(&chain_id) 150 | }) { 151 | Some(_) => { 152 | add_record( 153 | ic_cdk::caller(), 154 | "delete_chain".to_string(), 155 | DetailsBuilder::new() 156 | .insert("chain_id", DetailValue::U64(chain_id as u64)) 157 | ); 158 | Ok(true) 159 | } 160 | None => { Err("Chain id not exist".to_string()) } 161 | } 162 | } 163 | 164 | // update chain settings 165 | #[update(guard = "is_authorized")] 166 | #[candid_method(update, rename = "update_chain")] 167 | fn update_chain( 168 | chain_id: u32, 169 | urls: Vec, 170 | gateway_canister_addr: Principal, 171 | omnic_addr: String, 172 | start_block: u64 173 | ) -> Result { 174 | // add chain config 175 | CHAINS.with(|chains| { 176 | let mut chains = chains.borrow_mut(); 177 | if chains.contains_key(&chain_id) { 178 | chains.insert(chain_id, ChainState::new( 179 | ChainConfig::new( 180 | ChainType::Evm, 181 | chain_id, 182 | urls.clone(), 183 | gateway_canister_addr, 184 | omnic_addr.clone(), 185 | start_block, 186 | ) 187 | )); 188 | } 189 | }); 190 | add_record( 191 | ic_cdk::caller(), 192 | "update_chain".to_string(), 193 | DetailsBuilder::new() 194 | .insert("chain_id", DetailValue::U64(chain_id as u64)) 195 | .insert("urls", DetailValue::Text(urls.join(","))) 196 | .insert("gateway_addr", DetailValue::Principal(gateway_canister_addr)) 197 | .insert("omnic_addr", DetailValue::Text(omnic_addr)) 198 | .insert("start_block", DetailValue::U64(start_block)) 199 | ); 200 | Ok(true) 201 | } 202 | 203 | // update chain settings 204 | #[update(guard = "is_authorized")] 205 | #[candid_method(update, rename = "set_next_index")] 206 | fn set_next_index( 207 | chain_id: u32, 208 | next_index: u32 209 | ) -> Result { 210 | // add chain config 211 | CHAINS.with(|chains| { 212 | let mut chains = chains.borrow_mut(); 213 | let mut chain = chains.get_mut(&chain_id).expect("chain id not found!"); 214 | chain.next_index = next_index; 215 | }); 216 | add_record( 217 | ic_cdk::caller(), 218 | "set_next_index".to_string(), 219 | DetailsBuilder::new() 220 | .insert("chain_id", DetailValue::U64(chain_id as u64)) 221 | .insert("next_index", DetailValue::U64(next_index as u64)) 222 | ); 223 | Ok(true) 224 | } 225 | 226 | #[query(name = "get_chains")] 227 | #[candid_method(query, rename = "get_chains")] 228 | fn get_chains() -> Result, String> { 229 | // add chain config 230 | CHAINS.with(|chains| { 231 | let chains = chains.borrow(); 232 | Ok(chains.clone().into_iter().map(|(_id, c)| c).collect()) 233 | }) 234 | } 235 | 236 | #[update(name = "fetch_root")] 237 | #[candid_method(update, rename = "fetch_root")] 238 | async fn fetch(chain_id: u32, height: u64) -> Result<(String, u64, u64), String> { 239 | let (_caller, omnic_addr, rpc) = CHAINS.with(|chains| { 240 | let chains = chains.borrow(); 241 | let c = chains.get(&chain_id).expect("chain not found"); 242 | (c.canister_addr.clone(), c.config.omnic_addr.clone(), c.config.rpc_urls[0].clone()) 243 | }); 244 | 245 | let client = EVMChainClient::new(rpc, omnic_addr, MAX_RESP_BYTES, CYCLES_PER_CALL) 246 | .map_err(|e| format!("init client failed: {:?}", e))?; 247 | 248 | let start_cycles = ic_cdk::api::canister_balance(); 249 | let start_time = ic_cdk::api::time(); 250 | 251 | let root = client.get_latest_root(Some(height)) 252 | .await 253 | .map(|v| hex::encode(v)) 254 | .map_err(|e| format!("get root err: {:?}", e))?; 255 | 256 | let end_cycles = ic_cdk::api::canister_balance(); 257 | let end_time = ic_cdk::api::time(); 258 | 259 | let cycle_cost = start_cycles - end_cycles; 260 | let time_cost = end_time - start_time; 261 | Ok((root, cycle_cost, time_cost)) 262 | } 263 | 264 | #[update(name = "get_tx_count")] 265 | #[candid_method(update, rename = "get_tx_count")] 266 | async fn get_tx_count(chain_id: u32, addr: String) -> Result { 267 | // check cycles 268 | let available = ic_cdk::api::call::msg_cycles_available(); 269 | let need_cycles = 10u64 * CYCLES_PER_BYTE; 270 | if available < need_cycles { 271 | return Err(format!("Insufficient cycles: require {} cycles. Received {}.", need_cycles, available)); 272 | } 273 | // accept cycles 274 | let _accepted = ic_cdk::api::call::msg_cycles_accept(need_cycles); 275 | 276 | // get tx count 277 | let (chain_type, rpc_url, omnic_addr) = CHAINS.with(|c| { 278 | let chains = c.borrow(); 279 | let chain = chains.get(&chain_id).expect("src chain id not exist"); 280 | (chain.chain_type(), chain.config.rpc_urls[0].clone(), chain.config.omnic_addr.clone()) 281 | }); 282 | match chain_type { 283 | ChainType::Evm => {}, 284 | _ => return Err("chain type not supported yet".into()), 285 | } 286 | 287 | let client = EVMChainClient::new(rpc_url, omnic_addr, MAX_RESP_BYTES, CYCLES_PER_CALL) 288 | .map_err(|e| format!("init client failed: {:?}", e))?; 289 | 290 | client.get_tx_count(addr) 291 | .await 292 | .map_err(|e| format!("{:?}", e)) 293 | } 294 | 295 | #[update(name = "get_gas_price")] 296 | #[candid_method(update, rename = "get_gas_price")] 297 | async fn get_gas_price(chain_id: u32) -> Result { 298 | // check cycles 299 | let available = ic_cdk::api::call::msg_cycles_available(); 300 | let need_cycles = 10u64 * CYCLES_PER_BYTE; 301 | if available < need_cycles { 302 | return Err(format!("Insufficient cycles: require {} cycles. Received {}.", need_cycles, available)); 303 | } 304 | // accept cycles 305 | let _accepted = ic_cdk::api::call::msg_cycles_accept(need_cycles); 306 | 307 | // get gas price 308 | let (chain_type, rpc_url, omnic_addr) = CHAINS.with(|c| { 309 | let chains = c.borrow(); 310 | let chain = chains.get(&chain_id).expect("src chain id not exist"); 311 | (chain.chain_type(), chain.config.rpc_urls[0].clone(), chain.config.omnic_addr.clone()) 312 | }); 313 | match chain_type { 314 | ChainType::Evm => {}, 315 | _ => return Err("chain type not supported yet".into()), 316 | } 317 | 318 | let client = EVMChainClient::new(rpc_url, omnic_addr, MAX_RESP_BYTES, CYCLES_PER_CALL) 319 | .map_err(|e| format!("init client failed: {:?}", e))?; 320 | 321 | client.get_gas_price() 322 | .await 323 | .map_err(|e| format!("{:?}", e)) 324 | } 325 | 326 | // relayer canister call this to check if a message is valid before process_message 327 | #[update(name = "is_valid")] 328 | #[candid_method(query, rename = "is_valid")] 329 | async fn is_valid(message: Vec, proof: Vec>, leaf_index: u32) -> Result { 330 | // verify message proof: use proof, message to calculate the merkle root, 331 | // check if the merkle root exists in corresponding chain state 332 | let m = Message::from_raw(message.clone()).map_err(|e| { 333 | format!("parse message from bytes failed: {:?}", e) 334 | })?; 335 | // call to gate way canister 336 | let gateway: Principal = CHAINS.with(|c| { 337 | let chains = c.borrow(); 338 | let chain = chains.get(&m.origin).ok_or("src chain id not exist".to_string())?; 339 | Ok::(chain.config.gateway_addr) 340 | })?; 341 | 342 | let res = ic_cdk::call(gateway, "is_valid", (message, proof, leaf_index, )).await; 343 | match res { 344 | Ok((validation_result, )) => { 345 | validation_result 346 | } 347 | Err((_code, msg)) => { 348 | Err(msg) 349 | } 350 | } 351 | } 352 | 353 | #[update(name = "get_latest_root")] 354 | #[candid_method(query, rename = "get_latest_root")] 355 | async fn get_latest_root(chain_id: u32) -> Result { 356 | let gateway: Principal = CHAINS.with(|c| { 357 | let chains = c.borrow(); 358 | let chain = chains.get(&chain_id).ok_or("chain id not exist".to_string())?; 359 | Ok::(chain.config.gateway_addr) 360 | })?; 361 | 362 | let res = ic_cdk::call(gateway, "get_latest_root", ()).await; 363 | match res { 364 | Ok((root, )) => { 365 | Ok(root) 366 | } 367 | Err((_code, msg)) => { 368 | Err(msg) 369 | } 370 | } 371 | } 372 | 373 | // application canister call this method to send tx to destination chain 374 | #[update(name = "send_raw_tx")] 375 | #[candid_method(update, rename = "send_raw_tx")] 376 | async fn send_raw_tx(dst_chain: u32, raw_tx: Vec) -> Result, String> { 377 | // check cycles 378 | let available = ic_cdk::api::call::msg_cycles_available(); 379 | let need_cycles = raw_tx.len() as u64 * CYCLES_PER_BYTE; 380 | if available < need_cycles { 381 | return Err(format!("Insufficient cycles: require {} cycles. Received {}.", need_cycles, available)); 382 | } 383 | // accept cycles 384 | let _accepted = ic_cdk::api::call::msg_cycles_accept(need_cycles); 385 | 386 | // send tx 387 | let (chain_type, rpc_url, omnic_addr) = CHAINS.with(|c| { 388 | let chains = c.borrow(); 389 | let chain = chains.get(&dst_chain).expect("src chain id not exist"); 390 | (chain.chain_type(), chain.config.rpc_urls[0].clone(), chain.config.omnic_addr.clone()) 391 | }); 392 | match chain_type { 393 | ChainType::Evm => {}, 394 | _ => return Err("chain type not supported yet".into()), 395 | } 396 | 397 | let client = EVMChainClient::new(rpc_url, omnic_addr, MAX_RESP_BYTES, CYCLES_PER_CALL) 398 | .map_err(|e| format!("init client failed: {:?}", e))?; 399 | 400 | // client.send_raw_tx will always end up with error because the same tx will be submitted multiple times 401 | // by the node in the subnet, first submission response ok, the rest will response error, 402 | // so we should ignore return value of send_raw_tx, then query by the txhash to make sure the tx is correctly sent 403 | client.send_raw_tx(raw_tx) 404 | .await 405 | .map_err(|e| format!("{:?}", e)) 406 | // TODO: fetch via client.get_tx_by_hash to make sure the tx is included 407 | } 408 | 409 | #[update(name = "process_message")] 410 | #[candid_method(update, rename = "process_message")] 411 | async fn process_message(message: Vec, proof: Vec>, leaf_index: u32) -> Result<(String, u64), String> { 412 | let origin_caller = ic_cdk::caller(); 413 | // verify message proof: use proof, message to calculate the merkle root, 414 | // check if the root exists in corresponding chain state 415 | // if valid, call dest canister.handleMessage or send tx to dest chain 416 | // if invalid, return error 417 | add_log(format!("got message: {:?}", leaf_index)); 418 | let valid = is_valid(message.clone(), proof, leaf_index).await?; 419 | if !valid { 420 | add_log("message does not pass verification!".to_string()); 421 | return Err("message does not pass verification!".into()); 422 | } 423 | let m = Message::from_raw(message.clone()).map_err(|e| { 424 | format!("parse message from bytes failed: {:?}", e) 425 | })?; 426 | // check leaf_index == next_index, then bump next_index 427 | let next_index = CHAINS.with(|chains| { 428 | let chains = chains.borrow(); 429 | let c = chains.get(&m.origin).expect("chain not found"); 430 | c.next_index() 431 | }); 432 | if next_index != leaf_index { 433 | add_log(format!("next_index: {} != leaf_index: {}, ", next_index, leaf_index)); 434 | return Err(format!("next_index: {} != leaf_index: {}, ", next_index, leaf_index)); 435 | } 436 | CHAINS.with(|chains| { 437 | let mut chains = chains.borrow_mut(); 438 | let c = chains.get_mut(&m.origin).expect("chain not found"); 439 | c.bump_index(); 440 | }); 441 | // send msg to destination 442 | // TODO reset next index after call error? 443 | let res = if m.destination == 0 { 444 | // take last 10 bytes 445 | let recipient = Principal::from_slice(&m.recipient.as_bytes()[22..]); 446 | add_log(format!("recipient: {:?}", Principal::to_text(&recipient))); 447 | call_to_canister(recipient, m.to_leaf().0.to_vec(), &m).await 448 | } else { 449 | // send tx to dst chain 450 | // call_to_chain(m.destination, message).await 451 | let (caller, omnic_addr, rpc) = CHAINS.with(|chains| { 452 | let chains = chains.borrow(); 453 | let c = chains.get(&m.destination).expect("chain not found"); 454 | (c.canister_addr.clone(), c.config.omnic_addr.clone(), c.config.rpc_urls[0].clone()) 455 | }); 456 | if caller == "" || omnic_addr == "" { 457 | return Err("caller address is empty".into()); 458 | } 459 | call_to_chain(caller, omnic_addr, rpc, m.destination, message).await 460 | }; 461 | 462 | add_record( 463 | origin_caller, 464 | "process_message".to_string(), 465 | DetailsBuilder::new() 466 | .insert("origin", DetailValue::U64(m.origin as u64)) 467 | .insert("send", DetailValue::Text(m.sender.to_string())) 468 | .insert("nonce", DetailValue::U64(m.nonce as u64)) 469 | .insert("destination", DetailValue::U64(m.destination as u64)) 470 | .insert("recipient", DetailValue::Text(m.recipient.to_string())) 471 | .insert("result", DetailValue::Text( 472 | match res.clone() { 473 | Ok(o) => { 474 | o 475 | } 476 | Err(e) => { 477 | e 478 | } 479 | } 480 | )) 481 | ); 482 | 483 | res.map(|o| (o, ic_cdk::api::time())) 484 | } 485 | 486 | #[update(name = "add_owner", guard = "is_authorized")] 487 | #[candid_method(update, rename = "add_owner")] 488 | async fn add_owner(owner: Principal) { 489 | STATE_INFO.with(|s| { 490 | s.borrow_mut().add_owner(owner); 491 | }); 492 | } 493 | 494 | #[update(name = "remove_owner", guard = "is_authorized")] 495 | #[candid_method(update, rename = "remove_owner")] 496 | async fn remove_owner(owner: Principal) { 497 | STATE_INFO.with(|s| { 498 | s.borrow_mut().delete_owner(owner); 499 | }); 500 | } 501 | 502 | #[query(name = "get_record_size", guard = "is_authorized")] 503 | #[candid_method(query, rename = "get_record_size")] 504 | fn get_record_size(operation: Option) -> usize { 505 | RECORDS.with(|r| { 506 | let records = r.borrow(); 507 | records.size(operation) 508 | }) 509 | } 510 | 511 | #[query(name = "get_record", guard = "is_authorized")] 512 | #[candid_method(query, rename = "get_record")] 513 | fn get_record(id: usize) -> Option { 514 | RECORDS.with(|r| { 515 | let records = r.borrow(); 516 | records.load_by_id(id) 517 | }) 518 | } 519 | 520 | #[query(name = "get_records", guard = "is_authorized")] 521 | #[candid_method(query, rename = "get_records")] 522 | fn get_records(range: Option<(usize, usize)>, operation: Option) -> Vec { 523 | RECORDS.with(|r| { 524 | let records = r.borrow(); 525 | let (start, end) = match range { 526 | Some((s, e)) => { 527 | (s, e) 528 | } 529 | None => { 530 | // range not set, default to last 50 records 531 | let size = records.size(operation.clone()); 532 | if size < 50 { 533 | (0, size) 534 | } else { 535 | (size-50, size) 536 | } 537 | } 538 | }; 539 | 540 | match operation { 541 | Some(op) => { 542 | // get specific operation 543 | records.load_by_opeation(op, start, end) 544 | } 545 | None => { 546 | // operation not set, get all 547 | records.load_by_id_range(start, end) 548 | } 549 | } 550 | }) 551 | } 552 | 553 | #[pre_upgrade] 554 | fn pre_upgrade() { 555 | let chains = CHAINS.with(|c| { 556 | c.replace(HashMap::default()) 557 | }); 558 | let state_info = STATE_INFO.with(|s| { 559 | s.replace(StateInfo::default()) 560 | }); 561 | let records = RECORDS.with(|r| { 562 | r.replace(RecordDB::new()) 563 | }); 564 | ic_cdk::storage::stable_save((chains, state_info, records, )).expect("pre upgrade error"); 565 | } 566 | 567 | #[post_upgrade] 568 | fn post_upgrade() { 569 | let (chains, 570 | state_info, 571 | records, 572 | ): (HashMap, 573 | StateInfo, 574 | RecordDB, 575 | ) = ic_cdk::storage::stable_restore().expect("post upgrade error"); 576 | 577 | CHAINS.with(|c| { 578 | c.replace(chains); 579 | }); 580 | STATE_INFO.with(|s| { 581 | s.replace(state_info); 582 | }); 583 | RECORDS.with(|r| { 584 | r.replace(records); 585 | }); 586 | } 587 | 588 | // get the unix timestamp in second 589 | fn get_time() -> u64 { 590 | ic_cdk::api::time() / 1000000000 591 | } 592 | 593 | fn is_authorized() -> Result<(), String> { 594 | let user = ic_cdk::api::caller(); 595 | STATE_INFO.with(|info| { 596 | let info = info.borrow(); 597 | if !info.is_owner(user) { 598 | Err("unauthorized!".into()) 599 | } else { 600 | Ok(()) 601 | } 602 | }) 603 | } 604 | 605 | fn add_log(log: String) { 606 | LOGS.with(|l| { 607 | let mut logs = l.borrow_mut(); 608 | if logs.len() == 1000 { 609 | logs.pop_front(); 610 | } 611 | logs.push_back(log); 612 | }); 613 | } 614 | 615 | fn add_record(caller: Principal, op: String, details_builder: DetailsBuilder) { 616 | RECORDS.with(|r| { 617 | let mut records = r.borrow_mut(); 618 | records.append( 619 | caller, 620 | get_time(), 621 | op, 622 | details_builder.build() 623 | ); 624 | }); 625 | } 626 | 627 | #[cfg(not(any(target_arch = "wasm32", test)))] 628 | fn main() { 629 | // The line below generates did types and service definition from the 630 | // methods annotated with `candid_method` above. The definition is then 631 | // obtained with `__export_service()`. 632 | candid::export_service!(); 633 | std::print!("{}", __export_service()); 634 | } 635 | 636 | #[cfg(any(target_arch = "wasm32", test))] 637 | fn main() {} 638 | -------------------------------------------------------------------------------- /core/ic/src/chain_state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use ic_web3::types::H256; 3 | use candid::{CandidType, Deserialize}; 4 | use crate::config::{ChainConfig, ChainType}; 5 | 6 | #[derive(CandidType, Deserialize, Clone, Default)] 7 | pub struct ChainState { 8 | pub config: ChainConfig, 9 | pub roots: VecDeque>, 10 | pub next_index: u32, // leaf_index for next message 11 | pub canister_addr: String, // the address controlled by the proxy canister on this chain 12 | // pub txs: Vec, // outgoging txs 13 | } 14 | 15 | impl ChainState { 16 | pub fn new( 17 | chain_config: ChainConfig, 18 | ) -> ChainState { 19 | ChainState { 20 | config: chain_config, 21 | roots: VecDeque::new(), 22 | next_index: 0, 23 | canister_addr: "".into(), 24 | } 25 | } 26 | 27 | pub fn update_config(&mut self, new_config: ChainConfig) { 28 | self.config = new_config; 29 | } 30 | 31 | pub fn add_urls(&mut self, urls: Vec) { 32 | self.config.add_urls(urls); 33 | } 34 | 35 | pub fn rpc_urls(&self) -> Vec { 36 | self.config.rpc_urls.clone() 37 | } 38 | 39 | pub fn chain_type(&self) -> ChainType { 40 | self.config.chain_type.clone() 41 | } 42 | 43 | pub fn set_canister_addr(&mut self, addr: String) { 44 | self.canister_addr = addr; 45 | } 46 | 47 | pub fn bump_index(&mut self) { 48 | self.next_index += 1; 49 | } 50 | 51 | pub fn next_index(&self) -> u32 { 52 | self.next_index 53 | } 54 | 55 | pub fn insert_root(&mut self, r: H256) { 56 | let root = r.as_bytes().to_vec(); 57 | if !self.roots.contains(&root) { 58 | self.roots.push_back(root); 59 | } 60 | } 61 | 62 | pub fn is_root_exist(&self, r: H256) -> bool { 63 | let root = r.as_bytes().to_vec(); 64 | self.roots.contains(&root) 65 | } 66 | 67 | pub fn latest_root(&self) -> H256 { 68 | match self.roots.back() { 69 | Some(v) => H256::from_slice(&v), 70 | None => H256::zero(), 71 | } 72 | } 73 | 74 | pub fn all_roots(&self) -> Vec { 75 | self.roots.iter().map(|r| { 76 | H256::from_slice(&r) 77 | }).collect() 78 | } 79 | } -------------------------------------------------------------------------------- /core/ic/src/chains/evm.rs: -------------------------------------------------------------------------------- 1 | use ic_web3::Web3; 2 | use ic_web3::transports::ICHttp; 3 | use ic_web3::contract::{Contract, Options}; 4 | use ic_web3::types::{U256, H256, Bytes, Address, BlockNumber, BlockId}; 5 | use ic_web3::ic::KeyInfo; 6 | 7 | use std::str::FromStr; 8 | use async_trait::async_trait; 9 | 10 | use crate::consts::KEY_NAME; 11 | use crate::error::OmnicError; 12 | use crate::error::OmnicError::*; 13 | use crate::traits::chain::HomeContract; 14 | 15 | const OMNIC_ABI: &[u8] = include_bytes!("./omnic.abi"); 16 | 17 | pub struct EVMChainClient { 18 | w3: Web3, 19 | contract: Contract, 20 | } 21 | 22 | impl EVMChainClient { 23 | pub fn new( 24 | rpc_url: String, 25 | omnic_addr: String, 26 | max_resp_bytes: Option, 27 | cycles_per_call: Option, 28 | ) -> Result { 29 | let http = ICHttp::new(&rpc_url, max_resp_bytes, cycles_per_call)?; 30 | let w3 = Web3::new(http); 31 | let contract_address = Address::from_str(&omnic_addr) 32 | .map_err(|e| Other(format!("address decode failed: {:?}", e)))?; 33 | let contract = Contract::from_json( 34 | w3.eth(), 35 | contract_address, 36 | OMNIC_ABI 37 | )?; 38 | 39 | Ok(EVMChainClient { 40 | w3: w3, 41 | contract: contract, 42 | }) 43 | } 44 | } 45 | 46 | #[async_trait] 47 | impl HomeContract for EVMChainClient { 48 | async fn dispatch_message(&self, caller: String, dst_chain: u32, msg_bytes: Vec) -> Result { 49 | let caller_addr = Address::from_str(&caller) 50 | .map_err(|e| Other(format!("address decode failed: {:?}", e)))?; 51 | // ecdsa key info 52 | let derivation_path = vec![ic_cdk::id().as_slice().to_vec()]; 53 | let key_info = KeyInfo{ derivation_path: derivation_path, key_name: KEY_NAME.to_string() }; 54 | // add nonce to options 55 | let tx_count = self.w3.eth() 56 | .transaction_count(caller_addr, None) 57 | .await 58 | .map_err(|e| ClientError(format!("get tx count error: {}", e)))?; 59 | // get gas_price 60 | let gas_price = self.w3.eth() 61 | .gas_price() 62 | .await 63 | .map_err(|e| ClientError(format!("get gas_price error: {}", e)))?; 64 | // legacy transaction type is still ok 65 | let options = Options::with(|op| { 66 | op.gas = Some(U256::from(100000)); 67 | op.nonce = Some(tx_count); 68 | op.gas_price = Some(gas_price); 69 | }); 70 | ic_cdk::println!("gas price: {:?}", gas_price); 71 | let txhash = self.contract 72 | .signed_call("processMessage", (msg_bytes,), options, caller, key_info, dst_chain as u64) 73 | .await 74 | .map_err(|e| ClientError(format!("processMessage failed: {}", e)))?; 75 | 76 | ic_cdk::println!("txhash: {}", hex::encode(txhash)); 77 | 78 | Ok(txhash) 79 | } 80 | 81 | async fn send_raw_tx(&self, raw_tx: Vec) -> Result, OmnicError> { 82 | let raw = Bytes::from(raw_tx); 83 | self.w3.eth().send_raw_transaction(raw) 84 | .await 85 | .map(|res| res.as_bytes().to_vec()) 86 | .map_err(|err| ClientError(format!("send_raw_tx failed: {:?}", err))) 87 | } 88 | 89 | async fn get_latest_root(&self, height: Option) -> Result { 90 | // query root in block height 91 | let h = match height { 92 | Some(v) => BlockId::Number(BlockNumber::Number(v.into())), 93 | None => BlockId::Number(BlockNumber::Latest), 94 | }; 95 | let root: Result = self.contract 96 | .query( 97 | "getLatestRoot", (), None, Options::default(), 98 | h 99 | ) 100 | .await; 101 | match root { 102 | Ok(r) => Ok(r), 103 | Err(e) => Err(ClientError(format!("get root error: {:?}", e))) 104 | } 105 | } 106 | 107 | async fn get_block_number(&self) -> Result { 108 | self.w3.eth().block_number() 109 | .await 110 | .map(|v| v.as_u64()) 111 | .map_err(|e| ClientError(format!("get block number error: {:?}", e))) 112 | } 113 | 114 | async fn get_tx_count(&self, addr: String) -> Result { 115 | let addr = Address::from_str(&addr).map_err(|e| ClientError(format!("address convert faild: {:?}", e)))?; 116 | self.w3.eth().transaction_count(addr, None) 117 | .await 118 | .map(|v| v.as_u64()) 119 | .map_err(|e| ClientError(format!("get tx count error: {:?}", e))) 120 | } 121 | 122 | async fn get_gas_price(&self) -> Result { 123 | self.w3.eth().gas_price() 124 | .await 125 | .map(|v| v.as_u64()) 126 | .map_err(|e| ClientError(format!("get tx count error: {:?}", e))) 127 | } 128 | } -------------------------------------------------------------------------------- /core/ic/src/chains/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod evm; 2 | 3 | pub use evm::*; -------------------------------------------------------------------------------- /core/ic/src/chains/omnic.abi: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": false, 12 | "internalType": "uint8", 13 | "name": "version", 14 | "type": "uint8" 15 | } 16 | ], 17 | "name": "Initialized", 18 | "type": "event" 19 | }, 20 | { 21 | "anonymous": false, 22 | "inputs": [ 23 | { 24 | "indexed": true, 25 | "internalType": "address", 26 | "name": "previousOwner", 27 | "type": "address" 28 | }, 29 | { 30 | "indexed": true, 31 | "internalType": "address", 32 | "name": "newOwner", 33 | "type": "address" 34 | } 35 | ], 36 | "name": "OwnershipTransferred", 37 | "type": "event" 38 | }, 39 | { 40 | "anonymous": false, 41 | "inputs": [ 42 | { 43 | "indexed": true, 44 | "internalType": "bytes32", 45 | "name": "messageHash", 46 | "type": "bytes32" 47 | }, 48 | { 49 | "indexed": true, 50 | "internalType": "bytes", 51 | "name": "returnData", 52 | "type": "bytes" 53 | }, 54 | { 55 | "indexed": false, 56 | "internalType": "bool", 57 | "name": "success", 58 | "type": "bool" 59 | } 60 | ], 61 | "name": "ProcessMessage", 62 | "type": "event" 63 | }, 64 | { 65 | "anonymous": false, 66 | "inputs": [ 67 | { 68 | "indexed": true, 69 | "internalType": "bytes32", 70 | "name": "messageHash", 71 | "type": "bytes32" 72 | }, 73 | { 74 | "indexed": true, 75 | "internalType": "uint256", 76 | "name": "leafIndex", 77 | "type": "uint256" 78 | }, 79 | { 80 | "indexed": true, 81 | "internalType": "uint32", 82 | "name": "dstChainId", 83 | "type": "uint32" 84 | }, 85 | { 86 | "indexed": false, 87 | "internalType": "uint32", 88 | "name": "nonce", 89 | "type": "uint32" 90 | }, 91 | { 92 | "indexed": false, 93 | "internalType": "bytes", 94 | "name": "message", 95 | "type": "bytes" 96 | } 97 | ], 98 | "name": "SendMessage", 99 | "type": "event" 100 | }, 101 | { 102 | "anonymous": false, 103 | "inputs": [ 104 | { 105 | "indexed": false, 106 | "internalType": "address", 107 | "name": "oldProxyCanisterAddr", 108 | "type": "address" 109 | }, 110 | { 111 | "indexed": false, 112 | "internalType": "address", 113 | "name": "newProxyCanisterAddr", 114 | "type": "address" 115 | } 116 | ], 117 | "name": "UpdateProxyCanister", 118 | "type": "event" 119 | }, 120 | { 121 | "inputs": [], 122 | "name": "MAX_MESSAGE_BODY_BYTES", 123 | "outputs": [ 124 | { 125 | "internalType": "uint256", 126 | "name": "", 127 | "type": "uint256" 128 | } 129 | ], 130 | "stateMutability": "view", 131 | "type": "function" 132 | }, 133 | { 134 | "inputs": [], 135 | "name": "chainId", 136 | "outputs": [ 137 | { 138 | "internalType": "uint32", 139 | "name": "", 140 | "type": "uint32" 141 | } 142 | ], 143 | "stateMutability": "view", 144 | "type": "function" 145 | }, 146 | { 147 | "inputs": [], 148 | "name": "committedRoot", 149 | "outputs": [ 150 | { 151 | "internalType": "bytes32", 152 | "name": "", 153 | "type": "bytes32" 154 | } 155 | ], 156 | "stateMutability": "view", 157 | "type": "function" 158 | }, 159 | { 160 | "inputs": [], 161 | "name": "getLatestRoot", 162 | "outputs": [ 163 | { 164 | "internalType": "bytes32", 165 | "name": "", 166 | "type": "bytes32" 167 | } 168 | ], 169 | "stateMutability": "view", 170 | "type": "function" 171 | }, 172 | { 173 | "inputs": [ 174 | { 175 | "internalType": "address", 176 | "name": "_proxyCanisterAddr", 177 | "type": "address" 178 | } 179 | ], 180 | "name": "initialize", 181 | "outputs": [], 182 | "stateMutability": "nonpayable", 183 | "type": "function" 184 | }, 185 | { 186 | "inputs": [ 187 | { 188 | "internalType": "uint32", 189 | "name": "", 190 | "type": "uint32" 191 | } 192 | ], 193 | "name": "nonces", 194 | "outputs": [ 195 | { 196 | "internalType": "uint32", 197 | "name": "", 198 | "type": "uint32" 199 | } 200 | ], 201 | "stateMutability": "view", 202 | "type": "function" 203 | }, 204 | { 205 | "inputs": [], 206 | "name": "omnicProxyCanisterAddr", 207 | "outputs": [ 208 | { 209 | "internalType": "address", 210 | "name": "", 211 | "type": "address" 212 | } 213 | ], 214 | "stateMutability": "view", 215 | "type": "function" 216 | }, 217 | { 218 | "inputs": [], 219 | "name": "owner", 220 | "outputs": [ 221 | { 222 | "internalType": "address", 223 | "name": "", 224 | "type": "address" 225 | } 226 | ], 227 | "stateMutability": "view", 228 | "type": "function" 229 | }, 230 | { 231 | "inputs": [ 232 | { 233 | "internalType": "bytes", 234 | "name": "_message", 235 | "type": "bytes" 236 | } 237 | ], 238 | "name": "processMessage", 239 | "outputs": [ 240 | { 241 | "internalType": "bool", 242 | "name": "success", 243 | "type": "bool" 244 | } 245 | ], 246 | "stateMutability": "nonpayable", 247 | "type": "function" 248 | }, 249 | { 250 | "inputs": [ 251 | { 252 | "internalType": "bytes32", 253 | "name": "_item", 254 | "type": "bytes32" 255 | } 256 | ], 257 | "name": "queueContains", 258 | "outputs": [ 259 | { 260 | "internalType": "bool", 261 | "name": "", 262 | "type": "bool" 263 | } 264 | ], 265 | "stateMutability": "view", 266 | "type": "function" 267 | }, 268 | { 269 | "inputs": [], 270 | "name": "queueEnd", 271 | "outputs": [ 272 | { 273 | "internalType": "bytes32", 274 | "name": "", 275 | "type": "bytes32" 276 | } 277 | ], 278 | "stateMutability": "view", 279 | "type": "function" 280 | }, 281 | { 282 | "inputs": [], 283 | "name": "queueLength", 284 | "outputs": [ 285 | { 286 | "internalType": "uint256", 287 | "name": "", 288 | "type": "uint256" 289 | } 290 | ], 291 | "stateMutability": "view", 292 | "type": "function" 293 | }, 294 | { 295 | "inputs": [], 296 | "name": "renounceOwnership", 297 | "outputs": [], 298 | "stateMutability": "nonpayable", 299 | "type": "function" 300 | }, 301 | { 302 | "inputs": [ 303 | { 304 | "internalType": "bytes32", 305 | "name": "_root", 306 | "type": "bytes32" 307 | } 308 | ], 309 | "name": "rootExists", 310 | "outputs": [ 311 | { 312 | "internalType": "bool", 313 | "name": "", 314 | "type": "bool" 315 | } 316 | ], 317 | "stateMutability": "view", 318 | "type": "function" 319 | }, 320 | { 321 | "inputs": [ 322 | { 323 | "internalType": "uint32", 324 | "name": "_dstChainId", 325 | "type": "uint32" 326 | }, 327 | { 328 | "internalType": "bytes32", 329 | "name": "_recipientAddress", 330 | "type": "bytes32" 331 | }, 332 | { 333 | "internalType": "bool", 334 | "name": "_waitOptimistic", 335 | "type": "bool" 336 | }, 337 | { 338 | "internalType": "bytes", 339 | "name": "_payload", 340 | "type": "bytes" 341 | } 342 | ], 343 | "name": "sendMessage", 344 | "outputs": [], 345 | "stateMutability": "nonpayable", 346 | "type": "function" 347 | }, 348 | { 349 | "inputs": [ 350 | { 351 | "internalType": "address", 352 | "name": "_newProxyCanisterAddr", 353 | "type": "address" 354 | } 355 | ], 356 | "name": "setOmnicCanisterAddr", 357 | "outputs": [], 358 | "stateMutability": "nonpayable", 359 | "type": "function" 360 | }, 361 | { 362 | "inputs": [], 363 | "name": "state", 364 | "outputs": [ 365 | { 366 | "internalType": "enum Omnic.States", 367 | "name": "", 368 | "type": "uint8" 369 | } 370 | ], 371 | "stateMutability": "view", 372 | "type": "function" 373 | }, 374 | { 375 | "inputs": [ 376 | { 377 | "internalType": "address", 378 | "name": "newOwner", 379 | "type": "address" 380 | } 381 | ], 382 | "name": "transferOwnership", 383 | "outputs": [], 384 | "stateMutability": "nonpayable", 385 | "type": "function" 386 | }, 387 | { 388 | "inputs": [], 389 | "name": "tree", 390 | "outputs": [ 391 | { 392 | "internalType": "uint256", 393 | "name": "count", 394 | "type": "uint256" 395 | } 396 | ], 397 | "stateMutability": "view", 398 | "type": "function" 399 | } 400 | ] -------------------------------------------------------------------------------- /core/ic/src/config.rs: -------------------------------------------------------------------------------- 1 | use candid::{Deserialize, CandidType, Principal}; 2 | 3 | #[derive(CandidType, Deserialize, Clone)] 4 | pub enum ChainType { 5 | Evm, 6 | Cosmos, 7 | Solana, 8 | } 9 | 10 | #[derive(CandidType, Deserialize, Clone)] 11 | pub struct ChainConfig { 12 | pub chain_type: ChainType, 13 | pub chain_id: u32, 14 | pub rpc_urls: Vec, // multiple rpc providers 15 | pub gateway_addr: Principal, // gateway canister address 16 | pub omnic_addr: String, // omnic contract address on that chain 17 | pub omnic_start_block: u64, // omnic contract deployment block 18 | } 19 | 20 | impl Default for ChainConfig { 21 | fn default() -> Self { 22 | Self { 23 | chain_type: ChainType::Evm, 24 | chain_id: Default::default(), 25 | rpc_urls: Default::default(), 26 | gateway_addr: Principal::anonymous(), 27 | omnic_addr: Default::default(), 28 | omnic_start_block: Default::default() 29 | } 30 | } 31 | } 32 | 33 | impl ChainConfig { 34 | pub fn new( 35 | chain_type: ChainType, 36 | chain_id: u32, 37 | rpc_urls: Vec, 38 | gateway_addr: Principal, 39 | omnic_addr: String, 40 | omnic_start_block: u64, 41 | ) -> ChainConfig { 42 | ChainConfig { 43 | chain_type: chain_type, 44 | chain_id: chain_id, 45 | rpc_urls: rpc_urls, 46 | gateway_addr: gateway_addr, 47 | omnic_addr: omnic_addr, 48 | omnic_start_block: omnic_start_block, 49 | } 50 | } 51 | 52 | pub fn add_rpc_url(&mut self, url: String) { 53 | self.rpc_urls.push(url); 54 | } 55 | 56 | pub fn add_urls(&mut self, urls: Vec) { 57 | self.rpc_urls.extend(urls); 58 | } 59 | } -------------------------------------------------------------------------------- /core/ic/src/consts.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // mainnet 5 | pub const KEY_NAME: &str = "test_key_1"; 6 | 7 | // local replica 8 | // pub const KEY_NAME: &str = "dfx_test_key"; 9 | 10 | pub const MAX_RESP_BYTES: Option = Some(500); 11 | pub const CYCLES_PER_CALL: Option = None; 12 | pub const CYCLES_PER_BYTE: u64 = 10_000; -------------------------------------------------------------------------------- /core/ic/src/error.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | use ic_web3::ethabi; 4 | 5 | /// Error types for Nomad 6 | #[derive(Debug, thiserror::Error)] 7 | pub enum OmnicError { 8 | /// IO error from Read/Write usage 9 | #[error(transparent)] 10 | IoError(#[from] std::io::Error), 11 | 12 | #[error(transparent)] 13 | ABIDecodeError(#[from] ethabi::Error), 14 | 15 | #[error("decode failed: `{0}`")] 16 | DecodeError(String), 17 | 18 | #[error("db error: `{0}`")] 19 | DBError(String), 20 | 21 | #[error("home error: `{0}`")] 22 | ClientError(String), 23 | 24 | #[error(transparent)] 25 | RPCError(#[from] ic_web3::error::Error), 26 | 27 | #[error(transparent)] 28 | ProveError(#[from] accumulator::error::ProvingError), 29 | 30 | #[error("other: `{0}`")] 31 | Other(String), 32 | } -------------------------------------------------------------------------------- /core/ic/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | pub mod utils; 4 | pub mod types; 5 | pub mod traits; 6 | pub mod config; 7 | pub mod chains; 8 | pub mod chain_state; 9 | pub mod consts; 10 | pub mod state; 11 | pub mod call; 12 | pub mod error; 13 | 14 | pub use types::*; 15 | pub use traits::*; 16 | pub use config::*; 17 | pub use chains::*; 18 | pub use chain_state::*; 19 | pub use consts::*; 20 | pub use state::*; 21 | pub use call::*; 22 | pub use error::*; 23 | -------------------------------------------------------------------------------- /core/ic/src/state.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Deserialize, Principal}; 2 | use std::collections::{HashSet, HashMap, BTreeMap}; 3 | use ic_web3::types::H256; 4 | use std::iter::FromIterator; 5 | use serde::Serialize; 6 | 7 | #[derive(CandidType, Deserialize, Copy, Clone, PartialEq, Eq)] 8 | pub enum State { 9 | Init, 10 | Fetching(usize), 11 | End, 12 | Fail, 13 | } 14 | 15 | impl Default for State { 16 | fn default() -> Self { 17 | Self::Init 18 | } 19 | } 20 | 21 | /// state transition 22 | /// chain ids: record all chain ids 23 | /// rpc urls: record rpc urls for this round 24 | /// block height: specific block height to query root 25 | /// root: root 26 | /// main state: loop forever to fetch root from each chain 27 | /// sub state: each time issue an sub task to handle fetch root from specific rpc url 28 | /// 29 | /// Init: inital state 30 | /// Fetching(idx): fetching roots 31 | /// End: Round Finish 32 | /// Fail: Fail to fetch or root mismatch 33 | /// 34 | /// Main State transition: 35 | /// Init => Move to Fetching(0) 36 | /// Fetching(idx) => 37 | /// Sub state: Init => init rpc urls, chain_ids into state machine for this round, issue a sub task for fetch root of current chain id 38 | /// Sub state: Fetching => fetching, do nothing 39 | /// Sub state: Fail => Move sub state and state to Init 40 | /// Sub state: End => Update the root, move sub state and state to Init 41 | /// End, Fail => can't reach this 2 state in main state 42 | /// 43 | /// Sub state transition: 44 | /// Init => get block height, move state to fail or fetching, issue a sub task 45 | /// Fetching => fetch root, compare and set the root, move state accordingly, issue a sub task 46 | /// Fail => _ 47 | /// End => _ 48 | #[derive(Default, Clone)] 49 | pub struct StateMachine { 50 | pub chain_id: u32, 51 | pub rpc_urls: Vec, 52 | pub block_height: u64, 53 | pub omnic_addr: String, 54 | pub roots: HashMap, 55 | pub state: State, 56 | pub sub_state: State 57 | } 58 | 59 | #[derive(CandidType, Deserialize)] 60 | pub struct StateMachineStable { 61 | chain_id: u32, 62 | rpc_urls: Vec, 63 | block_height: u64, 64 | omnic_addr: String, 65 | roots: Vec<([u8;32], usize)>, 66 | state: State, 67 | sub_state: State 68 | } 69 | 70 | impl StateMachine { 71 | pub fn set_chain_id(&mut self, chain_id: u32) { 72 | self.chain_id = chain_id; 73 | } 74 | 75 | pub fn set_rpc_urls(&mut self, rpc_urls: Vec) { 76 | self.rpc_urls = rpc_urls; 77 | } 78 | 79 | pub fn set_omnic_addr(&mut self, omnic_addr: String) { 80 | self.omnic_addr = omnic_addr; 81 | } 82 | 83 | pub fn rpc_count(&self) -> usize { 84 | self.rpc_urls.len() 85 | } 86 | 87 | pub fn get_fetching_next_state(&self) -> (State, State) { 88 | if let State::Fetching(_) = self.state { 89 | // state and sub state always move to init 90 | (State::Init, State::Init) 91 | } else { 92 | panic!("state not in fetching") 93 | } 94 | } 95 | 96 | pub fn get_fetching_next_sub_state(&self, check_result: bool) -> State { 97 | if let State::Fetching(idx) = self.sub_state { 98 | if!check_result { 99 | State::Fail 100 | } else if idx + 1 == self.rpc_count() { 101 | State::End 102 | } else { 103 | State::Fetching(idx + 1) 104 | } 105 | } else { 106 | panic!("sub state not in fetching") 107 | } 108 | } 109 | } 110 | 111 | impl From for StateMachine { 112 | fn from(s: StateMachineStable) -> Self { 113 | Self { 114 | chain_id: s.chain_id, 115 | rpc_urls: s.rpc_urls, 116 | block_height: s.block_height, 117 | omnic_addr: s.omnic_addr, 118 | roots: HashMap::from_iter(s.roots.into_iter().map(|(x, y)| (H256::from(x), y))), 119 | state: s.state, 120 | sub_state: s.sub_state, 121 | } 122 | } 123 | } 124 | 125 | impl From for StateMachineStable { 126 | fn from(s: StateMachine) -> Self { 127 | Self { 128 | chain_id: s.chain_id, 129 | rpc_urls: s.rpc_urls, 130 | block_height: s.block_height, 131 | omnic_addr: s.omnic_addr, 132 | roots: Vec::from_iter(s.roots.into_iter().map(|(x, y)| (x.to_fixed_bytes(), y))), 133 | state: s.state, 134 | sub_state: s.sub_state, 135 | } 136 | } 137 | } 138 | 139 | #[derive(CandidType, Deserialize, Clone)] 140 | pub struct StateInfo { 141 | pub owners: HashSet, 142 | pub fetch_root_period: u64, 143 | pub fetch_roots_period: u64, 144 | pub query_rpc_number: u64, 145 | } 146 | 147 | impl StateInfo { 148 | pub fn default() -> StateInfo { 149 | StateInfo { 150 | owners: HashSet::default(), 151 | fetch_root_period: 1_000_000_000 * 60, 152 | fetch_roots_period: 1_000_000_000 * 90, 153 | query_rpc_number: 1, 154 | } 155 | } 156 | 157 | pub fn add_owner(&mut self, owner: Principal) { 158 | self.owners.insert(owner); 159 | } 160 | 161 | pub fn delete_owner(&mut self, owner: Principal) { 162 | self.owners.remove(&owner); 163 | } 164 | 165 | pub fn is_owner(&self, user: Principal) -> bool { 166 | self.owners.contains(&user) 167 | } 168 | 169 | pub fn set_fetch_period(&mut self, v1: u64, v2: u64) { 170 | self.fetch_root_period = v1; 171 | self.fetch_roots_period = v2; 172 | } 173 | 174 | pub fn set_rpc_number(&mut self, n: u64) { 175 | self.query_rpc_number = n 176 | } 177 | } 178 | 179 | #[derive(CandidType, Deserialize, Default)] 180 | pub struct RecordDB { 181 | pub records: Vec, 182 | // index 183 | pub op_index: BTreeMap>, 184 | } 185 | 186 | #[derive(CandidType, Deserialize, Clone)] 187 | pub struct Record { 188 | pub id: usize, 189 | pub caller: Principal, 190 | pub timestamp: u64, 191 | pub operation: String, 192 | pub details: Vec<(String, DetailValue)>, 193 | } 194 | 195 | // refer to caps https://github.com/Psychedelic/cap/blob/main/common/src/transaction.rs 196 | #[derive(CandidType, Serialize, Deserialize, Clone, PartialEq)] 197 | pub enum DetailValue { 198 | True, 199 | False, 200 | U64(u64), 201 | I64(i64), 202 | Float(f64), 203 | Text(String), 204 | Principal(Principal), 205 | #[serde(with = "serde_bytes")] 206 | Slice(Vec), 207 | Vec(Vec), 208 | } 209 | 210 | impl RecordDB { 211 | pub fn new() -> Self { 212 | Self::default() 213 | } 214 | 215 | pub fn size(&self, op: Option) -> usize { 216 | match op { 217 | Some(o) => { 218 | match self.op_index.get(&o) { 219 | Some(i) => i.len(), 220 | None => 0, 221 | } 222 | } 223 | None => { 224 | self.records.len() 225 | } 226 | } 227 | } 228 | 229 | pub fn append(&mut self, caller: Principal, ts: u64, op: String, details: Vec<(String, DetailValue)>) -> usize { 230 | let id = self.size(None); 231 | let record = Record{ 232 | id, 233 | caller, 234 | timestamp: ts, 235 | operation: op.clone(), 236 | details, 237 | }; 238 | self.records.push(record); 239 | // store the operation index 240 | self.op_index 241 | .entry(op) 242 | .and_modify(|v| v.push(id)) 243 | .or_insert(vec![id]); 244 | id 245 | } 246 | 247 | pub fn load_by_id(&self, id: usize) -> Option { 248 | self.records.get(id).cloned() 249 | } 250 | 251 | // start: inclusive, end: exclusive 252 | pub fn load_by_id_range(&self, start: usize, mut end: usize) -> Vec { 253 | if start > end { 254 | panic!("Invalid range"); 255 | } 256 | let len = self.size(None); 257 | if len == 0 { 258 | return Vec::default(); 259 | } 260 | if end > len { 261 | end = len 262 | } 263 | self.records.get(start..end).expect("error load by range").to_vec().clone() 264 | } 265 | 266 | // op: operation, start: inclusive, end: exclusive 267 | pub fn load_by_opeation(&self, op: String, start: usize, mut end: usize) -> Vec { 268 | if start > end { 269 | panic!("Invalid range"); 270 | } 271 | let default = Vec::default(); 272 | let ops = self.op_index.get(&op).unwrap_or(default.as_ref()); 273 | let len = op.len(); 274 | if len == 0 { 275 | return Vec::default(); 276 | } 277 | if end > len { 278 | end = len 279 | } 280 | 281 | let mut res: Vec = Vec::default(); 282 | for id in &ops[start..end] { 283 | let record = self.records.get(id.to_owned()).expect("error load by id").clone(); 284 | res.push(record); 285 | } 286 | 287 | res 288 | } 289 | } -------------------------------------------------------------------------------- /core/ic/src/traits/chain.rs: -------------------------------------------------------------------------------- 1 | use ic_web3::types::H256; 2 | use async_trait::async_trait; 3 | 4 | use crate::error::OmnicError; 5 | 6 | // each chain client should impl this trait 7 | #[async_trait] 8 | pub trait HomeContract { 9 | async fn dispatch_message(&self, caller: String, dst_chain: u32, msg: Vec) -> Result; 10 | async fn get_tx_count(&self, addr: String) -> Result; 11 | async fn get_gas_price(&self) -> Result; 12 | async fn send_raw_tx(&self, raw_tx: Vec) -> Result, OmnicError>; 13 | async fn get_latest_root(&self, height: Option) -> Result; 14 | async fn get_block_number(&self) -> Result; 15 | } -------------------------------------------------------------------------------- /core/ic/src/traits/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chain; 2 | 3 | pub use chain::*; -------------------------------------------------------------------------------- /core/ic/src/types/message.rs: -------------------------------------------------------------------------------- 1 | 2 | use candid::Deserialize; 3 | use ic_web3::types::H256; 4 | use crate::{utils::keccak256, OmnicError}; 5 | use crate::OmnicError::DecodeError; 6 | use ic_web3::ethabi::{decode, encode, ParamType, Token, Error}; 7 | 8 | #[derive(Debug, Default, Clone, Deserialize)] 9 | pub struct Message { 10 | /// 4 SLIP-44 ID 11 | pub origin: u32, 12 | /// 32 Address in home convention 13 | pub sender: H256, 14 | /// 4 Count of all previous messages to destination 15 | pub nonce: u32, 16 | /// 4 SLIP-44 ID 17 | pub destination: u32, 18 | /// 32 Address in destination convention 19 | pub recipient: H256, 20 | /// 0+ Message contents 21 | pub body: Vec, 22 | } 23 | 24 | fn decode_body(data: &[u8]) -> Result, Error> { 25 | let types = vec![ 26 | ParamType::Uint(32), ParamType::FixedBytes(32), ParamType::Uint(32), 27 | ParamType::Uint(32), ParamType::FixedBytes(32), ParamType::Bytes 28 | ]; 29 | decode(&types, data) 30 | } 31 | 32 | fn encode_body(msg: &Message) -> Vec { 33 | let tokens = [ 34 | Token::Uint(msg.origin.into()), 35 | Token::FixedBytes(msg.sender.as_bytes().to_vec()), 36 | Token::Uint(msg.nonce.into()), 37 | Token::Uint(msg.destination.into()), 38 | Token::FixedBytes(msg.recipient.as_bytes().to_vec()), 39 | Token::Bytes(msg.body.clone()) 40 | ]; 41 | encode(&tokens) 42 | } 43 | 44 | impl Message { 45 | pub fn from_raw(raw_bytes: Vec) -> Result { 46 | let res = decode_body(&raw_bytes)?; 47 | let origin = res[0].clone().into_uint().ok_or(DecodeError("get origin failed".into()))?.as_u32(); 48 | let sender_bytes = res[1].clone().into_fixed_bytes().ok_or(DecodeError("get sender failed".into()))?; 49 | let sender = H256::from_slice(&sender_bytes); 50 | let nonce = res[2].clone().into_uint().ok_or(DecodeError("get nonce failed".into()))?.as_u32(); 51 | let destination = res[3].clone().into_uint().ok_or(DecodeError("get destination failed".into()))?.as_u32(); 52 | let recipient_bytes = res[4].clone().into_fixed_bytes().ok_or(DecodeError("get recipient failed".into()))?; 53 | let recipient = H256::from_slice(&recipient_bytes); 54 | let body = res[5].clone().into_bytes().ok_or(DecodeError("get body failed".into()))?; 55 | 56 | Ok(Message { 57 | origin, 58 | sender, 59 | nonce, 60 | destination, 61 | recipient, 62 | body, 63 | }) 64 | } 65 | } 66 | 67 | impl Message { 68 | /// Convert the message to a leaf 69 | pub fn to_leaf(&self) -> H256 { 70 | let raw = encode_body(&self); 71 | keccak256(&raw).into() 72 | } 73 | } 74 | 75 | impl std::fmt::Display for Message { 76 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | write!( 78 | f, 79 | "Message {}->{}:{}", 80 | self.origin, self.destination, self.nonce, 81 | ) 82 | } 83 | } -------------------------------------------------------------------------------- /core/ic/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | pub mod message; 4 | 5 | pub use message::*; -------------------------------------------------------------------------------- /core/ic/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::state::DetailValue; 2 | 3 | use std::collections::HashMap; 4 | 5 | use ic_web3::types::H256; 6 | use tiny_keccak::{Hasher, Keccak}; 7 | 8 | pub fn keccak256(msg: &[u8]) -> [u8; 32] { 9 | let mut hasher = Keccak::v256(); 10 | let mut result = [0u8; 32]; 11 | hasher.update(msg); 12 | hasher.finalize(&mut result); 13 | result 14 | } 15 | 16 | /// check if the roots match the criteria so far, return the check result and root 17 | pub fn check_roots_result(roots: &HashMap, total_result: usize) -> (bool, H256) { 18 | // when rpc fail, the root is vec![0; 32] 19 | if total_result <= 2 { 20 | // rpc len <= 2, all roots must match 21 | if roots.len() != 1 { 22 | return (false, H256::zero()); 23 | } else { 24 | let r = roots.keys().next().unwrap().clone(); 25 | return (r != H256::zero(), r); 26 | } 27 | } else { 28 | // rpc len > 2, half of the rpc result should be the same 29 | let limit = total_result / 2; 30 | // if contains > n/2 root, def fail 31 | let root_count = roots.keys().len(); 32 | if root_count > limit { 33 | return (false, H256::zero()); 34 | } 35 | 36 | // if #ZERO_HASH > n/2, def fail 37 | let error_count = roots.get(&H256::zero()).unwrap_or(&0); 38 | if error_count > &limit { 39 | return (false, H256::zero()); 40 | } 41 | 42 | // if the #(root of most count) + #(rest rpc result) <= n / 2, def fail 43 | let mut possible_root = H256::zero(); 44 | let mut possible_count: usize = 0; 45 | let mut current_count = 0; 46 | for (k ,v ) in roots { 47 | if v > &possible_count { 48 | possible_count = *v; 49 | possible_root = k.clone(); 50 | } 51 | current_count += *v; 52 | } 53 | if possible_count + (total_result - current_count) <= limit { 54 | return (false, H256::zero()); 55 | } 56 | 57 | // otherwise return true and root of most count 58 | return (true, possible_root.clone()) 59 | } 60 | } 61 | 62 | /// Allows creating details for an event. 63 | #[derive(Default, Clone)] 64 | pub struct DetailsBuilder { 65 | inner: Vec<(String, DetailValue)>, 66 | } 67 | 68 | impl DetailsBuilder { 69 | /// Creates a new, empty builder. 70 | #[inline(always)] 71 | pub fn new() -> Self { 72 | Self::default() 73 | } 74 | 75 | /// Inserts a new element. 76 | #[inline(always)] 77 | pub fn insert(mut self, key: impl Into, value: impl Into) -> Self { 78 | self.inner.push((key.into(), value.into())); 79 | 80 | self 81 | } 82 | 83 | #[inline(always)] 84 | pub fn build(self) -> Vec<(String, DetailValue)> { 85 | self.inner 86 | } 87 | } -------------------------------------------------------------------------------- /core/ic/start.sh: -------------------------------------------------------------------------------- 1 | pkill dfx 2 | sleep 3 3 | 4 | dfx start --background --clean --enable-canister-http 5 | 6 | dfx canister create proxy 7 | dfx canister create demo 8 | 9 | dfx build proxy 10 | dfx build demo 11 | 12 | dfx canister install proxy 13 | dfx canister install demo 14 | 15 | dfx canister --network ic call proxy add_chain "(5:nat32, vec {\"https://eth-goerli.g.alchemy.com/v2/0QCHDmgIEFRV48r1U1QbtOyFInib3ZAm\"}, principal \"$(dfx canister --network ic id goerli-gateway)\", \"c7D718dC3C9248c91813A98dCbFEC6CF57619520\", 7685333:nat64)" 16 | 17 | dfx canister --network ic call proxy add_chain "(80001:nat32, vec {\"https://polygon-mumbai.g.alchemy.com/v2/0QCHDmgIEFRV48r1U1QbtOyFInib3ZAm\"}, principal \"$(dfx canister --network ic id mumbai-gateway)\", \"2F711bEbA7a30242f4ba24544eA3869815c41413\", 28370114:nat64)" 18 | 19 | dfx canister --network ic call proxy set_canister_addrs 20 | 21 | dfx canister --network ic call proxy get_canister_addr "(variant {Evm})" 22 | 23 | dfx canister --network ic call proxy get_chains 24 | 25 | #echo "waiting for proxy to get latest root from eth..." 26 | #sleep 30 27 | #dfx canister call proxy get_latest_root "(5:nat32)" 28 | -------------------------------------------------------------------------------- /examples/placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocklabs-io/omnic/519d43e91978d204891e7b9c65ccf08e1f2afc5c/examples/placeholder -------------------------------------------------------------------------------- /pics/arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocklabs-io/omnic/519d43e91978d204891e7b9c65ccf08e1f2afc5c/pics/arch.jpg -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | ## Omnic Core Spec 2 | 3 | 4 | 5 | ### 1. Data Spec 6 | 7 | #### 1.1 Domains 8 | 9 | Different chains are refered to as domains, each with an id, type `u32`, used for specify the message destination: 10 | 11 | | Domain | ID | 12 | | --------------- | ----- | 13 | | IC | 0 | 14 | | Ethereum | 1 | 15 | | Ethereum Goerli | 5 | 16 | | Polygon Mumbai | 80001 | 17 | 18 | More domains to be added. 19 | 20 | #### 1.2 Message format 21 | 22 | Crosschain message format: 23 | 24 | ``` 25 | uint32 origin // message origin chain 26 | bytes32 sender // sender on origin chain 27 | uint32 nonce // current nonce for destination chain 28 | uint32 destination // destination chain 29 | bytes32 recipient // message recipient on destination chain 30 | bytes payload // message data in bytes 31 | ``` 32 | 33 | If the destination is 0, then it's a message sent to canister `recipient` on the IC. 34 | 35 | The sender & recipient are padded into `bytes32` format, principal IDs should be converted into bytes format first then left padded with zeros. 36 | 37 | ### 2. EVM gateway contract 38 | 39 | EVM side gateway contract receives messages from application contracts, organize messages into a merkle tree, core interfaces: 40 | 41 | ``` 42 | sendMessage(uint32 destination, bytes32 receipient, bytes memory payload) // called by application contracts, enqueue a crosschain message 43 | processMessage(bytes memory message) // called by Omnic proxy canister, process an incoming message from another chain, this function will call recipient contranct's handleMessage function 44 | ``` 45 | 46 | Application contracts must implement `handleMessage` to be able to receive crosschain message from Omnic gateway contract: 47 | 48 | ``` 49 | handleMessage(uint32 origin, bytes32 sender, uint32 nonce, bytes memory payload) // handle the crosschain message on the application side 50 | ``` 51 | 52 | 53 | 54 | ### 3. IC proxy canister 55 | 56 | The IC side proxy canister periodically fetch message merkle roots from supported chains, verify crosschain messages submitted by offchain relayer, and process crosschain messages: 57 | 58 | * if the message destination is IC, notify the recipient canister 59 | * if the message destination is another chain, construct and sign a tx to the Omnic gateway contract on the destination chain, tx is signed via threshold ECDSA, and sent by outbound http call 60 | 61 | ``` 62 | get_latest_root(chain_id: u32) -> Result // get latest merkle root for given chain 63 | process_message(message: Vec, proof: Vec>, leaf_index: u32) -> Result // called by the offchain relayer, verify & process a crosschain message 64 | ``` 65 | 66 | In order to receive crosschain message notification, application canisters must implement `handle_message` function: 67 | 68 | ``` 69 | handle_message(origin: u32, nonce: u32, sender: Vec, body: Vec) -> Result 70 | ``` 71 | 72 | 73 | 74 | --------------------------------------------------------------------------------