diff --git a/javascore/nativecoinIRC2/build.gradle b/javascore/nativecoinIRC2/build.gradle new file mode 100644 index 00000000..c193c7f7 --- /dev/null +++ b/javascore/nativecoinIRC2/build.gradle @@ -0,0 +1,68 @@ +version = '0.1.0' + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.0' + compileOnly 'foundation.icon:javaee-rt:0.9.0' + compileOnly 'foundation.icon:icon-sdk:2.0.0' + compileOnly 'foundation.icon:javaee-tooling:0.8.7' + + implementation project(':score-util') + implementation project(':lib') + implementation 'com.github.sink772:javaee-scorex:0.5.2' + implementation 'com.github.sink772:javaee-tokens:0.5.5' + implementation 'org.msgpack:msgpack-core:0.8.17' + + testImplementation 'foundation.icon:javaee-api:0.9.0' + testImplementation 'foundation.icon:javaee-rt:0.9.0' + testImplementation 'foundation.icon:icon-sdk:2.0.0' + testImplementation 'foundation.icon:javaee-tooling:0.9.0' + testImplementation 'org.mockito:mockito-core:3.3.3' + testImplementation 'com.squareup.okhttp3:okhttp:3.11.0' + testImplementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' + + testImplementation fileTree(dir: './lib', include: 'goloop-testsuite.jar') + testImplementation fileTree(dir: './lib', include: 'testsvc.jar') + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.0' +} + +optimizedJar { + mainClassName = 'foundation.icon.btp.bsh.NativeCoinService' + archivesBaseName = 'native-bsh' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} +logging.println("BMC Address :" + System.getProperty("BMC_ADDRESS")) +deployJar { + endpoints { + gangnam { + uri = 'https://gicon.net.solidwallet.io/api/v3' + nid = 7 + } + Sejong { + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 3 + } + BTPTestnet { + uri = 'https://btp.net.solidwallet.io/api/v3' + nid = 0x42 + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + + parameters { + arg('_bmc', System.getProperty("BMC_ADDRESS")) + } +} + +test { + useJUnitPlatform() +} diff --git a/javascore/nativecoinIRC2/lib/goloop-testsuite.jar b/javascore/nativecoinIRC2/lib/goloop-testsuite.jar new file mode 100644 index 00000000..931df1cb Binary files /dev/null and b/javascore/nativecoinIRC2/lib/goloop-testsuite.jar differ diff --git a/javascore/nativecoinIRC2/lib/testsvc.jar b/javascore/nativecoinIRC2/lib/testsvc.jar new file mode 100644 index 00000000..885a76cf Binary files /dev/null and b/javascore/nativecoinIRC2/lib/testsvc.jar differ diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/Asset.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/Asset.java new file mode 100644 index 00000000..4694f54f --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/Asset.java @@ -0,0 +1,98 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +import java.math.BigInteger; + +public class Asset { + private String coinName; + private BigInteger amount; + + public Asset() {} + + public Asset(Asset asset) { + setCoinName(asset.getCoinName()); + setAmount(asset.getAmount()); + } + + public Asset(String coinName, BigInteger amount) { + this.coinName = coinName; + this.amount = amount; + } + + public String getCoinName() { + return coinName; + } + + public void setCoinName(String coinName) { + this.coinName = coinName; + } + + public BigInteger getAmount() { + return amount; + } + + public void setAmount(BigInteger amount) { + this.amount = amount; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Asset{"); + sb.append("coinName='").append(coinName).append('\''); + sb.append(", amount=").append(amount); + sb.append('}'); + return sb.toString(); + } + + public static void writeObject(ObjectWriter writer, Asset obj) { + obj.writeObject(writer); + } + + public static Asset readObject(ObjectReader reader) { + Asset obj = new Asset(); + reader.beginList(); + obj.setCoinName(reader.readNullable(String.class)); + obj.setAmount(reader.readNullable(BigInteger.class)); + reader.end(); + return obj; + } + + + public void writeObject(ObjectWriter writer) { + writer.beginList(2); + writer.writeNullable(this.getCoinName()); + writer.writeNullable(this.getAmount()); + writer.end(); + } + + public static Asset fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return Asset.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + Asset.writeObject(writer, this); + return writer.toByteArray(); + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/AssetTransferDetail.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/AssetTransferDetail.java new file mode 100644 index 00000000..b2373081 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/AssetTransferDetail.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +import java.math.BigInteger; + +public class AssetTransferDetail extends Asset { + private BigInteger fee; + + public BigInteger getFee() { + return fee; + } + + public void setFee(BigInteger fee) { + this.fee = fee; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("AssetTransferDetail{"); + sb.append("fee=").append(fee); + sb.append('}').append(super.toString()); + return sb.toString(); + } + + public static void writeObject(ObjectWriter writer, AssetTransferDetail obj) { + obj.writeObject(writer); + } + + public static AssetTransferDetail readObject(ObjectReader reader) { + AssetTransferDetail obj = new AssetTransferDetail(); + reader.beginList(); + obj.setCoinName(reader.readNullable(String.class)); + obj.setAmount(reader.readNullable(BigInteger.class)); + obj.setFee(reader.readNullable(BigInteger.class)); + reader.end(); + return obj; + } + + public void writeObject(ObjectWriter writer) { + writer.beginList(3); + writer.writeNullable(this.getCoinName()); + writer.writeNullable(this.getAmount()); + writer.writeNullable(this.getFee()); + writer.end(); + } + + public static AssetTransferDetail fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return AssetTransferDetail.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + AssetTransferDetail.writeObject(writer, this); + return writer.toByteArray(); + } + +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMC.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMC.java new file mode 100644 index 00000000..1b094d27 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMC.java @@ -0,0 +1,195 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.Address; +import score.annotation.External; + +import java.math.BigInteger; +import java.util.Map; + +public interface BMC { + /** + * Registers BMV for the network. + * Called by the operator to manage the BTP network. + * + * @param _net String (Network Address of the blockchain ) + * @param _addr Address (the address of BMV) + */ + @External + void addVerifier(String _net, Address _addr); + + /** + * Unregisters BMV for the network. + * May fail if it's referred by the link. + * Called by the operator to manage the BTP network. + * + * @param _net String (Network Address of the blockchain ) + */ + @External + void removeVerifier(String _net); + + /** + * Get registered verifiers. + * + * @return A dictionary with the Network Address as a key and smart contract address of the BMV as a value. + *
+ * For Example:: + *
+ * { + * "0x1.iconee": "cx72eaed466599ca5ea377637c6fa2c5c0978537da" + * } + */ + @External(readonly = true) + Map getVerifiers(); + + /** + * Registers the smart contract for the service. + * Called by the operator to manage the BTP network. + * + * @param _svc String (the name of the service) + * @param _addr Address (the address of the smart contract handling the service) + */ + @External + void addService(String _svc, Address _addr); + + /** + * Unregisters the smart contract for the service. + * Called by the operator to manage the BTP network. + * + * @param _svc String (the name of the service) + */ + @External + void removeService(String _svc); + /** + * Get registered services. + * + * @return A dictionary with the name of the service as key and address of the BSH related to the service as value. + *
For example::
+ * { + * "token": "cx72eaed466599ca5ea377637c6fa2c5c0978537da" + * } + */ + @External(readonly = true) + Map getServices(); + + /** + * If it generates the event related to the link, the relay shall handle the event to deliver BTP Message to the BMC. + * If the link is already registered, or its network is already registered then it fails. + * If there is no verifier related with the network of the link, then it fails. + * Initializes status information for the link. + * Called by the operator to manage the BTP network. + * + * @param _link String (BTP Address of connected BMC) + */ + @External + void addLink(String _link); + + /** + * Removes the link and status information. + * Called by the operator to manage the BTP network. + * + * @param _link String (BTP Address of connected BMC) + */ + @External + void removeLink(String _link); + + /** + * Get status of BMC. + * Used by the relay to resolve next BTP Message to send. + * If target is not registered, it will fail. + * + * @param _link String ( BTP Address of the connected BMC ) + * @return The object contains followings fields. + */ + @External(readonly = true) + BMCStatus getStatus(String _link); + + /** + * Get registered links. + * + * @return A list of links ( BTP Addresses of the BMCs ) + *
For Example::
+ * [ + * "btp://0x1.iconee/cx9f8a75111fd611710702e76440ba9adaffef8656" + * ] + */ + @External(readonly = true) + String[] getLinks(); + + /** + * Add route to the BMC. + * May fail if there more than one BMC for the network. + * Called by the operator to manage the BTP network. + * + * @param _dst String ( BTP Address of the destination BMC ) + * @param _link String ( BTP Address of the next BMC for the destination ) + */ + @External + void addRoute(String _dst, String _link); + /** + * Remove route to the BMC. + * Called by the operator to manage the BTP network. + * + * @param _dst String ( BTP Address of the destination BMC ) + */ + @External + void removeRoute(String _dst); + /** + * Get routing information. + * + * @return A dictionary with the BTP Address of the destination BMC as key and the BTP Address of the next as value. + * + *
For Example::
+ * { + * "btp://0x2.iconee/cx1d6e4decae8160386f4ecbfc7e97a1bc5f74d35b": "btp://0x1.iconee/cx9f8a75111fd611710702e76440ba9adaffef8656" + * } + */ + @External(readonly = true) + Map getRoutes(); + + /** + * Sends the message to a specific network. + * Only allowed to be called by registered BSHs. + * + * @param _to String ( Network Address of destination network ) + * @param _svc String ( name of the service ) + * @param _sn Integer ( serial number of the message, must be positive ) + * @param _msg Bytes ( serialized bytes of Service Message ) + */ + @External + void sendMessage(String _to, String _svc, BigInteger _sn, byte[] _msg); + + /** + * It verifies and decodes the Relay Message with BMV and dispatches BTP Messages to registered BSHs. + * It's allowed to be called by registered Relay. + * + * @param _prev String ( BTP Address of the previous BMC ) + * @param _msg String ( base64 encoded string of serialized bytes of Relay Message ) + */ + @External + void handleRelayMessage(String _prev, String _msg); + + /** + * TODO [TBD] add 'getBtpAddress' to IIP-25.BMC.Read-only methods + * Returns BTP Address of BMC + * + * @return String (BTP Address of BMC) + */ + @External(readonly = true) + String getBtpAddress(); +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCMock.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCMock.java new file mode 100644 index 00000000..dec4ec00 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCMock.java @@ -0,0 +1,56 @@ +package foundation.icon.btp.nativecoinIRC2; + +import foundation.icon.btp.lib.BTPAddress; +import score.Address; +import score.Context; +import score.DictDB; +import score.annotation.External; + +import java.math.BigInteger; + +public class BMCMock { + private final DictDB bshServices = Context.newDictDB("bshService", Address.class); + private final BTPAddress btpAddr; + + public BMCMock(String _net) { + this.btpAddr = new BTPAddress(BTPAddress.PROTOCOL_BTP, _net, Context.getAddress().toString()); + } + + + @External + public void addService(String _svc, Address _addr) { + bshServices.set(_svc, _addr); + } + + @External + public void sendMessage(String _to, String _svc, BigInteger _sn, byte[] _msg) { + Address addr = bshServices.get(_svc); + if (addr == null) { + Context.revert("BSH doesnt exist"); + } + if (!Context.getCaller().equals(addr)) { + Context.revert("unauthorized"); + } + if (_sn.compareTo(BigInteger.ZERO) < 1) { + //Context.revert("invalid sn"); + } + } + + @External + public void handleBTPMessage(String from, String svc, BigInteger sn, byte[] msg) { + Address _addr = bshServices.get(svc); + Context.call(_addr, "handleBTPMessage", from, svc, sn, msg); + } + + @External + public void handleFeeGathering(String _fa, String _svc) { + Address _addr = bshServices.get(_svc); + Context.call(_addr, "handleFeeGathering", _fa, _svc); + } + + + @External(readonly = true) + public String getBtpAddress() { + return btpAddr.toString(); + } +} \ No newline at end of file diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCScoreInterface.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCScoreInterface.java new file mode 100644 index 00000000..b6709259 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCScoreInterface.java @@ -0,0 +1,135 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.Address; +import score.Context; + +import java.math.BigInteger; +import java.util.Map; + +public final class BMCScoreInterface implements BMC { + protected final Address address; + + protected final BigInteger valueForPayable; + + public BMCScoreInterface(Address address) { + this.address = address; + this.valueForPayable = null; + } + + public BMCScoreInterface(Address address, BigInteger valueForPayable) { + this.address = address; + this.valueForPayable = valueForPayable; + } + + public Address _getAddress() { + return this.address; + } + + public BMCScoreInterface _payable(BigInteger valueForPayable) { + return new BMCScoreInterface(address,valueForPayable); + } + + public BMCScoreInterface _payable(long valueForPayable) { + return this._payable(BigInteger.valueOf(valueForPayable)); + } + + public BigInteger _getICX() { + return this.valueForPayable; + } + + @Override + public void addVerifier(String _net, Address _addr) { + Context.call(this.address, "addVerifier", _net, _addr); + } + + @Override + public void removeVerifier(String _net) { + Context.call(this.address, "removeVerifier", _net); + } + + @Override + public Map getVerifiers() { + return Context.call(Map.class, this.address, "getVerifiers"); + } + + @Override + public void addService(String _svc, Address _addr) { + Context.call(this.address, "addService", _svc, _addr); + } + + @Override + public void removeService(String _svc) { + Context.call(this.address, "removeService", _svc); + } + + @Override + public Map getServices() { + return Context.call(Map.class, this.address, "getServices"); + } + + @Override + public void addLink(String _link) { + Context.call(this.address, "addLink", _link); + } + + @Override + public void removeLink(String _link) { + Context.call(this.address, "removeLink", _link); + } + + @Override + public BMCStatus getStatus(String _link) { + return Context.call(BMCStatus.class, this.address, "getStatus", _link); + } + + @Override + public String[] getLinks() { + return Context.call(String[].class, this.address, "getLinks"); + } + + @Override + public void addRoute(String _dst, String _link) { + Context.call(this.address, "addRoute", _dst, _link); + } + + @Override + public void removeRoute(String _dst) { + Context.call(this.address, "removeRoute", _dst); + } + + @Override + public Map getRoutes() { + return Context.call(Map.class, this.address, "getRoutes"); + } + + @Override + public void sendMessage(String _to, String _svc, BigInteger _sn, byte[] _msg) { + Context.call(this.address, "sendMessage", _to, _svc, _sn, _msg); + } + + @Override + public void handleRelayMessage(String _prev, String _msg) { + Context.call(this.address, "handleRelayMessage", _prev, _msg); + } + + @Override + public String getBtpAddress() { + return Context.call(String.class, this.address, "getBtpAddress"); + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCStatus.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCStatus.java new file mode 100644 index 00000000..a4fb6410 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMCStatus.java @@ -0,0 +1,252 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import foundation.icon.score.util.StringUtil; +import score.annotation.Keep; + +import java.math.BigInteger; + +public class BMCStatus { + private BigInteger rx_seq; + private BigInteger tx_seq; + private BMVStatus verifier; + private BMRStatus[] relays; + + private int block_interval_src; + private int block_interval_dst; + private int max_agg; + private int delay_limit; + + private int relay_idx; + private long rotate_height; + private int rotate_term; + private long rx_height; //initialize with BMC.block_height + private long rx_height_src; //initialize with BMV._offset + + private int sack_term; //0: disable sack + private long sack_next; + private long sack_height; + private BigInteger sack_seq; + + private long cur_height; + + @Keep + public BigInteger getRx_seq() { + return rx_seq; + } + + @Keep + public void setRx_seq(BigInteger rx_seq) { + this.rx_seq = rx_seq; + } + + @Keep + public BigInteger getTx_seq() { + return tx_seq; + } + + @Keep + public void setTx_seq(BigInteger tx_seq) { + this.tx_seq = tx_seq; + } + + @Keep + public BMVStatus getVerifier() { + return verifier; + } + + @Keep + public void setVerifier(BMVStatus verifier) { + this.verifier = verifier; + } + + @Keep + public BMRStatus[] getRelays() { + return relays; + } + + @Keep + public void setRelays(BMRStatus[] relays) { + this.relays = relays; + } + + @Keep + public int getBlock_interval_src() { + return block_interval_src; + } + + @Keep + public void setBlock_interval_src(int block_interval_src) { + this.block_interval_src = block_interval_src; + } + + @Keep + public int getBlock_interval_dst() { + return block_interval_dst; + } + + @Keep + public void setBlock_interval_dst(int block_interval_dst) { + this.block_interval_dst = block_interval_dst; + } + + @Keep + public int getMax_agg() { + return max_agg; + } + + @Keep + public void setMax_agg(int max_agg) { + this.max_agg = max_agg; + } + + @Keep + public int getDelay_limit() { + return delay_limit; + } + + @Keep + public void setDelay_limit(int delay_limit) { + this.delay_limit = delay_limit; + } + + @Keep + public int getRelay_idx() { + return relay_idx; + } + + @Keep + public void setRelay_idx(int relay_idx) { + this.relay_idx = relay_idx; + } + + @Keep + public long getRotate_height() { + return rotate_height; + } + + @Keep + public void setRotate_height(long rotate_height) { + this.rotate_height = rotate_height; + } + + @Keep + public int getRotate_term() { + return rotate_term; + } + + @Keep + public void setRotate_term(int rotate_term) { + this.rotate_term = rotate_term; + } + + @Keep + public long getRx_height() { + return rx_height; + } + + @Keep + public void setRx_height(long rx_height) { + this.rx_height = rx_height; + } + + @Keep + public long getRx_height_src() { + return rx_height_src; + } + + @Keep + public void setRx_height_src(long rx_height_src) { + this.rx_height_src = rx_height_src; + } + + @Keep + public int getSack_term() { + return sack_term; + } + + @Keep + public void setSack_term(int sack_term) { + this.sack_term = sack_term; + } + + @Keep + public long getSack_next() { + return sack_next; + } + + @Keep + public void setSack_next(long sack_next) { + this.sack_next = sack_next; + } + + @Keep + public long getSack_height() { + return sack_height; + } + + @Keep + public void setSack_height(long sack_height) { + this.sack_height = sack_height; + } + + @Keep + public BigInteger getSack_seq() { + return sack_seq; + } + + @Keep + public void setSack_seq(BigInteger sack_seq) { + this.sack_seq = sack_seq; + } + + @Keep + public long getCur_height() { + return cur_height; + } + + @Keep + public void setCur_height(long cur_height) { + this.cur_height = cur_height; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("BMCStatus{"); + sb.append("rx_seq=").append(rx_seq); + sb.append(", tx_seq=").append(tx_seq); + sb.append(", verifier=").append(verifier); + sb.append(", relays=").append(StringUtil.toString(relays)); + sb.append(", block_interval_src=").append(block_interval_src); + sb.append(", block_interval_dst=").append(block_interval_dst); + sb.append(", max_agg=").append(max_agg); + sb.append(", delay_limit=").append(delay_limit); + sb.append(", relay_idx=").append(relay_idx); + sb.append(", rotate_height=").append(rotate_height); + sb.append(", rotate_term=").append(rotate_term); + sb.append(", rx_height=").append(rx_height); + sb.append(", rx_height_src=").append(rx_height_src); + sb.append(", sack_term=").append(sack_term); + sb.append(", sack_next=").append(sack_next); + sb.append(", sack_height=").append(sack_height); + sb.append(", sack_seq=").append(sack_seq); + sb.append(", cur_height=").append(cur_height); + sb.append('}'); + return sb.toString(); + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMRStatus.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMRStatus.java new file mode 100644 index 00000000..eb02fc66 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMRStatus.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.Address; +import score.annotation.Keep; + +import java.math.BigInteger; + +public class BMRStatus { + private Address address; + private long block_count; + private BigInteger msg_count; + + @Keep + public Address getAddress() { + return address; + } + + @Keep + public void setAddress(Address address) { + this.address = address; + } + + @Keep + public long getBlock_count() { + return block_count; + } + + @Keep + public void setBlock_count(long block_count) { + this.block_count = block_count; + } + + @Keep + public BigInteger getMsg_count() { + return msg_count; + } + + @Keep + public void setMsg_count(BigInteger msg_count) { + this.msg_count = msg_count; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("BMRStatus{"); + sb.append("address=").append(address); + sb.append(", block_count=").append(block_count); + sb.append(", msg_count=").append(msg_count); + sb.append('}'); + return sb.toString(); + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMVStatus.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMVStatus.java new file mode 100644 index 00000000..2c9a786d --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/BMVStatus.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.annotation.Keep; + +public class BMVStatus { + private long height; + private long offset; + private long last_height; + + @Keep + public BMVStatus() { + } + + @Keep + public long getHeight() { + return height; + } + + @Keep + public void setHeight(long height) { + this.height = height; + } + + @Keep + public long getOffset() { + return offset; + } + + @Keep + public void setOffset(long offset) { + this.offset = offset; + } + + @Keep + public long getLast_height() { + return last_height; + } + + @Keep + public void setLast_height(long last_height) { + this.last_height = last_height; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("BMVStatus{"); + sb.append("height=").append(height); + sb.append(", offset=").append(offset); + sb.append(", last_height=").append(last_height); + sb.append('}'); + return sb.toString(); + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/Balance.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/Balance.java new file mode 100644 index 00000000..1e57b931 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/Balance.java @@ -0,0 +1,106 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +import java.math.BigInteger; + +public class Balance { + + private BigInteger usable; + private BigInteger locked; + private BigInteger refundable; + + + public Balance(BigInteger usable, BigInteger locked, BigInteger refundable) { + this.usable = usable; + this.locked = locked; + this.refundable = refundable; + } + + public BigInteger getUsable() { + return usable; + } + + public void setUsable(BigInteger usable) { + this.usable = usable; + } + + public BigInteger getLocked() { + return locked; + } + + public void setLocked(BigInteger locked) { + this.locked = locked; + } + + public BigInteger getRefundable() { + return refundable; + } + + public void setRefundable(BigInteger refundable) { + this.refundable = refundable; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Balance{"); + sb.append("usable=").append(usable); + sb.append(", locked=").append(locked); + sb.append(", refundable=").append(refundable); + sb.append('}'); + return sb.toString(); + } + + public static void writeObject(ObjectWriter writer, Balance obj) { + obj.writeObject(writer); + } + + public static Balance readObject(ObjectReader reader) { + reader.beginList(); + //obj.setUsable(reader.readNullable(BigInteger.class)); + //obj.setLocked(reader.readNullable(BigInteger.class)); + //obj.setRefundable(reader.readNullable(BigInteger.class)); + Balance obj = new Balance(reader.readNullable(BigInteger.class),reader.readNullable(BigInteger.class),reader.readNullable(BigInteger.class)); + reader.end(); + return obj; + } + + public void writeObject(ObjectWriter writer) { + writer.beginList(3); + writer.writeNullable(this.getUsable()); + writer.writeNullable(this.getLocked()); + writer.writeNullable(this.getRefundable()); + writer.end(); + } + + public static Balance fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return Balance.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + Balance.writeObject(writer, this); + return writer.toByteArray(); + } + +} \ No newline at end of file diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCS.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCS.java new file mode 100644 index 00000000..5a2cff69 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCS.java @@ -0,0 +1,167 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.Address; +import score.annotation.External; +import score.annotation.Payable; + +import java.math.BigInteger; + +public interface NCS { + + /** + * Registers a wrapped token smart contract and id number of a supporting coin. + * Caller must be the owner + * + * {@code _name} It must be listed in the code name registry.. + * The native coin (ICX) must be registered through the constructor. + * + * @param _name A coin name. + */ + @External + void register(String _name); + + /** + * Return all supported coins names in other networks by the BSH contract + * + * @return An array of strings. + */ + @External(readonly = true) + String[] coinNames(); + + /** + * Return an _id number and a symbol of Coin whose name is the same with given _coinName. + * + * @implSpec Return nullempty if not found. + * + * @param _coinName Coin name. + * @return id of Native Coin Token. + */ + @External(readonly = true) + BigInteger coinId(String _coinName); + + /** + * Return a usable/locked balance of an account based on coinName. + * + * @implSpec Locked Balance means an amount of Coins/Wrapped Coins is currently + * at a pending state when a transfer tx is requested from this chain to another + * @implSpec Return 0 if not found + * + * @param _owner + * @param _coinName Coin name. + * @return Balance + * { + * "locked" : an amount of locked Coins/WrappedCoins, + * "refundable" : an amount of refundable Coins/WrappedCoins + * } + */ + @External(readonly = true) + Balance balanceOf(Address _owner, String _coinName); + + /** + * Return a list locked/usable balance of an account. + * + * @implSpec The order of request's coinNames must be the same with the order of return balance + * @implSpec Return 0 if not found. + * + * @param _owner + * @param _coinNames + * @return Balance[] + * [ + * { + * "locked" : an amount of locked Coins/WrappedCoins, + * "refundable" : an amount of refundable Coins/WrappedCoins + * } + * ] + */ + @External(readonly = true) + Balance[] balanceOfBatch(Address _owner, String[] _coinNames); + + /** + * Reclaim the coin's refundable balance by an owner. + * + * @apiNote Caller must be an owner of coin + * @implSpec This function only applies on native coin (not wrapped coins) + * The amount to claim must be smaller than refundable balance + * + * @param _coinName A given name of coin to be re-claim + * @param _value An amount of re-claiming + */ + @External + void reclaim(String _coinName, BigInteger _value); + + /** + * Allow users to deposit `msg.value` native coin into a BSH contract. + * + * @implSpec MUST specify msg.value + * + * @param _to An address that a user expects to receive an equivalent amount of tokens. + */ + @Payable + @External + void transferNativeCoin(String _to); + + /** + * Allow users to deposit an amount of wrapped native coin `_coinName` from the `msg.sender` address into the BSH contract. + * + * @apiNote Caller must set to approve that the wrapped tokens can be transferred out of the `msg.sender` account by the operator. + * @implSpec It MUST revert if the balance of the holder for token `_coinName` is lower than the `_value` sent. + * + * @param _coinName A given name of coin that is equivalent to retrieve a wrapped Token Contract's address, i.e. list["Token A"] = 0x12345678 + * @param _value Transferring amount. + * @param _to Target address. + */ + @External + void transfer(String _coinName, BigInteger _value, String _to); + + /** + * Allows users to deposit `_values` amounts of coins `_coinName` from the `msg.sender` address into the BSH contract. + * + * The caller must set to approve that the wrapped tokens can be transferred out of the `msg.sender` account by the contract. It MUST revert if the balance of the holder for token `_coinName` is lower than the `_value` sent. + * The order of coinName and value should be matched + * + * @param _coinNames Given names of each coin that is equivalent to retrieve a wrapped Token Contract's address, i.e. list["Token A"] = 0x12345678 + * @param _values Transferring amounts per coin + * @param _to Target BTP Address. + */ + @Payable + @External + void transferBatch(String[] _coinNames, BigInteger[] _values, String _to); + + /** + * Sets a new transfer fee ratio. + * + * The caller must be the owner. + * The transfer fee is calculated by feeNumerator/FEE_DEMONINATOR. The feeNumetator should be less than FEE_DEMONINATOR and greater than 1 + * feeNumetator is set to `10` in construction by default, which means the default fee ratio is 0.1%. + * + * @param _feeNumerator the fee numerator + */ + @External + void setFeeRatio(BigInteger _feeNumerator); + + /** + * Get transfer fee ratio. + * + * @return BigInteger the fee ratio + * + */ + @External(readonly = true) + BigInteger feeRatio(); + +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSEvents.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSEvents.java new file mode 100644 index 00000000..fc700104 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSEvents.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.Address; +import score.annotation.EventLog; + +import java.math.BigInteger; + +public interface NCSEvents { + + /** + * (EventLog) Sends a receipt to sender + * + * @param _from The {@code _from} sender. (Indexed) + * @param _to The {@code _to} receiver. + * @param _sn The {@code _sn} sequence number of the service message. + * @param _assets The {@code _assets} asset details that is the serialized data of AssetTransferDetail + */ + @EventLog(indexed = 1) + void TransferStart(Address _from, String _to, BigInteger _sn, byte[] _assets); + + /** + * (EventLog) Sends a receipt to sender to notify the transfer's result + * + * @param _sender The {@code _sender} account sends the service message. (Indexed) + * @param _sn The {@code _sn} sequence number of the service message. + * @param _code The {@code _code} response code. + * @param _msg The {@code _msg} response message. + */ + @EventLog(indexed = 1) + void TransferEnd(Address _sender, BigInteger _sn, BigInteger _code, byte[] _msg); + + /** + * Notify to the BSH owner that it has received unknown response + * + * @param _from The {@code _from} Network Address of source network. + * @param _sn The {@code _sn} sequence number of the service message. + */ + @EventLog(indexed = 1) + void UnknownResponse(String _from, BigInteger _sn); +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSException.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSException.java new file mode 100644 index 00000000..9a5e8d54 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSException.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import foundation.icon.btp.lib.BTPException; + +public class NCSException extends BTPException.BSH { + + public NCSException(Code c) { + super(c, c.name()); + } + + public NCSException(Code c, String message) { + super(c, message); + } + + public static NCSException unknown(String message) { + return new NCSException(Code.Unknown, message); + } + + public static NCSException unauthorized() { + return new NCSException(Code.Unauthorized); + } + public static NCSException unauthorized(String message) { + return new NCSException(Code.Unauthorized, message); + } + + public static NCSException irc31Failure() { + return new NCSException(Code.IRC31Failure); + } + public static NCSException irc31Failure(String message) { + return new NCSException(Code.IRC31Failure, message); + } + + public static NCSException irc31Reverted() { + return new NCSException(Code.IRC31Reverted); + } + public static NCSException irc31Reverted(String message) { + return new NCSException(Code.IRC31Reverted, message); + } + + + //BTPException.BSH => 40 ~ 54 + public enum Code implements BTPException.Coded{ + Unknown(0), + Unauthorized(1), + IRC31Failure(2), + IRC31Reverted(3); + + final int code; + Code(int code){ this.code = code; } + + @Override + public int code() { return code; } + + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSMessage.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSMessage.java new file mode 100644 index 00000000..8d71a4b8 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSMessage.java @@ -0,0 +1,89 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import foundation.icon.score.util.StringUtil; +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +public class NCSMessage { + public static int REQUEST_COIN_TRANSFER = 0; + public static int REQUEST_COIN_REGISTER = 1; + public static int REPONSE_HANDLE_SERVICE = 2; + public static int UNKNOWN_TYPE = 3; + + private int serviceType; + private byte[] data; + + public int getServiceType() { + return serviceType; + } + + public void setServiceType(int serviceType) { + this.serviceType = serviceType; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ServiceMessage{"); + sb.append("serviceType='").append(serviceType).append('\''); + sb.append(", data=").append(StringUtil.toString(data)); + sb.append('}'); + return sb.toString(); + } + + public static void writeObject(ObjectWriter writer, NCSMessage obj) { + obj.writeObject(writer); + } + + public static NCSMessage readObject(ObjectReader reader) { + NCSMessage obj = new NCSMessage(); + reader.beginList(); + obj.setServiceType(reader.readInt()); + obj.setData(reader.readNullable(byte[].class)); + reader.end(); + return obj; + } + + public void writeObject(ObjectWriter writer) { + writer.beginList(2); + writer.write(this.getServiceType()); + writer.writeNullable(this.getData()); + writer.end(); + } + + public static NCSMessage fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return NCSMessage.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + NCSMessage.writeObject(writer, this); + return writer.toByteArray(); + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSProperties.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSProperties.java new file mode 100644 index 00000000..bf5c4646 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NCSProperties.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; + +import java.math.BigInteger; + +public class NCSProperties { + public static final NCSProperties DEFAULT; + + static { + DEFAULT = new NCSProperties(); + DEFAULT.setSn(BigInteger.ZERO); + DEFAULT.setFeeRatio(BigInteger.valueOf(10)); + } + + private BigInteger sn; + private BigInteger feeRatio; + + public BigInteger getSn() { + return sn; + } + + public void setSn(BigInteger sn) { + this.sn = sn; + } + + public BigInteger getFeeRatio() { + return feeRatio; + } + + public void setFeeRatio(BigInteger feeRatio) { + this.feeRatio = feeRatio; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("NCSProperties{"); + sb.append("sn=").append(sn); + sb.append(", feeRatio=").append(feeRatio); + sb.append('}'); + return sb.toString(); + } + + public static void writeObject(ObjectWriter writer, NCSProperties obj) { + obj.writeObject(writer); + } + + public static NCSProperties readObject(ObjectReader reader) { + NCSProperties obj = new NCSProperties(); + reader.beginList(); + obj.setSn(reader.readNullable(BigInteger.class)); + obj.setFeeRatio(reader.readNullable(BigInteger.class)); + reader.end(); + return obj; + } + + public void writeObject(ObjectWriter writer) { + writer.beginList(2); + writer.writeNullable(this.getSn()); + writer.writeNullable(this.getFeeRatio()); + writer.end(); + } + + public static NCSProperties fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return NCSProperties.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + NCSProperties.writeObject(writer, this); + return writer.toByteArray(); + } + +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NativeCoinService.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NativeCoinService.java new file mode 100644 index 00000000..75a6ed98 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/NativeCoinService.java @@ -0,0 +1,579 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import foundation.icon.btp.lib.BSH; +import foundation.icon.btp.lib.BTPAddress; +import foundation.icon.btp.lib.OwnerManager; +import foundation.icon.btp.lib.OwnerManagerImpl; +import foundation.icon.score.util.ArrayUtil; +import foundation.icon.score.util.Logger; +import foundation.icon.score.util.StringUtil; +import score.*; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; + +public class NativeCoinService implements NCSEvents, BSH, OwnerManager { + private static final Logger logger = Logger.getLogger(NativeCoinService.class); + + public static final String SERVICE = "nativecoin"; + public static final BigInteger NATIVE_COIN_ID = BigInteger.ZERO; + public static final BigInteger FEE_DENOMINATOR = BigInteger.valueOf(10000); + protected static final Address ZERO_ADDRESS = new Address(new byte[Address.LENGTH]); + + // + private final Address bmc; + private final String net; + private final Address irc2; + private final String name; + private final String tokenName; + private final VarDB properties = Context.newVarDB("properties", NCSProperties.class); + + // + private final OwnerManager ownerManager = new OwnerManagerImpl("owners"); + + // + private final ArrayDB coinNames = Context.newArrayDB("coinNames", String.class); + private final BranchDB> balances = Context.newBranchDB("balances", Balance.class); + private final DictDB feeBalances = Context.newDictDB("feeBalances", BigInteger.class); + private final DictDB transactions = Context.newDictDB("transactions", TransferTransaction.class); + + public NativeCoinService(Address _bmc, Address _irc2, String _name, String _tokenName) { + bmc = _bmc; + BMCScoreInterface bmcInterface = new BMCScoreInterface(bmc); + //BTPAddress btpAddress = BTPAddress.valueOf(bmcInterface.getBtpAddress()); + String btpAddr = (String) Context.call(bmcInterface._getAddress(), "getBtpAddress"); + BTPAddress btpAddress = BTPAddress.valueOf(btpAddr); + net = btpAddress.net(); + irc2 = _irc2; + name = _name; + tokenName = _tokenName; + coinNames.add(_tokenName); + } + + public NCSProperties getProperties() { + return properties.getOrDefault(NCSProperties.DEFAULT); + } + + public void setProperties(NCSProperties properties) { + this.properties.set(properties); + } + + private boolean isRegistered(String name) { + if (tokenName.equals(name)) { + return true; + } + return false; + } + + static void require(boolean condition, String message) { + if (!condition) { + throw NCSException.unknown(message); + } + } + + @External(readonly = true) + public String[] coinNames() { + String[] names = new String[2]; + names[0] = name; + names[1] = tokenName; + return names; + } + + @External(readonly = true) + public Balance balanceOf(Address _owner, String _coinName) { + if (_owner.equals(Context.getAddress())) { + Balance balance = new Balance(BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO); + balance.setRefundable(feeBalances.getOrDefault(_coinName, BigInteger.ZERO)); + return balance; + } else { + return getBalance(_coinName, _owner); + } + } + + @External + public void reclaim(String _coinName, BigInteger _value) { + require(_value.compareTo(BigInteger.ZERO) > 0, "_value must be positive"); + + Address owner = Context.getCaller(); + Balance balance = getBalance(_coinName, owner); + require(balance.getRefundable().compareTo(_value) > -1, "invalid value"); + balance.setRefundable(balance.getRefundable().subtract(_value)); + setBalance(_coinName, owner, balance); + + if (name.equals(_coinName)) { + Context.transfer(owner, _value); + } else { + transfer(irc2, owner, _value, "Reclaim: transferring to Caller".getBytes()); + } + } + + @External + public void tokenFallback(Address from, BigInteger value, @Optional byte[] data) { + require(value.compareTo(BigInteger.ZERO) > 0, "Invalid Amount"); + require(Context.getCaller() == irc2, "Invalid Token Address"); + require(tokenName != null && !tokenName.equals(""), "Token not registered"); + deposit(tokenName, from, value); + } + + @Payable + @External + public void transferNativeCoin(String _to) { + BigInteger value = Context.getValue(); + require(value != null && value.compareTo(BigInteger.ZERO) > 0, "Invalid amount"); + sendRequest(Context.getCaller(), BTPAddress.valueOf(_to), List.of(name), List.of(value)); + } + + @External + public void transfer(String _coinName, BigInteger _value, String _to) { + require(_value != null && _value.compareTo(BigInteger.ZERO) > 0, "Invalid amount"); + require(!name.equals(_coinName) && isRegistered(_coinName), "Not supported Token"); + + Address owner = Context.getCaller(); + Balance balance = getBalance(_coinName, owner); + require(balance.getUsable().compareTo(_value) >= 0, "Overdrawn: Insufficient Balance"); + sendRequest(owner, BTPAddress.valueOf(_to), List.of(_coinName), List.of(_value)); + } + + @EventLog(indexed = 1) + public void TransferStart(Address _from, String _to, BigInteger _sn, byte[] _assetDetails) { + } + + @EventLog(indexed = 1) + public void TransferEnd(Address _from, BigInteger _sn, BigInteger _code, byte[] _msg) { + } + + @EventLog(indexed = 1) + public void UnknownResponse(String _from, BigInteger _sn) { + } + + @External(readonly = true) + public TransferTransaction getTransaction(BigInteger _sn) { + return transactions.get(_sn); + } + + private void sendRequest(Address owner, BTPAddress to, List coinNames, List amounts) { + logger.println("sendRequest", "begin"); + NCSProperties properties = getProperties(); + + BigInteger feeRatio = properties.getFeeRatio(); + if (owner.equals(Context.getAddress())) { + feeRatio = BigInteger.ZERO; + } + int len = coinNames.size(); + AssetTransferDetail[] assetTransferDetails = new AssetTransferDetail[len]; + Asset[] assets = new Asset[len]; + for (int i = 0; i < len; i++) { + String coinName = coinNames.get(i); + BigInteger amount = amounts.get(i); + AssetTransferDetail assetTransferDetail = newAssetTransferDetail(coinName, amount, feeRatio); + lock(coinName, owner, amount); + assetTransferDetails[i] = assetTransferDetail; + assets[i] = new Asset(assetTransferDetail); + } + + TransferRequest request = new TransferRequest(); + request.setFrom(owner.toString()); + request.setTo(to.account()); + request.setAssets(assets); + + TransferTransaction transaction = new TransferTransaction(); + transaction.setFrom(owner.toString()); + transaction.setTo(to.toString()); + transaction.setAssets(assetTransferDetails); + + BigInteger sn = properties.getSn().add(BigInteger.ONE); + properties.setSn(sn); + setProperties(properties); + transactions.set(sn, transaction); + + sendMessage(to.net(), NCSMessage.REQUEST_COIN_TRANSFER, sn, request.toBytes()); + TransferStart(owner, to.toString(), sn, encode(assetTransferDetails)); + logger.println("sendRequest", "end"); + } + + static byte[] encode(AssetTransferDetail[] assetTransferDetails) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(assetTransferDetails.length); + for (AssetTransferDetail v : assetTransferDetails) { + //writer.write(v); + AssetTransferDetail.writeObject(writer, v); + } + writer.end(); + return writer.toByteArray(); + } + + private void sendMessage(String to, int serviceType, BigInteger sn, byte[] data) { + logger.println("sendMessage", "begin"); + NCSMessage message = new NCSMessage(); + message.setServiceType(serviceType); + message.setData(data); + Context.call(bmc, "sendMessage", to, SERVICE, sn, message.toBytes()); + logger.println("sendMessage", "end"); + } + + private void responseSuccess(String to, BigInteger sn) { + TransferResponse response = new TransferResponse(); + response.setCode(TransferResponse.RC_OK); + response.setMessage(TransferResponse.OK_MSG); + sendMessage(to, NCSMessage.REPONSE_HANDLE_SERVICE, sn, response.toBytes()); + } + + private void responseError(String to, BigInteger sn, String message) { + TransferResponse response = new TransferResponse(); + response.setCode(TransferResponse.RC_ERR); + response.setMessage(message); + sendMessage(to, NCSMessage.REPONSE_HANDLE_SERVICE, sn, response.toBytes()); + } + + @External + public void handleBTPMessage(String _from, String _svc, BigInteger _sn, byte[] _msg) { + require(Context.getCaller().equals(bmc), "Only BMC"); + + NCSMessage message = NCSMessage.fromBytes(_msg); + int serviceType = message.getServiceType(); + if (serviceType == NCSMessage.REQUEST_COIN_TRANSFER) { + TransferRequest request = TransferRequest.fromBytes(message.getData()); + handleRequest(request, _from, _sn); + } else if (serviceType == NCSMessage.REPONSE_HANDLE_SERVICE) { + TransferResponse response = TransferResponse.fromBytes(message.getData()); + handleResponse(_sn, response); + } else if (serviceType == NCSMessage.UNKNOWN_TYPE) { + // If receiving a RES_UNKNOWN_TYPE, ignore this message + // or re-send another correct message + UnknownResponse(_from, _sn); + } else { + // If none of those types above, BSH responds a message of RES_UNKNOWN_TYPE + TransferResponse response = new TransferResponse(); + response.setCode(TransferResponse.RC_ERR); + response.setMessage(TransferResponse.ERR_MSG_UNKNOWN_TYPE); + sendMessage(_from, NCSMessage.UNKNOWN_TYPE, _sn, response.toBytes()); + } + } + + @External + public void handleBTPError(String _src, String _svc, BigInteger _sn, long _code, String _msg) { + require(Context.getCaller().equals(bmc), "Only BMC"); + TransferResponse response = new TransferResponse(); + response.setCode(TransferResponse.RC_ERR); + response.setMessage("BTPError [code:" + _code + ",msg:" + _msg); + handleResponse(_sn, response); + } + + @External + public void handleFeeGathering(String _fa, String _svc) { + require(Context.getCaller().equals(bmc), "Only BMC"); + BTPAddress from = BTPAddress.valueOf(_fa); + Address owner = Context.getAddress(); + + List coinNames = new ArrayList<>(); + List feeAmounts = new ArrayList<>(); + for (String coinName : coinNames()) { + BigInteger feeAmount = clearFee(coinName); + if (feeAmount.compareTo(BigInteger.ZERO) > 0) { + coinNames.add(coinName); + feeAmounts.add(feeAmount); + } + } + + if (coinNames.size() > 0) { + if (from.net().equals(net)) { + Address fa = Address.fromString(from.account()); + int idx = coinNames.indexOf(name); + if (idx >= 0) { + coinNames.remove(idx); + BigInteger feeAmount = feeAmounts.remove(idx); + Context.transfer(fa, feeAmount); + } + _transferBatch(fa, ArrayUtil.toStringArray(coinNames), ArrayUtil.toBigIntegerArray(feeAmounts)); + } else { + sendRequest(owner, from, coinNames, feeAmounts); + } + } + } + + private void _transferBatch(Address to, String[] coinNames, BigInteger[] amounts) { + logger.println("transferBatch", to, StringUtil.toString(coinNames), StringUtil.toString(amounts)); + for (int i = 0; i < coinNames.length; i++) { + _transfer(irc2, to, amounts[i]); + } + } + + private Balance getBalance(String coinName, Address owner) { + Balance balance = balances.at(coinName).get(owner); + if (balance == null) { + balance = new Balance(BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO); + return balance; + } + Balance newBal = new Balance(balance.getUsable(), balance.getLocked(), balance.getRefundable()); + return newBal; + } + + private void setBalance(String coinName, Address owner, Balance balance) { + Balance newBalance = new Balance(balance.getUsable(), balance.getLocked(), balance.getRefundable()); + balances.at(coinName).set(owner, newBalance); + } + + private void lock(String coinName, Address owner, BigInteger value) { + logger.println("lock", "coinName:", coinName, "owner:", owner, "value:", value); + Balance balance = getBalance(coinName, owner); + balance.setUsable(balance.getUsable().subtract(value)); + balance.setLocked(balance.getLocked().add(value)); + setBalance(coinName, owner, balance); + } + + private void unlock(String coinName, Address owner, BigInteger value) { + logger.println("unlock", "coinName:", coinName, "owner:", owner, "value:", value); + Balance balance = getBalance(coinName, owner); + balance.setLocked(balance.getLocked().subtract(value)); + setBalance(coinName, owner, balance); + } + + private void refund(String coinName, Address owner, BigInteger value) { + logger.println("refund", "coinName:", coinName, "owner:", owner, "value:", value); + //unlock and add refundable + Balance balance = getBalance(coinName, owner); + balance.setLocked(balance.getLocked().subtract(value)); + if (!owner.equals(Context.getAddress())) { + balance.setRefundable(balance.getRefundable().add(value)); + } + setBalance(coinName, owner, balance); + } + + private void deposit(String coinName, Address owner, BigInteger value) { + logger.println("deposit", "coinName:", coinName, "owner:", owner, "value:", value); + Balance balance = getBalance(coinName, owner); + balance.setUsable(balance.getUsable().add(value)); + setBalance(coinName, owner, balance); + } + + private void addFee(String coinName, BigInteger amount) { + BigInteger fee = feeBalances.getOrDefault(coinName, BigInteger.ZERO); + feeBalances.set(coinName, fee.add(amount)); + } + + private BigInteger clearFee(String coinName) { + BigInteger fee = feeBalances.getOrDefault(coinName, BigInteger.ZERO); + if (fee.compareTo(BigInteger.ZERO) > 0) { + feeBalances.set(coinName, BigInteger.ZERO); + } + return fee; + } + + private void handleRequest(TransferRequest request, String from, BigInteger sn) { + logger.println("handleRequest", "begin", "sn:", sn); + Address to; + try { + to = Address.fromString(request.getTo()); + } catch (IllegalArgumentException | NullPointerException e) { + throw NCSException.unknown(e.getMessage()); + } + + BigInteger nativeCoinTransferAmount = null; + Asset[] assets = request.getAssets(); + for (Asset asset : assets) { + String coinName = asset.getCoinName(); + BigInteger amount = asset.getAmount(); + if (amount == null || amount.compareTo(BigInteger.ZERO) < 1) { + throw NCSException.unknown("Amount must be positive value"); + } + + if (tokenName.equals(coinName)) { + _transfer(irc2, to, amount); + } else if (name.equals(coinName)) { + nativeCoinTransferAmount = amount; + } else { + throw NCSException.unknown("Invalid Token"); + } + } + + if (nativeCoinTransferAmount != null) { + try { + Context.transfer(to, nativeCoinTransferAmount); + } catch (Exception e) { + throw NCSException.unknown("fail to transfer err:" + e.getMessage()); + } + } + + logger.println("handleRequest", "responseSuccess"); + responseSuccess(from, sn); + logger.println("handleRequest", "end"); + } + + private void handleResponse(BigInteger sn, TransferResponse response) { + logger.println("handleResponse", "begin", "sn:", sn); + TransferTransaction transaction = transactions.get(sn); + // ignore when not exists pending request + if (transaction != null) { + BigInteger code = response.getCode(); + Address owner = Address.fromString(transaction.getFrom()); + AssetTransferDetail[] assets = transaction.getAssets(); + + logger.println("handleResponse", "code:", code); + if (TransferResponse.RC_OK.equals(code)) { + for (AssetTransferDetail asset : assets) { + String coinName = asset.getCoinName(); + BigInteger amount = asset.getAmount(); + BigInteger fee = asset.getFee(); + BigInteger locked = amount.add(fee); + boolean isNativeCoin = name.equals(coinName); + if (isNativeCoin || tokenName.equals(coinName)) { + unlock(coinName, owner, locked); + addFee(coinName, fee); + if (!isNativeCoin) { + _burn(irc2, amount); + } + } else { + //This should not happen + throw NCSException.unknown("invalid transaction, invalid coinName"); + } + } + } else { + for (AssetTransferDetail asset : assets) { + String coinName = asset.getCoinName(); + BigInteger amount = asset.getAmount(); + BigInteger fee = asset.getFee(); + BigInteger locked = amount.add(fee); + boolean isNativeCoin = name.equals(coinName); + if (isNativeCoin || tokenName.equals(coinName)) { + refund(coinName, owner, locked); + } else { + //This should not happen + throw NCSException.unknown("invalid transaction, invalid coinName"); + } + } + } + + transactions.set(sn, null); + TransferEnd(owner, sn, code, response.getMessage() != null ? response.getMessage().getBytes() : null); + } + logger.println("handleResponse", "end"); + } + + @External + public void setFeeRatio(BigInteger _feeNumerator) { + requireOwnerAccess(); + require(_feeNumerator.compareTo(BigInteger.ONE) >= 0 && + _feeNumerator.compareTo(FEE_DENOMINATOR) < 0, + "The feeNumetator should be less than FEE_DEMONINATOR and greater than 1"); + NCSProperties properties = getProperties(); + properties.setFeeRatio(_feeNumerator); + setProperties(properties); + } + + @External(readonly = true) + public BigInteger feeRatio() { + NCSProperties properties = getProperties(); + return properties.getFeeRatio(); + } + + private AssetTransferDetail newAssetTransferDetail(String coinName, BigInteger amount, BigInteger feeRatio) { + logger.println("newAssetTransferDetail", "begin"); + BigInteger fee = amount.multiply(feeRatio).divide(FEE_DENOMINATOR); + if (feeRatio.compareTo(BigInteger.ZERO) > 0 && fee.compareTo(BigInteger.ZERO) == 0) { + fee = BigInteger.ONE; + } + BigInteger transferAmount = amount.subtract(fee); + logger.println("newAssetTransferDetail", "amount:", amount, "fee:", fee); + if (transferAmount.compareTo(BigInteger.ZERO) < 1) { + throw NCSException.unknown("not enough value"); + } + AssetTransferDetail asset = new AssetTransferDetail(); + asset.setCoinName(coinName); + asset.setAmount(transferAmount); + asset.setFee(fee); + logger.println("newAssetTransferDetail", "end"); + return asset; + } + + private void _transfer(Address token, Address to, BigInteger amount) { + logger.println("transfer", to, amount); + try { + transfer(token, to, amount, "transfer to Receiver".getBytes()); + } catch (UserRevertedException e) { + logger.println("transfer", "code:", e.getCode(), "msg:", e.getMessage()); + throw NCSException.irc31Reverted("code:" + e.getCode() + "msg:" + e.getMessage()); + } catch (IllegalArgumentException | RevertedException e) { + logger.println("transfer", "Exception:", e.toString()); + throw NCSException.irc31Failure("Exception:" + e); + } + } + + private void _burn(Address token, BigInteger amount) { + logger.println("burn", ZERO_ADDRESS, amount); + try { + transfer(token, ZERO_ADDRESS, amount, "Burn Transfer to Zero Address".getBytes()); + } catch (UserRevertedException e) { + logger.println("burn", "code:", e.getCode(), "msg:", e.getMessage()); + throw NCSException.irc31Reverted("code:" + e.getCode() + "msg:" + e.getMessage()); + } catch (IllegalArgumentException | RevertedException e) { + logger.println("burn", "Exception:", e.toString()); + throw NCSException.irc31Failure("Exception:" + e); + } + } + + + /* Delegate OwnerManager */ + private void requireOwnerAccess() { + if (!ownerManager.isOwner(Context.getCaller())) { + throw NCSException.unauthorized("require owner access"); + } + } + + @External + public void addOwner(Address _addr) { + try { + ownerManager.addOwner(_addr); + } catch (IllegalStateException e) { + throw NCSException.unauthorized(e.getMessage()); + } catch (IllegalArgumentException e) { + throw NCSException.unknown(e.getMessage()); + } + } + + @External + public void removeOwner(Address _addr) { + try { + ownerManager.removeOwner(_addr); + } catch (IllegalStateException e) { + throw NCSException.unauthorized(e.getMessage()); + } catch (IllegalArgumentException e) { + throw NCSException.unknown(e.getMessage()); + } + } + + @External(readonly = true) + public Address[] getOwners() { + return ownerManager.getOwners(); + } + + @External(readonly = true) + public boolean isOwner(Address _addr) { + return ownerManager.isOwner(_addr); + } + + public void transfer(Address tokenAddr, Address _to, BigInteger _value, byte[] _data) { + Context.call(tokenAddr, "transfer", _to, _value, _data); + } + +} \ No newline at end of file diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/TransferAsset.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/TransferAsset.java new file mode 100644 index 00000000..73c9da60 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/TransferAsset.java @@ -0,0 +1,74 @@ +package foundation.icon.btp.nativecoinIRC2; + +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.util.List; + +public class TransferAsset { + private String from; + private String to; + private List assets; + private List test; + + public TransferAsset(String from, String to, List assets) { + this.from = from; + this.to = to; + this.assets = assets; + } + + + public static void writeObject(ObjectWriter w, TransferAsset v) { + w.beginList(3); + w.write(v.getFrom()); + w.write(v.getTo()); + w.beginList(v.getAssets().size()); + for (int i = 0; i < v.getAssets().size(); i++) { + Asset.writeObject(w, v.getAssets().get(i)); + //w.write( v.getAsset()); + } + w.end(); + w.end(); + } + + public static TransferAsset readObject(ObjectReader r) { + r.beginList(); + String _from = r.readString(); + String _to = r.readString(); + List assets = new ArrayList<>(); + r.beginList(); + while (r.hasNext()) { + Asset _asset = Asset.readObject(r); + assets.add(_asset); + } + r.end(); + r.end(); + TransferAsset result = new TransferAsset(_from, _to, assets); + return result; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + public List getAssets() { + return assets; + } + + public void setAssets(List assets) { + this.assets = assets; + } +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/TransferRequest.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/TransferRequest.java new file mode 100644 index 00000000..7e9564e4 --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/TransferRequest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2021 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 foundation.icon.btp.nativecoinIRC2; + +import foundation.icon.score.util.StringUtil; +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.util.List; + +public class TransferRequest { + private String from; + private String to; + private Asset[] assets; + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public Asset[] getAssets() { + return assets; + } + + public void setAssets(Asset[] assets) { + this.assets = assets; + } + + public void setTo(String to) { + this.to = to; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TransferRequest{"); + sb.append("from='").append(from).append('\''); + sb.append(", to='").append(to).append('\''); + sb.append(", assets=").append(StringUtil.toString(assets)); + sb.append('}'); + return sb.toString(); + } + + public static void writeObject(ObjectWriter writer, TransferRequest obj) { + obj.writeObject(writer); + } + + public static TransferRequest readObject(ObjectReader reader) { + TransferRequest obj = new TransferRequest(); + reader.beginList(); + obj.setFrom(reader.readNullable(String.class)); + obj.setTo(reader.readNullable(String.class)); + if (reader.beginNullableList()) { + Asset[] assets = null; + List assetsList = new ArrayList<>(); + while(reader.hasNext()) { +// assetsList.add(reader.readNullable(Asset.class)); + assetsList.add(Asset.readObject(reader)); + } + assets = new Asset[assetsList.size()]; + for(int i=0; i balances = Context.newDictDB("balances", BigInteger.class); + + public IRC2Basic(String _name, String _symbol, int _decimals, BigInteger _initialSupply) { + this.name = _name; + this.symbol = _symbol; + this.decimals = _decimals; + + // decimals must be larger than 0 and less than 21 + Context.require(this.decimals >= 0); + Context.require(this.decimals <= 21); + Context.require(_initialSupply.compareTo(BigInteger.ZERO) >= 0); + _mint(Context.getCaller(), _initialSupply.multiply(pow10(_decimals))); + } + + private static BigInteger pow10(int exponent) { + BigInteger result = BigInteger.ONE; + for (int i = 0; i < exponent; i++) { + result = result.multiply(BigInteger.TEN); + } + return result; + } + + @External(readonly=true) + public String name() { + return name; + } + + @External(readonly=true) + public String symbol() { + return symbol; + } + + @External(readonly=true) + public int decimals() { + return decimals; + } + + @External(readonly=true) + public BigInteger totalSupply() { + return totalSupply; + } + + @External(readonly=true) + public BigInteger balanceOf(Address _owner) { + return safeGetBalance(_owner); + } + + @External + public void transfer(Address _to, BigInteger _value, @Optional byte[] _data) { + Address _from = Context.getCaller(); + + // check some basic requirements + Context.require(_value.compareTo(BigInteger.ZERO) >= 0); + Context.require(safeGetBalance(_from).compareTo(_value) >= 0); + + // adjust the balances + safeSetBalance(_from, safeGetBalance(_from).subtract(_value)); + safeSetBalance(_to, safeGetBalance(_to).add(_value)); + + // if the recipient is SCORE, call 'tokenFallback' to handle further operation + byte[] dataBytes = (_data == null) ? new byte[0] : _data; + if (_to.isContract()) { + Context.call(_to, "tokenFallback", _from, _value, dataBytes); + } + + // emit Transfer event + Transfer(_from, _to, _value, dataBytes); + } + + /** + * Creates `amount` tokens and assigns them to `owner`, increasing the total supply. + */ + protected void _mint(Address owner, BigInteger amount) { + Context.require(!ZERO_ADDRESS.equals(owner)); + Context.require(amount.compareTo(BigInteger.ZERO) >= 0); + + this.totalSupply = this.totalSupply.add(amount); + safeSetBalance(owner, safeGetBalance(owner).add(amount)); + Transfer(ZERO_ADDRESS, owner, amount, "mint".getBytes()); + } + + /** + * Destroys `amount` tokens from `owner`, reducing the total supply. + */ + protected void _burn(Address owner, BigInteger amount) { + Context.require(!ZERO_ADDRESS.equals(owner)); + Context.require(amount.compareTo(BigInteger.ZERO) >= 0); + Context.require(safeGetBalance(owner).compareTo(amount) >= 0); + + safeSetBalance(owner, safeGetBalance(owner).subtract(amount)); + this.totalSupply = this.totalSupply.subtract(amount); + Transfer(owner, ZERO_ADDRESS, amount, "burn".getBytes()); + } + + private BigInteger safeGetBalance(Address owner) { + return balances.getOrDefault(owner, BigInteger.ZERO); + } + + private void safeSetBalance(Address owner, BigInteger amount) { + balances.set(owner, amount); + } + + @EventLog(indexed=3) + public void Transfer(Address _from, Address _to, BigInteger _value, byte[] _data) {} +} diff --git a/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/irc2/IRC2ScoreInterface.java b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/irc2/IRC2ScoreInterface.java new file mode 100644 index 00000000..0531089d --- /dev/null +++ b/javascore/nativecoinIRC2/src/main/java/foundation/icon/btp/nativecoinIRC2/irc2/IRC2ScoreInterface.java @@ -0,0 +1,52 @@ +package foundation.icon.btp.nativecoinIRC2.irc2; + +import score.Address; +import score.Context; + +import java.math.BigInteger; + +public class IRC2ScoreInterface { + protected final Address address; + + public IRC2ScoreInterface(Address address) { + this.address = address; + } + + public Address _address() { + return this.address; + } + + public Address _check() { + return this.address; + } + + public BigInteger _balanceOf(Address _owner) { + return (BigInteger)Context.call( this.address, "balanceOf", _owner); + //return BigInteger.valueOf(1); + } + + public void transfer(Address _to, BigInteger _value, byte[] _data) { + Context.call(this.address, "transfer", _to, _value, _data); + } + + public void _Transfer(Address _from, Address _to, BigInteger _value, byte[] _data) { + throw new RuntimeException("not supported EventLog method"); + } + + public String _name() { + return (String)Context.call(this.address, "name"); + } + + public String _symbol() { + return (String)Context.call(this.address, "symbol"); + } + + public int _decimals() { + return (Integer)Context.call(this.address, "decimals"); + } + + public BigInteger _totalSupply() { + return Context.call(BigInteger.class, this.address, "totalSupply"); + } + +} diff --git a/javascore/nativecoinIRC2/src/test/java/foundation/icon/btp/nativecoinIRC2/IRC2NativeServiceHandler.java b/javascore/nativecoinIRC2/src/test/java/foundation/icon/btp/nativecoinIRC2/IRC2NativeServiceHandler.java new file mode 100644 index 00000000..41707e1c --- /dev/null +++ b/javascore/nativecoinIRC2/src/test/java/foundation/icon/btp/nativecoinIRC2/IRC2NativeServiceHandler.java @@ -0,0 +1,117 @@ +package foundation.icon.btp.nativecoinIRC2; + + +import com.iconloop.testsvc.Account; +import com.iconloop.testsvc.Score; +import com.iconloop.testsvc.ServiceManager; +import com.iconloop.testsvc.TestBase; +import foundation.icon.btp.nativecoinIRC2.irc2.IRC2Basic; +import org.junit.jupiter.api.*; +import score.ByteArrayObjectWriter; +import score.Context; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; + +import static java.math.BigInteger.TEN; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class IRC2NativeServiceHandler extends TestBase { + final static String RLPn = "RLPn"; + final static String _svc = "nativecoin"; + final static String _net = "icon"; + final static String tokenName = "MOVR"; + final static String symbol = "MOVR"; + final static int decimals = 18; + final static BigInteger transferAmount = new BigInteger("10000000000000000000"); + private static final BigInteger initialSupply = BigInteger.valueOf(2000); + private static final BigInteger totalSupply = initialSupply.multiply(TEN.pow(decimals)); + private static final int REQUEST_TOKEN_TRANSFER = 0; + private static final int RESPONSE_HANDLE_SERVICE = 2; + private static final ServiceManager sm = getServiceManager(); + private static Account[] owners; + private static Score bsh; + private static Score token; + private static Score bmc; + + @BeforeAll + public static void setup() throws Exception { + // setup accounts and deploy + owners = new Account[5]; + for (int i = 0; i < owners.length; i++) { + owners[i] = sm.createAccount(100); + } + + bmc = sm.deploy(owners[0], BMCMock.class, "0x03.icon"); + token = sm.deploy(owners[0], IRC2Basic.class, tokenName, symbol, decimals, initialSupply); + bsh = sm.deploy(owners[0], NativeCoinService.class, bmc.getAddress(), token.getAddress(), "ICX", "MOVR"); + + BigInteger balance = (BigInteger) token.call("balanceOf", owners[0].getAddress()); + assertEquals(totalSupply, balance); + } + + + /** + * Secnario#: Transfer IRC2 tokens from Token contract to BSH via fallback - success + */ + @Order(1) + @Test + public void scenario1() { + String _to, _from; + _from = _to = "btp://0x97.bsc/0xa36a32c114ee13090e35cb086459a690f5c1f8e8"; + + bmc.invoke(owners[0], "addService", _svc, bsh.getAddress()); + //Add some enough token balance for BSH + Balance balanceBefore = ((Balance) bsh.call("balanceOf", owners[0].getAddress(), tokenName)); + BigInteger bshTokenBalanceBefore = (BigInteger) token.call("balanceOf", bsh.getAddress()); + token.invoke(owners[0], "transfer", bsh.getAddress(), transferAmount.multiply(BigInteger.valueOf(10)), new byte[0]); + BigInteger bshTokenBalanceAfter = (BigInteger) token.call("balanceOf", bsh.getAddress()); + + //Initiate a transfer and check for locked balance + bsh.invoke(owners[0], "transfer", tokenName, transferAmount, _to); + Balance balanceAfterTransfer = ((Balance) bsh.call("balanceOf", owners[0].getAddress(), tokenName)); + assertEquals(balanceBefore.getLocked().add(transferAmount), balanceAfterTransfer.getLocked()); + + //BSH receives HandleBTPMessgae for token transfer request + BigInteger userBalanceBefore = (BigInteger) token.call("balanceOf", owners[1].getAddress()); + bmc.invoke(owners[0], "handleBTPMessage", _from, _svc, BigInteger.ZERO, handleBTPRequestBtpMsg(_from, owners[1].getAddress().toString())); + BigInteger userBalanceAfter = (BigInteger) token.call("balanceOf", owners[1].getAddress()); + assertEquals(userBalanceBefore.add(transferAmount), userBalanceAfter); + + //Send Success response after successfull transfer + Balance balanceBeforeSuccess = (Balance) bsh.call("balanceOf", owners[0].getAddress(), tokenName); + bmc.invoke(owners[0], "handleBTPMessage", _from, _svc, BigInteger.ONE, handleBTPResponseBtpMsg(0, "Transfer Success")); + Balance balanceAfterSuccess = (Balance) bsh.call("balanceOf", owners[0].getAddress(), tokenName); + assertEquals(balanceAfterSuccess.getLocked().add(transferAmount), balanceBeforeSuccess.getLocked()); + } + + public byte[] handleBTPRequestBtpMsg(String from, String to) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter(RLPn); + writer.beginList(2); + writer.write(REQUEST_TOKEN_TRANSFER);//ActionType + List assets = new ArrayList(); + assets.add(new Asset(tokenName, transferAmount)); + ByteArrayObjectWriter writerTa = Context.newByteArrayObjectWriter(RLPn); + TransferAsset _ta = new TransferAsset(from, to, assets); + TransferAsset.writeObject(writerTa, _ta); + writer.write(writerTa.toByteArray()); + writer.end(); + return writer.toByteArray(); + } + + public byte[] handleBTPResponseBtpMsg(int code, String msg) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter(RLPn); + writer.beginList(2); + writer.write(RESPONSE_HANDLE_SERVICE);//ActionType + ByteArrayObjectWriter writerRespMsg = Context.newByteArrayObjectWriter(RLPn); + writerRespMsg.beginList(2); + writerRespMsg.write(code);//Code + writerRespMsg.write(msg);//Msg + writerRespMsg.end(); + writer.write(writerRespMsg.toByteArray()); + writer.end(); + return writer.toByteArray(); + } +} diff --git a/javascore/settings.gradle b/javascore/settings.gradle index 2887d619..c89a2fd1 100644 --- a/javascore/settings.gradle +++ b/javascore/settings.gradle @@ -6,4 +6,5 @@ include ( 'bmc', 'bmv:icon', 'nativecoin', + 'nativecoinIRC2', ) diff --git a/solidity/nativecoinERC20/README.md b/solidity/nativecoinERC20/README.md new file mode 100644 index 00000000..dde536fa --- /dev/null +++ b/solidity/nativecoinERC20/README.md @@ -0,0 +1,33 @@ +## Set up +Node >= 10.x +``` +$ node --version +v15.12.0 +``` +Install tools +``` +$ npm install --global yarn truffle@5.3.0 +``` +Install dependencies +``` +$ yarn +``` + +## Test +1. Run in a background process or seperate terminal window +``` +$ docker run --rm -d -p 9933:9933 -p 9944:9944 purestake/moonbeam:v0.9.2 --dev --ws-external --rpc-external +``` +2. Compile contracts +``` +$ yarn contract:compile +``` +3. Run unit and integration test +``` +$ yarn test +``` +- Run specific test +``` +$ yarn test:unit +$ yarn test:integration +``` diff --git a/solidity/nativecoinERC20/contracts/BSHCore.sol b/solidity/nativecoinERC20/contracts/BSHCore.sol new file mode 100644 index 00000000..2c17c745 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/BSHCore.sol @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; +import "./interfaces/IBSHPeriphery.sol"; +import "./interfaces/IBSHCore.sol"; +import "./libraries/String.sol"; +import "./libraries/Types.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +/** + @title BSHCore contract + @dev This contract is used to handle coin transferring service + Note: The coin of following contract can be: + Native Coin : The native coin of this chain + Wrapped Native Coin : A tokenized ERC1155 version of another native coin like ICX +*/ +contract BSHCore is + Initializable, + IBSHCore, + ERC20Upgradeable, + ReentrancyGuardUpgradeable +{ + using SafeMathUpgradeable for uint256; + using String for string; + event SetOwnership(address indexed promoter, address indexed newOwner); + event RemoveOwnership(address indexed remover, address indexed formerOwner); + + modifier onlyOwner() { + require(owners[msg.sender] == true, "Unauthorized"); + _; + } + + modifier onlyBSHPeriphery() { + require(msg.sender == address(bshPeriphery), "Unauthorized"); + _; + } + + uint256 private constant FEE_DENOMINATOR = 10**4; + uint256 public feeNumerator; + uint256 public fixedFee; + uint256 private constant RC_OK = 0; + uint256 private constant RC_ERR = 1; + + IBSHPeriphery internal bshPeriphery; + + address[] private listOfOwners; + uint256[] private chargedAmounts; // a list of amounts have been charged so far (use this when Fee Gathering occurs) + string[] internal coinsName; // a string array stores names of supported coins + string[] private chargedCoins; // a list of coins' names have been charged so far (use this when Fee Gathering occurs) + + mapping(address => bool) private owners; + mapping(string => uint256) internal aggregationFee; // storing Aggregation Fee in state mapping variable. + mapping(address => mapping(string => Types.Balance)) internal balances; + mapping(string => uint256) private coins; // a list of all supported coins + + function initialize( + string calldata _nativeCoinName, + uint256 _feeNumerator, + uint256 _fixedFee, + string calldata _tokenName, + string calldata _tokenSymbol, + uint256 _initialSupply + ) public initializer { + __ERC20_init(_tokenName, _tokenSymbol); + owners[msg.sender] = true; + listOfOwners.push(msg.sender); + emit SetOwnership(address(0), msg.sender); + + coins[_nativeCoinName] = 0; + coins[_tokenName] = 1; + feeNumerator = _feeNumerator; + fixedFee = _fixedFee; + coinsName.push(_nativeCoinName); + coinsName.push(_tokenName); + _mint(msg.sender, _initialSupply); + } + + /** + @notice Adding another Onwer. + @dev Caller must be an Onwer of BTP network + @param _owner Address of a new Onwer. + */ + function addOwner(address _owner) external override onlyOwner { + require(owners[_owner] == false, "ExistedOwner"); + owners[_owner] = true; + listOfOwners.push(_owner); + emit SetOwnership(msg.sender, _owner); + } + + /** + @notice Removing an existing Owner. + @dev Caller must be an Owner of BTP network + @dev If only one Owner left, unable to remove the last Owner + @param _owner Address of an Owner to be removed. + */ + function removeOwner(address _owner) external override onlyOwner { + require(listOfOwners.length > 1, "Unable to remove last Owner"); + require(owners[_owner] == true, "Removing Owner not found"); + delete owners[_owner]; + _remove(_owner); + emit RemoveOwnership(msg.sender, _owner); + } + + function _remove(address _addr) internal { + for (uint256 i = 0; i < listOfOwners.length; i++) + if (listOfOwners[i] == _addr) { + listOfOwners[i] = listOfOwners[listOfOwners.length - 1]; + listOfOwners.pop(); + break; + } + } + + /** + @notice Checking whether one specific address has Owner role. + @dev Caller can be ANY + @param _owner Address needs to verify. + */ + function isOwner(address _owner) external view override returns (bool) { + return owners[_owner]; + } + + /** + @notice Get a list of current Owners + @dev Caller can be ANY + @return An array of addresses of current Owners + */ + function getOwners() external view override returns (address[] memory) { + return listOfOwners; + } + + /** + @notice update BSH Periphery address. + @dev Caller must be an Owner of this contract + _bshPeriphery Must be different with the existing one. + @param _bshPeriphery BSHPeriphery contract address. + */ + function updateBSHPeriphery(address _bshPeriphery) + external + override + onlyOwner + { + require(_bshPeriphery != address(0), "InvalidSetting"); + if (address(bshPeriphery) != address(0)) { + require( + bshPeriphery.hasPendingRequest() == false, + "HasPendingRequest" + ); + } + bshPeriphery = IBSHPeriphery(_bshPeriphery); + } + + /** + @notice set fee ratio. + @dev Caller must be an Owner of this contract + The transfer fee is calculated by feeNumerator/FEE_DEMONINATOR. + The feeNumetator should be less than FEE_DEMONINATOR + _feeNumerator is set to `10` in construction by default, which means the default fee ratio is 0.1%. + @param _feeNumerator the fee numerator + */ + function setFeeRatio(uint256 _feeNumerator) external override onlyOwner { + require(_feeNumerator <= FEE_DENOMINATOR, "InvalidSetting"); + feeNumerator = _feeNumerator; + } + + /** + @notice set Fixed Fee. + @dev Caller must be an Owner + @param _fixedFee A new value of Fixed Fee + */ + function setFixedFee(uint256 _fixedFee) external override onlyOwner { + require(_fixedFee > 0, "InvalidSetting"); + fixedFee = _fixedFee; + } + + /** + @notice Registers a wrapped coin and id number of a supporting coin. + @dev Caller must be an Owner of this contract + _name Must be different with the native coin name. + @dev '_id' of a wrapped coin is generated by using keccak256 + '_id' = 0 is fixed to assign to native coin + @param _name Coin name. + */ + function register(string calldata _name) external override onlyOwner { + require(coins[_name] == 0, "ExistToken"); + coins[_name] = uint256(keccak256(abi.encodePacked(_name))); + coinsName.push(_name); + } + + /** + @notice Return all supported coins names + @dev + @return _names An array of strings. + */ + function coinNames() + external + view + override + returns (string[] memory _names) + { + return coinsName; + } + + /** + @notice Check Validity of a _coinName + @dev Call by BSHPeriphery contract to validate a requested _coinName + @return _valid true of false + */ + function isValidCoin(string calldata _coinName) + external + view + override + returns (bool _valid) + { + return (coins[_coinName] != 0 || _coinName.compareTo(coinsName[0])); + } + + /** + @notice Return a usable/locked/refundable balance of an account based on coinName. + @return _usableBalance the balance that users are holding. + @return _lockedBalance when users transfer the coin, + it will be locked until getting the Service Message Response. + @return _refundableBalance refundable balance is the balance that will be refunded to users. + */ + function getBalanceOf(address _owner, string memory _coinName) + external + view + override + returns ( + uint256 _usableBalance, + uint256 _lockedBalance, + uint256 _refundableBalance + ) + { + if (_coinName.compareTo(coinsName[0])) { + return ( + address(_owner).balance, + balances[_owner][_coinName].lockedBalance, + balances[_owner][_coinName].refundableBalance + ); + } + return ( + this.balanceOf(_owner), + balances[_owner][_coinName].lockedBalance, + balances[_owner][_coinName].refundableBalance + ); + } + + /** + @notice Return a list accumulated Fees. + @dev only return the asset that has Asset's value greater than 0 + @return _accumulatedFees An array of Asset + */ + function getAccumulatedFees() + external + view + override + returns (Types.Asset[] memory _accumulatedFees) + { + _accumulatedFees = new Types.Asset[](coinsName.length); + for (uint256 i = 0; i < coinsName.length; i++) { + _accumulatedFees[i] = ( + Types.Asset(coinsName[i], aggregationFee[coinsName[i]]) + ); + } + return _accumulatedFees; + } + + /** + @notice Allow users to deposit `msg.value` native coin into a BSHCore contract. + @dev MUST specify msg.value + @param _to An address that a user expects to receive an amount of tokens. + */ + function transferNativeCoin(string calldata _to) external payable override { + // Aggregation Fee will be charged on BSH Contract + // A new charging fee has been proposed. `fixedFee` is introduced + // _chargeAmt = fixedFee + msg.value * feeNumerator / FEE_DENOMINATOR + // Thus, it's likely that _chargeAmt is always greater than 0 + // require(_chargeAmt > 0) can be omitted + // If msg.value less than _chargeAmt, it likely fails when calculating + // _amount = _value - _chargeAmt + uint256 _chargeAmt = msg + .value + .mul(feeNumerator) + .div(FEE_DENOMINATOR) + .add(fixedFee); + + // @dev msg.value is an amount request to transfer (include fee) + // Later on, it will be calculated a true amount that should be received at a destination + _sendServiceMessage( + msg.sender, + _to, + coinsName[0], + msg.value, + _chargeAmt + ); + } + + /** + @notice Allow users to deposit an amount of wrapped native coin `_coinName` from the `msg.sender` address into the BSHCore contract. + @dev Caller must set to approve that the wrapped tokens can be transferred out of the `msg.sender` account by BSHCore contract. + It MUST revert if the balance of the holder for token `_coinName` is lower than the `_value` sent. + @param _coinName A given name of a wrapped coin + @param _value An amount request to transfer from a Requester (include fee) + @param _to Target BTP address. + */ + function transfer( + string calldata _coinName, + uint256 _value, + string calldata _to + ) external override { + //TODO: check how to keep method name with overloading + require(this.isValidCoin(_coinName), "UnregisterCoin"); + // _chargeAmt = fixedFee + msg.value * feeNumerator / FEE_DENOMINATOR + // Thus, it's likely that _chargeAmt is always greater than 0 + // require(_chargeAmt > 0) can be omitted + // If _value less than _chargeAmt, it likely fails when calculating + // _amount = _value - _chargeAmt + uint256 _chargeAmt = _value.mul(feeNumerator).div(FEE_DENOMINATOR).add( + fixedFee + ); + // Transfer and Lock Token processes: + // BSHCore contract calls transferFrom() to transfer the Token from Caller's account (msg.sender) + // Before that, Caller must approve (setApproveForAll) to accept + // token being transfer out by an Operator + // If this requirement is failed, a transaction is reverted. + // After transferring token, BSHCore contract updates Caller's locked balance + // as a record of pending transfer transaction + // When a transaction is completed without any error on another chain, + // Locked Token amount (bind to an address of caller) will be reset/subtract, + // then emit a successful TransferEnd event as a notification + // Otherwise, the locked amount will also be updated + // but BSHCore contract will issue a refund to Caller before emitting an error TransferEnd event + this.transferFrom(msg.sender, address(this), _value); + // @dev _value is an amount request to transfer (include fee) + // Later on, it will be calculated a true amount that should be received at a destination + _sendServiceMessage(msg.sender, _to, _coinName, _value, _chargeAmt); + } + + /** + @notice This private function handles overlapping procedure before sending a service message to BSHPeriphery + @param _from An address of a Requester + @param _to BTP address of of Receiver on another chain + @param _coinName A given name of a requested coin + @param _value A requested amount to transfer from a Requester (include fee) + @param _chargeAmt An amount being charged for this request + */ + function _sendServiceMessage( + address _from, + string calldata _to, + string memory _coinName, + uint256 _value, + uint256 _chargeAmt + ) private { + // Lock this requested _value as a record of a pending transferring transaction + // @dev `_value` is a requested amount to transfer, from a Requester, including charged fee + // The true amount to receive at a destination receiver is calculated by + // _amounts[0] = _value.sub(_chargeAmt); + lockBalance(_from, _coinName, _value); + string[] memory _coins = new string[](1); + _coins[0] = _coinName; + uint256[] memory _amounts = new uint256[](1); + _amounts[0] = _value.sub(_chargeAmt); + uint256[] memory _fees = new uint256[](1); + _fees[0] = _chargeAmt; + + // @dev `_amounts` is a true amount to receive at a destination after deducting a charged fee + bshPeriphery.sendServiceMessage(_from, _to, _coins, _amounts, _fees); + } + + /** + @notice Reclaim the token's refundable balance by an owner. + @dev Caller must be an owner of coin + The amount to claim must be smaller or equal than refundable balance + @param _coinName A given name of coin + @param _value An amount of re-claiming tokens + */ + function reclaim(string calldata _coinName, uint256 _value) + external + override + nonReentrant + { + require( + balances[msg.sender][_coinName].refundableBalance >= _value, + "Imbalance" + ); + + balances[msg.sender][_coinName].refundableBalance = balances[ + msg.sender + ][_coinName].refundableBalance.sub(_value); + + this.refund(msg.sender, _coinName, _value); + } + + // Solidity does not allow using try_catch with interal/private function + // Thus, this function would be set as 'external` + // But, it has restriction. It should be called by this contract only + // In addition, there are only two functions calling this refund() + // + handleRequestService(): this function only called by BSHPeriphery + // + reclaim(): this function can be called by ANY + // In case of reentrancy attacks, the chance happenning on BSHPeriphery + // since it requires a request from BMC which requires verification fron BMV + // reclaim() has higher chance to have reentrancy attacks. + // So, it must be prevented by adding 'nonReentrant' + function refund( + address _to, + string calldata _coinName, + uint256 _value + ) external { + require(msg.sender == address(this), "Unauthorized"); + uint256 _id = coins[_coinName]; + if (_id == 0) { + paymentTransfer(payable(_to), _value); + } else { + this.transferFrom(address(this), _to, _value); + } + } + + function paymentTransfer(address payable _to, uint256 _amount) private { + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Payment transfer failed"); + } + + /** + @notice mint the wrapped coin. + @dev Caller must be an BSHPeriphery contract + Invalid _coinName will have an _id = 0. However, _id = 0 is also dedicated to Native Coin + Thus, BSHPeriphery will check a validity of a requested _coinName before calling + for the _coinName indicates with id = 0, it should send the Native Coin (Example: PRA) to user account + @param _to the account receive the minted coin + @param _coinName coin name + @param _value the minted amount + */ + function mint( + address _to, + string calldata _coinName, + uint256 _value + ) external override onlyBSHPeriphery { + uint256 _id = coins[_coinName]; + if (_id == 0) { + paymentTransfer(payable(_to), _value); + } else { + _mint(_to, _value); + } + } + + /** + @notice Handle a response of a requested service + @dev Caller must be an BSHPeriphery contract + @param _requester An address of originator of a requested service + @param _coinName A name of requested coin + @param _value An amount to receive on a destination chain + @param _fee An amount of charged fee + */ + function handleResponseService( + address _requester, + string calldata _coinName, + uint256 _value, + uint256 _fee, + uint256 _rspCode + ) external override onlyBSHPeriphery { + // Fee Gathering and Transfer Coin Request use the same method + // and both have the same response + // In case of Fee Gathering's response, `_requester` is this contract's address + // Thus, check that first + // -- If `_requester` is this contract's address, then check whethere response's code is RC_ERR + // In case of RC_ERR, adding back charged fees to `aggregationFee` state variable + // In case of RC_OK, ignore and return + // -- Otherwise, handle service's response as normal + if (_requester == address(this)) { + if (_rspCode == RC_ERR) { + aggregationFee[_coinName] = aggregationFee[_coinName].add( + _value + ); + } + return; + } + uint256 _amount = _value.add(_fee); + balances[_requester][_coinName].lockedBalance = balances[_requester][ + _coinName + ].lockedBalance.sub(_amount); + + // A new implementation has been proposed to prevent spam attacks + // In receiving error response, BSHCore refunds `_value`, not including `_fee`, back to Requestor + if (_rspCode == RC_ERR) { + try this.refund(_requester, _coinName, _value) {} catch { + balances[_requester][_coinName].refundableBalance = balances[ + _requester + ][_coinName].refundableBalance.add(_value); + } + } else if (_rspCode == RC_OK) { + uint256 _id = coins[_coinName]; + if (_id != 0) { + _burn(address(this), _value); + } + } + aggregationFee[_coinName] = aggregationFee[_coinName].add(_fee); + } + + /** + @notice Handle a request of Fee Gathering + Usage: Copy all charged fees to an array + @dev Caller must be an BSHPeriphery contract + */ + function transferFees(string calldata _fa) + external + override + onlyBSHPeriphery + { + // @dev Due to uncertainty in identifying a size of returning memory array + // and Solidity does not allow to use 'push' with memory array (only storage) + // thus, must use 'temp' storage state variable + for (uint256 i = 0; i < coinsName.length; i++) { + if (aggregationFee[coinsName[i]] != 0) { + chargedCoins.push(coinsName[i]); + chargedAmounts.push(aggregationFee[coinsName[i]]); + delete aggregationFee[coinsName[i]]; + } + } + bshPeriphery.sendServiceMessage( + address(this), + _fa, + chargedCoins, + chargedAmounts, + new uint256[](chargedCoins.length) // chargedFees is an array of 0 since this is a fee gathering request + ); + delete chargedCoins; + delete chargedAmounts; + } + + function lockBalance( + address _to, + string memory _coinName, + uint256 _value + ) private { + balances[_to][_coinName].lockedBalance = balances[_to][_coinName] + .lockedBalance + .add(_value); + } +} \ No newline at end of file diff --git a/solidity/nativecoinERC20/contracts/BSHPeriphery.sol b/solidity/nativecoinERC20/contracts/BSHPeriphery.sol new file mode 100644 index 00000000..78639287 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/BSHPeriphery.sol @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; +import "./interfaces/IBSHPeriphery.sol"; +import "./interfaces/IBSHCore.sol"; +import "./interfaces/IBMCPeriphery.sol"; +import "./libraries/Types.sol"; +import "./libraries/RLPEncodeStruct.sol"; +import "./libraries/RLPDecodeStruct.sol"; +import "./libraries/ParseAddress.sol"; +import "./libraries/String.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; + +/** + @title BSHPeriphery contract + @dev This contract is used to handle communications among BMCService and BSHCore contract + @dev OwnerUpgradeable has been removed. This contract does not have its own Owners + Instead, BSHCore manages ownership roles. + Thus, BSHPeriphery should call bshCore.isOwner() and pass an address for verification + in case of implementing restrictions, if needed, in the future. +*/ +contract BSHPeriphery is Initializable, IBSHPeriphery { + using RLPEncodeStruct for Types.TransferCoin; + using RLPEncodeStruct for Types.ServiceMessage; + using RLPEncodeStruct for Types.Response; + using RLPDecodeStruct for bytes; + using SafeMathUpgradeable for uint256; + using ParseAddress for address; + using ParseAddress for string; + using String for string; + using String for uint256; + + /** @notice Sends a receipt to user + The `_from` sender + The `_to` receiver. + The `_sn` sequence number of service message. + The `_assetDetails` a list of `_coinName` and `_value` + */ + event TransferStart( + address indexed _from, + string _to, + uint256 _sn, + Types.AssetTransferDetail[] _assetDetails + ); + + /** @notice Sends a final notification to a user + The `_from` sender + The `_sn` sequence number of service message. + The `_code` response code, i.e. RC_OK = 0, RC_ERR = 1 + The `_response` message of response if error + */ + event TransferEnd( + address indexed _from, + uint256 _sn, + uint256 _code, + string _response + ); + + /** @notice Notify that BSH contract has received unknown response + The `_from` sender + The `_sn` sequence number of service message + */ + event UnknownResponse(string _from, uint256 _sn); + + IBMCPeriphery private bmc; + IBSHCore internal bshCore; + mapping(uint256 => Types.PendingTransferCoin) public requests; // a list of transferring requests + string public serviceName; // BSH Service Name + + uint256 private constant RC_OK = 0; + uint256 private constant RC_ERR = 1; + uint256 private serialNo; // a counter of sequence number of service message + uint256 private numOfPendingRequests; + + modifier onlyBMC { + require(msg.sender == address(bmc), "Unauthorized"); + _; + } + + function initialize( + address _bmc, + address _bshCore, + string memory _serviceName + ) public initializer { + bmc = IBMCPeriphery(_bmc); + bshCore = IBSHCore(_bshCore); + serviceName = _serviceName; + } + + /** + @notice Check whether BSHPeriphery has any pending transferring requests + @return true or false + */ + function hasPendingRequest() external view override returns (bool) { + return numOfPendingRequests != 0; + } + + function sendServiceMessage( + address _from, + string memory _to, + string[] memory _coinNames, + uint256[] memory _values, + uint256[] memory _fees + ) external override { + // Send Service Message to BMC + // If '_to' address is an invalid BTP Address format + // VM throws an error and revert(). Thus, it does not need + // a try_catch at this point + (string memory _toNetwork, string memory _toAddress) = + _to.splitBTPAddress(); + Types.Asset[] memory _assets = new Types.Asset[](_coinNames.length); + Types.AssetTransferDetail[] memory _assetDetails = + new Types.AssetTransferDetail[](_coinNames.length); + for (uint256 i = 0; i < _coinNames.length; i++) { + _assets[i] = Types.Asset(_coinNames[i], _values[i]); + _assetDetails[i] = Types.AssetTransferDetail( + _coinNames[i], + _values[i], + _fees[i] + ); + } + + serialNo++; + + // Because `stack is too deep`, must create `_strFrom` to waive this error + // `_strFrom` is a string type of an address `_from` + string memory _strFrom = _from.toString(); + bmc.sendMessage( + _toNetwork, + serviceName, + serialNo, + Types + .ServiceMessage( + Types + .ServiceType + .REQUEST_COIN_TRANSFER, + Types + .TransferCoin(_strFrom, _toAddress, _assets) + .encodeTransferCoinMsg() + ) + .encodeServiceMessage() + ); + // Push pending tx into Record list + requests[serialNo] = Types.PendingTransferCoin( + _strFrom, + _to, + _coinNames, + _values, + _fees + ); + numOfPendingRequests++; + emit TransferStart(_from, _to, serialNo, _assetDetails); + } + + /** + @notice BSH handle BTP Message from BMC contract + @dev Caller must be BMC contract only + @param _from An originated network address of a request + @param _svc A service name of BSH contract + @param _sn A serial number of a service request + @param _msg An RLP message of a service request/service response + */ + function handleBTPMessage( + string calldata _from, + string calldata _svc, + uint256 _sn, + bytes calldata _msg + ) external override onlyBMC { + require(_svc.compareTo(serviceName) == true, "InvalidSvc"); + Types.ServiceMessage memory _sm = _msg.decodeServiceMessage(); + string memory errMsg; + + if (_sm.serviceType == Types.ServiceType.REQUEST_COIN_TRANSFER) { + Types.TransferCoin memory _tc = _sm.data.decodeTransferCoinMsg(); + // checking receiving address whether is a valid address + // revert() if not a valid one + try this.checkParseAddress(_tc.to) { + try this.handleRequestService(_tc.to, _tc.assets) { + sendResponseMessage( + Types.ServiceType.REPONSE_HANDLE_SERVICE, + _from, + _sn, + "", + RC_OK + ); + return; + } catch Error(string memory _err) { + errMsg = _err; + } + } catch { + errMsg = "InvalidAddress"; + } + sendResponseMessage( + Types.ServiceType.REPONSE_HANDLE_SERVICE, + _from, + _sn, + errMsg, + RC_ERR + ); + } else if ( + _sm.serviceType == Types.ServiceType.REPONSE_HANDLE_SERVICE + ) { + // Check whether '_sn' is pending state + require(bytes(requests[_sn].from).length != 0, "InvalidSN"); + Types.Response memory response = _sm.data.decodeResponse(); + // @dev Not implement try_catch at this point + // + If RESPONSE_REQUEST_SERVICE: + // If RC_ERR, BSHCore proceeds a refund. If a refund is failed, BSHCore issues refundable Balance + // If RC_OK: + // - requested coin = native -> update aggregation fee (likely no issue) + // - requested coin = wrapped coin -> BSHCore calls itself to burn its tokens and update aggregation fee (likely no issue) + // The only issue, which might happen, is BSHCore's token balance lower than burning amount + // If so, there might be something went wrong before + // + If RESPONSE_FEE_GATHERING + // If RC_ERR, BSHCore saves charged fees back to `aggregationFee` state mapping variable + // If RC_OK: do nothing + handleResponseService(_sn, response.code, response.message); + } else if (_sm.serviceType == Types.ServiceType.UNKNOWN_TYPE) { + emit UnknownResponse(_from, _sn); + } else { + // If none of those types above, BSH responds a message of RES_UNKNOWN_TYPE + sendResponseMessage( + Types.ServiceType.UNKNOWN_TYPE, + _from, + _sn, + "Unknown", + RC_ERR + ); + } + } + + /** + @notice BSH handle BTP Error from BMC contract + @dev Caller must be BMC contract only + @param _svc A service name of BSH contract + @param _sn A serial number of a service request + @param _code A response code of a message (RC_OK / RC_ERR) + @param _msg A response message + */ + function handleBTPError( + string calldata, /* _src */ + string calldata _svc, + uint256 _sn, + uint256 _code, + string calldata _msg + ) external override onlyBMC { + require(_svc.compareTo(serviceName) == true, "InvalidSvc"); + require(bytes(requests[_sn].from).length != 0, "InvalidSN"); + string memory _emitMsg = + string("errCode: ") + .concat(_code.toString()) + .concat(", errMsg: ") + .concat(_msg); + handleResponseService(_sn, RC_ERR, _emitMsg); + } + + function handleResponseService( + uint256 _sn, + uint256 _code, + string memory _msg + ) private { + address _caller = requests[_sn].from.parseAddress(); + uint256 loop = requests[_sn].coinNames.length; + for (uint256 i = 0; i < loop; i++) { + bshCore.handleResponseService( + _caller, + requests[_sn].coinNames[i], + requests[_sn].amounts[i], + requests[_sn].fees[i], + _code + ); + } + delete requests[_sn]; + numOfPendingRequests--; + emit TransferEnd(_caller, _sn, _code, _msg); + } + + /** + @notice Handle a list of minting/transferring coins/tokens + @dev Caller must be BMC contract only + @param _to An address to receive coins/tokens + @param _assets A list of requested coin respectively with an amount + */ + function handleRequestService( + string memory _to, + Types.Asset[] memory _assets + ) external { + require(msg.sender == address(this), "Unauthorized"); + for (uint256 i = 0; i < _assets.length; i++) { + require( + bshCore.isValidCoin(_assets[i].coinName) == true, + "UnregisteredCoin" + ); + // @dev There might be many errors generating by BSHCore contract + // which includes also low-level error + // Thus, must use try_catch at this point so that it can return an expected response + try + bshCore.mint( + _to.parseAddress(), + _assets[i].coinName, + _assets[i].value + ) + {} catch { + revert("TransferFailed"); + } + } + } + + function sendResponseMessage( + Types.ServiceType _serviceType, + string memory _to, + uint256 _sn, + string memory _msg, + uint256 _code + ) private { + bmc.sendMessage( + _to, + serviceName, + _sn, + Types + .ServiceMessage( + _serviceType, + Types.Response(_code, _msg).encodeResponse() + ) + .encodeServiceMessage() + ); + } + + /** + @notice BSH handle Gather Fee Message request from BMC contract + @dev Caller must be BMC contract only + @param _fa A BTP address of fee aggregator + @param _svc A name of the service + */ + function handleFeeGathering(string calldata _fa, string calldata _svc) + external + override + onlyBMC + { + require(_svc.compareTo(serviceName) == true, "InvalidSvc"); + // If adress of Fee Aggregator (_fa) is invalid BTP address format + // revert(). Then, BMC will catch this error + // @dev this part simply check whether `_fa` is splittable (`prefix` + `_net` + `dstAddr`) + // checking validity of `_net` and `dstAddr` does not belong to BSHPeriphery's scope + _fa.splitBTPAddress(); + bshCore.transferFees(_fa); + } + + // @dev Solidity does not allow to use try_catch with internal function + // Thus, this is a work-around solution + // Since this function is basically checking whether a string address + // can be parsed to address type. Hence, it would not have any restrictions + function checkParseAddress(string calldata _to) external pure { + _to.parseAddress(); + } +} diff --git a/solidity/nativecoinERC20/contracts/interfaces/IBMCPeriphery.sol b/solidity/nativecoinERC20/contracts/interfaces/IBMCPeriphery.sol new file mode 100644 index 00000000..78787903 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/interfaces/IBMCPeriphery.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "../libraries/Types.sol"; + +interface IBMCPeriphery { + /** + @notice Get BMC BTP address + */ + function getBmcBtpAddress() external view returns (string memory); + + /** + @notice Verify and decode RelayMessage with BMV, and dispatch BTP Messages to registered BSHs + @dev Caller must be a registered relayer. + @param _prev BTP Address of the BMC generates the message + @param _msg base64 encoded string of serialized bytes of Relay Message refer RelayMessage structure + */ + function handleRelayMessage(string calldata _prev, string calldata _msg) + external; + + /** + @notice Send the message to a specific network. + @dev Caller must be an registered BSH. + @param _to Network Address of destination network + @param _svc Name of the service + @param _sn Serial number of the message, it should be positive + @param _msg Serialized bytes of Service Message + */ + function sendMessage( + string calldata _to, + string calldata _svc, + uint256 _sn, + bytes calldata _msg + ) external; + + /* + @notice Get status of BMC. + @param _link BTP Address of the connected BMC + @return _linkStats The link status + */ + function getStatus(string calldata _link) + external + view + returns (Types.LinkStats memory _linkStats); +} diff --git a/solidity/nativecoinERC20/contracts/interfaces/IBMV.sol b/solidity/nativecoinERC20/contracts/interfaces/IBMV.sol new file mode 100644 index 00000000..e370af4e --- /dev/null +++ b/solidity/nativecoinERC20/contracts/interfaces/IBMV.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +interface IBMV { + /** + @return base64EncodedMTA Base64 encode of Merkle Tree + */ + function getMTA() external view returns (string memory base64EncodedMTA); + + /** + @return addr connected BMC address + */ + function getConnectedBMC() external view returns (address addr); + + /** + @return net network address of the blockchain + */ + function getNetAddress() external view returns (string memory net); + + /** + @return serializedHash hash of RLP encode from given list of validators + @return addresses list of validators' addresses + */ + function getValidators() + external + view + returns (bytes32 serializedHash, address[] memory addresses); + + /** + @notice Used by the relay to resolve next BTP Message to send. + Called by BMC. + @return height height of MerkleTreeAccumulator + @return offset offset of MerkleTreeAccumulator + @return lastHeight block height of last relayed BTP Message + */ + function getStatus() + external + view + returns ( + uint256 height, + uint256 offset, + uint256 lastHeight + ); + + /** + @notice Decodes Relay Messages and process BTP Messages. + If there is an error, then it sends a BTP Message containing the Error Message. + BTP Messages with old sequence numbers are ignored. A BTP Message contains future sequence number will fail. + @param _bmc BTP Address of the BMC handling the message + @param _prev BTP Address of the previous BMC + @param _seq next sequence number to get a message + @param _msg serialized bytes of Relay Message + @return serializedMessages List of serialized bytes of a BTP Message + */ + function handleRelayMessage( + string calldata _bmc, + string calldata _prev, + uint256 _seq, + string calldata _msg + ) external returns (bytes[] memory serializedMessages); +} diff --git a/solidity/nativecoinERC20/contracts/interfaces/IBSH.sol b/solidity/nativecoinERC20/contracts/interfaces/IBSH.sol new file mode 100644 index 00000000..26769f6e --- /dev/null +++ b/solidity/nativecoinERC20/contracts/interfaces/IBSH.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +interface IBSH { + /** + @notice BSH handle BTP Message from BMC contract + @dev Caller must be BMC contract only + @param _from An originated network address of a request + @param _svc A service name of BSH contract + @param _sn A serial number of a service request + @param _msg An RLP message of a service request/service response + */ + function handleBTPMessage( + string calldata _from, + string calldata _svc, + uint256 _sn, + bytes calldata _msg + ) external; + + /** + @notice BSH handle BTP Error from BMC contract + @dev Caller must be BMC contract only + @param _svc A service name of BSH contract + @param _sn A serial number of a service request + @param _code A response code of a message (RC_OK / RC_ERR) + @param _msg A response message + */ + function handleBTPError( + string calldata _src, + string calldata _svc, + uint256 _sn, + uint256 _code, + string calldata _msg + ) external; + + /** + @notice BSH handle Gather Fee Message request from BMC contract + @dev Caller must be BMC contract only + @param _fa A BTP address of fee aggregator + @param _svc A name of the service + */ + function handleFeeGathering(string calldata _fa, string calldata _svc) + external; +} diff --git a/solidity/nativecoinERC20/contracts/interfaces/IBSHCore.sol b/solidity/nativecoinERC20/contracts/interfaces/IBSHCore.sol new file mode 100644 index 00000000..770e37ee --- /dev/null +++ b/solidity/nativecoinERC20/contracts/interfaces/IBSHCore.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "../libraries/Types.sol"; + +/** + @title Interface of BSHCore contract + @dev This contract is used to handle coin transferring service + Note: The coin of following interface can be: + Native Coin : The native coin of this chain + Wrapped Native Coin : A tokenized ERC1155 version of another native coin like ICX +*/ +interface IBSHCore{ + /** + @notice Adding another Onwer. + @dev Caller must be an Onwer of BTP network + @param _owner Address of a new Onwer. + */ + function addOwner(address _owner) external; + + /** + @notice Removing an existing Owner. + @dev Caller must be an Owner of BTP network + @dev If only one Owner left, unable to remove the last Owner + @param _owner Address of an Owner to be removed. + */ + function removeOwner(address _owner) external; + + /** + @notice Checking whether one specific address has Owner role. + @dev Caller can be ANY + @param _owner Address needs to verify. + */ + function isOwner(address _owner) external view returns (bool); + + /** + @notice Get a list of current Owners + @dev Caller can be ANY + @return An array of addresses of current Owners + */ + + function getOwners() external view returns (address[] memory); + + /** + @notice update BSH Periphery address. + @dev Caller must be an Owner of this contract + _bshPeriphery Must be different with the existing one. + @param _bshPeriphery BSHPeriphery contract address. + */ + function updateBSHPeriphery(address _bshPeriphery) external; + + + /** + @notice set fee ratio. + @dev Caller must be an Owner of this contract + The transfer fee is calculated by feeNumerator/FEE_DEMONINATOR. + The feeNumetator should be less than FEE_DEMONINATOR + _feeNumerator is set to `10` in construction by default, which means the default fee ratio is 0.1%. + @param _feeNumerator the fee numerator + */ + function setFeeRatio(uint256 _feeNumerator) external; + + /** + @notice set Fixed Fee. + @dev Caller must be an Owner + @param _fixedFee A new value of Fixed Fee + */ + function setFixedFee(uint256 _fixedFee) external; + + /** + @notice Registers a wrapped coin and id number of a supporting coin. + @dev Caller must be an Owner of this contract + _name Must be different with the native coin name. + @dev '_id' of a wrapped coin is generated by using keccak256 + '_id' = 0 is fixed to assign to native coin + @param _name Coin name. + */ + function register(string calldata _name) external; + + /** + @notice Return all supported coins names + @dev + @return _names An array of strings. + */ + function coinNames() external view returns (string[] memory _names); + + /** + @notice Check Validity of a _coinName + @dev Call by BSHPeriphery contract to validate a requested _coinName + @return _valid true of false + */ + function isValidCoin(string calldata _coinName) + external + view + returns (bool _valid); + + /** + @notice Return a usable/locked/refundable balance of an account based on coinName. + @return _usableBalance the balance that users are holding. + @return _lockedBalance when users transfer the coin, + it will be locked until getting the Service Message Response. + @return _refundableBalance refundable balance is the balance that will be refunded to users. + */ + function getBalanceOf(address _owner, string memory _coinName) + external + view + returns ( + uint256 _usableBalance, + uint256 _lockedBalance, + uint256 _refundableBalance + ); + + /** + @notice Return a list accumulated Fees. + @dev only return the asset that has Asset's value greater than 0 + @return _accumulatedFees An array of Asset + */ + function getAccumulatedFees() + external + view + returns (Types.Asset[] memory _accumulatedFees); + + /** + @notice Allow users to deposit `msg.value` native coin into a BSHCore contract. + @dev MUST specify msg.value + @param _to An address that a user expects to receive an amount of tokens. + */ + function transferNativeCoin(string calldata _to) external payable; + + /** + @notice Allow users to deposit an amount of wrapped native coin `_coinName` from the `msg.sender` address into the BSHCore contract. + @dev Caller must set to approve that the wrapped tokens can be transferred out of the `msg.sender` account by BSHCore contract. + It MUST revert if the balance of the holder for token `_coinName` is lower than the `_value` sent. + @param _coinName A given name of a wrapped coin + @param _value An amount request to transfer. + @param _to Target BTP address. + */ + function transfer( + string calldata _coinName, + uint256 _value, + string calldata _to + ) external; + + /** + @notice Reclaim the token's refundable balance by an owner. + @dev Caller must be an owner of coin + The amount to claim must be smaller or equal than refundable balance + @param _coinName A given name of coin + @param _value An amount of re-claiming tokens + */ + function reclaim(string calldata _coinName, uint256 _value) external; + + /** + @notice mint the wrapped coin. + @dev Caller must be an BSHPeriphery contract + Invalid _coinName will have an _id = 0. However, _id = 0 is also dedicated to Native Coin + Thus, BSHPeriphery will check a validity of a requested _coinName before calling + for the _coinName indicates with id = 0, it should send the Native Coin (Example: PRA) to user account + @param _to the account receive the minted coin + @param _coinName coin name + @param _value the minted amount + */ + function mint( + address _to, + string calldata _coinName, + uint256 _value + ) external; + + /** + @notice Handle a request of Fee Gathering + @dev Caller must be an BSHPeriphery contract + @param _fa BTP Address of Fee Aggregator + */ + function transferFees(string calldata _fa) external; + + /** + @notice Handle a response of a requested service + @dev Caller must be an BSHPeriphery contract + @param _requester An address of originator of a requested service + @param _coinName A name of requested coin + @param _value An amount to receive on a destination chain + @param _fee An amount of charged fee + */ + function handleResponseService( + address _requester, + string calldata _coinName, + uint256 _value, + uint256 _fee, + uint256 _rspCode + ) external; +} diff --git a/solidity/nativecoinERC20/contracts/interfaces/IBSHPeriphery.sol b/solidity/nativecoinERC20/contracts/interfaces/IBSHPeriphery.sol new file mode 100644 index 00000000..7fbe4534 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/interfaces/IBSHPeriphery.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "./IBSH.sol"; + +/** + @title Interface of BSHPeriphery contract + @dev This contract is used to handle communications among BMCService and BSHCore contract +*/ +interface IBSHPeriphery is IBSH { + /** + @notice Check whether BSHPeriphery has any pending transferring requests + @return true or false + */ + function hasPendingRequest() external view returns (bool); + + /** + @notice Send Service Message from BSHCore contract to BMCService contract + @dev Caller must be BSHCore only + @param _to A network address of destination chain + @param _coinNames A list of coin name that are requested to transfer + @param _values A list of an amount to receive at destination chain respectively with its coin name + @param _fees A list of an amount of charging fee respectively with its coin name + */ + function sendServiceMessage( + address _from, + string calldata _to, + string[] memory _coinNames, + uint256[] memory _values, + uint256[] memory _fees + ) external; + + /** + @notice BSH handle BTP Message from BMC contract + @dev Caller must be BMC contract only + @param _from An originated network address of a request + @param _svc A service name of BSHPeriphery contract + @param _sn A serial number of a service request + @param _msg An RLP message of a service request/service response + */ + function handleBTPMessage( + string calldata _from, + string calldata _svc, + uint256 _sn, + bytes calldata _msg + ) external override; + + /** + @notice BSH handle BTP Error from BMC contract + @dev Caller must be BMC contract only + @param _svc A service name of BSHPeriphery contract + @param _sn A serial number of a service request + @param _code A response code of a message (RC_OK / RC_ERR) + @param _msg A response message + */ + function handleBTPError( + string calldata _src, + string calldata _svc, + uint256 _sn, + uint256 _code, + string calldata _msg + ) external override; + + /** + @notice BSH handle Gather Fee Message request from BMC contract + @dev Caller must be BMC contract only + @param _fa A BTP address of fee aggregator + @param _svc A name of the service + */ + function handleFeeGathering(string calldata _fa, string calldata _svc) + external + override; +} diff --git a/solidity/nativecoinERC20/contracts/libraries/DecodeBase64.sol b/solidity/nativecoinERC20/contracts/libraries/DecodeBase64.sol new file mode 100644 index 00000000..283540d5 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/DecodeBase64.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +/** + * @title DecodeBase64 + * @dev A simple Base64 decoding library. + * @author Quang Tran + */ + +library DecodeBase64 { + function decode(string memory _str) internal pure returns (bytes memory) { + bytes memory _bs = bytes(_str); + uint256 remove = 0; + if (_bs[_bs.length - 1] == "=" && _bs[_bs.length - 2] == "=") { + remove += 2; + } else if (_bs[_bs.length - 1] == "=") { + remove++; + } + uint256 resultLength = (_bs.length / 4) * 3 - remove; + bytes memory result = new bytes(resultLength); + + uint256 i = 0; + uint256 j = 0; + for (; i + 4 < _bs.length; i += 4) { + (result[j], result[j + 1], result[j + 2]) = decode4( + mapBase64Char(_bs[i]), + mapBase64Char(_bs[i + 1]), + mapBase64Char(_bs[i + 2]), + mapBase64Char(_bs[i + 3]) + ); + j += 3; + } + if (remove == 1) { + (result[j], result[j + 1], ) = decode4( + mapBase64Char(_bs[_bs.length - 4]), + mapBase64Char(_bs[_bs.length - 3]), + mapBase64Char(_bs[_bs.length - 2]), + 0 + ); + } else if (remove == 2) { + (result[j], , ) = decode4( + mapBase64Char(_bs[_bs.length - 4]), + mapBase64Char(_bs[_bs.length - 3]), + 0, + 0 + ); + } else { + (result[j], result[j + 1], result[j + 2]) = decode4( + mapBase64Char(_bs[_bs.length - 4]), + mapBase64Char(_bs[_bs.length - 3]), + mapBase64Char(_bs[_bs.length - 2]), + mapBase64Char(_bs[_bs.length - 1]) + ); + } + return result; + } + + function mapBase64Char(bytes1 _char) private pure returns (uint8) { + // solhint-disable-next-line + uint8 A = 0; + uint8 a = 26; + uint8 zero = 52; + if (uint8(_char) == 45) { + return 62; + } else if (uint8(_char) == 95) { + return 63; + } else if (uint8(_char) >= 48 && uint8(_char) <= 57) { + return zero + (uint8(_char) - 48); + } else if (uint8(_char) >= 65 && uint8(_char) <= 90) { + return A + (uint8(_char) - 65); + } else if (uint8(_char) >= 97 && uint8(_char) <= 122) { + return a + (uint8(_char) - 97); + } + return 0; + } + + function decode4( + uint256 a0, + uint256 a1, + uint256 a2, + uint256 a3 + ) + private + pure + returns ( + bytes1, + bytes1, + bytes1 + ) + { + uint256 n = + ((a0 & 63) << 18) | + ((a1 & 63) << 12) | + ((a2 & 63) << 6) | + (a3 & 63); + uint256 b0 = (n >> 16) & 255; + uint256 b1 = (n >> 8) & 255; + uint256 b2 = (n) & 255; + return (bytes1(uint8(b0)), bytes1(uint8(b1)), bytes1(uint8(b2))); + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/EncodeBase64.sol b/solidity/nativecoinERC20/contracts/libraries/EncodeBase64.sol new file mode 100644 index 00000000..5a09250f --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/EncodeBase64.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +/** + * @title EncodeBase64 + * @dev A simple Base64 encoding library. + * The original library was modified to make it work for our case + * For more info, please check the link: https://github.com/OpenZeppelin/solidity-jwt.git + */ +library EncodeBase64 { + bytes private constant BASE64URLCHARS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + + function encode(bytes memory _bs) internal pure returns (string memory) { + // bytes memory _bs = bytes(_str); + uint256 rem = _bs.length % 3; + uint256 resLength; + if (_bs.length % 3 != 0) { + resLength = (_bs.length / 3) * 4 + 4; + } else { + resLength = (_bs.length / 3) * 4; + } + + // uint256 res_length = (_bs.length + 2) / 3 * 4 - ((3 - rem) % 3); + bytes memory res = new bytes(resLength); + uint256 i = 0; + uint256 j = 0; + + for (; i + 3 <= _bs.length; i += 3) { + (res[j], res[j + 1], res[j + 2], res[j + 3]) = encode3( + uint8(_bs[i]), + uint8(_bs[i + 1]), + uint8(_bs[i + 2]) + ); + j += 4; + } + + if (rem != 0) { + uint8 la0 = uint8(_bs[_bs.length - rem]); + uint8 la1 = 0; + if (rem == 2) { + la1 = uint8(_bs[_bs.length - 1]); + } + (bytes1 b0, bytes1 b1, bytes1 b2, ) = encode3(la0, la1, 0); + res[j] = b0; + res[j + 1] = b1; + if (rem == 1) { + res[j + 2] = "="; + res[j + 3] = "="; + } else if (rem == 2) { + res[j + 2] = b2; + res[j + 3] = "="; + } + } + return string(res); + } + + function encode3( + uint256 a0, + uint256 a1, + uint256 a2 + ) + private + pure + returns ( + bytes1 b0, + bytes1 b1, + bytes1 b2, + bytes1 b3 + ) + { + uint256 n = (a0 << 16) | (a1 << 8) | a2; + uint256 c0 = (n >> 18) & 63; + uint256 c1 = (n >> 12) & 63; + uint256 c2 = (n >> 6) & 63; + uint256 c3 = (n) & 63; + b0 = BASE64URLCHARS[c0]; + b1 = BASE64URLCHARS[c1]; + b2 = BASE64URLCHARS[c2]; + b3 = BASE64URLCHARS[c3]; + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/ParseAddress.sol b/solidity/nativecoinERC20/contracts/libraries/ParseAddress.sol new file mode 100644 index 00000000..59475be0 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/ParseAddress.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +/* + * Utility library of inline functions on addresses + */ +library ParseAddress { + function parseAddress(string memory account) + internal + pure + returns (address accountAddress) + { + bytes memory accountBytes = bytes(account); + require( + accountBytes.length == 42 && + accountBytes[0] == bytes1("0") && + accountBytes[1] == bytes1("x"), + "Invalid address format" + ); + + // create a new fixed-size byte array for the ascii bytes of the address. + bytes memory accountAddressBytes = new bytes(20); + + // declare variable types. + uint8 b; + uint8 nibble; + uint8 asciiOffset; + + for (uint256 i = 0; i < 40; i++) { + // get the byte in question. + b = uint8(accountBytes[i + 2]); + + bool isValidASCII = true; + // ensure that the byte is a valid ascii character (0-9, A-F, a-f) + if (b < 48) isValidASCII = false; + if (57 < b && b < 65) isValidASCII = false; + if (70 < b && b < 97) isValidASCII = false; + if (102 < b) isValidASCII = false; //bytes(hex""); + + // If string contains invalid ASCII characters, revert() + if (!isValidASCII) revert("Invalid address"); + + // find the offset from ascii encoding to the nibble representation. + if (b < 65) { + // 0-9 + asciiOffset = 48; + } else if (70 < b) { + // a-f + asciiOffset = 87; + } else { + // A-F + asciiOffset = 55; + } + + // store left nibble on even iterations, then store byte on odd ones. + if (i % 2 == 0) { + nibble = b - asciiOffset; + } else { + accountAddressBytes[(i - 1) / 2] = ( + byte(16 * nibble + (b - asciiOffset)) + ); + } + } + + // pack up the fixed-size byte array and cast it to accountAddress. + bytes memory packed = abi.encodePacked(accountAddressBytes); + assembly { + accountAddress := mload(add(packed, 20)) + } + + // return false in the event the account conversion returned null address. + if (accountAddress == address(0)) { + // ensure that provided address is not also the null address first. + for (uint256 i = 2; i < accountBytes.length; i++) + require(accountBytes[i] == hex"30", "Invalid address"); + } + + // get the capitalized characters in the actual checksum. + string memory actual = _toChecksumString(accountAddress); + + // compare provided string to actual checksum string to test for validity. + //TODO: check with ICONDAO team, this fails due to the capitalization of the actual address + /* require( + keccak256(abi.encodePacked(actual)) == + keccak256(abi.encodePacked(account)), + "Invalid checksum" + ); */ + } + + /** + * @dev Get a checksummed string hex representation of an account address. + * @param account address The account to get the checksum for. + * @return The checksummed account string in ASCII format. Note that leading + * "0x" is not included. + */ + function toString(address account) internal pure returns (string memory) { + // call internal function for converting an account to a checksummed string. + return _toChecksumString(account); + } + + /** + * @dev Get a fixed-size array of whether or not each character in an account + * will be capitalized in the checksum. + * @param account address The account to get the checksum capitalization + * information for. + * @return A fixed-size array of booleans that signify if each character or + * "nibble" of the hex encoding of the address will be capitalized by the + * checksum. + */ + function getChecksumCapitalizedCharacters(address account) + internal + pure + returns (bool[40] memory) + { + // call internal function for computing characters capitalized in checksum. + return _toChecksumCapsFlags(account); + } + + /** + * @dev Determine whether a string hex representation of an account address + * matches the ERC-55 checksum of that address. + * @param accountChecksum string The checksummed account string in ASCII + * format. Note that a leading "0x" MUST NOT be included. + * @return A boolean signifying whether or not the checksum is valid. + */ + function isChecksumValid(string calldata accountChecksum) + internal + pure + returns (bool) + { + // call internal function for validating checksum strings. + return _isChecksumValid(accountChecksum); + } + + function _toChecksumString(address account) + internal + pure + returns (string memory asciiString) + { + // convert the account argument from address to bytes. + bytes20 data = bytes20(account); + + // create an in-memory fixed-size bytes array. + bytes memory asciiBytes = new bytes(40); + + // declare variable types. + uint8 b; + uint8 leftNibble; + uint8 rightNibble; + bool leftCaps; + bool rightCaps; + uint8 asciiOffset; + + // get the capitalized characters in the actual checksum. + bool[40] memory caps = _toChecksumCapsFlags(account); + + // iterate over bytes, processing left and right nibble in each iteration. + for (uint256 i = 0; i < data.length; i++) { + // locate the byte and extract each nibble. + b = uint8(uint160(data) / (2**(8 * (19 - i)))); + leftNibble = b / 16; + rightNibble = b - 16 * leftNibble; + + // locate and extract each capitalization status. + leftCaps = caps[2 * i]; + rightCaps = caps[2 * i + 1]; + + // get the offset from nibble value to ascii character for left nibble. + asciiOffset = _getAsciiOffset(leftNibble, leftCaps); + + // add the converted character to the byte array. + asciiBytes[2 * i] = byte(leftNibble + asciiOffset); + + // get the offset from nibble value to ascii character for right nibble. + asciiOffset = _getAsciiOffset(rightNibble, rightCaps); + + // add the converted character to the byte array. + asciiBytes[2 * i + 1] = byte(rightNibble + asciiOffset); + } + + return string(abi.encodePacked("0x", string(asciiBytes))); + } + + function _toChecksumCapsFlags(address account) + internal + pure + returns (bool[40] memory characterCapitalized) + { + // convert the address to bytes. + bytes20 a = bytes20(account); + + // hash the address (used to calculate checksum). + bytes32 b = keccak256(abi.encodePacked(_toAsciiString(a))); + + // declare variable types. + uint8 leftNibbleAddress; + uint8 rightNibbleAddress; + uint8 leftNibbleHash; + uint8 rightNibbleHash; + + // iterate over bytes, processing left and right nibble in each iteration. + for (uint256 i; i < a.length; i++) { + // locate the byte and extract each nibble for the address and the hash. + rightNibbleAddress = uint8(a[i]) % 16; + leftNibbleAddress = (uint8(a[i]) - rightNibbleAddress) / 16; + rightNibbleHash = uint8(b[i]) % 16; + leftNibbleHash = (uint8(b[i]) - rightNibbleHash) / 16; + + characterCapitalized[2 * i] = (leftNibbleAddress > 9 && + leftNibbleHash > 7); + characterCapitalized[2 * i + 1] = (rightNibbleAddress > 9 && + rightNibbleHash > 7); + } + } + + function _isChecksumValid(string memory provided) + internal + pure + returns (bool ok) + { + // convert the provided string into account type. + address account = _toAddress(provided); + + // return false in the event the account conversion returned null address. + if (account == address(0)) { + // ensure that provided address is not also the null address first. + bytes memory b = bytes(provided); + for (uint256 i; i < b.length; i++) { + if (b[i] != hex"30") { + return false; + } + } + } + + // get the capitalized characters in the actual checksum. + string memory actual = _toChecksumString(account); + + // compare provided string to actual checksum string to test for validity. + return (keccak256(abi.encodePacked(actual)) == + keccak256(abi.encodePacked(provided))); + } + + function _getAsciiOffset(uint8 nibble, bool caps) + internal + pure + returns (uint8 offset) + { + // to convert to ascii characters, add 48 to 0-9, 55 to A-F, & 87 to a-f. + if (nibble < 10) { + offset = 48; + } else if (caps) { + offset = 55; + } else { + offset = 87; + } + } + + function _toAddress(string memory account) + internal + pure + returns (address accountAddress) + { + // convert the account argument from address to bytes. + bytes memory accountBytes = bytes(account); + + // create a new fixed-size byte array for the ascii bytes of the address. + bytes memory accountAddressBytes = new bytes(20); + + // declare variable types. + uint8 b; + uint8 nibble; + uint8 asciiOffset; + + // only proceed if the provided string has a length of 40. + if (accountBytes.length == 40) { + for (uint256 i; i < 40; i++) { + // get the byte in question. + b = uint8(accountBytes[i]); + + // ensure that the byte is a valid ascii character (0-9, A-F, a-f) + if (b < 48) return address(0); + if (57 < b && b < 65) return address(0); + if (70 < b && b < 97) return address(0); + if (102 < b) return address(0); //bytes(hex""); + + // find the offset from ascii encoding to the nibble representation. + if (b < 65) { + // 0-9 + asciiOffset = 48; + } else if (70 < b) { + // a-f + asciiOffset = 87; + } else { + // A-F + asciiOffset = 55; + } + + // store left nibble on even iterations, then store byte on odd ones. + if (i % 2 == 0) { + nibble = b - asciiOffset; + } else { + accountAddressBytes[(i - 1) / 2] = ( + byte(16 * nibble + (b - asciiOffset)) + ); + } + } + + // pack up the fixed-size byte array and cast it to accountAddress. + bytes memory packed = abi.encodePacked(accountAddressBytes); + assembly { + accountAddress := mload(add(packed, 20)) + } + } + } + + // based on https://ethereum.stackexchange.com/a/56499/48410 + function _toAsciiString(bytes20 data) + internal + pure + returns (string memory asciiString) + { + // create an in-memory fixed-size bytes array. + bytes memory asciiBytes = new bytes(40); + + // declare variable types. + uint8 b; + uint8 leftNibble; + uint8 rightNibble; + + // iterate over bytes, processing left and right nibble in each iteration. + for (uint256 i = 0; i < data.length; i++) { + // locate the byte and extract each nibble. + b = uint8(uint160(data) / (2**(8 * (19 - i)))); + leftNibble = b / 16; + rightNibble = b - 16 * leftNibble; + + // to convert to ascii characters, add 48 to 0-9 and 87 to a-f. + asciiBytes[2 * i] = byte(leftNibble + (leftNibble < 10 ? 48 : 87)); + asciiBytes[2 * i + 1] = byte( + rightNibble + (rightNibble < 10 ? 48 : 87) + ); + } + + return string(asciiBytes); + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/RLPDecode.sol b/solidity/nativecoinERC20/contracts/libraries/RLPDecode.sol new file mode 100644 index 00000000..a7764aa9 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/RLPDecode.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +/* + * Change supporting solidity compiler version + * The original code can be found via this link: https://github.com/hamdiallam/Solidity-RLP.git + */ + +library RLPDecode { + uint8 private constant STRING_SHORT_START = 0x80; + uint8 private constant STRING_LONG_START = 0xb8; + uint8 private constant LIST_SHORT_START = 0xc0; + uint8 private constant LIST_LONG_START = 0xf8; + uint8 private constant WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + struct Iterator { + RLPItem item; // Item that's being iterated over. + uint256 nextPtr; // Position of the next item in the list. + } + + /* + * @dev Returns the next element in the iteration. Reverts if it has not next element. + * @param self The iterator. + * @return The next element in the iteration. + */ + function next(Iterator memory self) internal pure returns (RLPItem memory) { + require(hasNext(self), "Must have next elements"); + + uint256 ptr = self.nextPtr; + uint256 itemLength = _itemLength(ptr); + self.nextPtr = ptr + itemLength; + + return RLPItem(itemLength, ptr); + } + + /* + * @dev Returns true if the iteration has more elements. + * @param self The iterator. + * @return true if the iteration has more elements. + */ + function hasNext(Iterator memory self) internal pure returns (bool) { + RLPItem memory item = self.item; + return self.nextPtr < item.memPtr + item.len; + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) + internal + pure + returns (RLPItem memory) + { + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + + return RLPItem(item.length, memPtr); + } + + /* + * @dev Create an iterator. Reverts if item is not a list. + * @param self The RLP item. + * @return An 'Iterator' over the item. + */ + function iterator(RLPItem memory self) + internal + pure + returns (Iterator memory) + { + require(isList(self), "Must be a list"); + + uint256 ptr = self.memPtr + _payloadOffset(self.memPtr); + return Iterator(self, ptr); + } + + /* + * @param item RLP encoded bytes + */ + function rlpLen(RLPItem memory item) internal pure returns (uint256) { + return item.len; + } + + /* + * @param item RLP encoded bytes + */ + function payloadLen(RLPItem memory item) internal pure returns (uint256) { + return item.len - _payloadOffset(item.memPtr); + } + + /* + * @param item RLP encoded list in bytes + */ + function toList(RLPItem memory item) + internal + pure + returns (RLPItem[] memory) + { + require(isList(item), "Must be a list"); + + uint256 items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + + return result; + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + if (item.len == 0) return false; + + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) + internal + pure + returns (bytes memory) + { + bytes memory result = new bytes(item.len); + if (result.length == 0) return result; + + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + // any non-zero byte except "0x80" is considered true + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1, "Must have length 1"); + uint256 result; + uint256 memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + + // SEE Github Issue #5. + // Summary: Most commonly used RLP libraries (i.e Geth) will encode + // "0" as "0x80" instead of as "0". We handle this edge case explicitly + // here. + if (result == 0 || result == STRING_SHORT_START) { + return false; + } else { + return true; + } + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix + require(item.len == 21, "Must have length 21"); + + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + require(item.len > 0 && item.len <= 33, "Invalid uint number"); + + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; + + uint256 result; + uint256 memPtr = item.memPtr + offset; + assembly { + result := mload(memPtr) + + // shfit to the correct location if neccesary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + function toInt(RLPItem memory item) internal pure returns (int256) { + if ((toBytes(item)[0] & 0x80) == 0x80) { + return int256(toUint(item) - 2**(toBytes(item).length * 8)); + } + + return int256(toUint(item)); + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint256) { + // one byte prefix + require(item.len == 33, "Must have length 33"); + + uint256 result; + uint256 memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + require(item.len > 0, "Invalid length"); + + uint256 offset = _payloadOffset(item.memPtr); + uint256 len = item.len - offset; // data length + bytes memory result = new bytes(len); + + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(item.memPtr + offset, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) private pure returns (uint256) { + if (item.len == 0) return 0; + + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) private pure returns (uint256) { + uint256 itemLen; + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) itemLen = 1; + else if (byte0 < STRING_LONG_START) + itemLen = byte0 - STRING_SHORT_START + 1; + else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) private pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) return 0; + else if ( + byte0 < STRING_LONG_START || + (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START) + ) return 1; + else if (byte0 < LIST_SHORT_START) + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + else return byte0 - (LIST_LONG_START - 1) + 1; + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy( + uint256 src, + uint256 dest, + uint256 len + ) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256**(WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/RLPDecodeStruct.sol b/solidity/nativecoinERC20/contracts/libraries/RLPDecodeStruct.sol new file mode 100644 index 00000000..4ddf2029 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/RLPDecodeStruct.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "./RLPDecode.sol"; +import "./Types.sol"; + +//import "./RLPEncode.sol"; + +library RLPDecodeStruct { + using RLPDecode for RLPDecode.RLPItem; + using RLPDecode for RLPDecode.Iterator; + using RLPDecode for bytes; + + using RLPDecodeStruct for bytes; + + uint8 private constant LIST_SHORT_START = 0xc0; + uint8 private constant LIST_LONG_START = 0xf7; + + function decodeBMCService(bytes memory _rlp) + internal + pure + returns (Types.BMCService memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + return + Types.BMCService( + string(ls[0].toBytes()), + ls[1].toBytes() // bytes array of RLPEncode(Data) + ); + } + + function decodeGatherFeeMessage(bytes memory _rlp) + internal + pure + returns (Types.GatherFeeMessage memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + RLPDecode.RLPItem[] memory subList = ls[1].toList(); + string[] memory _svcs = new string[](subList.length); + for (uint256 i = 0; i < subList.length; i++) { + _svcs[i] = string(subList[i].toBytes()); + } + return Types.GatherFeeMessage(string(ls[0].toBytes()), _svcs); + } + + function decodeEventMessage(bytes memory _rlp) + internal + pure + returns (Types.EventMessage memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + return + Types.EventMessage( + string(ls[0].toBytes()), + Types.Connection( + string(ls[1].toList()[0].toBytes()), + string(ls[1].toList()[1].toBytes()) + ) + ); + } + + function decodeCoinRegister(bytes memory _rlp) + internal + pure + returns (string[] memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + string[] memory _coins = new string[](ls.length); + for (uint256 i = 0; i < ls.length; i++) { + _coins[i] = string(ls[i].toBytes()); + } + return _coins; + } + + function decodeBMCMessage(bytes memory _rlp) + internal + pure + returns (Types.BMCMessage memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + return + Types.BMCMessage( + string(ls[0].toBytes()), + string(ls[1].toBytes()), + string(ls[2].toBytes()), + ls[3].toInt(), + ls[4].toBytes() // bytes array of RLPEncode(ServiceMessage) + ); + } + + function decodeServiceMessage(bytes memory _rlp) + internal + pure + returns (Types.ServiceMessage memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + return + Types.ServiceMessage( + Types.ServiceType(ls[0].toUint()), + ls[1].toBytes() // bytes array of RLPEncode(Data) + ); + } + + function decodeTransferCoinMsg(bytes memory _rlp) + internal + pure + returns (Types.TransferCoin memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + Types.Asset[] memory assets = new Types.Asset[](ls[2].toList().length); + RLPDecode.RLPItem[] memory rlpAssets = ls[2].toList(); + for (uint256 i = 0; i < ls[2].toList().length; i++) { + assets[i] = Types.Asset( + string(rlpAssets[i].toList()[0].toBytes()), + rlpAssets[i].toList()[1].toUint() + ); + } + return + Types.TransferCoin( + string(ls[0].toBytes()), + string(ls[1].toBytes()), + assets + ); + } + + function decodeResponse(bytes memory _rlp) + internal + pure + returns (Types.Response memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + return Types.Response(ls[0].toUint(), string(ls[1].toBytes())); + } + + function decodeBlockHeader(bytes memory _rlp) + internal + pure + returns (Types.BlockHeader memory) + { + // Decode RLP bytes into a list of items + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + bool isSPREmpty = true; + if (ls[10].toBytes().length == 0) { + return + Types.BlockHeader( + ls[0].toUint(), + ls[1].toUint(), + ls[2].toUint(), + ls[3].toBytes(), + ls[4].toBytes(), + ls[5].toBytes(), + ls[6].toBytes(), + ls[7].toBytes(), + ls[8].toBytes(), + ls[9].toBytes(), + Types.SPR("", "", ""), + isSPREmpty + ); + } + RLPDecode.RLPItem[] memory subList = + ls[10].toBytes().toRlpItem().toList(); + isSPREmpty = false; + return + Types.BlockHeader( + ls[0].toUint(), + ls[1].toUint(), + ls[2].toUint(), + ls[3].toBytes(), + ls[4].toBytes(), + ls[5].toBytes(), + ls[6].toBytes(), + ls[7].toBytes(), + ls[8].toBytes(), + ls[9].toBytes(), + Types.SPR( + subList[0].toBytes(), + subList[1].toBytes(), + subList[2].toBytes() + ), + isSPREmpty + ); + } + + // Votes item consists of: + // round as integer + // blockPartSetID is a list that consists of two items - integer and bytes + // and TS[] ts_list (an array of list) + function decodeVotes(bytes memory _rlp) + internal + pure + returns (Types.Votes memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + + Types.TS[] memory tsList = new Types.TS[](ls[2].toList().length); + RLPDecode.RLPItem[] memory rlpTs = ls[2].toList(); + for (uint256 i = 0; i < ls[2].toList().length; i++) { + tsList[i] = Types.TS( + rlpTs[i].toList()[0].toUint(), + rlpTs[i].toList()[1].toBytes() + ); + } + return + Types.Votes( + ls[0].toUint(), + Types.BPSI( + ls[1].toList()[0].toUint(), + ls[1].toList()[1].toBytes() + ), + tsList + ); + } + + // Wait for confirmation + function decodeBlockWitness(bytes memory _rlp) + internal + pure + returns (Types.BlockWitness memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + + bytes[] memory witnesses = new bytes[](ls[1].toList().length); + // witnesses is an array of hash of leaf node + // The array size may also vary, thus loop is needed therein + for (uint256 i = 0; i < ls[1].toList().length; i++) { + witnesses[i] = ls[1].toList()[i].toBytes(); + } + return Types.BlockWitness(ls[0].toUint(), witnesses); + } + + function decodeEventProof(bytes memory _rlp) + internal + pure + returns (Types.EventProof memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + RLPDecode.RLPItem[] memory data = ls[1].toBytes().toRlpItem().toList(); + + bytes[] memory eventMptNode = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + eventMptNode[i] = data[i].toBytes(); + } + return Types.EventProof(ls[0].toUint(), eventMptNode); + } + + function decodeBlockUpdate(bytes memory _rlp) + internal + pure + returns (Types.BlockUpdate memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + + // Types.BlockHeader memory _bh; + Types.BlockHeader memory _bh = ls[0].toBytes().decodeBlockHeader(); + Types.Votes memory _v = ls[1].toBytes().decodeVotes(); + // Types.Votes memory _v; + + // BlockUpdate may or may not include the RLP of addresses of validators + // In that case, RLP_ENCODE([bytes]) == EMPTY_LIST_HEAD_START == 0xF800 + // Thus, length of data will be 0. Therein, loop will be skipped + // and the _validators[] will be empty + // Otherwise, executing normally to read and assign value into the array _validators[] + bytes[] memory _validators; + if (ls[2].toBytes().length != 0) { + _validators = new bytes[](ls[2].toList().length); + for (uint256 i = 0; i < ls[2].toList().length; i++) { + _validators[i] = ls[2].toList()[i].toBytes(); + } + } + return Types.BlockUpdate(_bh, _v, _validators); + } + + function decodeReceiptProof(bytes memory _rlp) + internal + pure + returns (Types.ReceiptProof memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + RLPDecode.RLPItem[] memory receiptList = + ls[1].toBytes().toRlpItem().toList(); + + bytes[] memory txReceipts = new bytes[](receiptList.length); + for (uint256 i = 0; i < receiptList.length; i++) { + txReceipts[i] = receiptList[i].toBytes(); + } + + Types.EventProof[] memory _ep = + new Types.EventProof[](ls[2].toList().length); + for (uint256 i = 0; i < ls[2].toList().length; i++) { + _ep[i] = Types.EventProof( + ls[2].toList()[i].toList()[0].toUint(), + ls[2].toList()[i].toList()[1].toBytes().decodeEventLog() + ); + } + + return Types.ReceiptProof(ls[0].toUint(), txReceipts, _ep); + } + + function decodeEventLog(bytes memory _rlp) + internal + pure + returns (bytes[] memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + bytes[] memory eventMptNode = new bytes[](ls.length); + for (uint256 i = 0; i < ls.length; i++) { + eventMptNode[i] = ls[i].toBytes(); + } + return eventMptNode; + } + + function decodeBlockProof(bytes memory _rlp) + internal + pure + returns (Types.BlockProof memory) + { + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + + Types.BlockHeader memory _bh = ls[0].toBytes().decodeBlockHeader(); + Types.BlockWitness memory _bw = ls[1].toBytes().decodeBlockWitness(); + + return Types.BlockProof(_bh, _bw); + } + + function decodeRelayMessage(bytes memory _rlp) + internal + pure + returns (Types.RelayMessage memory) + { + // _rlp.toRlpItem() removes the LIST_HEAD_START of RelayMessage + // then .toList() to itemize all fields in the RelayMessage + // which are [RLP_ENCODE(BlockUpdate)], RLP_ENCODE(BlockProof), and + // the RLP_ENCODE(ReceiptProof) + RLPDecode.RLPItem[] memory ls = _rlp.toRlpItem().toList(); + // return ( + // ls[0].toList()[0].toBytes().toRlpItem().toList()[1].toBytes().toRlpItem().toList()[2].toList()[0].toList()[1].toBytes() + // ); + + // If [RLP_ENCODE(BlockUpdate)] was empty, it should be started by 0xF800 + // therein, ls[0].toBytes() will be null (length = 0) + // Otherwise, create an array of BlockUpdate struct to decode + Types.BlockUpdate[] memory _buArray; + if (ls[0].toBytes().length != 0) { + _buArray = new Types.BlockUpdate[](ls[0].toList().length); + for (uint256 i = 0; i < ls[0].toList().length; i++) { + // Each of items inside an array [RLP_ENCODE(BlockUpdate)] + // is a string which defines RLP_ENCODE(BlockUpdate) + // that contains a LIST_HEAD_START and multiple RLP of data + // ls[0].toList()[i].toBytes() returns bytes presentation of + // RLP_ENCODE(BlockUpdate) + _buArray[i] = ls[0].toList()[i].toBytes().decodeBlockUpdate(); + } + } + bool isBPEmpty = true; + Types.BlockProof memory _bp; + // If RLP_ENCODE(BlockProof) is omitted, + // ls[1].toBytes() should be null (length = 0) + if (ls[1].toBytes().length != 0) { + _bp = ls[1].toBytes().decodeBlockProof(); + isBPEmpty = false; // add this field into RelayMessage + // to specify whether BlockProof is omitted + // to make it easy on encoding + // it will not be serialized thereafter + } + + bool isRPEmpty = true; + Types.ReceiptProof[] memory _rp; + // If [RLP_ENCODE(ReceiptProof)] is omitted, + // ls[2].toBytes() should be null (length = 0) + if (ls[2].toBytes().length != 0) { + _rp = new Types.ReceiptProof[](ls[2].toList().length); + for (uint256 i = 0; i < ls[2].toList().length; i++) { + _rp[i] = ls[2].toList()[i].toBytes().decodeReceiptProof(); + } + isRPEmpty = false; // add this field into RelayMessage + // to specify whether ReceiptProof is omitted + // to make it easy on encoding + // it will not be serialized thereafter + } + return Types.RelayMessage(_buArray, _bp, isBPEmpty, _rp, isRPEmpty); + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/RLPEncode.sol b/solidity/nativecoinERC20/contracts/libraries/RLPEncode.sol new file mode 100644 index 00000000..a7bb08db --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/RLPEncode.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +/** + * @title RLPEncode + * @dev A simple RLP encoding library. + * @author Bakaoh + * The original code was modified. For more info, please check the link: + * https://github.com/bakaoh/solidity-rlp-encode.git + */ +library RLPEncode { + int8 internal constant MAX_INT8 = type(int8).max; + int16 internal constant MAX_INT16 = type(int16).max; + int24 internal constant MAX_INT24 = type(int24).max; + int32 internal constant MAX_INT32 = type(int32).max; + int40 internal constant MAX_INT40 = type(int40).max; + int48 internal constant MAX_INT48 = type(int48).max; + int56 internal constant MAX_INT56 = type(int56).max; + int64 internal constant MAX_INT64 = type(int64).max; + int72 internal constant MAX_INT72 = type(int72).max; + int80 internal constant MAX_INT80 = type(int80).max; + int88 internal constant MAX_INT88 = type(int88).max; + int96 internal constant MAX_INT96 = type(int96).max; + int104 internal constant MAX_INT104 = type(int104).max; + int112 internal constant MAX_INT112 = type(int112).max; + int120 internal constant MAX_INT120 = type(int120).max; + int128 internal constant MAX_INT128 = type(int128).max; + + uint8 internal constant MAX_UINT8 = type(uint8).max; + uint16 internal constant MAX_UINT16 = type(uint16).max; + uint24 internal constant MAX_UINT24 = type(uint24).max; + uint32 internal constant MAX_UINT32 = type(uint32).max; + uint40 internal constant MAX_UINT40 = type(uint40).max; + uint48 internal constant MAX_UINT48 = type(uint48).max; + uint56 internal constant MAX_UINT56 = type(uint56).max; + uint64 internal constant MAX_UINT64 = type(uint64).max; + uint72 internal constant MAX_UINT72 = type(uint72).max; + uint80 internal constant MAX_UINT80 = type(uint80).max; + uint88 internal constant MAX_UINT88 = type(uint88).max; + uint96 internal constant MAX_UINT96 = type(uint96).max; + uint104 internal constant MAX_UINT104 = type(uint104).max; + uint112 internal constant MAX_UINT112 = type(uint112).max; + uint120 internal constant MAX_UINT120 = type(uint120).max; + uint128 internal constant MAX_UINT128 = type(uint128).max; + + /* + * Internal functions + */ + + /** + * @dev RLP encodes a byte string. + * @param self The byte string to encode. + * @return The RLP encoded string in bytes. + */ + function encodeBytes(bytes memory self) + internal + pure + returns (bytes memory) + { + bytes memory encoded; + if (self.length == 1 && uint8(self[0]) <= 128) { + encoded = self; + } else { + encoded = concat(encodeLength(self.length, 128), self); + } + return encoded; + } + + /** + * @dev RLP encodes a list of RLP encoded byte byte strings. + * @param self The list of RLP encoded byte strings. + * @return The RLP encoded list of items in bytes. + */ + function encodeList(bytes[] memory self) + internal + pure + returns (bytes memory) + { + bytes memory list = flatten(self); + return concat(encodeLength(list.length, 192), list); + } + + /** + * @dev RLP encodes a string. + * @param self The string to encode. + * @return The RLP encoded string in bytes. + */ + function encodeString(string memory self) + internal + pure + returns (bytes memory) + { + return encodeBytes(bytes(self)); + } + + /** + * @dev RLP encodes an address. + * @param self The address to encode. + * @return The RLP encoded address in bytes. + */ + function encodeAddress(address self) internal pure returns (bytes memory) { + bytes memory inputBytes; + assembly { + let m := mload(0x40) + mstore( + add(m, 20), + xor(0x140000000000000000000000000000000000000000, self) + ) + mstore(0x40, add(m, 52)) + inputBytes := m + } + return encodeBytes(inputBytes); + } + + /** + * @dev RLP encodes a uint. + * @param self The uint to encode. + * @return The RLP encoded uint in bytes. + */ + function encodeUint(uint256 self) internal pure returns (bytes memory) { + uint256 nBytes = bitLength(self) / 8 + 1; + bytes memory uintBytes = encodeUintByLength(self); + if (nBytes - uintBytes.length > 0) { + uintBytes = abi.encodePacked(bytes1(0), uintBytes); + } + return encodeBytes(uintBytes); + } + + /** + * @dev convert int to strict bytes. + * @notice only handle to int128 due to contract code size limit + * @param n The int to convert. + * @return The int in strict bytes without padding. + */ + function intToStrictBytes(int256 n) internal pure returns (bytes memory) { + if (-MAX_INT8 - 1 <= n && n <= MAX_INT8) { + return abi.encodePacked(int8(n)); + } else if (-MAX_INT16 - 1 <= n && n <= MAX_INT16) { + return abi.encodePacked(int16(n)); + } else if (-MAX_INT24 - 1 <= n && n <= MAX_INT24) { + return abi.encodePacked(int24(n)); + } else if (-MAX_INT32 - 1 <= n && n <= MAX_INT32) { + return abi.encodePacked(int32(n)); + } else if (-MAX_INT40 - 1 <= n && n <= MAX_INT40) { + return abi.encodePacked(int40(n)); + } else if (-MAX_INT48 - 1 <= n && n <= MAX_INT48) { + return abi.encodePacked(int48(n)); + } else if (-MAX_INT56 - 1 <= n && n <= MAX_INT56) { + return abi.encodePacked(int56(n)); + } else if (-MAX_INT64 - 1 <= n && n <= MAX_INT64) { + return abi.encodePacked(int64(n)); + } else if (-MAX_INT72 - 1 <= n && n <= MAX_INT72) { + return abi.encodePacked(int72(n)); + } else if (-MAX_INT80 - 1 <= n && n <= MAX_INT80) { + return abi.encodePacked(int80(n)); + } else if (-MAX_INT88 - 1 <= n && n <= MAX_INT88) { + return abi.encodePacked(int88(n)); + } else if (-MAX_INT96 - 1 <= n && n <= MAX_INT96) { + return abi.encodePacked(int96(n)); + } else if (-MAX_INT104 - 1 <= n && n <= MAX_INT104) { + return abi.encodePacked(int104(n)); + } else if (-MAX_INT112 - 1 <= n && n <= MAX_INT112) { + return abi.encodePacked(int112(n)); + } else if (-MAX_INT120 - 1 <= n && n <= MAX_INT120) { + return abi.encodePacked(int120(n)); + } + require( + -MAX_INT128 - 1 <= n && n <= MAX_INT128, + "outOfBounds: [-2^128-1, 2^128]" + ); + return abi.encodePacked(int128(n)); + } + + /** + * @dev RLP encodes an int. + * @param self The int to encode. + * @return The RLP encoded int in bytes. + */ + function encodeInt(int256 self) internal pure returns (bytes memory) { + return encodeBytes(intToStrictBytes(self)); + } + + /** + * @dev RLP encodes a bool. + * @param self The bool to encode. + * @return The RLP encoded bool in bytes. + */ + function encodeBool(bool self) internal pure returns (bytes memory) { + bytes memory encoded = new bytes(1); + encoded[0] = (self ? bytes1(0x01) : bytes1(0x00)); + return encoded; + } + + /* + * Private functions + */ + + /** + * @dev Encode the first byte, followed by the `len` in binary form if `length` is more than 55. + * @param len The length of the string or the payload. + * @param offset 128 if item is string, 192 if item is list. + * @return RLP encoded bytes. + */ + function encodeLength(uint256 len, uint256 offset) + private + pure + returns (bytes memory) + { + bytes memory encoded; + if (len < 56) { + encoded = new bytes(1); + encoded[0] = bytes32(len + offset)[31]; + } else { + uint256 lenLen; + uint256 i = 1; + while (len / i != 0) { + lenLen++; + i *= 256; + } + + encoded = new bytes(lenLen + 1); + encoded[0] = bytes32(lenLen + offset + 55)[31]; + for (i = 1; i <= lenLen; i++) { + encoded[i] = bytes32((len / (256**(lenLen - i))) % 256)[31]; + } + } + return encoded; + } + + /** + * @dev Encode integer in big endian binary form with no leading zeroes. + * @notice TODO: This should be optimized with assembly to save gas costs. + * @param _x The integer to encode. + * @return RLP encoded bytes. + */ + function toBinary(uint256 _x) private pure returns (bytes memory) { + // Modify library to make it work properly when _x = 0 + if (_x == 0) { + return abi.encodePacked(uint8(_x)); + } + bytes memory b = new bytes(32); + assembly { + mstore(add(b, 32), _x) + } + uint256 i; + for (i = 0; i < 32; i++) { + if (b[i] != 0) { + break; + } + } + bytes memory res = new bytes(32 - i); + for (uint256 j = 0; j < res.length; j++) { + res[j] = b[i++]; + } + return res; + } + + /** + * @dev Copies a piece of memory to another location. + * @notice From: https://github.com/Arachnid/solidity-stringutils/blob/master/src/strings.sol. + * @param _dest Destination location. + * @param _src Source location. + * @param _len Length of memory to copy. + */ + function memcpy( + uint256 _dest, + uint256 _src, + uint256 _len + ) private pure { + uint256 dest = _dest; + uint256 src = _src; + uint256 len = _len; + + for (; len >= 32; len -= 32) { + assembly { + mstore(dest, mload(src)) + } + dest += 32; + src += 32; + } + + uint256 mask = 256**(32 - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) + let destpart := and(mload(dest), mask) + mstore(dest, or(destpart, srcpart)) + } + } + + /** + * @dev Flattens a list of byte strings into one byte string. + * @notice From: https://github.com/sammayo/solidity-rlp-encoder/blob/master/RLPEncode.sol. + * @param _list List of byte strings to flatten. + * @return The flattened byte string. + */ + function flatten(bytes[] memory _list) private pure returns (bytes memory) { + if (_list.length == 0) { + return new bytes(0); + } + + uint256 len; + uint256 i; + for (i = 0; i < _list.length; i++) { + len += _list[i].length; + } + + bytes memory flattened = new bytes(len); + uint256 flattenedPtr; + assembly { + flattenedPtr := add(flattened, 0x20) + } + + for (i = 0; i < _list.length; i++) { + bytes memory item = _list[i]; + + uint256 listPtr; + assembly { + listPtr := add(item, 0x20) + } + + memcpy(flattenedPtr, listPtr, item.length); + flattenedPtr += _list[i].length; + } + + return flattened; + } + + /** + * @dev Concatenates two bytes. + * @notice From: https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol. + * @param _preBytes First byte string. + * @param _postBytes Second byte string. + * @return Both byte string combined. + */ + function concat(bytes memory _preBytes, bytes memory _postBytes) + private + pure + returns (bytes memory) + { + bytes memory tempBytes; + + assembly { + tempBytes := mload(0x40) + + let length := mload(_preBytes) + mstore(tempBytes, length) + + let mc := add(tempBytes, 0x20) + let end := add(mc, length) + + for { + let cc := add(_preBytes, 0x20) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + length := mload(_postBytes) + mstore(tempBytes, add(length, mload(tempBytes))) + + mc := end + end := add(mc, length) + + for { + let cc := add(_postBytes, 0x20) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore( + 0x40, + and( + add(add(end, iszero(add(length, mload(_preBytes)))), 31), + not(31) + ) + ) + } + + return tempBytes; + } + + /** + * @dev convert uint to strict bytes. + * @notice only handle to uint128 due to contract code size limit + * @param length The uint to convert. + * @return The uint in strict bytes without padding. + */ + function encodeUintByLength(uint256 length) + internal + pure + returns (bytes memory) + { + if (length < MAX_UINT8) { + return abi.encodePacked(uint8(length)); + } else if (length >= MAX_UINT8 && length < MAX_UINT16) { + return abi.encodePacked(uint16(length)); + } else if (length >= MAX_UINT16 && length < MAX_UINT24) { + return abi.encodePacked(uint24(length)); + } else if (length >= MAX_UINT24 && length < MAX_UINT32) { + return abi.encodePacked(uint32(length)); + } else if (length >= MAX_UINT32 && length < MAX_UINT40) { + return abi.encodePacked(uint40(length)); + } else if (length >= MAX_UINT40 && length < MAX_UINT48) { + return abi.encodePacked(uint48(length)); + } else if (length >= MAX_UINT48 && length < MAX_UINT56) { + return abi.encodePacked(uint56(length)); + } else if (length >= MAX_UINT56 && length < MAX_UINT64) { + return abi.encodePacked(uint64(length)); + } else if (length >= MAX_UINT64 && length < MAX_UINT72) { + return abi.encodePacked(uint72(length)); + } else if (length >= MAX_UINT72 && length < MAX_UINT80) { + return abi.encodePacked(uint80(length)); + } else if (length >= MAX_UINT80 && length < MAX_UINT88) { + return abi.encodePacked(uint88(length)); + } else if (length >= MAX_UINT88 && length < MAX_UINT96) { + return abi.encodePacked(uint96(length)); + } else if (length >= MAX_UINT96 && length < MAX_UINT104) { + return abi.encodePacked(uint104(length)); + } else if (length >= MAX_UINT104 && length < MAX_UINT112) { + return abi.encodePacked(uint112(length)); + } else if (length >= MAX_UINT112 && length < MAX_UINT120) { + return abi.encodePacked(uint120(length)); + } + require( + length >= MAX_UINT120 && length < MAX_UINT128, + "outOfBounds: [0, 2^128]" + ); + return abi.encodePacked(uint128(length)); + } + + function bitLength(uint256 n) internal pure returns (uint256) { + uint256 count; + while (n != 0) { + count += 1; + n >>= 1; + } + return count; + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/RLPEncodeStruct.sol b/solidity/nativecoinERC20/contracts/libraries/RLPEncodeStruct.sol new file mode 100644 index 00000000..d578afeb --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/RLPEncodeStruct.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "./RLPEncode.sol"; +import "./Types.sol"; + +library RLPEncodeStruct { + using RLPEncode for bytes; + using RLPEncode for string; + using RLPEncode for uint256; + using RLPEncode for address; + using RLPEncode for int256; + + using RLPEncodeStruct for Types.BlockHeader; + using RLPEncodeStruct for Types.BlockWitness; + using RLPEncodeStruct for Types.BlockUpdate; + using RLPEncodeStruct for Types.BlockProof; + using RLPEncodeStruct for Types.EventProof; + using RLPEncodeStruct for Types.ReceiptProof; + using RLPEncodeStruct for Types.Votes; + using RLPEncodeStruct for Types.RelayMessage; + + uint8 private constant LIST_SHORT_START = 0xc0; + uint8 private constant LIST_LONG_START = 0xf7; + + function encodeBMCService(Types.BMCService memory _bs) + internal + pure + returns (bytes memory) + { + bytes memory _rlp = + abi.encodePacked( + _bs.serviceType.encodeString(), + _bs.payload.encodeBytes() + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeGatherFeeMessage(Types.GatherFeeMessage memory _gfm) + internal + pure + returns (bytes memory) + { + bytes memory _rlp; + bytes memory temp; + for (uint256 i = 0; i < _gfm.svcs.length; i++) { + temp = abi.encodePacked(_gfm.svcs[i].encodeString()); + _rlp = abi.encodePacked(_rlp, temp); + } + _rlp = abi.encodePacked( + _gfm.fa.encodeString(), + addLength(_rlp.length, false), + _rlp + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeEventMessage(Types.EventMessage memory _em) + internal + pure + returns (bytes memory) + { + bytes memory _rlp = + abi.encodePacked( + _em.conn.from.encodeString(), + _em.conn.to.encodeString() + ); + _rlp = abi.encodePacked( + _em.eventType.encodeString(), + addLength(_rlp.length, false), + _rlp + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeCoinRegister(string[] memory _coins) + internal + pure + returns (bytes memory) + { + bytes memory _rlp; + bytes memory temp; + for (uint256 i = 0; i < _coins.length; i++) { + temp = abi.encodePacked(_coins[i].encodeString()); + _rlp = abi.encodePacked(_rlp, temp); + } + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeBMCMessage(Types.BMCMessage memory _bm) + internal + pure + returns (bytes memory) + { + bytes memory _rlp = + abi.encodePacked( + _bm.src.encodeString(), + _bm.dst.encodeString(), + _bm.svc.encodeString(), + _bm.sn.encodeInt(), + _bm.message.encodeBytes() + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeServiceMessage(Types.ServiceMessage memory _sm) + internal + pure + returns (bytes memory) + { + bytes memory _rlp = + abi.encodePacked( + uint256(_sm.serviceType).encodeUint(), + _sm.data.encodeBytes() + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeTransferCoinMsg(Types.TransferCoin memory _data) + internal + pure + returns (bytes memory) + { + bytes memory _rlp; + bytes memory temp; + for (uint256 i = 0; i < _data.assets.length; i++) { + temp = abi.encodePacked( + _data.assets[i].coinName.encodeString(), + _data.assets[i].value.encodeUint() + ); + _rlp = abi.encodePacked(_rlp, addLength(temp.length, false), temp); + } + _rlp = abi.encodePacked( + _data.from.encodeString(), + _data.to.encodeString(), + addLength(_rlp.length, false), + _rlp + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeResponse(Types.Response memory _res) + internal + pure + returns (bytes memory) + { + bytes memory _rlp = + abi.encodePacked( + _res.code.encodeUint(), + _res.message.encodeString() + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeBlockHeader(Types.BlockHeader memory _bh) + internal + pure + returns (bytes memory) + { + // Serialize the first 10 items in the BlockHeader + // patchTxHash and txHash might be empty. + // In that case, encoding these two items gives the result as 0xF800 + // Similarly, logsBloom might be also empty + // But, encoding this item gives the result as 0x80 + bytes memory _rlp = + abi.encodePacked( + _bh.version.encodeUint(), + _bh.height.encodeUint(), + _bh.timestamp.encodeUint(), + _bh.proposer.encodeBytes(), + _bh.prevHash.encodeBytes(), + _bh.voteHash.encodeBytes(), + _bh.nextValidators.encodeBytes() + ); + bytes memory temp1; + if (_bh.patchTxHash.length != 0) { + temp1 = _bh.patchTxHash.encodeBytes(); + } else { + temp1 = emptyListHeadStart(); + } + _rlp = abi.encodePacked(_rlp, temp1); + + if (_bh.txHash.length != 0) { + temp1 = _bh.txHash.encodeBytes(); + } else { + temp1 = emptyListHeadStart(); + } + _rlp = abi.encodePacked(_rlp, temp1, _bh.logsBloom.encodeBytes()); + bytes memory temp2; + // SPR struct could be an empty struct + // In that case, serialize(SPR) = 0xF800 + if (_bh.isSPREmpty) { + temp2 = emptyListHeadStart(); + } else { + // patchReceiptHash and receiptHash might be empty + // In that case, encoding these two items gives the result as 0xF800 + if (_bh.spr.patchReceiptHash.length != 0) { + temp1 = _bh.spr.patchReceiptHash.encodeBytes(); + } else { + temp1 = emptyListHeadStart(); + } + temp2 = abi.encodePacked(_bh.spr.stateHash.encodeBytes(), temp1); + + if (_bh.spr.receiptHash.length != 0) { + temp1 = _bh.spr.receiptHash.encodeBytes(); + } else { + temp1 = emptyListHeadStart(); + } + temp2 = abi.encodePacked(temp2, temp1); + temp2 = abi + .encodePacked(addLength(temp2.length, false), temp2) + .encodeBytes(); + } + _rlp = abi.encodePacked(_rlp, temp2); + + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeVotes(Types.Votes memory _vote) + internal + pure + returns (bytes memory) + { + bytes memory _rlp; + bytes memory temp; + + // First, serialize an array of TS + for (uint256 i = 0; i < _vote.ts.length; i++) { + temp = abi.encodePacked( + _vote.ts[i].timestamp.encodeUint(), + _vote.ts[i].signature.encodeBytes() + ); + _rlp = abi.encodePacked(_rlp, addLength(temp.length, false), temp); + } + + // Next, serialize the blockPartSetID + temp = abi.encodePacked( + _vote.blockPartSetID.n.encodeUint(), + _vote.blockPartSetID.b.encodeBytes() + ); + // Combine all of them + _rlp = abi.encodePacked( + _vote.round.encodeUint(), + addLength(temp.length, false), + temp, + addLength(_rlp.length, false), + _rlp + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeBlockWitness(Types.BlockWitness memory _bw) + internal + pure + returns (bytes memory) + { + bytes memory _rlp; + bytes memory temp; + for (uint256 i = 0; i < _bw.witnesses.length; i++) { + temp = _bw.witnesses[i].encodeBytes(); + _rlp = abi.encodePacked(_rlp, temp); + } + _rlp = abi.encodePacked( + _bw.height.encodeUint(), + addLength(_rlp.length, false), + _rlp + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeEventProof(Types.EventProof memory _ep) + internal + pure + returns (bytes memory) + { + bytes memory _rlp; + bytes memory temp; + for (uint256 i = 0; i < _ep.eventMptNode.length; i++) { + temp = _ep.eventMptNode[i].encodeBytes(); + _rlp = abi.encodePacked(_rlp, temp); + } + _rlp = abi + .encodePacked(addLength(_rlp.length, false), _rlp) + .encodeBytes(); + + _rlp = abi.encodePacked(_ep.index.encodeUint(), _rlp); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeBlockUpdate(Types.BlockUpdate memory _bu) + internal + pure + returns (bytes memory) + { + bytes memory temp; + bytes memory _rlp; + // In the case that _validators[] is an empty array, loop will be skipped + // and RLP_ENCODE([bytes]) == EMPTY_LIST_HEAD_START (0xF800) instead + if (_bu.validators.length != 0) { + for (uint256 i = 0; i < _bu.validators.length; i++) { + temp = _bu.validators[i].encodeBytes(); + _rlp = abi.encodePacked(_rlp, temp); + } + _rlp = abi + .encodePacked(addLength(_rlp.length, false), _rlp) + .encodeBytes(); + } else { + _rlp = emptyListHeadStart(); + } + + _rlp = abi.encodePacked( + _bu.bh.encodeBlockHeader().encodeBytes(), + _bu.votes.encodeVotes().encodeBytes(), + _rlp + ); + + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeReceiptProof(Types.ReceiptProof memory _rp) + internal + pure + returns (bytes memory) + { + bytes memory temp; + bytes memory _rlp; + // Serialize [bytes] which are transaction receipts + for (uint256 i = 0; i < _rp.txReceipts.length; i++) { + temp = _rp.txReceipts[i].encodeBytes(); + _rlp = abi.encodePacked(_rlp, temp); + } + _rlp = abi + .encodePacked(addLength(_rlp.length, false), _rlp) + .encodeBytes(); + + bytes memory eventProof; + for (uint256 i = 0; i < _rp.ep.length; i++) { + temp = _rp.ep[i].encodeEventProof(); + eventProof = abi.encodePacked(eventProof, temp); + } + _rlp = abi.encodePacked( + _rp.index.encodeUint(), + _rlp, + addLength(eventProof.length, false), + eventProof + ); + + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeBlockProof(Types.BlockProof memory _bp) + internal + pure + returns (bytes memory) + { + bytes memory _rlp = + abi.encodePacked( + _bp.bh.encodeBlockHeader().encodeBytes(), + _bp.bw.encodeBlockWitness().encodeBytes() + ); + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + function encodeRelayMessage(Types.RelayMessage memory _rm) + internal + pure + returns (bytes memory) + { + bytes memory temp; + bytes memory _rlp; + if (_rm.buArray.length != 0) { + for (uint256 i = 0; i < _rm.buArray.length; i++) { + temp = _rm.buArray[i].encodeBlockUpdate().encodeBytes(); + _rlp = abi.encodePacked(_rlp, temp); + } + _rlp = abi.encodePacked(addLength(_rlp.length, false), _rlp); + } else { + _rlp = emptyListShortStart(); + } + + if (_rm.isBPEmpty == false) { + temp = _rm.bp.encodeBlockProof(); + } else { + temp = emptyListHeadStart(); + } + _rlp = abi.encodePacked(_rlp, temp); + + bytes memory receiptProof; + if (_rm.isRPEmpty == false) { + for (uint256 i = 0; i < _rm.rp.length; i++) { + temp = _rm.rp[i].encodeReceiptProof().encodeBytes(); + receiptProof = abi.encodePacked(receiptProof, temp); + } + receiptProof = abi.encodePacked( + addLength(receiptProof.length, false), + receiptProof + ); + } else { + receiptProof = emptyListShortStart(); + } + _rlp = abi.encodePacked(_rlp, receiptProof); + + return abi.encodePacked(addLength(_rlp.length, false), _rlp); + } + + // Adding LIST_HEAD_START by length + // There are two cases: + // 1. List contains less than or equal 55 elements (total payload of the RLP) -> LIST_HEAD_START = LIST_SHORT_START + [0-55] = [0xC0 - 0xF7] + // 2. List contains more than 55 elements: + // - Total Payload = 512 elements = 0x0200 + // - Length of Total Payload = 2 + // => LIST_HEAD_START = \x (LIST_LONG_START + length of Total Payload) \x (Total Payload) = \x(F7 + 2) \x(0200) = \xF9 \x0200 = 0xF90200 + function addLength(uint256 length, bool isLongList) + internal + pure + returns (bytes memory) + { + if (length > 55 && !isLongList) { + bytes memory payLoadSize = RLPEncode.encodeUintByLength(length); + return + abi.encodePacked( + addLength(payLoadSize.length, true), + payLoadSize + ); + } else if (length <= 55 && !isLongList) { + return abi.encodePacked(uint8(LIST_SHORT_START + length)); + } + return abi.encodePacked(uint8(LIST_LONG_START + length)); + } + + function emptyListHeadStart() internal pure returns (bytes memory) { + bytes memory payLoadSize = RLPEncode.encodeUintByLength(0); + return + abi.encodePacked( + abi.encodePacked(uint8(LIST_LONG_START + payLoadSize.length)), + payLoadSize + ); + } + + function emptyListShortStart() internal pure returns (bytes memory) { + return abi.encodePacked(LIST_SHORT_START); + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/String.sol b/solidity/nativecoinERC20/contracts/libraries/String.sol new file mode 100644 index 00000000..55ce075c --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/String.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +/** + * String Library + * + * This is a simple library of string functions which try to simplify + * string operations in solidity. + * + * Please be aware some of these functions can be quite gas heavy so use them only when necessary + * + * The original library was modified. If you want to know more about the original version + * please check this link: https://github.com/willitscale/solidity-util.git + */ +library String { + /** + * splitBTPAddress + * + * Split the BTP Address format i.e. btp://1234.iconee/0x123456789 + * into Network_address (1234.iconee) and Server_address (0x123456789) + * + * @param _base String base BTP Address format to be split + * @dev _base must follow a BTP Address format + * + * @return string, string The resulting strings of Network_address and Server_address + */ + function splitBTPAddress(string memory _base) + internal + pure + returns (string memory, string memory) + { + string[] memory temp = split(_base, "/"); + return (temp[2], temp[3]); + } + + /** + * Concat + * + * Appends two strings together and returns a new value + * + * @param _base When being used for a data type this is the extended object + * otherwise this is the string which will be the concatenated + * prefix + * @param _value The value to be the concatenated suffix + * @return string The resulting string from combinging the base and value + */ + function concat(string memory _base, string memory _value) + internal + pure + returns (string memory) + { + return string(abi.encodePacked(_base, _value)); + } + + /** + * Index Of + * + * Locates and returns the position of a character within a string + * + * @param _base When being used for a data type this is the extended object + * otherwise this is the string acting as the haystack to be + * searched + * @param _value The needle to search for, at present this is currently + * limited to one character + * @return int The position of the needle starting from 0 and returning -1 + * in the case of no matches found + */ + function indexOf(string memory _base, string memory _value) + internal + pure + returns (int256) + { + return _indexOf(_base, _value, 0); + } + + /** + * Index Of + * + * Locates and returns the position of a character within a string starting + * from a defined offset + * + * @param _base When being used for a data type this is the extended object + * otherwise this is the string acting as the haystack to be + * searched + * @param _value The needle to search for, at present this is currently + * limited to one character + * @param _offset The starting point to start searching from which can start + * from 0, but must not exceed the length of the string + * @return int The position of the needle starting from 0 and returning -1 + * in the case of no matches found + */ + function _indexOf( + string memory _base, + string memory _value, + uint256 _offset + ) internal pure returns (int256) { + bytes memory _baseBytes = bytes(_base); + bytes memory _valueBytes = bytes(_value); + + assert(_valueBytes.length == 1); + + for (uint256 i = _offset; i < _baseBytes.length; i++) { + if (_baseBytes[i] == _valueBytes[0]) { + return int256(i); + } + } + + return -1; + } + + /** + * Length + * + * Returns the length of the specified string + * + * @param _base When being used for a data type this is the extended object + * otherwise this is the string to be measured + * @return uint The length of the passed string + */ + function length(string memory _base) internal pure returns (uint256) { + bytes memory _baseBytes = bytes(_base); + return _baseBytes.length; + } + + /* + * String Split (Very high gas cost) + * + * Splits a string into an array of strings based off the delimiter value. + * Please note this can be quite a gas expensive function due to the use of + * storage so only use if really required. + * + * @param _base When being used for a data type this is the extended object + * otherwise this is the string value to be split. + * @param _value The delimiter to split the string on which must be a single + * character + * @return string[] An array of values split based off the delimiter, but + * do not container the delimiter. + */ + function split(string memory _base, string memory _value) + internal + pure + returns (string[] memory splitArr) + { + bytes memory _baseBytes = bytes(_base); + + uint256 _offset = 0; + uint256 _splitsCount = 1; + while (_offset < _baseBytes.length - 1) { + int256 _limit = _indexOf(_base, _value, _offset); + if (_limit == -1) break; + else { + _splitsCount++; + _offset = uint256(_limit) + 1; + } + } + + splitArr = new string[](_splitsCount); + + _offset = 0; + _splitsCount = 0; + while (_offset < _baseBytes.length - 1) { + int256 _limit = _indexOf(_base, _value, _offset); + if (_limit == -1) { + _limit = int256(_baseBytes.length); + } + + string memory _tmp = new string(uint256(_limit) - _offset); + bytes memory _tmpBytes = bytes(_tmp); + + uint256 j = 0; + for (uint256 i = _offset; i < uint256(_limit); i++) { + _tmpBytes[j++] = _baseBytes[i]; + } + _offset = uint256(_limit) + 1; + splitArr[_splitsCount++] = string(_tmpBytes); + } + return splitArr; + } + + /** + * Compare To + * + * Compares the characters of two strings, to ensure that they have an + * identical footprint + * + * @param _base When being used for a data type this is the extended object + * otherwise this is the string base to compare against + * @param _value The string the base is being compared to + * @return bool Simply notates if the two string have an equivalent + */ + function compareTo(string memory _base, string memory _value) + internal + pure + returns (bool) + { + if ( + keccak256(abi.encodePacked(_base)) == + keccak256(abi.encodePacked(_value)) + ) { + return true; + } + return false; + } + + function toString(uint256 _i) internal pure returns (string memory) { + if (_i == 0) { + return "0"; + } + uint256 j = _i; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len - 1; + while (_i != 0) { + bstr[k--] = byte(uint8(48 + (_i % 10))); + _i /= 10; + } + return string(bstr); + } +} diff --git a/solidity/nativecoinERC20/contracts/libraries/Types.sol b/solidity/nativecoinERC20/contracts/libraries/Types.sol new file mode 100644 index 00000000..4e48e5f1 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/libraries/Types.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +library Types { + /** + * @Notice List of ALL Struct being used to Encode and Decode RLP Messages + */ + + // SPR = State Hash + Pathch Receipt Hash + Receipt Hash + struct SPR { + bytes stateHash; + bytes patchReceiptHash; + bytes receiptHash; + } + + struct BlockHeader { + uint256 version; + uint256 height; + uint256 timestamp; + bytes proposer; + bytes prevHash; + bytes voteHash; + bytes nextValidators; + bytes patchTxHash; + bytes txHash; + bytes logsBloom; + SPR spr; + bool isSPREmpty; // add to check whether SPR is an empty struct + // It will not be included in serializing thereafter + } + + // TS = Timestamp + Signature + struct TS { + uint256 timestamp; + bytes signature; + } + + // BPSI = blockPartSetID + struct BPSI { + uint256 n; + bytes b; + } + + struct Votes { + uint256 round; + BPSI blockPartSetID; + TS[] ts; + } + + struct BlockWitness { + uint256 height; + bytes[] witnesses; + } + + struct EventProof { + uint256 index; + bytes[] eventMptNode; + } + + struct BlockUpdate { + BlockHeader bh; + Votes votes; + bytes[] validators; + } + + struct ReceiptProof { + uint256 index; + bytes[] txReceipts; + EventProof[] ep; + } + + struct BlockProof { + BlockHeader bh; + BlockWitness bw; + } + + struct RelayMessage { + BlockUpdate[] buArray; + BlockProof bp; + bool isBPEmpty; // add to check in a case BlockProof is an empty struct + // when RLP RelayMessage, this field will not be serialized + ReceiptProof[] rp; + bool isRPEmpty; // add to check in a case ReceiptProof is an empty struct + // when RLP RelayMessage, this field will not be serialized + } + + /** + * @Notice List of ALL Structs being used by a BSH contract + */ + enum ServiceType { + REQUEST_COIN_TRANSFER, + REQUEST_COIN_REGISTER, + REPONSE_HANDLE_SERVICE, + UNKNOWN_TYPE + } + + struct PendingTransferCoin { + string from; + string to; + string[] coinNames; + uint256[] amounts; + uint256[] fees; + } + + struct TransferCoin { + string from; + string to; + Asset[] assets; + } + + struct Asset { + string coinName; + uint256 value; + } + + struct AssetTransferDetail { + string coinName; + uint256 value; + uint256 fee; + } + + struct Response { + uint256 code; + string message; + } + + struct ServiceMessage { + ServiceType serviceType; + bytes data; + } + + struct Coin { + uint256 id; + string symbol; + uint256 decimals; + } + + struct Balance { + uint256 lockedBalance; + uint256 refundableBalance; + } + + struct Request { + string serviceName; + address bsh; + } + + /** + * @Notice List of ALL Structs being used by a BMC contract + */ + + struct VerifierStats { + uint256 heightMTA; // MTA = Merkle Trie Accumulator + uint256 offsetMTA; + uint256 lastHeight; // Block height of last verified message which is BTP-Message contained + bytes extra; + } + + struct Service { + string svc; + address addr; + } + + struct Verifier { + string net; + address addr; + } + + struct Route { + string dst; // BTP Address of destination BMC + string next; // BTP Address of a BMC before reaching dst BMC + } + + struct Link { + address[] relays; // Address of multiple Relays handle for this link network + uint256 rxSeq; + uint256 txSeq; + uint256 blockIntervalSrc; + uint256 blockIntervalDst; + uint256 maxAggregation; + uint256 delayLimit; + uint256 relayIdx; + uint256 rotateHeight; + uint256 rxHeight; + uint256 rxHeightSrc; + bool isConnected; + } + + struct LinkStats { + uint256 rxSeq; + uint256 txSeq; + VerifierStats verifier; + RelayStats[] relays; + uint256 relayIdx; + uint256 rotateHeight; + uint256 rotateTerm; + uint256 delayLimit; + uint256 maxAggregation; + uint256 rxHeightSrc; + uint256 rxHeight; + uint256 blockIntervalSrc; + uint256 blockIntervalDst; + uint256 currentHeight; + } + + struct RelayStats { + address addr; + uint256 blockCount; + uint256 msgCount; + } + + struct BMCMessage { + string src; // an address of BMC (i.e. btp://1234.PARA/0x1234) + string dst; // an address of destination BMC + string svc; // service name of BSH + int256 sn; // sequence number of BMC + bytes message; // serializef Service Message from BSH + } + + struct Connection { + string from; + string to; + } + + struct EventMessage { + string eventType; + Connection conn; + } + + struct BMCService { + string serviceType; + bytes payload; + } + + struct GatherFeeMessage { + string fa; // BTP address of Fee Aggregator + string[] svcs; // a list of services + } +} diff --git a/solidity/nativecoinERC20/contracts/test/BMC.sol b/solidity/nativecoinERC20/contracts/test/BMC.sol new file mode 100644 index 00000000..8f977373 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/BMC.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; +import "../interfaces/IBSH.sol"; +import "../interfaces/IBMCPeriphery.sol"; +import "../interfaces/IBMV.sol"; +import "../libraries/ParseAddress.sol"; +import "../libraries/RLPEncodeStruct.sol"; +import "../libraries/RLPDecodeStruct.sol"; +import "../libraries/String.sol"; +import "../libraries/EncodeBase64.sol"; +import "../libraries/DecodeBase64.sol"; +import "../libraries/Types.sol"; + +contract BMC is IBMCPeriphery { + using ParseAddress for address; + using ParseAddress for string; + using RLPDecodeStruct for bytes; + using RLPEncodeStruct for Types.BMCMessage; + using RLPEncodeStruct for Types.ServiceMessage; + using RLPEncodeStruct for Types.EventMessage; + using RLPEncodeStruct for Types.Response; + using String for string; + + uint256 internal constant RC_OK = 0; + uint256 internal constant RC_ERR = 1; + + event Message( + string _next, // an address of the next BMC (it could be a destination BMC) + uint256 _seq, // a sequence number of BMC (NOT sequence number of BSH) + bytes _msg + ); + + // emit EVENT to announce link/unlink service + event Event(string _next, uint256 _seq, bytes _msg); + + event ErrorOnBTPError( + string _svc, + int256 _sn, + uint256 _code, + string _errMsg, + uint256 _svcErrCode, + string _svcErrMsg + ); + + mapping(address => bool) private _owners; + uint256 private numOfOwner; + + mapping(string => address) internal bshServices; + mapping(string => address) private bmvServices; + mapping(string => string) private connectedBMC; + mapping(address => Types.RelayStats) private relayStats; + mapping(string => string) private routes; + mapping(string => Types.Link) internal links; // should be private, temporarily set internal for testing + mapping(string => string[]) private reachable; + string[] private listBMVNames; + string[] private listBSHNames; + string[] private listRouteKeys; + string[] private listLinkNames; + address[] private addrs; + + string public bmcAddress; // a network address BMV, i.e. btp://1234.pra + uint256 private numOfBMVService; + uint256 private numOfBSHService; + uint256 private numOfLinks; + uint256 private numOfRoutes; + + uint256 private constant BLOCK_INTERVAL_MSEC = 1000; + uint256 internal constant UNKNOWN_ERR = 0; + uint256 internal constant BMC_ERR = 10; + uint256 internal constant BMV_ERR = 25; + uint256 internal constant BSH_ERR = 40; + uint256 private constant DECIMAL_PRECISION = 10**6; + + modifier owner { + require(_owners[msg.sender] == true, "BMCRevertUnauthorized"); + _; + } + + constructor(string memory _network) { + bmcAddress = string("btp://").concat(_network).concat("/").concat( + address(this).toString() + ); + _owners[msg.sender] = true; + numOfOwner++; + } + + /***************************************************************************************** + + *****************************************************************************************/ + function getBmcBtpAddress() external view override returns (string memory) { + return bmcAddress; + } + + function handleRelayMessage(string calldata _prev, string calldata _msg) + external + override + {} + + function handleMessage(string calldata _prev, Types.BMCMessage memory _msg) + internal + { + if (_msg.svc.compareTo("bmc")) { + Types.BMCService memory _sm; + try this.tryDecodeBMCService(_msg.message) returns ( + Types.BMCService memory ret + ) { + _sm = ret; + } catch { + _sendError(_prev, _msg, BMC_ERR, "BMCRevertParseFailure"); + } + if (_sm.serviceType.compareTo("FeeGathering")) { + Types.GatherFeeMessage memory _gatherFee; + try this.tryDecodeGatherFeeMessage(_sm.payload) returns ( + Types.GatherFeeMessage memory ret + ) { + _gatherFee = ret; + } catch { + _sendError(_prev, _msg, BMC_ERR, "BMCRevertParseFailure"); + } + for (uint256 i = 0; i < _gatherFee.svcs.length; i++) { + // If 'svc' not found, ignore + if (bshServices[_gatherFee.svcs[i]] != address(0)) { + try + IBSH(bshServices[_gatherFee.svcs[i]]) + .handleFeeGathering( + _gatherFee.fa, + _gatherFee.svcs[i] + ) + {} catch { + // If BSH contract throws a revert error, ignore and continue + } + } + } + } + } else { + if (bshServices[_msg.svc] == address(0)) { + _sendError(_prev, _msg, BMC_ERR, "BMCRevertNotExistsBSH"); + return; + } + + if (_msg.sn >= 0) { + (string memory _net, ) = _msg.src.splitBTPAddress(); + try + IBSH(bshServices[_msg.svc]).handleBTPMessage( + _net, + _msg.svc, + uint256(_msg.sn), + _msg.message + ) + {} catch Error(string memory _error) { + _sendError(_prev, _msg, BSH_ERR, _error); + } + } else { + Types.Response memory _errMsg = _msg.message.decodeResponse(); + try + IBSH(bshServices[_msg.svc]).handleBTPError( + _msg.src, + _msg.svc, + uint256(int256(_msg.sn) * -1), + _errMsg.code, + _errMsg.message + ) + {} catch Error(string memory _error) { + emit ErrorOnBTPError( + _msg.svc, + int256(_msg.sn) * -1, + _errMsg.code, + _errMsg.message, + BSH_ERR, + _error + ); + } catch (bytes memory _error) { + emit ErrorOnBTPError( + _msg.svc, + int256(_msg.sn) * -1, + _errMsg.code, + _errMsg.message, + UNKNOWN_ERR, + string(_error) + ); + } + } + } + } + + // @dev Solidity does not allow using try_catch with internal function + // Thus, work-around solution is the followings + // If there is any error throwing, BMC contract can catch it, then reply back a RC_ERR Response + function tryDecodeBMCService(bytes calldata _msg) + external + pure + returns (Types.BMCService memory) + { + return _msg.decodeBMCService(); + } + + function tryDecodeGatherFeeMessage(bytes calldata _msg) + external + pure + returns (Types.GatherFeeMessage memory) + { + return _msg.decodeGatherFeeMessage(); + } + + function _sendMessage(string memory _to, bytes memory _serializedMsg) + internal + { + links[_to].txSeq += 1; + emit Message(_to, links[_to].txSeq, _serializedMsg); + } + + function _sendError( + string calldata _prev, + Types.BMCMessage memory _message, + uint256 _errCode, + string memory _errMsg + ) internal { + if (_message.sn > 0) { + bytes memory _serializedMsg = + Types + .BMCMessage( + bmcAddress, + _message + .src, + _message + .svc, + int256(_message.sn) * -1, + Types + .ServiceMessage( + Types + .ServiceType + .REPONSE_HANDLE_SERVICE, + Types.Response(_errCode, _errMsg).encodeResponse() + ) + .encodeServiceMessage() + ) + .encodeBMCMessage(); + _sendMessage(_prev, _serializedMsg); + } + } + + /** + @notice Send the message to a specific network. + @dev Caller must be an registered BSH. + @param _to Network Address of destination network + @param _svc Name of the service + @param _sn Serial number of the message, it should be positive + @param _msg Serialized bytes of Service Message + */ + function sendMessage( + string memory _to, + string memory _svc, + uint256 _sn, + bytes memory _msg + ) external override { + require( + msg.sender == address(this) || bshServices[_svc] == msg.sender, + "BMCRevertUnauthorized" + ); + require(_sn >= 0, "BMCRevertInvalidSN"); + // In case BSH sends a REQUEST_COIN_TRANSFER, + // but '_to' is a network which is not supported by BMC + // revert() therein + if (bmvServices[_to] == address(0)) { + revert("BMCRevertNotExistsBMV"); + } + string memory _toBMC = connectedBMC[_to]; + bytes memory _rlp = + Types + .BMCMessage(bmcAddress, _toBMC, _svc, int256(_sn), _msg) + .encodeBMCMessage(); + if (_svc.compareTo("_EVENT")) { + links[_toBMC].txSeq += 1; + emit Event(_toBMC, links[_toBMC].txSeq, _rlp); + } else { + links[_toBMC].txSeq += 1; + emit Message(_toBMC, links[_toBMC].txSeq, _rlp); + } + } + + /** + @notice Add the smart contract for the service. + @dev Caller must be an operator of BTP network. + @param _svc Name of the service + @param _addr Service's contract address + */ + function addService(string memory _svc, address _addr) external owner { + require(_addr != address(0), "BMCRevertInvalidAddress"); + require(bshServices[_svc] == address(0), "BMCRevertAlreadyExistsBSH"); + bshServices[_svc] = _addr; + listBSHNames.push(_svc); + } + + /** + @notice Registers BMV for the network. + @dev Caller must be an operator of BTP network. + @param _net Network Address of the blockchain + @param _addr Address of BMV + */ + function addVerifier(string memory _net, address _addr) external owner { + require(bmvServices[_net] == address(0), "BMCRevertAlreadyExistsBMV"); + bmvServices[_net] = _addr; + listBMVNames.push(_net); + numOfBMVService++; + } + + /** + @notice Initializes status information for the link. + @dev Caller must be an operator of BTP network. + @param _link BTP Address of connected BMC + */ + function addLink(string calldata _link) external owner { + string memory _net; + (_net, ) = _link.splitBTPAddress(); + require(bmvServices[_net] != address(0), "BMCRevertNotExistsBMV"); + require( + links[_link].isConnected == false, + "BMCRevertAlreadyExistsLink" + ); + links[_link] = Types.Link( + new address[](0), + 0, + 0, + BLOCK_INTERVAL_MSEC, + 0, + 10, + 3, + 0, + 0, + 0, + 0, + true + ); + listLinkNames.push(_link); + connectedBMC[_net] = _link; + numOfLinks++; + + // propagate an event "LINK" + propagateEvent("Link", _link); + } + + function propagateEvent(string memory _eventType, string calldata _link) + private + {} + + /* + @notice Get status of BMC. + @param _link BTP Address of the connected BMC. + @return tx_seq Next sequence number of the next sending message. + @return rx_seq Next sequence number of the message to receive. + @return verifier VerifierStatus Object contains status information of the BMV. + */ + function getStatus(string calldata _link) + external + view + override + returns (Types.LinkStats memory _linkStats) + { + _linkStats.txSeq = links[_link].txSeq; + } +} diff --git a/solidity/nativecoinERC20/contracts/test/CheckParseAddress.sol b/solidity/nativecoinERC20/contracts/test/CheckParseAddress.sol new file mode 100644 index 00000000..b319482e --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/CheckParseAddress.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "../libraries/ParseAddress.sol"; + +contract CheckParseAddress { + using ParseAddress for address; + using ParseAddress for string; + + function convertAddressToString(address _addr) + external + pure + returns (string memory strAddr) + { + strAddr = _addr.toString(); + } + + function convertStringToAddress(string calldata _addr) + external + pure + returns (address addr) + { + addr = _addr.parseAddress(); + } +} diff --git a/solidity/nativecoinERC20/contracts/test/EncodeMessage.sol b/solidity/nativecoinERC20/contracts/test/EncodeMessage.sol new file mode 100644 index 00000000..2dc4a2b2 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/EncodeMessage.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; +import "../libraries/ParseAddress.sol"; +import "../libraries/RLPEncodeStruct.sol"; +import "../libraries/RLPDecodeStruct.sol"; +import "../libraries/String.sol"; +import "../libraries/Types.sol"; + +contract EncodeMessage { + using RLPEncodeStruct for Types.BMCMessage; + using RLPEncodeStruct for Types.ServiceMessage; + using RLPEncodeStruct for Types.TransferCoin; + using RLPEncodeStruct for Types.Response; + using RLPEncodeStruct for Types.GatherFeeMessage; + using RLPEncodeStruct for Types.BMCService; + using RLPEncodeStruct for string[]; + using ParseAddress for address; + using ParseAddress for string; + using String for string; + + function encodeTransferFeesBMCMessage( + string memory _fromBMC, + string memory _toBMC, + string memory _toAddress, + string memory _svc, + int256 _sn, + address _bsh, + Types.Asset[] memory _fees + ) external pure returns (bytes memory) { + (, string memory _to) = _toAddress.splitBTPAddress(); + return + Types + .BMCMessage( + _fromBMC, + _toBMC, + _svc, + _sn, + encodeServiceMessage(_bsh.toString(), _to, _fees) + ) + .encodeBMCMessage(); + } + + function encodeBMCService(string calldata _fa, string[] memory _svcs) + external + pure + returns (bytes memory) + { + return + Types + .BMCService( + "FeeGathering", + Types.GatherFeeMessage(_fa, _svcs).encodeGatherFeeMessage() + ) + .encodeBMCService(); + } + + function encodeResponseBMCMessage( + string memory _from, + string memory _to, + string memory _svc, + int256 _sn, + uint256 _code, + string memory _msg + ) external view returns (bytes memory) { + return + Types + .BMCMessage( + _from, + _to, + _svc, + _sn, + this.encodeResponseMsg( + Types.ServiceType.REPONSE_HANDLE_SERVICE, + _code, + _msg + ) + ) + .encodeBMCMessage(); + } + + function hashCoinName(string memory _coinName) + external + pure + returns (uint256) + { + return uint256(keccak256(abi.encodePacked(_coinName))); + } + + function encodeResponseMsg( + Types.ServiceType _serviceType, + uint256 _code, + string calldata _msg + ) external pure returns (bytes memory) { + return + Types + .ServiceMessage( + _serviceType, + Types.Response(_code, _msg).encodeResponse() + ) + .encodeServiceMessage(); + } + + function encodeBatchTransferMsgWithAddress( + string calldata _from, + address _to, + Types.Asset[] memory _assets + ) external pure returns (bytes memory) { + return encodeServiceMessage(_from, _to.toString(), _assets); + } + + function encodeBatchTransferMsgWithStringAddress( + string calldata _from, + string calldata _to, + Types.Asset[] memory _assets + ) external pure returns (bytes memory) { + return encodeServiceMessage(_from, _to, _assets); + } + + function encodeTransferMsgWithAddress( + string calldata _from, + address _to, + string memory _coinName, + uint256 _value + ) external pure returns (bytes memory) { + Types.Asset[] memory asset = new Types.Asset[](1); + asset[0] = Types.Asset(_coinName, _value); + return encodeServiceMessage(_from, _to.toString(), asset); + } + + function encodeTransferMsgWithStringAddress( + string calldata _from, + string calldata _to, + string calldata _coinName, + uint256 _value + ) external pure returns (bytes memory) { + Types.Asset[] memory asset = new Types.Asset[](1); + asset[0] = Types.Asset(_coinName, _value); + return encodeServiceMessage(_from, _to, asset); + } + + function encodeServiceMessage( + string memory _from, + string memory _to, + Types.Asset[] memory _asset + ) private pure returns (bytes memory) { + return + Types + .ServiceMessage( + Types + .ServiceType + .REQUEST_COIN_TRANSFER, + Types.TransferCoin(_from, _to, _asset).encodeTransferCoinMsg() + ) + .encodeServiceMessage(); + } +} diff --git a/solidity/nativecoinERC20/contracts/test/Holder.sol b/solidity/nativecoinERC20/contracts/test/Holder.sol new file mode 100644 index 00000000..14ed74e3 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/Holder.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155Holder.sol"; +import "../interfaces/IBSHPeriphery.sol"; +import "../interfaces/IBSHCore.sol"; +import "../libraries/String.sol"; + +contract Holder is ERC1155Holder { + IBSHPeriphery private bshp; + IBSHCore private bshc; + using String for string; + + function deposit() external payable {} + + function addBSHContract(address _bshp, address _bshc) external { + bshp = IBSHPeriphery(_bshp); + bshc = IBSHCore(_bshc); + } + + function callTransfer( + string calldata _coinName, + uint256 _value, + string calldata _to + ) external { + bshc.transfer(_coinName, _value, _to); + } + + // function isSendingNative(string[] memory _coinNames) + // private + // pure + // returns (int256) + // { + // for (uint256 i = 0; i < _coinNames.length; i++) { + // if (_coinNames[i].compareTo("PARA")) { + // return int256(i); + // } + // } + // return -1; + // } +} diff --git a/solidity/nativecoinERC20/contracts/test/MockBMC.sol b/solidity/nativecoinERC20/contracts/test/MockBMC.sol new file mode 100644 index 00000000..9f9eff1d --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/MockBMC.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; +import "./BMC.sol"; + +contract MockBMC is BMC { + constructor(string memory _network) BMC(_network) {} + + function receiveRequest( + string calldata _src, + string memory _dst, + string memory _svc, + uint256 _sn, + bytes calldata _msg + ) external { + handleMessage( + _src, + Types.BMCMessage(_src, _dst, _svc, int256(_sn), _msg) + ); + } + + function receiveResponse( + string calldata _from, + string memory _svc, + uint256 _sn, + bytes calldata _msg + ) external { + IBSH(bshServices[_svc]).handleBTPMessage(_from, _svc, _sn, _msg); + } + + function getBalance(address _addr) external view returns (uint256) { + return _addr.balance; + } +} diff --git a/solidity/nativecoinERC20/contracts/test/MockBSHCore.sol b/solidity/nativecoinERC20/contracts/test/MockBSHCore.sol new file mode 100644 index 00000000..848f4806 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/MockBSHCore.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; +import "../BSHCore.sol"; + +contract MockBSHCore is BSHCore { + function mintMock( + address _acc, + uint256 _id, + uint256 _value + ) external { + _mint(_acc, _value); + } + + function burnMock( + address _acc, + uint256 _id, + uint256 _value + ) external { + _burn(_acc, _value); + } + + function setAggregationFee(string calldata _coinName, uint256 _value) + external + { + aggregationFee[_coinName] += _value; + } + + function clearAggregationFee() external { + for (uint256 i = 0; i < coinsName.length; i++) { + delete aggregationFee[coinsName[i]]; + } + } + + function clearBSHPerifSetting() external { + bshPeriphery = IBSHPeriphery(address(0)); + } + + function setRefundableBalance( + address _acc, + string calldata _coinName, + uint256 _value + ) external { + balances[_acc][_coinName].refundableBalance += _value; + } +} diff --git a/solidity/nativecoinERC20/contracts/test/MockBSHPerif.sol b/solidity/nativecoinERC20/contracts/test/MockBSHPerif.sol new file mode 100644 index 00000000..8fe1a50f --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/MockBSHPerif.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; +import "../BSHPeriphery.sol"; +import "../BSHCore.sol"; + +contract MockBSHPeriphery is BSHPeriphery { + using String for string; + + function getFees(uint256 _sn) + external + view + returns (Types.PendingTransferCoin memory) + { + return requests[_sn]; + } + + function getAggregationFeeOf(string calldata _coinName) + external + view + returns (uint256 _fee) + { + Types.Asset[] memory _fees = bshCore.getAccumulatedFees(); + for (uint256 i = 0; i < _fees.length; i++) { + if (_coinName.compareTo(_fees[i].coinName)) return _fees[i].value; + } + } +} diff --git a/solidity/nativecoinERC20/contracts/test/NonRefundable.sol b/solidity/nativecoinERC20/contracts/test/NonRefundable.sol new file mode 100644 index 00000000..27308a66 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/NonRefundable.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +// This contract does not have any method +// that allows to receives coins, i.e. receive() external payable/fallback() external payable +// Instead, it has a method deposit() payable to receive coins +contract NonRefundable { + function deposit() external payable {} + + function transfer( + address _bsh, + string calldata _to, + uint256 _amt + ) external { + (bool success, bytes memory err) = + _bsh.call{value: _amt}( + abi.encodeWithSignature("transferNativeCoin(string)", _to) + ); + require(success, string(err)); + } +} diff --git a/solidity/nativecoinERC20/contracts/test/NotPayable.sol b/solidity/nativecoinERC20/contracts/test/NotPayable.sol new file mode 100644 index 00000000..cb4d6d77 --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/NotPayable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +// This contract does not have any method +// that allows to receives coins, i.e. receive() external payable/fallback() external payable +contract NotPayable { + +} diff --git a/solidity/nativecoinERC20/contracts/test/Refundable.sol b/solidity/nativecoinERC20/contracts/test/Refundable.sol new file mode 100644 index 00000000..79db438a --- /dev/null +++ b/solidity/nativecoinERC20/contracts/test/Refundable.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.8.0; + +// This contract does not have any method +// that allows to receives coins, i.e. receive() external payable/fallback() external payable +// Instead, it has a method deposit() payable to receive coins +contract Refundable { + function deposit() external payable {} + + function transfer( + address _bsh, + string calldata _to, + uint256 _amt + ) external { + (bool success, bytes memory err) = + _bsh.call{value: _amt}( + abi.encodeWithSignature("transferNativeCoin(string)", _to) + ); + require(success, string(err)); + } + + receive() external payable {} +} diff --git a/solidity/nativecoinERC20/migrations/1_deploy_bsh.js b/solidity/nativecoinERC20/migrations/1_deploy_bsh.js new file mode 100644 index 00000000..d372eba9 --- /dev/null +++ b/solidity/nativecoinERC20/migrations/1_deploy_bsh.js @@ -0,0 +1,12 @@ +const BSHPeriphery = artifacts.require("BSHPeriphery"); +const BSHCore = artifacts.require("BSHCore"); +const { deployProxy } = require('@openzeppelin/truffle-upgrades'); + +module.exports = async function (deployer, network) { + if (network !== "development") { + await deployProxy(BSHCore, [process.env.BSH_COIN_URL, process.env.BSH_COIN_NAME, parseInt(process.env.BSH_COIN_FEE), parseInt(process.env.BSH_FIXED_FEE)], { deployer }); + await deployProxy(BSHPeriphery, [process.env.BMC_PERIPHERY_ADDRESS, BSHCore.address, process.env.BSH_SERVICE], { deployer }); + const bshCore = await BSHCore.deployed(); + await bshCore.updateBSHPeriphery(BSHPeriphery.address); + } +}; diff --git a/solidity/nativecoinERC20/package.json b/solidity/nativecoinERC20/package.json new file mode 100644 index 00000000..d6a1930c --- /dev/null +++ b/solidity/nativecoinERC20/package.json @@ -0,0 +1,34 @@ +{ + "name": "nativecoin-erc20", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@openzeppelin/contracts": "^3.4.1-solc-0.7-2", + "@openzeppelin/contracts-upgradeable": "^3.4.1-solc-0.7-2", + "@openzeppelin/truffle-upgrades": "^1.7.0", + "@truffle/hdwallet-provider": "^1.4.1" + }, + "devDependencies": { + "chai": "^4.3.4", + "husky": "^6.0.0", + "prettier": "^2.2.1", + "prettier-plugin-solidity": "^1.0.0-beta.7", + "rlp": "^2.2.6", + "solhint": "^3.3.4", + "solhint-plugin-prettier": "^0.0.5", + "truffle-assertions": "^0.9.2" + }, + "scripts": { + "linter": "./node_modules/.bin/solhint -f table ./contracts/**/*.sol -f table ./contracts/*.sol", + "prettier": "./node_modules/.bin/prettier --write ./contracts -l", + "contract:compile": "truffle compile --all", + "test": "yarn test:unit && yarn test:integration", + "test:unit": "rm -rf .openzeppelin && truffle test test/unit/*.js", + "test:integration": "rm -rf .openzeppelin && truffle test test/integration/*.js" + }, + "husky": { + "hooks": { + "pre-push": "yarn linter && yarn prettier" + } + } +} diff --git a/solidity/nativecoinERC20/test/integration/2-native-coin-bsh.js b/solidity/nativecoinERC20/test/integration/2-native-coin-bsh.js new file mode 100644 index 00000000..51566f5b --- /dev/null +++ b/solidity/nativecoinERC20/test/integration/2-native-coin-bsh.js @@ -0,0 +1,328 @@ +const MockBSHPeriphery = artifacts.require("MockBSHPeriphery"); +const BSHPeriphery = artifacts.require("BSHPeriphery"); +const BSHCore = artifacts.require("BSHCore"); +const BMC = artifacts.require("MockBMC"); +const Holder = artifacts.require("Holder"); +const NotPayable = artifacts.require("NotPayable"); +const NonRefundable = artifacts.require("NonRefundable"); +const Refundable = artifacts.require("Refundable"); +const EncodeMsg = artifacts.require("EncodeMessage"); +const { assert, AssertionError } = require('chai'); +const truffleAssert = require('truffle-assertions'); +const rlp = require('rlp'); + +let toHex = (buf) => { + buf = buf.toString('hex'); + if (buf.substring(0, 2) == '0x') + return buf; + return '0x' + buf.toString('hex'); +}; + +contract('As a user, I want to send MOVR to ICON blockchain', (accounts) => { + let bsh_perif, bsh_core, bmc, nonrefundable, refundable; + let service = 'Coin/WrappedCoin'; let _bmcICON = 'btp://1234.iconee/0x1234567812345678'; + let _net = '1234.iconee'; let _to = 'btp://1234.iconee/0x12345678'; + let RC_OK = 0; let RC_ERR = 1; + let _native = 'MOVR'; let deposit = 1000000000000; + let _fee = 10; let _fixed_fee = 500000; + let REPONSE_HANDLE_SERVICE = 2; let _uri = 'https://github.com/icon-project/btp'; + let _tokenName = 'ICX'; + let _tokenSymbol = 'ICX'; + let _initialSupply = 1; + + let DECIMALS = 18; + let INITIAL_SUPPLY = web3.utils.toBN(100000) // 100000 tokens + before(async () => { + bsh_perif = await BSHPeriphery.new(); + bsh_core = await BSHCore.new(); + bmc = await BMC.new('1234.movr'); + encode_msg = await EncodeMsg.new(); + await bsh_perif.initialize(bmc.address, bsh_core.address, service); + await bsh_core.initialize(_native, _fee, _fixed_fee, _tokenName, _tokenSymbol, INITIAL_SUPPLY); + await bsh_core.updateBSHPeriphery(bsh_perif.address); + nonrefundable = await NonRefundable.new(); + refundable = await Refundable.new(); + await bmc.addService(service, bsh_perif.address); + await bmc.addVerifier(_net, accounts[1]); + await bmc.addLink(_bmcICON); + }); + + it('Scenario 5: Account client transfers a valid native coin to a side chain', async () => { + let amount = 600000; + let account_balanceBefore = await bsh_core.getBalanceOf(accounts[0], _native); + let tx = await bsh_core.transferNativeCoin(_to, { from: accounts[0], value: amount }); + let account_balanceAfter = await bsh_core.getBalanceOf(accounts[0], _native); + let bsh_coin_balance = await bsh_core.getBalanceOf(bsh_core.address, _native); + let chargedFee = Math.floor(amount / 1000) + _fixed_fee; + + const transferEvents = await bsh_perif.getPastEvents('TransferStart', { fromBlock: tx.receipt.blockNumber, toBlock: 'latest' }); + let event = transferEvents[0].returnValues; + assert.equal(event._from, accounts[0]); + assert.equal(event._to, _to); + assert.equal(event._sn, 1); + assert.equal(event._assetDetails.length, 1); + assert.equal(event._assetDetails[0].coinName, 'MOVR'); + assert.equal(event._assetDetails[0].value, amount - chargedFee); + assert.equal(event._assetDetails[0].fee, chargedFee); + + const linkStatus = await bmc.getStatus(_bmcICON); + const bmcBtpAddress = await bmc.getBmcBtpAddress(); + + const messageEvents = await bmc.getPastEvents('Message', { fromBlock: tx.receipt.blockNumber, toBlock: 'latest' }); + event = messageEvents[0].returnValues; + assert.equal(event._next, _bmcICON); + assert.equal(event._seq, linkStatus.txSeq); + + const bmcMsg = rlp.decode(event._msg); + + assert.equal(web3.utils.hexToUtf8(toHex(bmcMsg[0])), bmcBtpAddress); + assert.equal(web3.utils.hexToUtf8(toHex(bmcMsg[1])), _bmcICON); + assert.equal(web3.utils.hexToUtf8(toHex(bmcMsg[2])), service); + assert.equal(web3.utils.hexToNumber(toHex(bmcMsg[3])), 1); + + const ServiceMsg = rlp.decode(bmcMsg[4]); + assert.equal(web3.utils.hexToUtf8(toHex(ServiceMsg[0])), 0); + + const coinTransferMsg = rlp.decode(ServiceMsg[1]); + assert.equal(web3.utils.hexToUtf8(toHex(coinTransferMsg[0])), accounts[0]); + assert.equal(web3.utils.hexToUtf8(toHex(coinTransferMsg[1])), _to.split('/').slice(-1)[0]); + assert.equal(web3.utils.hexToUtf8(toHex(coinTransferMsg[2][0][0])), _native); + assert.equal(web3.utils.hexToNumber(toHex(coinTransferMsg[2][0][1])), amount - chargedFee); + + assert( + web3.utils.BN(bsh_coin_balance._usableBalance).toNumber() === amount && + web3.utils.BN(account_balanceBefore._lockedBalance).toNumber() === 0 && + web3.utils.BN(account_balanceAfter._lockedBalance).toNumber() === amount + ); + }); + + it('Scenario 6: BSHPeriphery receives a successful response of a recent request', async () => { + let amount = 600000; + let account_balanceBefore = await bsh_core.getBalanceOf(accounts[0], _native); + let _msg = await encode_msg.encodeResponseMsg(REPONSE_HANDLE_SERVICE, RC_OK, ""); + let tx = await bmc.receiveResponse(_net, service, 1, _msg); + let account_balanceAfter = await bsh_core.getBalanceOf(accounts[0], _native); + let fees = await bsh_core.getAccumulatedFees(); + + const transferEvents = await bsh_perif.getPastEvents('TransferEnd', { fromBlock: tx.receipt.blockNumber, toBlock: 'latest' }); + let event = transferEvents[0].returnValues; + + assert.equal(event._from, accounts[0]); + assert.equal(event._sn, 1); + assert.equal(event._code, 0); + assert.equal(event._response, ''); + + assert( + fees[0].coinName === _native && + Number(fees[0].value) === (Math.floor(amount / 1000) + _fixed_fee) && + web3.utils.BN(account_balanceBefore._lockedBalance).toNumber() === amount && + web3.utils.BN(account_balanceAfter._lockedBalance).toNumber() === 0 + ); + }); +}); + +contract('As a user, I want to send ERC20_ICX to ICON blockchain', (accounts) => { + let bsh_perif, bsh_core, bmc, holder; + let service = 'Coin/WrappedCoin'; + let _native = 'MOVR'; let _fee = 10; let _fixed_fee = 500000; let _bmcICON = 'btp://1234.iconee/0x1234567812345678'; + let _net = '1234.iconee'; let _from = '0x12345678'; let _value = 999999999999999; + let REPONSE_HANDLE_SERVICE = 2; let RC_OK = 0; let RC_ERR = 1; + let _tokenName = 'ICX'; + let _tokenSymbol = 'ICX'; + let INITIAL_SUPPLY = web3.utils.toBN(10000000000000) // 100000 tokens + + before(async () => { + bsh_perif = await BSHPeriphery.new(); + bsh_core = await BSHCore.new(); + bmc = await BMC.new('1234.movr'); + encode_msg = await EncodeMsg.new(); + await bsh_perif.initialize(bmc.address, bsh_core.address, service); + await bsh_core.initialize(_native, _fee, _fixed_fee, _tokenName, _tokenSymbol, INITIAL_SUPPLY); + await bsh_core.updateBSHPeriphery(bsh_perif.address); + holder = await Holder.new(); + await bmc.addService(service, bsh_perif.address); + await bmc.addVerifier(_net, accounts[1]); + await bmc.addLink(_bmcICON); + await holder.addBSHContract(bsh_perif.address, bsh_core.address); + let _msg = await encode_msg.encodeTransferMsgWithAddress(_from, holder.address, _tokenName, _value); + await bmc.receiveRequest(_bmcICON, "", service, 0, _msg); + let balanceBefore = await bsh_core.balanceOf(accounts[0]); + }); + + it('Scenario 8: User sends a valid transferring request', async () => { + let _to = 'btp://1234.iconee/0x12345678'; + let amount = 600000; + await bsh_core.transfer(accounts[1], amount); + let balanceBefore = await bsh_core.getBalanceOf(accounts[1], _tokenName); + await bsh_core.approve(bsh_core.address, amount, { from: accounts[1] }); + const data = await bsh_core.contract.methods["transfer(string,uint256,string)"](_tokenName, amount, _to).encodeABI(); + let tx = await bsh_core.sendTransaction({ data, from: accounts[1] }); + //let tx = await bsh_core.transfer(_tokenName, amount, _to, {from: accounts[1]}); + let balanceAfter = await bsh_core.getBalanceOf(accounts[1], _tokenName); + let bsh_core_balance = await bsh_core.getBalanceOf(bsh_core.address, _tokenName); + let chargedFee = Math.floor(amount / 1000) + _fixed_fee; + + const transferEvents = await bsh_perif.getPastEvents('TransferStart', { fromBlock: tx.receipt.blockNumber, toBlock: 'latest' }); + let event = transferEvents[0].returnValues; + assert.equal(event._from, accounts[1]); + assert.equal(event._to, _to); + assert.equal(event._sn, 1); + assert.equal(event._assetDetails.length, 1); + assert.equal(event._assetDetails[0].coinName, _tokenName); + assert.equal(event._assetDetails[0].value, amount - chargedFee); + assert.equal(event._assetDetails[0].fee, chargedFee); + + const linkStatus = await bmc.getStatus(_bmcICON); + const bmcBtpAddress = await bmc.getBmcBtpAddress(); + + const messageEvents = await bmc.getPastEvents('Message', { fromBlock: tx.receipt.blockNumber, toBlock: 'latest' }); + event = messageEvents[0].returnValues; + assert.equal(event._next, _bmcICON); + assert.equal(event._seq, linkStatus.txSeq); + + const bmcMsg = rlp.decode(event._msg); + + assert.equal(web3.utils.hexToUtf8(toHex(bmcMsg[0])), bmcBtpAddress); + assert.equal(web3.utils.hexToUtf8(toHex(bmcMsg[1])), _bmcICON); + assert.equal(web3.utils.hexToUtf8(toHex(bmcMsg[2])), service); + assert.equal(web3.utils.hexToNumber(toHex(bmcMsg[3])), 1); + + const ServiceMsg = rlp.decode(bmcMsg[4]); + assert.equal(web3.utils.hexToUtf8(toHex(ServiceMsg[0])), 0); + + const coinTransferMsg = rlp.decode(ServiceMsg[1]); + assert.equal(web3.utils.hexToUtf8(toHex(coinTransferMsg[0])), accounts[1]); + assert.equal(web3.utils.hexToUtf8(toHex(coinTransferMsg[1])), _to.split('/').slice(-1)[0]); + assert.equal(web3.utils.hexToUtf8(toHex(coinTransferMsg[2][0][0])), _tokenName); + assert.equal(web3.utils.hexToNumber(toHex(coinTransferMsg[2][0][1])), amount - chargedFee); + + assert.equal(web3.utils.BN(balanceBefore._lockedBalance).toNumber(), 0); + assert.equal(web3.utils.BN(balanceAfter._lockedBalance).toNumber(), amount); + assert.equal( + web3.utils.BN(balanceAfter._usableBalance).toNumber(), + web3.utils.BN(balanceBefore._usableBalance).toNumber() - amount + ); + assert.equal(web3.utils.BN(bsh_core_balance._usableBalance).toNumber(), amount); + }); + + it('Scenario 9: BSHPeriphery receives a successful response of a recent request', async () => { + let amount = 600000; + let chargedFee = Math.floor(amount / 1000) + _fixed_fee; + let contract_balanceBefore = await bsh_core.getBalanceOf(accounts[1], _tokenName); + let _msg = await encode_msg.encodeResponseMsg(REPONSE_HANDLE_SERVICE, RC_OK, ""); + let tx = await bmc.receiveResponse(_net, service, 1, _msg); + let contract_balanceAfter = await bsh_core.getBalanceOf(accounts[1], _tokenName); + let fees = await bsh_core.getAccumulatedFees(); + let bsh_core_balance = await bsh_core.getBalanceOf(bsh_core.address, _tokenName); + + const transferEvents = await bsh_perif.getPastEvents('TransferEnd', { fromBlock: tx.receipt.blockNumber, toBlock: 'latest' }); + let event = transferEvents[0].returnValues; + + assert.equal(event._from, accounts[1]); + assert.equal(event._sn, 1); + assert.equal(event._code, 0); + assert.equal(event._response, ''); + + assert.equal(web3.utils.BN(contract_balanceBefore._lockedBalance).toNumber(), amount); + assert.equal(web3.utils.BN(contract_balanceAfter._lockedBalance).toNumber(), 0); + assert.equal( + web3.utils.BN(contract_balanceBefore._usableBalance).toNumber(), + web3.utils.BN(contract_balanceAfter._usableBalance).toNumber() + ); + assert.equal(web3.utils.BN(bsh_core_balance._usableBalance).toNumber(), chargedFee); + assert.equal(fees[1].coinName, _tokenName);//todo: check this + assert.equal(Number(fees[1].value), chargedFee) + }); +}); + +contract('As a user, I want to receive MOVR from ICON blockchain', (accounts) => { + let bmc, bsh_perif, bsh_core, notpayable, refundable; + let service = 'Coin/WrappedCoin'; let _bmcICON = 'btp://1234.iconee/0x1234567812345678'; + let _net = '1234.iconee'; let _to = 'btp://1234.iconee/0x12345678'; + let _native = 'MOVR'; let _fee = 10; let _fixed_fee = 500000; + let RC_ERR = 1; let RC_OK = 0; + let _uri = 'https://github.com/icon-project/btp'; + let _tokenName = 'ICX'; + let _tokenSymbol = 'ICX'; + let INITIAL_SUPPLY = web3.utils.toBN(10000000000000) // 100000 tokens + + before(async () => { + bsh_perif = await BSHPeriphery.new(); + bsh_core = await BSHCore.new(); + bmc = await BMC.new('1234.movr'); + encode_msg = await EncodeMsg.new(); + await bsh_perif.initialize(bmc.address, bsh_core.address, service); + await bsh_core.initialize(_native, _fee, _fixed_fee, _tokenName, _tokenSymbol, INITIAL_SUPPLY); + await bsh_core.updateBSHPeriphery(bsh_perif.address); + notpayable = await NotPayable.new(); + refundable = await Refundable.new(); + await bmc.addService(service, bsh_perif.address); + await bmc.addVerifier(_net, accounts[1]); + await bmc.addLink(_bmcICON); + await bsh_core.transferNativeCoin(_to, { from: accounts[0], value: 100000000 }); + btpAddr = await bmc.bmcAddress(); + }); + + it('Scenario 4: BSHPeriphery receives a request of transferring coins', async () => { + let _from = '0x12345678'; + let _value = 12345; + let balanceBefore = await bmc.getBalance(accounts[1]); + let _eventMsg = await encode_msg.encodeResponseBMCMessage(btpAddr, _bmcICON, service, 10, RC_OK, ''); + let _msg = await encode_msg.encodeTransferMsgWithAddress(_from, accounts[1], _native, _value); + let output = await bmc.receiveRequest(_bmcICON, '', service, 10, _msg); + let balanceAfter = await bmc.getBalance(accounts[1]); + + assert.equal( + web3.utils.BN(balanceAfter).toString(), + web3.utils.BN(balanceBefore).add(new web3.utils.BN(_value)).toString() + ); + assert.equal(output.logs[0].args._next, _bmcICON); + assert.equal(output.logs[0].args._msg, _eventMsg); + }); +}); + +contract('As a user, I want to receive ERC20_ICX from ICON blockchain', (accounts) => { + let bmc, bsh_perif, bsh_core, holder, notpayable; + let service = 'Coin/WrappedCoin'; let _uri = 'https://github.com/icon-project/btp'; + let _native = 'MOVR'; let _fee = 10; let _fixed_fee = 500000; + let _name = 'ICON'; let _bmcICON = 'btp://1234.iconee/0x1234567812345678'; + let _net = '1234.iconee'; let _from = '0x12345678'; + let RC_ERR = 1; let RC_OK = 0; + let _tokenName = 'ICX'; + let _tokenSymbol = 'ICX'; + let INITIAL_SUPPLY = web3.utils.toBN(10000000000000) // 100000 tokens + + + before(async () => { + bsh_perif = await BSHPeriphery.new(); + bsh_core = await BSHCore.new(); + bmc = await BMC.new('1234.movr'); + encode_msg = await EncodeMsg.new(); + await bsh_perif.initialize(bmc.address, bsh_core.address, service); + await bsh_core.initialize(_native, _fee, _fixed_fee, _tokenName, _tokenSymbol, INITIAL_SUPPLY); + await bsh_core.updateBSHPeriphery(bsh_perif.address); + holder = await Holder.new(); + notpayable = await NotPayable.new(); + await bmc.addService(service, bsh_perif.address); + await bmc.addVerifier(_net, accounts[1]); + await bmc.addLink(_bmcICON); + await holder.addBSHContract(bsh_perif.address, bsh_core.address); + btpAddr = await bmc.bmcAddress(); + }); + + it('Scenario 5: Receiver is an account client', async () => { + let _value = 5500; + let balanceBefore = await bsh_core.balanceOf(accounts[1]); + let _eventMsg = await encode_msg.encodeResponseBMCMessage(btpAddr, _bmcICON, service, 10, RC_OK, ''); + let _msg = await encode_msg.encodeTransferMsgWithAddress(_from, accounts[1], _tokenName, _value); + let output = await bmc.receiveRequest(_bmcICON, '', service, 10, _msg); + let balanceAfter = await bsh_core.balanceOf(accounts[1]); + + assert.equal( + web3.utils.BN(balanceAfter).toNumber(), + web3.utils.BN(balanceBefore).toNumber() + _value + ); + assert.equal(output.logs[0].args._next, _bmcICON); + assert.equal(output.logs[0].args._msg, _eventMsg); + }); +}); \ No newline at end of file diff --git a/solidity/nativecoinERC20/test/integration/native-coin-bsh b/solidity/nativecoinERC20/test/integration/native-coin-bsh new file mode 100644 index 00000000..19ba07a8 --- /dev/null +++ b/solidity/nativecoinERC20/test/integration/native-coin-bsh @@ -0,0 +1,33 @@ +const BSHPerif = artifacts.require("BSHPeriphery"); +const MockBSHCore = artifacts.require("MockBSHCore"); +const CheckParseAddress = artifacts.require("CheckParseAddress"); +const { assert } = require('chai'); +const truffleAssert = require('truffle-assertions'); + + +// BSHPeriphery is being used for communications among BSHCore and BMCPeriphery contract +// Thus, all tests relating to BSHPeriphery will be moved to Integration Test +// This part just covers some basic feature which is checking an authorization +contract('BSHPeriphery Unit Tests', (accounts) => { + let bsh_perif; + + before(async () => { + bsh_perif = await BSHPerif.new(); + }); + + + it('Scenario 40: Should succeed when a client, which owns a refundable, tries to reclaim', async () => { + let _coin = 'ICON'; let _value = 10000; + let _id = await bsh_core.coinId(_coin); + await bsh_core.mintMock(bsh_core.address, _id, _value); + let balanceBefore = await bsh_core.getBalanceOf(accounts[2], _coin); + await bsh_core.reclaim(_coin, _value, {from: accounts[2]}); + let balanceAfter = await bsh_core.getBalanceOf(accounts[2], _coin); + assert( + web3.utils.BN(balanceAfter._usableBalance).toNumber() === + web3.utils.BN(balanceBefore._usableBalance).toNumber() + _value + ); + }); + +}); + \ No newline at end of file diff --git a/solidity/nativecoinERC20/truffle-config.js b/solidity/nativecoinERC20/truffle-config.js new file mode 100644 index 00000000..f5352c5e --- /dev/null +++ b/solidity/nativecoinERC20/truffle-config.js @@ -0,0 +1,59 @@ + +const HDWalletProvider = require("@truffle/hdwallet-provider"); + +var privKeys = [ + "1deb607f38b0bd1390df3b312a1edc11a00a34f248b5d53f4157de054f3c71ae", + "a1617c7e1691ee5691d0c750125e96a2630f75ef8e87cdd87f363c00d42163e7", + "3d5f8ff132c7f10a03e138b952e556976707725c9aae98e4ed3df6172b8aaa4f", + "fd52d799e21ad6d35a4e0c1679fd82eecbe3e3ccfdeceb8a1eed3a742423f688" +] + +module.exports = { + networks: { + testnet: { + provider: () => new HDWalletProvider(privKeys, + `http://127.0.0.1:9545`), + network_id: 1000, + confirmations: 0, + timeoutBlocks: 900, + skipDryRun: true + }, + bscLocal: { + networkCheckTimeout: 10000, + provider: () => new HDWalletProvider(privKeys, "ws://localhost:8546", + ), + network_id: '97', + confirmations: 0, + skipDryRun: true, + gas: 8000000 + }, + bscDocker: { + provider: () => new HDWalletProvider({ + privateKeys: privKeys, + providerOrUrl: "BSC_RPC_URI", + chainId: 97, + }), + network_id: '97', + skipDryRun: true, + networkCheckTimeout: 1000000000 + }, + development: { + host: "localhost", + port: 9545, + network_id: "*", // Match any network id + } + }, + compilers: { + solc: { + version: "0.7.6", + settings: { + optimizer: { + enabled: true, // Default: false + runs: 200 // Default: 200 + }, + evmVersion: "petersburg" + } + } + }, + plugins: ["truffle-contract-size"] +};