├── .gitignore ├── README.md ├── ape-config.yaml ├── contracts ├── LzApp.vy ├── NonBlockingLzApp.vy ├── OmniCounter.vy ├── PingPong.vy └── mocks │ └── EndpointMock.sol ├── poetry.lock ├── pyproject.toml └── tests └── test_lzapp.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ape stuff 2 | .build/ 3 | .cache/ 4 | 5 | # Python 6 | .env 7 | .venv 8 | .pytest_cache 9 | __pycache__ 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Layer Zero Vyper Examples 2 | 3 | This repository contains examples of Omnichain contracts implemented in Vyper. 4 | 5 | ## Contracts 6 | 7 | - OmniCounter 8 | - LzApp 9 | - NonBlockingLzApp 10 | 11 | ## Running Tests 12 | make sure you have ape installed, along with the vyper plugin 13 | 14 | `ape test` 15 | -------------------------------------------------------------------------------- /ape-config.yaml: -------------------------------------------------------------------------------- 1 | name: LayerZeroVyper 2 | dependencies: 3 | - name: SolidityExamples 4 | github: LayerZero-Labs/solidity-examples 5 | branch: main 6 | contracts_folder: contracts 7 | 8 | solidity: 9 | import_remapping: 10 | - "@layerzero=SolidityExamples/main" 11 | -------------------------------------------------------------------------------- /contracts/LzApp.vy: -------------------------------------------------------------------------------- 1 | PAYLOAD_SIZE: constant(uint256) = 128 2 | CONFIG_SIZE: constant(uint256) = 512 3 | 4 | # filler implementation of _blockingLzReceive 5 | # body is just a `pass` statement 6 | @internal 7 | def _blockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[32], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 8 | pass 9 | 10 | interface ILayerZeroReceiver: 11 | def lzReceive(srcChainId: uint16, srcAddress: Bytes[32], nonce: uint64, payload: Bytes[PAYLOAD_SIZE]): nonpayable 12 | 13 | interface ILayerZeroEndpoint: 14 | # def send(dstChainId: uint16, destination: Bytes[32], payload: Bytes[CONFIG_SIZE], refundAddress: address, zroPaymentAddress: address, adapterParams: Bytes[CONFIG_SIZE]): payable 15 | def receivePayload(srcChainId: uint16, srcAddress: Bytes[32], dstAddress: address, nonce: uint64, gasLimit: uint256, payload: Bytes[PAYLOAD_SIZE]): nonpayable 16 | def getInboundNonce(srcChainId: uint16, srcAddress: Bytes[32]) -> uint64: view 17 | def getOutboundNonce(dstChainId: uint16, srcAddress: address) -> uint64: view 18 | def estimateFees(dstChainId: uint16, userApplication: address, payload: Bytes[PAYLOAD_SIZE], payInZRO: bool, adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 19 | def getChainId() -> uint16: view 20 | def retryPayload(srcChainId: uint16, srcAddress: Bytes[32], payload: Bytes[PAYLOAD_SIZE]): nonpayable 21 | def hasStoredPayload(srcChainId: uint16, srcAddress: Bytes[32]) -> bool: view 22 | def getSendLibraryAddress(userApplication: address) -> address: view 23 | def getReceiveLibraryAddress(userApplication: address) -> address: view 24 | def isSendingPayload() -> bool: view 25 | def isReceivingPayload() -> bool: view 26 | def getConfig(version: uint16, chainId: uint16, userApplication: address, configType: uint256) -> Bytes[CONFIG_SIZE]: view 27 | def getSendVersion(userApplication: address) -> uint16: view 28 | def getReceiveVersion(userApplication: address) -> uint16: view 29 | def setConfig(version: uint16, chainId: uint16, configType: uint256, config: Bytes[CONFIG_SIZE]): nonpayable 30 | def setSendVersion(version: uint16): nonpayable 31 | def setReceiveVersion(version: uint16): nonpayable 32 | def forceResumeReceive(srcChainId: uint16, srcAddress: Bytes[32]): nonpayable 33 | 34 | interface ILayerZeroMessagingLibrary: 35 | # def send(_userApplication: address, _lastNonce: uint64, _chainId: uint16, _destination: Bytes[32], _payload: Bytes[CONFIG_SIZE], refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE]): payable 36 | def estimateFees(_chainId: uint16, _userApplication: address, _payload: Bytes[PAYLOAD_SIZE], _payInZRO: bool, _adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 37 | def setConfig(_chainId: uint16, _userApplication: address, _configType: uint256, _config: Bytes[CONFIG_SIZE]): nonpayable 38 | def getConfig(_chainId: uint16, _userApplication: address, _configType: uint256) -> Bytes[CONFIG_SIZE]: view 39 | 40 | interface ILayerZeroOracle: 41 | def getPrice(dstChainId: uint16, outboundProofType: uint16) -> uint256: view 42 | def notifyOracle(dstChainId: uint16, outboundProofType: uint16, outboundBlockConfirmations: uint64): nonpayable 43 | def isApproved(_address: address) -> bool: view 44 | 45 | interface ILayerZeroRelayer: 46 | def getPrice(dstChainId: uint16, outboundProofType: uint16, userApplication: address, payloadSize: uint256, adapterParams: Bytes[CONFIG_SIZE]) -> uint256: view 47 | def notifyRelayer(dstChainId: uint16, outboundProofType: uint16, adapterParams: Bytes[CONFIG_SIZE]): nonpayable 48 | def isApproved(_address: address) -> bool: view 49 | 50 | owner: address 51 | 52 | lzEndpoint: public(ILayerZeroEndpoint) 53 | DEFAULT_PAYLOAD_SIZE_LIMIT: constant(uint256) = 1000000 54 | trustedRemoteLookup: public(HashMap[uint16, Bytes[40]]) 55 | payloadSizeLimitLookup: public(HashMap[uint16, uint256]) 56 | minDstGasLookup: public(HashMap[uint16, HashMap[uint16, uint256]]) 57 | precrime: public(address) 58 | 59 | event SetPrecrime: 60 | precrime: address 61 | 62 | event SetTrustedRemote: 63 | _remoteChainId: uint16 64 | _path: Bytes[40] 65 | 66 | event SetTrustedRemoteAddress: 67 | _remoteChainId: uint16 68 | _remoteAddress: Bytes[20] 69 | 70 | event SetMinDstGas: 71 | _dstChainId: uint16 72 | _type: uint16 73 | _minDstGas: uint256 74 | 75 | @external 76 | def __init__(_lzEndpoint: ILayerZeroEndpoint): 77 | self.lzEndpoint = _lzEndpoint 78 | self.owner = msg.sender 79 | 80 | @internal 81 | def _onlyOwner(): 82 | assert msg.sender == self.owner 83 | 84 | @external 85 | def _lzReceive(_srcChainId: uint16, _srcAddress: Bytes[32], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 86 | assert msg.sender == self.lzEndpoint.address 87 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 88 | assert len(_srcAddress) == len(trustedRemote) 89 | assert len(trustedRemote) > 0 90 | assert keccak256(_srcAddress) == keccak256(trustedRemote) 91 | 92 | self._blockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) 93 | 94 | 95 | @internal 96 | def _lzSend(_dstChainId: uint16, _payload: Bytes[PAYLOAD_SIZE], _refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE], _nativeFee: uint256): 97 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_dstChainId] 98 | assert len(trustedRemote) != 0 99 | self._checkPayloadSize(_dstChainId, len(_payload)) 100 | # usually, we would call the send function like this 101 | # self.lzEndpoint.send(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, value=_nativeFee) 102 | # 103 | # the interface definition for this function in solidity is: 104 | # function send(uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams) external payable; 105 | 106 | # 107 | # because send is a reserved keyword in Vyper, we have to use raw_call to call this function 108 | # we will use _abiEncode to encode the arguments to be passed 109 | 110 | # encode the arguments 111 | payload: Bytes[2404] = _abi_encode(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, method_id=method_id("send(uint16,bytes,bytes,address,address,bytes)")) 112 | # call the function 113 | raw_call(self.lzEndpoint.address, payload, value=_nativeFee) 114 | 115 | 116 | @external 117 | def _checkGasLimit(_dstChainId: uint16, _type: uint16, _adapterParams: Bytes[CONFIG_SIZE], _extraGas: uint256): 118 | providedGasLimit: uint256 = self._getGasLimit(_adapterParams) 119 | minGasLimit: uint256 = self.minDstGasLookup[_dstChainId][_type] + _extraGas 120 | assert minGasLimit > 0, "LzApp: minGasLimit not set" 121 | assert providedGasLimit >= minGasLimit, "LzApp: gas limit is too low" 122 | 123 | 124 | @internal 125 | @pure 126 | def _getGasLimit(_adapterParams: Bytes[CONFIG_SIZE]) -> uint256: 127 | assert len(_adapterParams) >= 34 128 | return convert(slice(_adapterParams, 34, 32), uint256) 129 | 130 | @internal 131 | def _checkPayloadSize(_dstChainId: uint16, _payloadSize: uint256): 132 | payloadSizeLimit: uint256 = self.payloadSizeLimitLookup[_dstChainId] 133 | if payloadSizeLimit == 0: 134 | payloadSizeLimit = DEFAULT_PAYLOAD_SIZE_LIMIT 135 | assert _payloadSize <= payloadSizeLimit, "LzApp: payload size is too large" 136 | 137 | @external 138 | def getConfig(_version: uint16, _chainId: uint16, _configType: uint256) -> Bytes[CONFIG_SIZE]: 139 | return self.lzEndpoint.getConfig(_version, _chainId, self, _configType) 140 | 141 | @external 142 | def setConfig(_version: uint16, _chainId: uint16, _configType: uint256, _config: Bytes[CONFIG_SIZE]): 143 | self._onlyOwner() 144 | self.lzEndpoint.setConfig(_version, _chainId, _configType, _config) 145 | 146 | @external 147 | def setSendVersion(_version: uint16): 148 | self._onlyOwner() 149 | self.lzEndpoint.setSendVersion(_version) 150 | 151 | @external 152 | def setReceiveVersion(_version: uint16): 153 | self._onlyOwner() 154 | self.lzEndpoint.setReceiveVersion(_version) 155 | 156 | @external 157 | def forceResumeReceive(_srcChainId: uint16, _srcAddress: Bytes[32]): 158 | self.lzEndpoint.forceResumeReceive(_srcChainId, _srcAddress) 159 | 160 | @external 161 | def setTrustedRemote(_srcChainId: uint16, _path: Bytes[32]): 162 | self._onlyOwner() 163 | self.trustedRemoteLookup[_srcChainId] = _path 164 | log SetTrustedRemote(_srcChainId, _path) 165 | 166 | @external 167 | def setTrustedRemoteAddress(_remoteChainId: uint16, _remoteAddress: address): 168 | # convert address to bytes 169 | _remoteAddressBytes: Bytes[20] = slice(concat(b"", convert(_remoteAddress, bytes32)), 12, 20) 170 | selfAddrAsBytes: Bytes[20] = slice(concat(b"", convert(self, bytes32)), 12, 20) 171 | self.trustedRemoteLookup[_remoteChainId] = concat(_remoteAddressBytes, selfAddrAsBytes) 172 | log SetTrustedRemoteAddress(_remoteChainId, _remoteAddressBytes) 173 | 174 | @external 175 | @view 176 | def getTrustedRemoteAddress(_remoteChainId: uint16) -> Bytes[20]: 177 | path: Bytes[40] = self.trustedRemoteLookup[_remoteChainId] 178 | assert len(path) != 0 179 | return slice(path, 0, 20) 180 | 181 | @external 182 | def setPrecrime(precrime: address): 183 | self._onlyOwner() 184 | self.precrime = precrime 185 | log SetPrecrime(precrime) 186 | 187 | @external 188 | def setMinDstGas(_dstChainId: uint16, _packetType: uint16, _minGas: uint256): 189 | self._onlyOwner() 190 | assert _minGas > 0, "LzApp: invalid minGas" 191 | self.minDstGasLookup[_dstChainId][_packetType] = _minGas 192 | log SetMinDstGas(_dstChainId, _packetType, _minGas) 193 | 194 | @external 195 | @view 196 | def isTrustedRemote(_srcChainId: uint16, _srcAddress: Bytes[32]) -> bool: 197 | trustedSource: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 198 | return keccak256(trustedSource) == keccak256(_srcAddress) 199 | -------------------------------------------------------------------------------- /contracts/NonBlockingLzApp.vy: -------------------------------------------------------------------------------- 1 | PAYLOAD_SIZE: constant(uint256) = 128 2 | CONFIG_SIZE: constant(uint256) = 512 3 | 4 | @internal 5 | def _nonblockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 6 | # contract body upon cross-chain call goes here 7 | pass 8 | 9 | failedMessages: public(HashMap[uint16, HashMap[Bytes[40], HashMap[uint64, bytes32]]]) 10 | 11 | event MessageFailed: 12 | _srcChainId: uint16 13 | _srcAddress: Bytes[40] 14 | _nonce: uint64 15 | _payload: Bytes[PAYLOAD_SIZE] 16 | _reason: Bytes[1024] 17 | 18 | 19 | event RetryMessageSuccess: 20 | _srcChainId: uint16 21 | _srcAddress: Bytes[40] 22 | _nonce: uint64 23 | _payloadHash: bytes32 24 | 25 | # filler implementation of _blockingLzReceive 26 | # body is just a `pass` statement 27 | @internal 28 | def _blockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 29 | # call self via raw_call with revert_on_failure=False and max_outsize=256 30 | # raw_call signature is pasted below 31 | # raw_call(to: address, data: Bytes, max_outsize: int = 0, gas: uint256 = gasLeft, value: uint256 = 0, is_delegate_call: bool = False, is_static_call: bool = False, revert_on_failure: bool = True)→ Bytes[max_outsize] 32 | success: bool = False 33 | data: Bytes[256] = b"" 34 | success, data = raw_call(self, concat(0x66ad5c8a, _abi_encode(_srcChainId, _srcAddress, _nonce, _payload)), max_outsize=256, revert_on_failure=False) 35 | if not success: 36 | self._storeFailedMessage(_srcChainId, _srcAddress, _nonce, _payload) 37 | 38 | # implementation of _storeFailedMessage 39 | @internal 40 | def _storeFailedMessage(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 41 | payloadHash: bytes32 = keccak256(_payload) 42 | self.failedMessages[_srcChainId][_srcAddress][_nonce] = payloadHash 43 | log MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, convert("Failed to send payload", Bytes[100])) 44 | 45 | @external 46 | def nonblockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 47 | assert msg.sender == self, "ONLYSELF" 48 | self._nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) 49 | 50 | interface ILayerZeroReceiver: 51 | def lzReceive(srcChainId: uint16, srcAddress: Bytes[40], nonce: uint64, payload: Bytes[PAYLOAD_SIZE]): nonpayable 52 | 53 | interface ILayerZeroEndpoint: 54 | # def send(dstChainId: uint16, destination: Bytes[40], payload: Bytes[CONFIG_SIZE], refundAddress: address, zroPaymentAddress: address, adapterParams: Bytes[CONFIG_SIZE]): payable 55 | def receivePayload(srcChainId: uint16, srcAddress: Bytes[40], dstAddress: address, nonce: uint64, gasLimit: uint256, payload: Bytes[PAYLOAD_SIZE]): nonpayable 56 | def getInboundNonce(srcChainId: uint16, srcAddress: Bytes[40]) -> uint64: view 57 | def getOutboundNonce(dstChainId: uint16, srcAddress: address) -> uint64: view 58 | def estimateFees(dstChainId: uint16, userApplication: address, payload: Bytes[PAYLOAD_SIZE], payInZRO: bool, adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 59 | def getChainId() -> uint16: view 60 | def retryPayload(srcChainId: uint16, srcAddress: Bytes[40], payload: Bytes[PAYLOAD_SIZE]): nonpayable 61 | def hasStoredPayload(srcChainId: uint16, srcAddress: Bytes[40]) -> bool: view 62 | def getSendLibraryAddress(userApplication: address) -> address: view 63 | def getReceiveLibraryAddress(userApplication: address) -> address: view 64 | def isSendingPayload() -> bool: view 65 | def isReceivingPayload() -> bool: view 66 | def getConfig(version: uint16, chainId: uint16, userApplication: address, configType: uint256) -> Bytes[CONFIG_SIZE]: view 67 | def getSendVersion(userApplication: address) -> uint16: view 68 | def getReceiveVersion(userApplication: address) -> uint16: view 69 | def setConfig(version: uint16, chainId: uint16, configType: uint256, config: Bytes[CONFIG_SIZE]): nonpayable 70 | def setSendVersion(version: uint16): nonpayable 71 | def setReceiveVersion(version: uint16): nonpayable 72 | def forceResumeReceive(srcChainId: uint16, srcAddress: Bytes[40]): nonpayable 73 | 74 | interface ILayerZeroMessagingLibrary: 75 | # def send(_userApplication: address, _lastNonce: uint64, _chainId: uint16, _destination: Bytes[40], _payload: Bytes[CONFIG_SIZE], refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE]): payable 76 | def estimateFees(_chainId: uint16, _userApplication: address, _payload: Bytes[PAYLOAD_SIZE], _payInZRO: bool, _adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 77 | def setConfig(_chainId: uint16, _userApplication: address, _configType: uint256, _config: Bytes[CONFIG_SIZE]): nonpayable 78 | def getConfig(_chainId: uint16, _userApplication: address, _configType: uint256) -> Bytes[CONFIG_SIZE]: view 79 | 80 | interface ILayerZeroOracle: 81 | def getPrice(dstChainId: uint16, outboundProofType: uint16) -> uint256: view 82 | def notifyOracle(dstChainId: uint16, outboundProofType: uint16, outboundBlockConfirmations: uint64): nonpayable 83 | def isApproved(_address: address) -> bool: view 84 | 85 | interface ILayerZeroRelayer: 86 | def getPrice(dstChainId: uint16, outboundProofType: uint16, userApplication: address, payloadSize: uint256, adapterParams: Bytes[CONFIG_SIZE]) -> uint256: view 87 | def notifyRelayer(dstChainId: uint16, outboundProofType: uint16, adapterParams: Bytes[CONFIG_SIZE]): nonpayable 88 | def isApproved(_address: address) -> bool: view 89 | 90 | owner: address 91 | 92 | lzEndpoint: public(ILayerZeroEndpoint) 93 | DEFAULT_PAYLOAD_SIZE_LIMIT: constant(uint256) = 1000000 94 | trustedRemoteLookup: public(HashMap[uint16, Bytes[40]]) 95 | payloadSizeLimitLookup: public(HashMap[uint16, uint256]) 96 | minDstGasLookup: public(HashMap[uint16, HashMap[uint16, uint256]]) 97 | precrime: public(address) 98 | 99 | event SetPrecrime: 100 | precrime: address 101 | 102 | event SetTrustedRemote: 103 | _remoteChainId: uint16 104 | _path: Bytes[40] 105 | 106 | event SetTrustedRemoteAddress: 107 | _remoteChainId: uint16 108 | _remoteAddress: Bytes[20] 109 | 110 | event SetMinDstGas: 111 | _dstChainId: uint16 112 | _type: uint16 113 | _minDstGas: uint256 114 | 115 | @external 116 | def __init__(_lzEndpoint: ILayerZeroEndpoint): 117 | self.lzEndpoint = _lzEndpoint 118 | self.owner = msg.sender 119 | 120 | @internal 121 | def _onlyOwner(): 122 | assert msg.sender == self.owner 123 | 124 | @external 125 | def lzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 126 | assert msg.sender == self.lzEndpoint.address 127 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 128 | assert len(_srcAddress) == len(trustedRemote) 129 | assert len(trustedRemote) > 0 130 | assert keccak256(_srcAddress) == keccak256(trustedRemote) 131 | 132 | self._blockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) 133 | 134 | 135 | @internal 136 | def _lzSend(_dstChainId: uint16, _payload: Bytes[PAYLOAD_SIZE], _refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE], _nativeFee: uint256): 137 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_dstChainId] 138 | assert len(trustedRemote) != 0 139 | self._checkPayloadSize(_dstChainId, len(_payload)) 140 | # usually, we would call the send function like this 141 | # self.lzEndpoint.send(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, value=_nativeFee) 142 | # 143 | # the interface definition for this function in solidity is: 144 | # function send(uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams) external payable; 145 | 146 | # 147 | # because send is a reserved keyword in Vyper, we have to use raw_call to call this function 148 | # we will use _abiEncode to encode the arguments to be passed 149 | 150 | # encode the arguments 151 | payload: Bytes[2404] = _abi_encode(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, method_id=method_id("send(uint16,bytes,bytes,address,address,bytes)")) 152 | # call the function 153 | raw_call(self.lzEndpoint.address, payload, value=_nativeFee) 154 | 155 | 156 | @external 157 | def _checkGasLimit(_dstChainId: uint16, _type: uint16, _adapterParams: Bytes[CONFIG_SIZE], _extraGas: uint256): 158 | providedGasLimit: uint256 = self._getGasLimit(_adapterParams) 159 | minGasLimit: uint256 = self.minDstGasLookup[_dstChainId][_type] + _extraGas 160 | assert minGasLimit > 0, "LzApp: minGasLimit not set" 161 | assert providedGasLimit >= minGasLimit, "LzApp: gas limit is too low" 162 | 163 | 164 | @internal 165 | @pure 166 | def _getGasLimit(_adapterParams: Bytes[CONFIG_SIZE]) -> uint256: 167 | assert len(_adapterParams) >= 34 168 | return convert(slice(_adapterParams, 34, 32), uint256) 169 | 170 | @internal 171 | def _checkPayloadSize(_dstChainId: uint16, _payloadSize: uint256): 172 | payloadSizeLimit: uint256 = self.payloadSizeLimitLookup[_dstChainId] 173 | if payloadSizeLimit == 0: 174 | payloadSizeLimit = DEFAULT_PAYLOAD_SIZE_LIMIT 175 | assert _payloadSize <= payloadSizeLimit, "LzApp: payload size is too large" 176 | 177 | @external 178 | def getConfig(_version: uint16, _chainId: uint16, _configType: uint256) -> Bytes[CONFIG_SIZE]: 179 | return self.lzEndpoint.getConfig(_version, _chainId, self, _configType) 180 | 181 | @external 182 | def setConfig(_version: uint16, _chainId: uint16, _configType: uint256, _config: Bytes[CONFIG_SIZE]): 183 | self._onlyOwner() 184 | self.lzEndpoint.setConfig(_version, _chainId, _configType, _config) 185 | 186 | @external 187 | def setSendVersion(_version: uint16): 188 | self._onlyOwner() 189 | self.lzEndpoint.setSendVersion(_version) 190 | 191 | @external 192 | def setReceiveVersion(_version: uint16): 193 | self._onlyOwner() 194 | self.lzEndpoint.setReceiveVersion(_version) 195 | 196 | @external 197 | def forceResumeReceive(_srcChainId: uint16, _srcAddress: Bytes[40]): 198 | self.lzEndpoint.forceResumeReceive(_srcChainId, _srcAddress) 199 | 200 | @external 201 | def setTrustedRemote(_srcChainId: uint16, _path: Bytes[40]): 202 | self._onlyOwner() 203 | self.trustedRemoteLookup[_srcChainId] = _path 204 | log SetTrustedRemote(_srcChainId, _path) 205 | 206 | @external 207 | def setTrustedRemoteAddress(_remoteChainId: uint16, _remoteAddress: address): 208 | # convert address to bytes 209 | _remoteAddressBytes: Bytes[20] = slice(concat(b"", convert(_remoteAddress, bytes32)), 12, 20) 210 | selfAddrAsBytes: Bytes[20] = slice(concat(b"", convert(self, bytes32)), 12, 20) 211 | self.trustedRemoteLookup[_remoteChainId] = concat(_remoteAddressBytes, selfAddrAsBytes) 212 | log SetTrustedRemoteAddress(_remoteChainId, _remoteAddressBytes) 213 | 214 | @external 215 | @view 216 | def getTrustedRemoteAddress(_remoteChainId: uint16) -> Bytes[20]: 217 | path: Bytes[40] = self.trustedRemoteLookup[_remoteChainId] 218 | assert len(path) != 0 219 | return slice(path, 0, 20) 220 | 221 | @external 222 | def setPrecrime(precrime: address): 223 | self._onlyOwner() 224 | self.precrime = precrime 225 | log SetPrecrime(precrime) 226 | 227 | @external 228 | def setMinDstGas(_dstChainId: uint16, _packetType: uint16, _minGas: uint256): 229 | self._onlyOwner() 230 | assert _minGas > 0, "LzApp: invalid minGas" 231 | self.minDstGasLookup[_dstChainId][_packetType] = _minGas 232 | log SetMinDstGas(_dstChainId, _packetType, _minGas) 233 | 234 | @external 235 | @view 236 | def isTrustedRemote(_srcChainId: uint16, _srcAddress: Bytes[40]) -> bool: 237 | trustedSource: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 238 | return keccak256(trustedSource) == keccak256(_srcAddress) 239 | -------------------------------------------------------------------------------- /contracts/OmniCounter.vy: -------------------------------------------------------------------------------- 1 | counter: public(uint256) 2 | 3 | @internal 4 | def _nonblockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 5 | # contract body upon cross-chain call goes here 6 | self.counter += 1 7 | 8 | # increment counter on other chain by calling _lzSend 9 | # argument to this fn is _dstChainId of type uint16 10 | @external 11 | @payable 12 | def incrementCounter(_dstChainId: uint16): 13 | self._lzSend(_dstChainId, b"", msg.sender, empty(address), b"", msg.value) 14 | 15 | ################################################################ 16 | # LZAPP BOILERPLATE # 17 | ################################################################ 18 | 19 | PAYLOAD_SIZE: constant(uint256) = 128 20 | CONFIG_SIZE: constant(uint256) = 512 21 | failedMessages: public(HashMap[uint16, HashMap[Bytes[40], HashMap[uint64, bytes32]]]) 22 | 23 | event MessageFailed: 24 | _srcChainId: uint16 25 | _srcAddress: Bytes[40] 26 | _nonce: uint64 27 | _payload: Bytes[PAYLOAD_SIZE] 28 | _reason: Bytes[1024] 29 | 30 | 31 | event RetryMessageSuccess: 32 | _srcChainId: uint16 33 | _srcAddress: Bytes[40] 34 | _nonce: uint64 35 | _payloadHash: bytes32 36 | 37 | # filler implementation of _blockingLzReceive 38 | # body is just a `pass` statement 39 | @internal 40 | def _blockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 41 | # call self via raw_call with revert_on_failure=False and max_outsize=256 42 | # raw_call signature is pasted below 43 | # raw_call(to: address, data: Bytes, max_outsize: int = 0, gas: uint256 = gasLeft, value: uint256 = 0, is_delegate_call: bool = False, is_static_call: bool = False, revert_on_failure: bool = True)→ Bytes[max_outsize] 44 | success: bool = False 45 | data: Bytes[256] = b"" 46 | success, data = raw_call(self, concat(0x66ad5c8a, _abi_encode(_srcChainId, _srcAddress, _nonce, _payload)), max_outsize=256, revert_on_failure=False) 47 | if not success: 48 | self._storeFailedMessage(_srcChainId, _srcAddress, _nonce, _payload) 49 | 50 | # implementation of _storeFailedMessage 51 | @internal 52 | def _storeFailedMessage(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 53 | payloadHash: bytes32 = keccak256(_payload) 54 | self.failedMessages[_srcChainId][_srcAddress][_nonce] = payloadHash 55 | log MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, convert("Failed to send payload", Bytes[100])) 56 | 57 | @external 58 | def nonblockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 59 | assert msg.sender == self, "ONLYSELF" 60 | self._nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) 61 | 62 | interface ILayerZeroReceiver: 63 | def lzReceive(srcChainId: uint16, srcAddress: Bytes[40], nonce: uint64, payload: Bytes[PAYLOAD_SIZE]): nonpayable 64 | 65 | interface ILayerZeroEndpoint: 66 | # def send(dstChainId: uint16, destination: Bytes[40], payload: Bytes[CONFIG_SIZE], refundAddress: address, zroPaymentAddress: address, adapterParams: Bytes[CONFIG_SIZE]): payable 67 | def receivePayload(srcChainId: uint16, srcAddress: Bytes[40], dstAddress: address, nonce: uint64, gasLimit: uint256, payload: Bytes[PAYLOAD_SIZE]): nonpayable 68 | def getInboundNonce(srcChainId: uint16, srcAddress: Bytes[40]) -> uint64: view 69 | def getOutboundNonce(dstChainId: uint16, srcAddress: address) -> uint64: view 70 | def estimateFees(dstChainId: uint16, userApplication: address, payload: Bytes[PAYLOAD_SIZE], payInZRO: bool, adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 71 | def getChainId() -> uint16: view 72 | def retryPayload(srcChainId: uint16, srcAddress: Bytes[40], payload: Bytes[PAYLOAD_SIZE]): nonpayable 73 | def hasStoredPayload(srcChainId: uint16, srcAddress: Bytes[40]) -> bool: view 74 | def getSendLibraryAddress(userApplication: address) -> address: view 75 | def getReceiveLibraryAddress(userApplication: address) -> address: view 76 | def isSendingPayload() -> bool: view 77 | def isReceivingPayload() -> bool: view 78 | def getConfig(version: uint16, chainId: uint16, userApplication: address, configType: uint256) -> Bytes[CONFIG_SIZE]: view 79 | def getSendVersion(userApplication: address) -> uint16: view 80 | def getReceiveVersion(userApplication: address) -> uint16: view 81 | def setConfig(version: uint16, chainId: uint16, configType: uint256, config: Bytes[CONFIG_SIZE]): nonpayable 82 | def setSendVersion(version: uint16): nonpayable 83 | def setReceiveVersion(version: uint16): nonpayable 84 | def forceResumeReceive(srcChainId: uint16, srcAddress: Bytes[40]): nonpayable 85 | 86 | interface ILayerZeroMessagingLibrary: 87 | # def send(_userApplication: address, _lastNonce: uint64, _chainId: uint16, _destination: Bytes[40], _payload: Bytes[CONFIG_SIZE], refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE]): payable 88 | def estimateFees(_chainId: uint16, _userApplication: address, _payload: Bytes[PAYLOAD_SIZE], _payInZRO: bool, _adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 89 | def setConfig(_chainId: uint16, _userApplication: address, _configType: uint256, _config: Bytes[CONFIG_SIZE]): nonpayable 90 | def getConfig(_chainId: uint16, _userApplication: address, _configType: uint256) -> Bytes[CONFIG_SIZE]: view 91 | 92 | interface ILayerZeroOracle: 93 | def getPrice(dstChainId: uint16, outboundProofType: uint16) -> uint256: view 94 | def notifyOracle(dstChainId: uint16, outboundProofType: uint16, outboundBlockConfirmations: uint64): nonpayable 95 | def isApproved(_address: address) -> bool: view 96 | 97 | interface ILayerZeroRelayer: 98 | def getPrice(dstChainId: uint16, outboundProofType: uint16, userApplication: address, payloadSize: uint256, adapterParams: Bytes[CONFIG_SIZE]) -> uint256: view 99 | def notifyRelayer(dstChainId: uint16, outboundProofType: uint16, adapterParams: Bytes[CONFIG_SIZE]): nonpayable 100 | def isApproved(_address: address) -> bool: view 101 | 102 | owner: address 103 | 104 | lzEndpoint: public(ILayerZeroEndpoint) 105 | DEFAULT_PAYLOAD_SIZE_LIMIT: constant(uint256) = 1000000 106 | trustedRemoteLookup: public(HashMap[uint16, Bytes[40]]) 107 | payloadSizeLimitLookup: public(HashMap[uint16, uint256]) 108 | minDstGasLookup: public(HashMap[uint16, HashMap[uint16, uint256]]) 109 | precrime: public(address) 110 | 111 | event SetPrecrime: 112 | precrime: address 113 | 114 | event SetTrustedRemote: 115 | _remoteChainId: uint16 116 | _path: Bytes[40] 117 | 118 | event SetTrustedRemoteAddress: 119 | _remoteChainId: uint16 120 | _remoteAddress: Bytes[20] 121 | 122 | event SetMinDstGas: 123 | _dstChainId: uint16 124 | _type: uint16 125 | _minDstGas: uint256 126 | 127 | @external 128 | def __init__(_lzEndpoint: ILayerZeroEndpoint): 129 | self.lzEndpoint = _lzEndpoint 130 | self.owner = msg.sender 131 | 132 | @internal 133 | def _onlyOwner(): 134 | assert msg.sender == self.owner 135 | 136 | @external 137 | def lzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 138 | assert msg.sender == self.lzEndpoint.address 139 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 140 | assert len(_srcAddress) == len(trustedRemote) 141 | assert len(trustedRemote) > 0 142 | assert keccak256(_srcAddress) == keccak256(trustedRemote) 143 | 144 | self._blockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) 145 | 146 | 147 | @internal 148 | def _lzSend(_dstChainId: uint16, _payload: Bytes[PAYLOAD_SIZE], _refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE], _nativeFee: uint256): 149 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_dstChainId] 150 | assert len(trustedRemote) != 0 151 | self._checkPayloadSize(_dstChainId, len(_payload)) 152 | # usually, we would call the send function like this 153 | # self.lzEndpoint.send(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, value=_nativeFee) 154 | # 155 | # the interface definition for this function in solidity is: 156 | # function send(uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams) external payable; 157 | 158 | # 159 | # because send is a reserved keyword in Vyper, we have to use raw_call to call this function 160 | # we will use _abiEncode to encode the arguments to be passed 161 | 162 | # encode the arguments 163 | payload: Bytes[2404] = _abi_encode(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, method_id=method_id("send(uint16,bytes,bytes,address,address,bytes)")) 164 | # call the function 165 | raw_call(self.lzEndpoint.address, payload, value=_nativeFee) 166 | 167 | 168 | @external 169 | def _checkGasLimit(_dstChainId: uint16, _type: uint16, _adapterParams: Bytes[CONFIG_SIZE], _extraGas: uint256): 170 | providedGasLimit: uint256 = self._getGasLimit(_adapterParams) 171 | minGasLimit: uint256 = self.minDstGasLookup[_dstChainId][_type] + _extraGas 172 | assert minGasLimit > 0, "LzApp: minGasLimit not set" 173 | assert providedGasLimit >= minGasLimit, "LzApp: gas limit is too low" 174 | 175 | 176 | @internal 177 | @pure 178 | def _getGasLimit(_adapterParams: Bytes[CONFIG_SIZE]) -> uint256: 179 | assert len(_adapterParams) >= 34 180 | return convert(slice(_adapterParams, 34, 32), uint256) 181 | 182 | @internal 183 | def _checkPayloadSize(_dstChainId: uint16, _payloadSize: uint256): 184 | payloadSizeLimit: uint256 = self.payloadSizeLimitLookup[_dstChainId] 185 | if payloadSizeLimit == 0: 186 | payloadSizeLimit = DEFAULT_PAYLOAD_SIZE_LIMIT 187 | assert _payloadSize <= payloadSizeLimit, "LzApp: payload size is too large" 188 | 189 | @external 190 | def getConfig(_version: uint16, _chainId: uint16, _configType: uint256) -> Bytes[CONFIG_SIZE]: 191 | return self.lzEndpoint.getConfig(_version, _chainId, self, _configType) 192 | 193 | @external 194 | def setConfig(_version: uint16, _chainId: uint16, _configType: uint256, _config: Bytes[CONFIG_SIZE]): 195 | self._onlyOwner() 196 | self.lzEndpoint.setConfig(_version, _chainId, _configType, _config) 197 | 198 | @external 199 | def setSendVersion(_version: uint16): 200 | self._onlyOwner() 201 | self.lzEndpoint.setSendVersion(_version) 202 | 203 | @external 204 | def setReceiveVersion(_version: uint16): 205 | self._onlyOwner() 206 | self.lzEndpoint.setReceiveVersion(_version) 207 | 208 | @external 209 | def forceResumeReceive(_srcChainId: uint16, _srcAddress: Bytes[40]): 210 | self.lzEndpoint.forceResumeReceive(_srcChainId, _srcAddress) 211 | 212 | @external 213 | def setTrustedRemote(_srcChainId: uint16, _path: Bytes[40]): 214 | self._onlyOwner() 215 | self.trustedRemoteLookup[_srcChainId] = _path 216 | log SetTrustedRemote(_srcChainId, _path) 217 | 218 | @external 219 | def setTrustedRemoteAddress(_remoteChainId: uint16, _remoteAddress: address): 220 | # convert address to bytes 221 | _remoteAddressBytes: Bytes[20] = slice(concat(b"", convert(_remoteAddress, bytes32)), 12, 20) 222 | selfAddrAsBytes: Bytes[20] = slice(concat(b"", convert(self, bytes32)), 12, 20) 223 | self.trustedRemoteLookup[_remoteChainId] = concat(_remoteAddressBytes, selfAddrAsBytes) 224 | log SetTrustedRemoteAddress(_remoteChainId, _remoteAddressBytes) 225 | 226 | @external 227 | @view 228 | def getTrustedRemoteAddress(_remoteChainId: uint16) -> Bytes[20]: 229 | path: Bytes[40] = self.trustedRemoteLookup[_remoteChainId] 230 | assert len(path) != 0 231 | return slice(path, 0, 20) 232 | 233 | @external 234 | def setPrecrime(precrime: address): 235 | self._onlyOwner() 236 | self.precrime = precrime 237 | log SetPrecrime(precrime) 238 | 239 | @external 240 | def setMinDstGas(_dstChainId: uint16, _packetType: uint16, _minGas: uint256): 241 | self._onlyOwner() 242 | assert _minGas > 0, "LzApp: invalid minGas" 243 | self.minDstGasLookup[_dstChainId][_packetType] = _minGas 244 | log SetMinDstGas(_dstChainId, _packetType, _minGas) 245 | 246 | @external 247 | @view 248 | def isTrustedRemote(_srcChainId: uint16, _srcAddress: Bytes[40]) -> bool: 249 | trustedSource: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 250 | return keccak256(trustedSource) == keccak256(_srcAddress) 251 | -------------------------------------------------------------------------------- /contracts/PingPong.vy: -------------------------------------------------------------------------------- 1 | event PingPong: 2 | pings: uint256 3 | 4 | paused: bool 5 | 6 | _chainId: uint256 7 | 8 | # function `enable` takes a boolean argument and sets self.paused 9 | @external 10 | def enable(_enable: bool): 11 | self.paused = _enable 12 | 13 | @external 14 | @payable 15 | def ping(_dstChainId: uint16, _dstAddress: address, _pings: uint256): 16 | assert self.paused == False 17 | 18 | # encode the payload with the number of pings 19 | payload: Bytes[32] = concat(b"", 20 | convert(_pings, bytes32) 21 | ) 22 | 23 | # use adapterParams v1 to specify more gas for the destination 24 | version: uint16 = 1 25 | gasForDestinationLzReceive: uint256 = 350000 26 | adapterParams: Bytes[CONFIG_SIZE] = concat( 27 | convert(version, bytes2), 28 | convert(gasForDestinationLzReceive, bytes32) 29 | ) 30 | 31 | # send LayerZero message 32 | self._lzSend( # {value: messageFee} will be paid out of this contract! 33 | _dstChainId, # destination chainId 34 | payload, # abi.encode()'ed bytes 35 | self, # (msg.sender will be this contract) refund address (LayerZero will refund any extra gas back to caller of send() 36 | _dstAddress, # future param, unused for this example 37 | adapterParams, # v1 adapterParams, specify custom destination gas qty 38 | msg.value 39 | ) 40 | 41 | @internal 42 | @payable 43 | def _nonblockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 44 | sendBackToAddress: address = convert(slice(_srcAddress, 20, 20), address) 45 | 46 | # decode the number of pings sent thus far 47 | pings: uint256 = _abi_decode(_payload, (uint16, address, uint256))[2] 48 | 49 | # *pong* back to the other side 50 | payload: Bytes[1000] = _abi_encode(self._chainId, self, pings + 1, method_id=method_id("ping(uint16,address,uint256)")) 51 | raw_call(self, payload, value=msg.value) 52 | 53 | 54 | PAYLOAD_SIZE: constant(uint256) = 128 55 | CONFIG_SIZE: constant(uint256) = 512 56 | 57 | failedMessages: public(HashMap[uint16, HashMap[Bytes[40], HashMap[uint64, bytes32]]]) 58 | 59 | event MessageFailed: 60 | _srcChainId: uint16 61 | _srcAddress: Bytes[40] 62 | _nonce: uint64 63 | _payload: Bytes[PAYLOAD_SIZE] 64 | _reason: Bytes[1024] 65 | 66 | 67 | event RetryMessageSuccess: 68 | _srcChainId: uint16 69 | _srcAddress: Bytes[40] 70 | _nonce: uint64 71 | _payloadHash: bytes32 72 | 73 | # filler implementation of _blockingLzReceive 74 | # body is just a `pass` statement 75 | @internal 76 | def _blockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 77 | # call self via raw_call with revert_on_failure=False and max_outsize=256 78 | # raw_call signature is pasted below 79 | # raw_call(to: address, data: Bytes, max_outsize: int = 0, gas: uint256 = gasLeft, value: uint256 = 0, is_delegate_call: bool = False, is_static_call: bool = False, revert_on_failure: bool = True)→ Bytes[max_outsize] 80 | success: bool = False 81 | data: Bytes[256] = b"" 82 | success, data = raw_call(self, concat(0x66ad5c8a, _abi_encode(_srcChainId, _srcAddress, _nonce, _payload)), max_outsize=256, revert_on_failure=False) 83 | if not success: 84 | self._storeFailedMessage(_srcChainId, _srcAddress, _nonce, _payload) 85 | 86 | # implementation of _storeFailedMessage 87 | @internal 88 | def _storeFailedMessage(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 89 | payloadHash: bytes32 = keccak256(_payload) 90 | self.failedMessages[_srcChainId][_srcAddress][_nonce] = payloadHash 91 | log MessageFailed(_srcChainId, _srcAddress, _nonce, _payload, convert("Failed to send payload", Bytes[100])) 92 | 93 | @external 94 | def nonblockingLzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 95 | assert msg.sender == self, "ONLYSELF" 96 | self._nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) 97 | 98 | interface ILayerZeroReceiver: 99 | def lzReceive(srcChainId: uint16, srcAddress: Bytes[40], nonce: uint64, payload: Bytes[PAYLOAD_SIZE]): nonpayable 100 | 101 | interface ILayerZeroEndpoint: 102 | # def send(dstChainId: uint16, destination: Bytes[40], payload: Bytes[CONFIG_SIZE], refundAddress: address, zroPaymentAddress: address, adapterParams: Bytes[CONFIG_SIZE]): payable 103 | def receivePayload(srcChainId: uint16, srcAddress: Bytes[40], dstAddress: address, nonce: uint64, gasLimit: uint256, payload: Bytes[PAYLOAD_SIZE]): nonpayable 104 | def getInboundNonce(srcChainId: uint16, srcAddress: Bytes[40]) -> uint64: view 105 | def getOutboundNonce(dstChainId: uint16, srcAddress: address) -> uint64: view 106 | def estimateFees(dstChainId: uint16, userApplication: address, payload: Bytes[PAYLOAD_SIZE], payInZRO: bool, adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 107 | def getChainId() -> uint16: view 108 | def retryPayload(srcChainId: uint16, srcAddress: Bytes[40], payload: Bytes[PAYLOAD_SIZE]): nonpayable 109 | def hasStoredPayload(srcChainId: uint16, srcAddress: Bytes[40]) -> bool: view 110 | def getSendLibraryAddress(userApplication: address) -> address: view 111 | def getReceiveLibraryAddress(userApplication: address) -> address: view 112 | def isSendingPayload() -> bool: view 113 | def isReceivingPayload() -> bool: view 114 | def getConfig(version: uint16, chainId: uint16, userApplication: address, configType: uint256) -> Bytes[CONFIG_SIZE]: view 115 | def getSendVersion(userApplication: address) -> uint16: view 116 | def getReceiveVersion(userApplication: address) -> uint16: view 117 | def setConfig(version: uint16, chainId: uint16, configType: uint256, config: Bytes[CONFIG_SIZE]): nonpayable 118 | def setSendVersion(version: uint16): nonpayable 119 | def setReceiveVersion(version: uint16): nonpayable 120 | def forceResumeReceive(srcChainId: uint16, srcAddress: Bytes[40]): nonpayable 121 | 122 | interface ILayerZeroMessagingLibrary: 123 | # def send(_userApplication: address, _lastNonce: uint64, _chainId: uint16, _destination: Bytes[40], _payload: Bytes[CONFIG_SIZE], refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE]): payable 124 | def estimateFees(_chainId: uint16, _userApplication: address, _payload: Bytes[PAYLOAD_SIZE], _payInZRO: bool, _adapterParam: Bytes[CONFIG_SIZE]) -> (uint256, uint256): view 125 | def setConfig(_chainId: uint16, _userApplication: address, _configType: uint256, _config: Bytes[CONFIG_SIZE]): nonpayable 126 | def getConfig(_chainId: uint16, _userApplication: address, _configType: uint256) -> Bytes[CONFIG_SIZE]: view 127 | 128 | interface ILayerZeroOracle: 129 | def getPrice(dstChainId: uint16, outboundProofType: uint16) -> uint256: view 130 | def notifyOracle(dstChainId: uint16, outboundProofType: uint16, outboundBlockConfirmations: uint64): nonpayable 131 | def isApproved(_address: address) -> bool: view 132 | 133 | interface ILayerZeroRelayer: 134 | def getPrice(dstChainId: uint16, outboundProofType: uint16, userApplication: address, payloadSize: uint256, adapterParams: Bytes[CONFIG_SIZE]) -> uint256: view 135 | def notifyRelayer(dstChainId: uint16, outboundProofType: uint16, adapterParams: Bytes[CONFIG_SIZE]): nonpayable 136 | def isApproved(_address: address) -> bool: view 137 | 138 | owner: address 139 | 140 | lzEndpoint: public(ILayerZeroEndpoint) 141 | DEFAULT_PAYLOAD_SIZE_LIMIT: constant(uint256) = 1000000 142 | trustedRemoteLookup: public(HashMap[uint16, Bytes[40]]) 143 | payloadSizeLimitLookup: public(HashMap[uint16, uint256]) 144 | minDstGasLookup: public(HashMap[uint16, HashMap[uint16, uint256]]) 145 | precrime: public(address) 146 | 147 | event SetPrecrime: 148 | precrime: address 149 | 150 | event SetTrustedRemote: 151 | _remoteChainId: uint16 152 | _path: Bytes[40] 153 | 154 | event SetTrustedRemoteAddress: 155 | _remoteChainId: uint16 156 | _remoteAddress: Bytes[20] 157 | 158 | event SetMinDstGas: 159 | _dstChainId: uint16 160 | _type: uint16 161 | _minDstGas: uint256 162 | 163 | @external 164 | def __init__(_lzEndpoint: ILayerZeroEndpoint, _chainId: uint256): 165 | self.lzEndpoint = _lzEndpoint 166 | self.owner = msg.sender 167 | self._chainId = _chainId 168 | 169 | @internal 170 | def _onlyOwner(): 171 | assert msg.sender == self.owner 172 | 173 | @external 174 | def lzReceive(_srcChainId: uint16, _srcAddress: Bytes[40], _nonce: uint64, _payload: Bytes[PAYLOAD_SIZE]): 175 | assert msg.sender == self.lzEndpoint.address 176 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 177 | assert len(_srcAddress) == len(trustedRemote) 178 | assert len(trustedRemote) > 0 179 | assert keccak256(_srcAddress) == keccak256(trustedRemote) 180 | 181 | self._blockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload) 182 | 183 | 184 | @internal 185 | def _lzSend(_dstChainId: uint16, _payload: Bytes[PAYLOAD_SIZE], _refundAddress: address, _zroPaymentAddress: address, _adapterParams: Bytes[CONFIG_SIZE], _nativeFee: uint256): 186 | trustedRemote: Bytes[40] = self.trustedRemoteLookup[_dstChainId] 187 | assert len(trustedRemote) != 0 188 | self._checkPayloadSize(_dstChainId, len(_payload)) 189 | # usually, we would call the send function like this 190 | # self.lzEndpoint.send(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, value=_nativeFee) 191 | # 192 | # the interface definition for this function in solidity is: 193 | # function send(uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable _refundAddress, address _zroPaymentAddress, bytes calldata _adapterParams) external payable; 194 | 195 | # 196 | # because send is a reserved keyword in Vyper, we have to use raw_call to call this function 197 | # we will use _abiEncode to encode the arguments to be passed 198 | 199 | # encode the arguments 200 | payload: Bytes[2404] = _abi_encode(_dstChainId, trustedRemote, _payload, _refundAddress, _zroPaymentAddress, _adapterParams, method_id=method_id("send(uint16,bytes,bytes,address,address,bytes)")) 201 | # call the function 202 | raw_call(self.lzEndpoint.address, payload, value=_nativeFee) 203 | 204 | 205 | @external 206 | def _checkGasLimit(_dstChainId: uint16, _type: uint16, _adapterParams: Bytes[CONFIG_SIZE], _extraGas: uint256): 207 | providedGasLimit: uint256 = self._getGasLimit(_adapterParams) 208 | minGasLimit: uint256 = self.minDstGasLookup[_dstChainId][_type] + _extraGas 209 | assert minGasLimit > 0, "LzApp: minGasLimit not set" 210 | assert providedGasLimit >= minGasLimit, "LzApp: gas limit is too low" 211 | 212 | 213 | @internal 214 | @pure 215 | def _getGasLimit(_adapterParams: Bytes[CONFIG_SIZE]) -> uint256: 216 | assert len(_adapterParams) >= 34 217 | return convert(slice(_adapterParams, 34, 32), uint256) 218 | 219 | @internal 220 | def _checkPayloadSize(_dstChainId: uint16, _payloadSize: uint256): 221 | payloadSizeLimit: uint256 = self.payloadSizeLimitLookup[_dstChainId] 222 | if payloadSizeLimit == 0: 223 | payloadSizeLimit = DEFAULT_PAYLOAD_SIZE_LIMIT 224 | assert _payloadSize <= payloadSizeLimit, "LzApp: payload size is too large" 225 | 226 | @external 227 | def getConfig(_version: uint16, _chainId: uint16, _configType: uint256) -> Bytes[CONFIG_SIZE]: 228 | return self.lzEndpoint.getConfig(_version, _chainId, self, _configType) 229 | 230 | @external 231 | def setConfig(_version: uint16, _chainId: uint16, _configType: uint256, _config: Bytes[CONFIG_SIZE]): 232 | self._onlyOwner() 233 | self.lzEndpoint.setConfig(_version, _chainId, _configType, _config) 234 | 235 | @external 236 | def setSendVersion(_version: uint16): 237 | self._onlyOwner() 238 | self.lzEndpoint.setSendVersion(_version) 239 | 240 | @external 241 | def setReceiveVersion(_version: uint16): 242 | self._onlyOwner() 243 | self.lzEndpoint.setReceiveVersion(_version) 244 | 245 | @external 246 | def forceResumeReceive(_srcChainId: uint16, _srcAddress: Bytes[40]): 247 | self.lzEndpoint.forceResumeReceive(_srcChainId, _srcAddress) 248 | 249 | @external 250 | def setTrustedRemote(_srcChainId: uint16, _path: Bytes[40]): 251 | self._onlyOwner() 252 | self.trustedRemoteLookup[_srcChainId] = _path 253 | log SetTrustedRemote(_srcChainId, _path) 254 | 255 | @external 256 | def setTrustedRemoteAddress(_remoteChainId: uint16, _remoteAddress: address): 257 | # convert address to bytes 258 | _remoteAddressBytes: Bytes[20] = slice(concat(b"", convert(_remoteAddress, bytes32)), 12, 20) 259 | selfAddrAsBytes: Bytes[20] = slice(concat(b"", convert(self, bytes32)), 12, 20) 260 | self.trustedRemoteLookup[_remoteChainId] = concat(_remoteAddressBytes, selfAddrAsBytes) 261 | log SetTrustedRemoteAddress(_remoteChainId, _remoteAddressBytes) 262 | 263 | @external 264 | @view 265 | def getTrustedRemoteAddress(_remoteChainId: uint16) -> Bytes[20]: 266 | path: Bytes[40] = self.trustedRemoteLookup[_remoteChainId] 267 | assert len(path) != 0 268 | return slice(path, 0, 20) 269 | 270 | @external 271 | def setPrecrime(precrime: address): 272 | self._onlyOwner() 273 | self.precrime = precrime 274 | log SetPrecrime(precrime) 275 | 276 | @external 277 | def setMinDstGas(_dstChainId: uint16, _packetType: uint16, _minGas: uint256): 278 | self._onlyOwner() 279 | assert _minGas > 0, "LzApp: invalid minGas" 280 | self.minDstGasLookup[_dstChainId][_packetType] = _minGas 281 | log SetMinDstGas(_dstChainId, _packetType, _minGas) 282 | 283 | @external 284 | @view 285 | def isTrustedRemote(_srcChainId: uint16, _srcAddress: Bytes[40]) -> bool: 286 | trustedSource: Bytes[40] = self.trustedRemoteLookup[_srcChainId] 287 | return keccak256(trustedSource) == keccak256(_srcAddress) 288 | -------------------------------------------------------------------------------- /contracts/mocks/EndpointMock.sol: -------------------------------------------------------------------------------- 1 | // import LZEndpointMock 2 | import "@layerzero/mocks/LZEndpointMock.sol"; 3 | 4 | contract EndpointMock is LZEndpointMock { 5 | constructor(uint16 _id) LZEndpointMock(_id) {} 6 | } 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "layerzerovyper" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["z80 "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.10,<3.11" 10 | eth-ape = "^0.6.3" 11 | pytest = "^7.2.1" 12 | ape-vyper = "^0.6.1" 13 | ape-solidity = "^0.6.0" 14 | eth-abi = "^3.0.1" 15 | asttokens = "^2.2.1" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | pytest = "^7.2.1" 19 | 20 | [build-system] 21 | requires = ["poetry-core"] 22 | build-backend = "poetry.core.masonry.api" 23 | -------------------------------------------------------------------------------- /tests/test_lzapp.py: -------------------------------------------------------------------------------- 1 | import ape 2 | import pytest 3 | from eth_abi.packed import encode_packed 4 | # import ipython to embed 5 | import IPython 6 | 7 | @pytest.fixture 8 | def lz_mock(accounts, project): 9 | lz_mock = project.EndpointMock.deploy(101, sender=accounts[0]) 10 | return lz_mock 11 | 12 | def test_omnicounter(lz_mock, project, accounts): 13 | lzapp = project.OmniCounter.deploy(lz_mock, sender=accounts[0]) 14 | lzapp2 = project.OmniCounter.deploy(lz_mock, sender=accounts[0]) 15 | assert lzapp.lzEndpoint() == lz_mock.address 16 | lz_mock.setDestLzEndpoint(lzapp.address, lz_mock.address, sender=accounts[0]) 17 | lz_mock.setDestLzEndpoint(lzapp2.address, lz_mock.address, sender=accounts[0]) 18 | print("endpoint set") 19 | path = encode_packed(['address', 'address'], [lzapp2.address, lzapp.address]) 20 | path2 = encode_packed(['address', 'address'], [lzapp.address, lzapp2.address]) 21 | print(f"path: {path.hex()}") 22 | lzapp.setTrustedRemote(101, path, sender=accounts[0]) 23 | lzapp2.setTrustedRemote(101, path2, sender=accounts[0]) 24 | # lzapp.setTrustedRemoteAddress(101, lzapp.address, sender=accounts[0]) 25 | print("trusted remote set") 26 | # embed to debug 27 | # IPython.embed() 28 | # print trusted remote address in hex 29 | print(f"trusted remote address: {lzapp.getTrustedRemoteAddress(101).hex()}") 30 | assert lzapp.getTrustedRemoteAddress(101).hex() == '0x' + bytes.fromhex(lzapp2.address[2:]).hex() 31 | print(lzapp.trustedRemoteLookup(101).hex()) 32 | 33 | r = lzapp.incrementCounter(101, sender=accounts[0], value=1000000000000000000) 34 | for e in r.events: 35 | print(e) 36 | assert lzapp2.counter() == 1 37 | --------------------------------------------------------------------------------