diff --git a/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallService.java b/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallService.java index 47f1cc4a..f8bdc803 100644 --- a/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallService.java +++ b/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallService.java @@ -34,7 +34,6 @@ public interface CallService { /*======== At the source CALL_BSH ========*/ /** * Sends a call message to the contract on the destination chain. - * Only allowed to be called from the contract. * * @param _to The BTP address of the callee on the destination chain * @param _data The calldata specific to the target contract diff --git a/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallServiceImpl.java b/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallServiceImpl.java index e354991a..a629a0c7 100644 --- a/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallServiceImpl.java +++ b/javascore/xcall/src/main/java/foundation/icon/btp/xcall/CallServiceImpl.java @@ -107,7 +107,8 @@ private void cleanupCallRequest(BigInteger sn) { @External public BigInteger sendCallMessage(String _to, byte[] _data, @Optional byte[] _rollback) { Address caller = Context.getCaller(); - Context.require(caller.isContract(), "SenderNotAContract"); + // check if caller is a contract or rollback data is null in case of EOA + Context.require(caller.isContract() || _rollback == null, "RollbackNotPossible"); // check size of payloads to avoid abusing Context.require(_data.length <= MAX_DATA_SIZE, "MaxDataSizeExceeded"); diff --git a/javascore/xcall/src/test/java/foundation/icon/btp/xcall/CallServiceImplTest.java b/javascore/xcall/src/test/java/foundation/icon/btp/xcall/CallServiceImplTest.java index 023048f1..01a8a09e 100644 --- a/javascore/xcall/src/test/java/foundation/icon/btp/xcall/CallServiceImplTest.java +++ b/javascore/xcall/src/test/java/foundation/icon/btp/xcall/CallServiceImplTest.java @@ -139,7 +139,7 @@ void handleBTPMessageShouldEmitCallMessage() { var csMsg = new CSMessage(CSMessage.REQUEST, request.toBytes()); var checker = CSIntegrationTest.callMessageEvent((el) -> { assertEquals(from.toString(), el.getFrom()); - assertEquals(sampleAddress.toString(), el.getTo()); + assertEquals(to.account(), el.getTo()); assertEquals(srcSn, el.getSn()); assertEquals(reqId, el.getReqId()); assertArrayEquals(request.getData(), el.getData()); @@ -206,6 +206,55 @@ void maxPayloadsTest() { } } + @Order(6) + @Test + void sendCallMessageFromEOA() { + byte[] data = "sendCallMessageFromEOA".getBytes(); + var sn = getNextSn(); + requestMap.put(sn, new MessageRequest(data, null)); + Address caller = Address.of(callSvc._wallet()); + var request = new CSMessageRequest(caller.toString(), to.account(), sn, false, data); + var checker = MockBMCIntegrationTest.sendMessageEvent((el) -> { + assertEquals(linkNet, el.getTo()); + assertEquals(CallService.NAME, el.getSvc()); + assertEquals(BigInteger.ZERO, el.getSn()); // one-way message + CSMessage csMessage = CSMessage.fromBytes(el.getMsg()); + assertEquals(CSMessage.REQUEST, csMessage.getType()); + AssertCallService.assertEqualsCSMessageRequest(request, CSMessageRequest.fromBytes(csMessage.getData())); + }); + BigInteger fee = getFee(to.net(), false); + assertEquals(forwardFee.add(protocolFee), fee); + accumulateFee(fee, protocolFee); + + // fail if rollback is provided + AssertRevertedException.assertUserReverted(0, () -> + callSvc.sendCallMessage(fee, to.toString(), data, "fakeRollback".getBytes()) + ); + // success if rollback is null + callSvc.sendCallMessage(checker, fee, to.toString(), data, null); + } + + @Order(7) + @Test + void handleBTPMessageShouldEmitCallMessageFromEOA() { + Address caller = Address.of(callSvc._wallet()); + var from = new BTPAddress(linkNet, caller.toString()); + var reqId = getNextReqId(); + byte[] data = requestMap.get(srcSn).getData(); + var request = new CSMessageRequest(from.account(), to.account(), srcSn, false, data); + var csMsg = new CSMessage(CSMessage.REQUEST, request.toBytes()); + var checker = CSIntegrationTest.callMessageEvent((el) -> { + assertEquals(from.toString(), el.getFrom()); + assertEquals(to.account(), el.getTo()); + assertEquals(srcSn, el.getSn()); + assertEquals(reqId, el.getReqId()); + assertArrayEquals(request.getData(), el.getData()); + }); + MockBMCIntegrationTest.mockBMC.handleBTPMessage( + checker, csAddress, + linkNet, CallService.NAME, srcSn, csMsg.toBytes()); + } + @Order(10) @Test void sendCallMessageWithRollback() { diff --git a/solidity/xcall/contracts/CallService.sol b/solidity/xcall/contracts/CallService.sol index 1c29e1fa..395e4c73 100644 --- a/solidity/xcall/contracts/CallService.sol +++ b/solidity/xcall/contracts/CallService.sol @@ -115,8 +115,8 @@ contract CallService is IBSH, ICallService, IFeeManage, Initializable { ) external payable override returns ( uint256 ) { - // Note if caller is a contract in construction, will revert - require(msg.sender.code.length > 0, "SenderNotAContract"); + // check if caller is a contract or rollback data is null in case of EOA + require(msg.sender.code.length > 0 || _rollback.length == 0, "RollbackNotPossible"); // check size of payloads to avoid abusing require(_data.length <= MAX_DATA_SIZE, "MaxDataSizeExceeded"); diff --git a/solidity/xcall/contracts/interfaces/ICallService.sol b/solidity/xcall/contracts/interfaces/ICallService.sol index 538f20d7..82e628e1 100644 --- a/solidity/xcall/contracts/interfaces/ICallService.sol +++ b/solidity/xcall/contracts/interfaces/ICallService.sol @@ -13,7 +13,6 @@ interface ICallService { /*======== At the source CALL_BSH ========*/ /** @notice Sends a call message to the contract on the destination chain. - Only allowed to be called from the contract. @param _to The BTP address of the callee on the destination chain @param _data The calldata specific to the target contract @param _rollback (Optional) The data for restoring the caller state when an error occurred diff --git a/solidity/xcall/test/java/foundation/icon/btp/xcall/CallServiceTest.java b/solidity/xcall/test/java/foundation/icon/btp/xcall/CallServiceTest.java index ac6639dd..eea775b3 100644 --- a/solidity/xcall/test/java/foundation/icon/btp/xcall/CallServiceTest.java +++ b/solidity/xcall/test/java/foundation/icon/btp/xcall/CallServiceTest.java @@ -132,7 +132,7 @@ void sendCallMessageWithoutRollback() throws Exception { @Order(1) @Test void handleBTPMessageShouldEmitCallMessage() throws Exception { - var from = new BTPAddress(linkNet, sampleAddress.toString()); + var from = new BTPAddress(linkNet, sampleAddress); var reqId = getNextReqId(); byte[] data = requestMap.get(srcSn).getData(); var request = new CSMessageRequest(from.account(), to.account(), srcSn, false, data); @@ -203,6 +203,55 @@ void maxPayloadsTest() throws Exception { } } + @Order(6) + @Test + void sendCallMessageFromEOA() throws Exception { + byte[] data = "sendCallMessageFromEOA".getBytes(); + var sn = getNextSn(); + requestMap.put(sn, new MessageRequest(data, null)); + var caller = EVMIntegrationTest.credentials.getAddress(); + var request = new CSMessageRequest(caller, to.account(), sn, false, data); + var checker = MockBMCIntegrationTest.sendMessageEvent((el) -> { + assertEquals(linkNet, el._to); + assertEquals(NAME, el._svc); + assertEquals(BigInteger.ZERO, el._sn); + CSMessage csMessage = CSMessage.fromBytes(el._msg); + assertEquals(CSMessage.REQUEST, csMessage.getType()); + AssertCallService.assertEqualsCSMessageRequest(request, CSMessageRequest.fromBytes(csMessage.getData())); + }); + BigInteger fee = getFee(to.net(), false); + assertEquals(forwardFee.add(protocolFee), fee); + accumulateFee(fee, protocolFee); + + // fail if rollback is provided + AssertTransactionException.assertRevertReason(null, () -> + callService.sendCallMessage(to.toString(), data, "fakeRollback".getBytes(), fee).send() + ); + // success if rollback is null + checker.accept(callService.sendCallMessage(to.toString(), data, new byte[]{}, fee).send()); + } + + @Order(7) + @Test + void handleBTPMessageShouldEmitCallMessageFromEOA() throws Exception { + var caller = EVMIntegrationTest.credentials.getAddress(); + var from = new BTPAddress(linkNet, caller); + var reqId = getNextReqId(); + byte[] data = requestMap.get(srcSn).getData(); + var request = new CSMessageRequest(from.account(), to.account(), srcSn, false, data); + var csMsg = new CSMessage(CSMessage.REQUEST, request.toBytes()); + var checker = CSIntegrationTest.callMessageEvent((el) -> { + assertEquals(Hash.sha3String(from.toString()), StringUtil.bytesToHex(el._from)); + assertEquals(Hash.sha3String(to.account()), StringUtil.bytesToHex(el._to)); + assertEquals(srcSn, el._sn); + assertEquals(reqId, el._reqId); + assertArrayEquals(request.getData(), el._data); + }); + checker.accept(MockBMCIntegrationTest.mockBMC.handleBTPMessage( + csAddress, + linkNet, NAME, srcSn, csMsg.toBytes()).send()); + } + @Order(10) @Test void sendCallMessageWithRollback() throws Exception { @@ -214,7 +263,7 @@ private void _sendCallMessageWithRollback(BigInteger inc) throws Exception { byte[] rollback = "ThisIsRollbackMessage".getBytes(); var sn = getNextSn(inc); requestMap.put(sn, new MessageRequest(data, rollback)); - var request = new CSMessageRequest(sampleAddress.toString(), fakeTo.account(), sn, true, data); + var request = new CSMessageRequest(sampleAddress, fakeTo.account(), sn, true, data); var checker = MockBMCIntegrationTest.sendMessageEvent( (el) -> { assertEquals(linkNet, el._to); @@ -235,9 +284,8 @@ private void _sendCallMessageWithRollback(BigInteger inc) throws Exception { @Test void executeCallWithFailureResponse() throws Exception { // relay the message first - var from = new BTPAddress(linkNet, sampleAddress.toString()); -// byte[] data = requestMap.get(srcSn).getData(); - byte[] data = "sendCallMessageWithRollback".getBytes(); + var from = new BTPAddress(linkNet, sampleAddress); + byte[] data = requestMap.get(srcSn).getData(); var request = new CSMessageRequest(from.account(), fakeTo.account(), srcSn, true, data); var csMsg = new CSMessage(CSMessage.REQUEST, request.toBytes()); MockBMCIntegrationTest.mockBMC.handleBTPMessage(