From 99f68b678fdaaf8b0e1e950d391a4dbb917db899 Mon Sep 17 00:00:00 2001 From: gcranju <134275268+gcranju@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:43:02 +0545 Subject: [PATCH] feat: add centralized connections in solidity and java (#196) * feat: solidity xcall connection added * feat: icon xcall connection added * fix: checkfee added * feat: relayer address in centralized connection * feat: added build scripts * feat: solidity tests for centralized connection added * feat: centralized connection name changed for icon * fix: review comments addressed for solidity part * fix: variable admin used for relayer/admin * fix: review comments addressed for javascore part * fix: variables in build.gradle in centralized cnnection * fix: java score constructor * fix: removed void from constructor * fix: type of receipts to boolean * fix: solitity tests resolved * fix: logic corrected for duplicate message * fix: fee check added in connection * fix: centralized connection test added in solidity * fix: admin check removed from getReceipts Co-authored-by: redlarva <91685111+redlarva@users.noreply.github.com> * fix: onlyAdmin function * fix: architecture updated (#214) * fix: architecture updated * fix: use connectio sn * style: formatting code * added scripts for centralized connections * feat: centralized-connection javascore tests added * feat: clam fees tests added --------- Co-authored-by: red__larva Co-authored-by: redlarva <91685111+redlarva@users.noreply.github.com> --- Makefile | 16 + .../adapters/CentralizedConnection.sol | 149 +++++++++ contracts/evm/script/CallService.s.sol | 13 +- .../test/adapters/CentralizedConnection.t.sol | 307 ++++++++++++++++++ .../centralized-connection/build.gradle | 49 +++ .../centralized/CentralizedConnection.java | 190 +++++++++++ .../CentralizedConnectionTest.java | 217 +++++++++++++ contracts/javascore/settings.gradle | 3 +- scripts/docker-compose.yml | 27 ++ scripts/optimize-jar.sh | 2 +- scripts/optimize-solidity.sh | 24 ++ scripts/wasm-builder.Dockerfile | 30 ++ 12 files changed, 1019 insertions(+), 8 deletions(-) create mode 100644 contracts/evm/contracts/adapters/CentralizedConnection.sol create mode 100644 contracts/evm/test/adapters/CentralizedConnection.t.sol create mode 100644 contracts/javascore/centralized-connection/build.gradle create mode 100644 contracts/javascore/centralized-connection/src/main/java/xcall/adapter/centralized/CentralizedConnection.java create mode 100644 contracts/javascore/centralized-connection/src/test/java/xcall/adapter/centralized/CentralizedConnectionTest.java create mode 100644 scripts/docker-compose.yml create mode 100755 scripts/optimize-solidity.sh create mode 100644 scripts/wasm-builder.Dockerfile diff --git a/Makefile b/Makefile index ffbe697b..6aabc138 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,22 @@ optimize-cosmwasm: @echo "Generating optimized cosmwasm for Archway contracts" sh ./scripts/optimize-cosmwasm.sh +build-solidity: + @echo "Build solidity contracts" + sh ./scripts/optimize-solc.sh build + +build-solidity-docker: + @echo "Build solidity contracts" + docker-compose -f ./scripts/docker-compose.yml up solidity + +build-java-docker: + @echo "Build java contracts" + docker-compose -f ./scripts/docker-compose.yml up java + +build-wasm-docker: + @echo "Build java contracts" + docker-compose -f ./scripts/docker-compose.yml up wasm + gobuild: go build . diff --git a/contracts/evm/contracts/adapters/CentralizedConnection.sol b/contracts/evm/contracts/adapters/CentralizedConnection.sol new file mode 100644 index 00000000..2443258e --- /dev/null +++ b/contracts/evm/contracts/adapters/CentralizedConnection.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; +pragma abicoder v2; + +import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@xcall/utils/Types.sol"; +import "@xcall/contracts/xcall/interfaces/IConnection.sol"; +import "@iconfoundation/btp2-solidity-library/interfaces/ICallService.sol"; + +contract CentralizedConnection is Initializable, IConnection { + mapping(string => uint256) private messageFees; + mapping(string => uint256) private responseFees; + mapping(string => mapping(uint256 => bool)) receipts; + address private xCall; + address private adminAddress; + uint256 public connSn; + + event Message(string targetNetwork, uint256 sn, bytes _msg); + + modifier onlyAdmin() { + require(msg.sender == this.admin(), "OnlyRelayer"); + _; + } + + function initialize(address _relayer, address _xCall) public initializer { + xCall = _xCall; + adminAddress = _relayer; + } + + /** + @notice Sets the fee to the target network + @param networkId String Network Id of target chain + @param messageFee Integer ( The fee needed to send a Message ) + @param responseFee Integer (The fee of the response ) + */ + function setFee( + string calldata networkId, + uint256 messageFee, + uint256 responseFee + ) external onlyAdmin { + messageFees[networkId] = messageFee; + responseFees[networkId] = responseFee; + } + + /** + @notice Gets the fee to the target network + @param to String Network Id of target chain + @param response Boolean ( Whether the responding fee is included ) + @return fee Integer (The fee of sending a message to a given destination network ) + */ + function getFee( + string memory to, + bool response + ) external view override returns (uint256 fee) { + uint256 messageFee = messageFees[to]; + if (response == true) { + uint256 responseFee = responseFees[to]; + return messageFee + responseFee; + } + return messageFee; + } + + /** + @notice Sends the message to a specific network. + @param sn : positive for two-way message, zero for one-way message, negative for response + @param to String ( Network Id of destination network ) + @param svc String ( name of the service ) + @param sn Integer ( serial number of the xcall message ) + @param _msg Bytes ( serialized bytes of Service Message ) + */ + function sendMessage( + string calldata to, + string calldata svc, + int256 sn, + bytes calldata _msg + ) external payable override { + require(msg.sender == xCall, "Only Xcall can call sendMessage"); + uint256 fee; + if (sn > 0) { + fee = this.getFee(to, true); + } else if (sn == 0) { + fee = this.getFee(to, false); + } + require(msg.value >= fee, "Fee is not Sufficient"); + connSn++; + emit Message(to, connSn, _msg); + } + + /** + @notice Sends the message to a xCall. + @param srcNetwork String ( Network Id ) + @param _connSn Integer ( connection message sn ) + @param _msg Bytes ( serialized bytes of Service Message ) + */ + function recvMessage( + string memory srcNetwork, + uint256 _connSn, + bytes calldata _msg + ) public onlyAdmin { + require(!receipts[srcNetwork][_connSn], "Duplicate Message"); + receipts[srcNetwork][_connSn] = true; + ICallService(xCall).handleMessage(srcNetwork, _msg); + } + + /** + @notice Sends the balance of the contract to the owner(relayer) + + */ + function claimFees() public onlyAdmin { + payable(adminAddress).transfer(address(this).balance); + } + + /** + @notice Revert a messages, used in special cases where message can't just be dropped + @param sn Integer ( serial number of the xcall message ) + */ + function revertMessage(uint256 sn) public onlyAdmin { + ICallService(xCall).handleError(sn); + } + + /** + @notice Gets a message receipt + @param srcNetwork String ( Network Id ) + @param _connSn Integer ( connection message sn ) + @return boolean if is has been recived or not + */ + function getReceipt( + string memory srcNetwork, + uint256 _connSn + ) public view returns (bool) { + return receipts[srcNetwork][_connSn]; + } + + /** + @notice Set the address of the admin. + @param _address The address of the admin. + */ + function setAdmin(address _address) external onlyAdmin { + adminAddress = _address; + } + + /** + @notice Gets the address of admin + @return (Address) the address of admin + */ + function admin() external view returns (address) { + return adminAddress; + } +} diff --git a/contracts/evm/script/CallService.s.sol b/contracts/evm/script/CallService.s.sol index eaa8bb74..83019127 100644 --- a/contracts/evm/script/CallService.s.sol +++ b/contracts/evm/script/CallService.s.sol @@ -8,6 +8,7 @@ import "@xcall/contracts/xcall/CallService.sol"; import "@xcall/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol"; import "@xcall/contracts/adapters/WormholeAdapter.sol"; import "@xcall/contracts/adapters/LayerZeroAdapter.sol"; +import "@xcall/contracts/adapters/CentralizedConnection.sol"; contract DeployCallService is Script { CallService private proxyXcall; @@ -96,16 +97,16 @@ contract DeployCallService is Script { ); } else if (contractA.compareTo("centralized")) { address xcall = vm.envAddress(chain.concat("_XCALL")); - address wormholeRelayer = vm.envAddress( - chain.concat("_WORMHOLE_RELAYER") + address centralizedRelayer = vm.envAddress( + chain.concat("_CENTRALIZED_RELAYER") ); address proxy = Upgrades.deployTransparentProxy( - "WormholeAdapter.sol", + "CentralizedConnection.sol", msg.sender, abi.encodeCall( - WormholeAdapter.initialize, - (wormholeRelayer, xcall) + CentralizedConnection.initialize, + (centralizedRelayer, xcall) ) ); } else if(contractA.compareTo("mock")) { @@ -143,7 +144,7 @@ contract DeployCallService is Script { Upgrades.upgradeProxy(proxy, contractName, ""); } else if (contractA.compareTo("centralized")) { address proxy = vm.envAddress( - capitalizeString(chain).concat("_XCALL") + capitalizeString(chain).concat("_CENTRALIZED_ADAPTER") ); Upgrades.upgradeProxy(proxy, contractName, ""); } diff --git a/contracts/evm/test/adapters/CentralizedConnection.t.sol b/contracts/evm/test/adapters/CentralizedConnection.t.sol new file mode 100644 index 00000000..22d74e4e --- /dev/null +++ b/contracts/evm/test/adapters/CentralizedConnection.t.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {LZEndpointMock} from "@lz-contracts/mocks/LZEndpointMock.sol"; +import "@xcall/contracts/adapters/CentralizedConnection.sol"; +import "@xcall/contracts/xcall/CallService.sol"; +import "@xcall/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol"; +import "@xcall/utils/Types.sol"; + +contract CentralizedConnectionTest is Test { + using RLPEncodeStruct for Types.CSMessage; + using RLPEncodeStruct for Types.CSMessageRequest; + using RLPEncodeStruct for Types.CSMessageResponse; + + event CallExecuted(uint256 indexed _reqId, int _code, string _msg); + + event RollbackExecuted(uint256 indexed _sn); + + event Message(string targetNetwork, int256 sn, bytes msg); + + event ResponseOnHold(uint256 indexed _sn); + + MultiProtocolSampleDapp dappSource; + MultiProtocolSampleDapp dappTarget; + + CallService xCallSource; + CallService xCallTarget; + + CentralizedConnection adapterSource; + CentralizedConnection adapterTarget; + + address public sourceRelayer; + address public destinationRelayer; + + string public nidSource = "nid.source"; + string public nidTarget = "nid.target"; + + address public owner = address(uint160(uint256(keccak256("owner")))); + address public admin = address(uint160(uint256(keccak256("admin")))); + address public user = address(uint160(uint256(keccak256("user")))); + + address public source_relayer = + address(uint160(uint256(keccak256("source_relayer")))); + address public destination_relayer = + address(uint160(uint256(keccak256("destination_relayer")))); + + function _setupSource() internal { + console2.log("------>setting up source<-------"); + xCallSource = new CallService(); + xCallSource.initialize(nidSource); + + dappSource = new MultiProtocolSampleDapp(); + dappSource.initialize(address(xCallSource)); + + adapterSource = new CentralizedConnection(); + adapterSource.initialize(source_relayer, address(xCallSource)); + + xCallSource.setDefaultConnection(nidTarget, address(adapterSource)); + + console2.log(ParseAddress.toString(address(xCallSource))); + console2.log(ParseAddress.toString(address(user))); + } + + function _setupTarget() internal { + console2.log("------>setting up target<-------"); + + xCallTarget = new CallService(); + xCallTarget.initialize(nidTarget); + + dappTarget = new MultiProtocolSampleDapp(); + dappTarget.initialize(address(xCallTarget)); + + adapterTarget = new CentralizedConnection(); + adapterTarget.initialize(destination_relayer, address(xCallTarget)); + + xCallTarget.setDefaultConnection(nidSource, address(adapterTarget)); + } + + /** + * @dev Sets up the initial state for the test. + */ + function setUp() public { + vm.startPrank(owner); + + _setupSource(); + _setupTarget(); + + vm.stopPrank(); + + // deal some gas + vm.deal(admin, 10 ether); + vm.deal(user, 10 ether); + } + + function testSetAdmin() public { + vm.prank(source_relayer); + adapterSource.setAdmin(user); + assertEq(adapterSource.admin(), user); + } + + function testSetAdminUnauthorized() public { + vm.prank(user); + vm.expectRevert("OnlyRelayer"); + adapterSource.setAdmin(user); + } + + function testSendMessage() public { + vm.startPrank(user); + string memory to = NetworkAddress.networkAddress( + nidTarget, + ParseAddress.toString(address(dappTarget)) + ); + + uint256 cost = adapterSource.getFee(nidTarget, false); + + bytes memory data = bytes("test"); + bytes memory rollback = bytes(""); + + dappSource.sendMessage{value: cost}(to, data, rollback); + vm.stopPrank(); + } + + function testRecvMessage() public { + bytes memory data = bytes("test"); + string memory iconDapp = NetworkAddress.networkAddress( + nidSource, + "0xa" + ); + Types.CSMessageRequest memory request = Types.CSMessageRequest( + iconDapp, + ParseAddress.toString(address(dappSource)), + 1, + false, + data, + new string[](0) + ); + Types.CSMessage memory message = Types.CSMessage( + Types.CS_REQUEST, + request.encodeCSMessageRequest() + ); + + vm.startPrank(destination_relayer); + adapterTarget.recvMessage( + nidSource, + 1, + RLPEncodeStruct.encodeCSMessage(message) + ); + vm.stopPrank(); + } + + function testRecvMessageUnAuthorized() public { + bytes memory data = bytes("test"); + string memory iconDapp = NetworkAddress.networkAddress( + nidSource, + "0xa" + ); + Types.CSMessageRequest memory request = Types.CSMessageRequest( + iconDapp, + ParseAddress.toString(address(dappSource)), + 1, + false, + data, + new string[](0) + ); + Types.CSMessage memory message = Types.CSMessage( + Types.CS_REQUEST, + request.encodeCSMessageRequest() + ); + + vm.startPrank(user); + vm.expectRevert("OnlyRelayer"); + adapterTarget.recvMessage( + nidSource, + 1, + RLPEncodeStruct.encodeCSMessage(message) + ); + vm.stopPrank(); + } + + function testRecvMessageDuplicateMsg() public { + bytes memory data = bytes("test"); + string memory iconDapp = NetworkAddress.networkAddress( + nidSource, + "0xa" + ); + Types.CSMessageRequest memory request = Types.CSMessageRequest( + iconDapp, + ParseAddress.toString(address(dappSource)), + 1, + false, + data, + new string[](0) + ); + Types.CSMessage memory message = Types.CSMessage( + Types.CS_REQUEST, + request.encodeCSMessageRequest() + ); + + vm.startPrank(destination_relayer); + adapterTarget.recvMessage( + nidSource, + 1, + RLPEncodeStruct.encodeCSMessage(message) + ); + + vm.expectRevert("Duplicate Message"); + adapterTarget.recvMessage( + nidSource, + 1, + RLPEncodeStruct.encodeCSMessage(message) + ); + vm.stopPrank(); + } + + function testRevertMessage() public { + vm.startPrank(destination_relayer); + adapterTarget.revertMessage(1); + vm.stopPrank(); + } + + function testRevertMessageUnauthorized() public { + vm.startPrank(user); + vm.expectRevert("OnlyRelayer"); + adapterTarget.revertMessage(1); + vm.stopPrank(); + } + + function testSetFees() public { + vm.prank(source_relayer); + adapterSource.setFee(nidTarget, 5 ether, 5 ether); + + assertEq(adapterSource.getFee(nidTarget, true), 10 ether); + assertEq(adapterSource.getFee(nidTarget, false), 5 ether); + } + + function testSetFeesUnauthorized() public { + vm.prank(user); + + vm.expectRevert("OnlyRelayer"); + adapterSource.setFee(nidTarget, 5 ether, 5 ether); + } + + function testClaimFeesUnauthorized() public { + vm.prank(user); + + vm.expectRevert("OnlyRelayer"); + adapterSource.claimFees(); + } + + function testClaimFees() public { + testSetFees(); + vm.startPrank(user); + string memory to = NetworkAddress.networkAddress( + nidTarget, + ParseAddress.toString(address(dappTarget)) + ); + + uint256 cost = adapterSource.getFee(nidTarget, true); + + bytes memory data = bytes("test"); + bytes memory rollback = bytes("rollback"); + + dappSource.sendMessage{value: cost}(to, data, rollback); + vm.stopPrank(); + + assert(address(adapterSource).balance == 10 ether); + + vm.startPrank(source_relayer); + adapterSource.claimFees(); + vm.stopPrank(); + + assert(source_relayer.balance == 10 ether); + } + + function testGetReceipt() public { + bytes memory data = bytes("test"); + string memory iconDapp = NetworkAddress.networkAddress( + nidSource, + "0xa" + ); + Types.CSMessageRequest memory request = Types.CSMessageRequest( + iconDapp, + ParseAddress.toString(address(dappSource)), + 1, + false, + data, + new string[](0) + ); + Types.CSMessage memory message = Types.CSMessage( + Types.CS_REQUEST, + request.encodeCSMessageRequest() + ); + + assert(adapterTarget.getReceipt(nidSource, 1) == false); + + vm.startPrank(destination_relayer); + adapterTarget.recvMessage( + nidSource, + 1, + RLPEncodeStruct.encodeCSMessage(message) + ); + vm.stopPrank(); + + assert(adapterTarget.getReceipt(nidSource, 1) == true); + } +} diff --git a/contracts/javascore/centralized-connection/build.gradle b/contracts/javascore/centralized-connection/build.gradle new file mode 100644 index 00000000..cb9557e9 --- /dev/null +++ b/contracts/javascore/centralized-connection/build.gradle @@ -0,0 +1,49 @@ +version = '0.1.0' + +dependencies { + implementation project(':xcall-lib') + testImplementation 'foundation.icon:javaee-unittest:0.11.1' + testImplementation project(':test-lib') +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +optimizedJar { + dependsOn(project(':xcall-lib').jar) + mainClassName = 'xcall.adapter.centralized.CentralizedConnection' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + berlin { + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + } + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + uat { + uri = project.findProperty('uat.host') as String + nid = property('uat.nid') as Integer + to = "$mockDApp"?:null + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_relayer', "hxb6b5791be0b5ef67063b3c10b840fb81514db2fd") + arg('_xCall', "$xCall") + } +} \ No newline at end of file diff --git a/contracts/javascore/centralized-connection/src/main/java/xcall/adapter/centralized/CentralizedConnection.java b/contracts/javascore/centralized-connection/src/main/java/xcall/adapter/centralized/CentralizedConnection.java new file mode 100644 index 00000000..e9719e9e --- /dev/null +++ b/contracts/javascore/centralized-connection/src/main/java/xcall/adapter/centralized/CentralizedConnection.java @@ -0,0 +1,190 @@ +/* + * Copyright 2022 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xcall.adapter.centralized; + +import java.math.BigInteger; +import score.Context; + +import score.Address; +import score.BranchDB; +import score.DictDB; +import score.VarDB; + +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Payable; + +public class CentralizedConnection { + protected final VarDB
xCall = Context.newVarDB("callService", Address.class); + protected final VarDB
adminAddress = Context.newVarDB("relayer", Address.class); + private final VarDB connSn = Context.newVarDB("connSn", BigInteger.class); + + protected final DictDB messageFees = Context.newDictDB("messageFees", BigInteger.class); + protected final DictDB responseFees = Context.newDictDB("responseFees", BigInteger.class); + protected final BranchDB> receipts = Context.newBranchDB("receipts", + Boolean.class); + + public CentralizedConnection(Address _relayer, Address _xCall) { + if (xCall.get() == null) { + xCall.set(_xCall); + adminAddress.set(_relayer); + connSn.set(BigInteger.ZERO); + } + } + + @EventLog(indexed = 2) + public void Message(String targetNetwork, BigInteger connSn, byte[] msg) { + } + + /** + * Sets the admin address. + * + * @param _relayer the new admin address + */ + @External + public void setAdmin(Address _relayer) { + OnlyAdmin(); + adminAddress.set(_relayer); + } + + /** + * Retrieves the admin address. + * + * @return The admin address. + */ + @External(readonly = true) + public Address admin() { + return adminAddress.get(); + } + + /** + * Sets the fee to the target network + * + * @param networkId String Network Id of target chain + * @param messageFee The fee needed to send a Message + * @param responseFee The fee of the response + */ + @External + public void setFee(String networkId, BigInteger messageFee, BigInteger responseFee) { + OnlyAdmin(); + messageFees.set(networkId, messageFee); + responseFees.set(networkId, responseFee); + } + + /** + * Returns the fee associated with the given destination address. + * + * @param to String Network Id of target chain + * @param response whether the responding fee is included + * @return The fee of sending a message to a given destination network + */ + @External(readonly = true) + public BigInteger getFee(String to, boolean response) { + BigInteger messageFee = messageFees.getOrDefault(to, BigInteger.ZERO); + if (response) { + BigInteger responseFee = responseFees.getOrDefault(to, BigInteger.ZERO); + return messageFee.add(responseFee); + } + return messageFee; + } + + /** + * Sends a message to the specified network. + * + * @param to Network Id of destination network + * @param svc name of the service + * @param sn positive for two-way message, zero for one-way message, negative + * for response(for xcall message) + * @param msg serialized bytes of Service Message + */ + @Payable + @External + public void sendMessage(String to, String svc, BigInteger sn, byte[] msg) { + Context.require(Context.getCaller().equals(xCall.get()), "Only xCall can send messages"); + BigInteger fee = BigInteger.ZERO; + if (sn.compareTo(BigInteger.ZERO) > 0) { + fee = getFee(to, true); + } else if (sn.equals(BigInteger.ZERO)) { + fee = getFee(to, false); + } + + BigInteger nextConnSn = connSn.get().add(BigInteger.ONE); + connSn.set(nextConnSn); + + Context.require(Context.getValue().compareTo(fee) >= 0, "Insufficient balance"); + Message(to, nextConnSn, msg); + } + + /** + * Receives a message from a source network. + * + * @param srcNetwork the source network id from which the message is received + * @param _connSn the serial number of the connection message + * @param msg serialized bytes of Service Message + */ + @External + public void recvMessage(String srcNetwork, BigInteger _connSn, byte[] msg) { + OnlyAdmin(); + Context.require(!receipts.at(srcNetwork).getOrDefault(_connSn, false), "Duplicate Message"); + receipts.at(srcNetwork).set(_connSn, true); + Context.call(xCall.get(), "handleMessage", srcNetwork, msg); + } + + /** + * Reverts a message. + * + * @param sn the serial number of xcall message representing the message to + * revert + */ + @External + public void revertMessage(BigInteger sn) { + OnlyAdmin(); + Context.call(xCall.get(), "handleError", sn); + } + + /** + * Claim the fees. + * + */ + @External + public void claimFees() { + OnlyAdmin(); + Context.transfer(admin(), Context.getBalance(Context.getAddress())); + } + + /** + * Get the receipts for a given source network and serial number. + * + * @param srcNetwork the source network id + * @param _connSn the serial number of connection message + * @return the receipt if is has been recived or not + */ + @External(readonly = true) + public boolean getReceipts(String srcNetwork, BigInteger _connSn) { + return receipts.at(srcNetwork).getOrDefault(_connSn, false); + } + + /** + * Checks if the caller of the function is the admin. + * + * @return true if the caller is the admin, false otherwise + */ + private void OnlyAdmin() { + Context.require(Context.getCaller().equals(adminAddress.get()), "Only admin can call this function"); + } + +} \ No newline at end of file diff --git a/contracts/javascore/centralized-connection/src/test/java/xcall/adapter/centralized/CentralizedConnectionTest.java b/contracts/javascore/centralized-connection/src/test/java/xcall/adapter/centralized/CentralizedConnectionTest.java new file mode 100644 index 00000000..d3dea74f --- /dev/null +++ b/contracts/javascore/centralized-connection/src/test/java/xcall/adapter/centralized/CentralizedConnectionTest.java @@ -0,0 +1,217 @@ +package xcall.adapter.centralized; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.beans.Transient; +import java.math.BigInteger; +import score.Context; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; + +import xcall.adapter.centralized.CentralizedConnection; +import score.UserRevertedException; +import foundation.icon.ee.types.Bytes; +import foundation.icon.icx.Call; +import foundation.icon.score.client.RevertedException; +import foundation.icon.xcall.CSMessage; +import foundation.icon.xcall.CSMessageRequest; +import foundation.icon.xcall.CallService; +import foundation.icon.xcall.CallServiceReceiver; +import foundation.icon.xcall.CallServiceScoreInterface; +import foundation.icon.xcall.ConnectionScoreInterface; +import foundation.icon.xcall.Connection; +import foundation.icon.xcall.NetworkAddress; +import s.java.math.BigDecimal; + +import xcall.icon.test.MockContract; + +public class CentralizedConnectionTest extends TestBase { + protected final ServiceManager sm = getServiceManager(); + + protected final Account owner = sm.createAccount(); + protected final Account user = sm.createAccount(); + protected final Account admin = sm.createAccount(); + protected final Account xcallMock = sm.createAccount(); + + protected final Account source_relayer = sm.createAccount(); + protected final Account destination_relayer = sm.createAccount(); + + protected Score xcall, connection; + protected CallService xcallSpy; + protected CentralizedConnection connectionSpy; + + protected static String nidSource = "nid.source"; + protected static String nidTarget = "nid.target"; + + // static MockedStatic contextMock; + + protected MockContract callservice; + + // @BeforeAll + // protected static void init() { + // contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS); + // } + + @BeforeEach + public void setup() throws Exception { + callservice = new MockContract<>(CallServiceScoreInterface.class, CallService.class, sm, owner); + + // xcall = sm.deploy(owner, CallService.class, nidSource); + // xcallSpy = (CallService) spy(xcall.getInstance()); + // xcall.setInstance(xcallSpy); + // contextMock.reset(); + + connection = sm.deploy(owner, CentralizedConnection.class, source_relayer.getAddress(), + callservice.getAddress()); + connectionSpy = (CentralizedConnection) spy(connection.getInstance()); + connection.setInstance(connectionSpy); + } + + @Test + public void testSetAdmin() { + // connection.invoke(source_relayer, "setFee", "0xevm", BigInteger.TEN, + // BigInteger.TEN); + + connection.invoke(source_relayer, "setAdmin", admin.getAddress()); + assertEquals(connection.call("admin"), admin.getAddress()); + } + + @Test + public void testSetAdmin_unauthorized() { + UserRevertedException e = assertThrows(UserRevertedException.class, + () -> connection.invoke(user, "setAdmin", admin.getAddress())); + assertEquals("Reverted(0): " + "Only admin can call this function", e.getMessage()); + } + + @Test + public void setFee() { + connection.invoke(source_relayer, "setFee", nidTarget, BigInteger.TEN, BigInteger.TEN); + assertEquals(connection.call("getFee", nidTarget, true), BigInteger.TEN.add(BigInteger.TEN)); + } + + @Test + public void sendMessage() { + connection.invoke(callservice.account, "sendMessage", nidTarget, "xcall", BigInteger.ONE, "test".getBytes()); + verify(connectionSpy).Message(nidTarget, BigInteger.ONE, "test".getBytes()); + } + + @Test + public void testRecvMessage() { + connection.invoke(source_relayer, "recvMessage", nidSource, BigInteger.ONE, "test".getBytes()); + verify(callservice.mock).handleMessage(eq(nidSource), eq("test".getBytes())); + } + + @Test + public void testRecvMessage_unauthorized(){ + + UserRevertedException e = assertThrows(UserRevertedException.class, ()-> connection.invoke(xcallMock, "recvMessage", nidSource, BigInteger.ONE, "test".getBytes())); + assertEquals("Reverted(0): "+"Only admin can call this function", e.getMessage()); + } + + @Test + public void testSendMessage_unauthorized() { + UserRevertedException e = assertThrows(UserRevertedException.class, + () -> connection.invoke(user, "sendMessage", nidTarget, "xcall", BigInteger.ONE, "test".getBytes())); + assertEquals("Reverted(0): " + "Only xCall can send messages", e.getMessage()); + } + + @Test + public void testRecvMessage_duplicateMsg(){ + connection.invoke(source_relayer, "recvMessage",nidSource, BigInteger.ONE, "test".getBytes()); + + UserRevertedException e = assertThrows(UserRevertedException.class,() -> connection.invoke(source_relayer, "recvMessage", + nidSource, BigInteger.ONE, "test".getBytes())); + assertEquals(e.getMessage(), "Reverted(0): "+"Duplicate Message"); + } + + @Test + public void testRevertMessage() { + + connection.invoke(source_relayer, "revertMessage", BigInteger.ONE); + } + + @Test + public void testRevertMessage_unauthorized(){ + UserRevertedException e = assertThrows(UserRevertedException.class, ()->connection.invoke(user, "revertMessage", BigInteger.ONE)); + assertEquals("Reverted(0): "+"Only admin can call this function", e.getMessage()); + + } + + @Test + public void testSetFeesUnauthorized(){ + + UserRevertedException e = assertThrows(UserRevertedException.class,() -> connection.invoke(user, "setFee", "0xevm", + BigInteger.TEN, BigInteger.TEN)); + assertEquals("Reverted(0): "+"Only admin can call this function", e.getMessage()); + } + + @Test + public void testClaimFees(){ + setFee(); + connection.invoke(source_relayer, "claimFees"); + assertEquals(source_relayer.getBalance(), BigInteger.ZERO); + + UserRevertedException e = assertThrows(UserRevertedException.class,() -> connection.invoke(callservice.account, "sendMessage", nidTarget, + "xcall", BigInteger.ONE, "null".getBytes())); + assertEquals(e.getMessage(), "Reverted(0): Insufficient balance"); + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.getValue()).thenReturn(BigInteger.valueOf(20)); + connection.invoke(callservice.account, "sendMessage", nidTarget,"xcall", BigInteger.ONE, "null".getBytes()); + } + + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.getBalance(connection.getAddress())).thenReturn(BigInteger.valueOf(20)); + contextMock.when(() -> Context.transfer(source_relayer.getAddress(),BigInteger.valueOf(20))).then(invocationOnMock -> null); + connection.invoke(source_relayer, "claimFees"); + } + } + + @Test + public void testClaimFees_unauthorized(){ + setFee(); + UserRevertedException e = assertThrows(UserRevertedException.class,() -> connection.invoke(user, "claimFees")); + assertEquals(e.getMessage(), "Reverted(0): "+"Only admin can call this function"); + } + + public MockedStatic.Verification value() { + return () -> Context.getValue(); + } + + @Test + public void testGetReceipt(){ + assertEquals(connection.call("getReceipts", nidSource, BigInteger.ONE), + false); + + connection.invoke(source_relayer, "recvMessage",nidSource, BigInteger.ONE, "test".getBytes()); + + assertEquals(connection.call("getReceipts", nidSource, BigInteger.ONE), + true); + } + +} \ No newline at end of file diff --git a/contracts/javascore/settings.gradle b/contracts/javascore/settings.gradle index f0b303e8..6e6fdcdf 100644 --- a/contracts/javascore/settings.gradle +++ b/contracts/javascore/settings.gradle @@ -2,7 +2,8 @@ rootProject.name = 'xcall-multi' include( 'test-lib', 'xcall', - 'xcall-lib' + 'xcall-lib', + 'centralized-connection' ) include(':dapp-simple') diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml new file mode 100644 index 00000000..96457984 --- /dev/null +++ b/scripts/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3' +services: + java: + container_name: java-builder + image: adoptopenjdk/openjdk11 + volumes: + - ../:/usr/local/src + working_dir: /usr/local/src + entrypoint: ["sh","./scripts/optimize-jar.sh"] + wasm: + container_name: wasm-builder + image: wasm-builder + build: + context: ../ + dockerfile: wasm-builder.Dockerfile + volumes: + - ../:/usr/local/src + working_dir: /usr/local/src + command: ["bash","./scripts/optimize-cosmwasm.sh"] + solidity: + container_name: solidity-builder + image: ghcr.io/foundry-rs/foundry + platform: linux/amd64 + volumes: + - ../:/usr/local/src + working_dir: /usr/local/src + entrypoint: ["sh","-c","apk update && apk add bash && bash ./scripts/optimize-solidity.sh"] diff --git a/scripts/optimize-jar.sh b/scripts/optimize-jar.sh index e031cffc..83b5674f 100755 --- a/scripts/optimize-jar.sh +++ b/scripts/optimize-jar.sh @@ -4,7 +4,7 @@ set -e mkdir -p artifacts/icon cd contracts/javascore -./gradlew clean optimizedJar +./gradlew clean build optimizedJar cd - for jar in $(find . -type f -name "*optimized.jar" | grep /build/libs/); do diff --git a/scripts/optimize-solidity.sh b/scripts/optimize-solidity.sh new file mode 100755 index 00000000..3c344e90 --- /dev/null +++ b/scripts/optimize-solidity.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e +# contracts +CONTRACTS=("CallService" "DAppProxySample" "MultiProtocolSampleDapp" "LayerZeroAdapter" "WormholeAdapter" "CentralizedConnection") + +# Directory paths +build_directory="build" +artifacts_directory="artifacts/evm" + +mkdir -p "$artifacts_directory" + + +cd contracts/evm + +forge clean +forge build --out "$build_directory" --extra-output-files abi bin +cd - +for file in "${CONTRACTS[@]}"; do + file_path="contracts/evm/$build_directory/$file.sol" + mv "$file_path/$file.abi.json" "$artifacts_directory/$file.abi.json" + mv "$file_path/$file.bin" "$artifacts_directory/$file.bin" +done + +cd - diff --git a/scripts/wasm-builder.Dockerfile b/scripts/wasm-builder.Dockerfile new file mode 100644 index 00000000..4b16f22c --- /dev/null +++ b/scripts/wasm-builder.Dockerfile @@ -0,0 +1,30 @@ +FROM rust:1.69.0-alpine3.16 + +ENV PATH=/opt/binaryen/bin:$PATH + + +RUN set -eux; \ + apk update; \ + apk add --no-cache \ + bash \ + curl \ + musl-dev \ + ; \ + BINARYEN_VERS=110; \ + BINARYEN_URL="https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERS}/binaryen-version_${BINARYEN_VERS}-x86_64-linux.tar.gz"; \ + ARCH="$(apk --print-arch)"; \ + case "${ARCH}" in \ + aarch64|armv8) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='673e336c81c65e6b16dcdede33f4cc9ed0f08bde1dbe7a935f113605292dc800' ;; \ + x86_64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='0b2f6c8f85a3d02fde2efc0ced4657869d73fccfce59defb4e8d29233116e6db' ;; \ + *) echo >&2 "unsupported architecture: ARCH"; exit 1 ;; \ + esac; \ + curl -LfsSo /tmp/binaryen.tar.gz ${BINARYEN_URL}; \ + cd /tmp; \ + mkdir -p /opt/binaryen; \ + cd /opt/binaryen; \ + tar -xf /tmp/binaryen.tar.gz --strip-components=1; \ + rm -rf /tmp/binaryen.tar.gz;\ + rustup component add clippy rustfmt;\ + rustup target add wasm32-unknown-unknown; + +RUN cargo install cosmwasm-check@1.4.1 --locked;