From b7ba0e7812c20c72cd3aa3849dac3db9a7f1700f Mon Sep 17 00:00:00 2001 From: gcranju Date: Mon, 18 Mar 2024 10:50:39 +0545 Subject: [PATCH] feat: ics20 transfer implementation added --- contracts/javascore/ics20/build.gradle | 85 +++ .../main/java/ibc/ics20/ICS20Transfer.java | 520 ++++++++++++++++++ .../java/ibc/ics20/ICS20TransferTest.java | 446 +++++++++++++++ .../src/main/java/ics20}/ICS20Lib.java | 36 +- .../lightclients/tendermint/build.gradle | 4 + .../javascore/modules/ics20app/build.gradle | 80 --- .../main/java/ibc/ics20app/ICS20Transfer.java | 214 ------- .../java/ibc/ics20app/ICS20TransferBank.java | 74 --- .../java/ibc.ics20app/ICS20TransferTest.java | 146 ----- .../javascore/modules/ics20bank/build.gradle | 79 --- .../main/java/ibc/ics20bank/ICS20Bank.java | 113 ---- .../java/ibc/ics20bank/ICS20BankTest.java | 127 ----- contracts/javascore/settings.gradle | 10 +- 13 files changed, 1065 insertions(+), 869 deletions(-) create mode 100644 contracts/javascore/ics20/build.gradle create mode 100644 contracts/javascore/ics20/src/main/java/ibc/ics20/ICS20Transfer.java create mode 100644 contracts/javascore/ics20/src/test/java/ibc/ics20/ICS20TransferTest.java rename contracts/javascore/{modules/ics20app/src/main/java/ibc/ics20App => lib/src/main/java/ics20}/ICS20Lib.java (65%) delete mode 100644 contracts/javascore/modules/ics20app/build.gradle delete mode 100644 contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20Transfer.java delete mode 100644 contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20TransferBank.java delete mode 100644 contracts/javascore/modules/ics20app/src/test/java/ibc.ics20app/ICS20TransferTest.java delete mode 100644 contracts/javascore/modules/ics20bank/build.gradle delete mode 100644 contracts/javascore/modules/ics20bank/src/main/java/ibc/ics20bank/ICS20Bank.java delete mode 100644 contracts/javascore/modules/ics20bank/src/test/java/ibc/ics20bank/ICS20BankTest.java diff --git a/contracts/javascore/ics20/build.gradle b/contracts/javascore/ics20/build.gradle new file mode 100644 index 000000000..5fb2998f8 --- /dev/null +++ b/contracts/javascore/ics20/build.gradle @@ -0,0 +1,85 @@ +version = '0.1.0' + +dependencies { + implementation project(':lib') + implementation project(':score-util') + implementation "com.github.sink772:minimal-json:0.9.6" + testImplementation project(':test-lib') + testImplementation project(':ibc') +} + + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} + +optimizedJar { + dependsOn(project(':lib').jar) + dependsOn(project(':score-util').jar) + mainClassName = 'ibc.ics20.ICS20Transfer' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + berlin { + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + } + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + uat { + uri = project.findProperty('uat.host') as String + nid = property('uat.nid') as Integer + to = "$xCallConnection"?:null + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + // parameters { + // arg('_xCall', "$xCallMultiProtocol") + // arg('_ibc', "$ibcCore") + // arg('_port', "mock") + // } +} + +task integrationTest(type: Test) { + useJUnitPlatform() + + rootProject.allprojects { + if (it.getTasks().findByName('optimizedJar')) { + dependsOn(it.getTasks().getByName('optimizedJar')) + } + } + + options { + testLogging.showStandardStreams = true + description = 'Runs integration tests.' + group = 'verification' + + testClassesDirs = sourceSets.intTest.output.classesDirs + classpath = sourceSets.intTest.runtimeClasspath + + systemProperty "java", optimizedJar.outputJarName + } + +} diff --git a/contracts/javascore/ics20/src/main/java/ibc/ics20/ICS20Transfer.java b/contracts/javascore/ics20/src/main/java/ibc/ics20/ICS20Transfer.java new file mode 100644 index 000000000..6e6e04b9d --- /dev/null +++ b/contracts/javascore/ics20/src/main/java/ibc/ics20/ICS20Transfer.java @@ -0,0 +1,520 @@ +package ibc.ics20; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +import ibc.icon.interfaces.IIBCModule; +import icon.proto.core.channel.Channel; +import icon.proto.core.channel.Packet; +import icon.proto.core.client.Height; +import ics20.ICS20Lib; +import score.Address; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; + +public class ICS20Transfer implements IIBCModule { + public static final String TAG = "ICS20"; + public static final String ICS20_VERSION = "ics20-1"; + + private final DictDB destinationPort = Context.newDictDB("destinationPort", String.class); + private final DictDB destinationChannel = Context.newDictDB("destinationChannel", String.class); + + private final VarDB
ibcHandler = Context.newVarDB("ibcHandler", Address.class); + private final DictDB tokenContracts = Context.newDictDB("tokenContracts", Address.class); + private final VarDB
admin = Context.newVarDB("admin", Address.class); + + public final byte[] serializedIrc2; + + public ICS20Transfer(Address _ibcHandler, byte[] _serializeIrc2) { + if (ibcHandler.get() == null) { + ibcHandler.set(_ibcHandler); + admin.set(Context.getCaller()); + } + serializedIrc2 = _serializeIrc2; + } + + /** + * Set the admin address and ensure only admin can call this function. + * + * @param _admin the new admin address + * @return void + */ + @External + public void setAdmin(Address _admin) { + onlyAdmin(); + admin.set(_admin); + } + + /** + * Retrieves the admin address. + * + * @return the admin address + */ + @External(readonly = true) + public Address getAdmin() { + return admin.get(); + } + + /** + * Retrieves the IBC handler address. + * + * @return the IBC handler address + */ + @External(readonly = true) + public Address getIBCAddress() { + return ibcHandler.get(); + } + + /** + * Retrieves the destination port for the given channel ID. + * + * @param channelId the source channel id + * @return the destination port associated with the channel ID + */ + @External(readonly = true) + public String getDestinationPort(String channelId) { + return destinationPort.get(channelId); + } + + /** + * Retrieves the destination channel for the given channel ID. + * + * @param channelId the source channel id + * @return the destination channel associated with the channel ID + */ + @External(readonly = true) + public String getDestinationChannel(String channelId) { + return destinationChannel.get(channelId); + } + + /** + * Retrieves the token contract address for the given denom. + * + * @param denom the token denom + * @return the token contract address + */ + @External(readonly = true) + public Address getTokenContractAddress(String denom) { + Context.require(tokenContracts.get(denom) != null, TAG + " : Token not registered"); + return tokenContracts.get(denom); + } + + /** + * Register a token contract for cosmos chain. + * + * @param name + * @param symbol + * @param decimals + */ + @External + public void registerCosmosToken(String name, String symbol, int decimals) { + onlyAdmin(); + Address tokenAddress = Context.deploy(serializedIrc2, name, symbol, decimals); + tokenContracts.set(name, tokenAddress); + } + + /** + * Register a token contract for icon chain. + * + * @param tokenAddress the irc2 token contract address + */ + @External + public void registerIconToken(Address tokenAddress) { + onlyAdmin(); + tokenContracts.set(tokenAddress.toString(), tokenAddress); + } + + /** + * Fallback function for token transfer. + * + * @param from Sender address + * @param value Amount + * @param _data Data in json bytes in format of + * { + * "method": "sendFungibleTokens", + * "params": { + * "denomination": "string", + * "amount": "uint64", + * "sender": "string", + * "receiver": "string", + * "sourcePort": "string", + * "sourceChannel": "string", + * "timeoutHeight": { + * "latestHeight": "uint64", + * "revisionNumber": "uint64", + * }, + * "timeoutTimestamp": "uint64", + * "memo":"string" + * } + * } + * + */ + @External + public void tokenFallback(Address from, BigInteger value, byte[] _data) { + String method = ""; + JsonValue params = null; + + try { + String data = new String(_data); + JsonObject json = Json.parse(data).asObject(); + + method = json.get("method").asString(); + params = json.get("params"); + } catch (Exception e) { + Context.revert(TAG + " Invalid data: " + _data.toString()); + } + + if (method.equals("sendFungibleTokens")) { + JsonObject fungibleToken = params.asObject(); + String denomination = fungibleToken.getString("denomination", ""); + BigInteger amount = BigInteger.valueOf(fungibleToken.getLong("amount", 0)); + String sender = fungibleToken.getString("sender", ""); + String receiver = fungibleToken.getString("receiver", ""); + String sourcePort = fungibleToken.getString("sourcePort", ""); + String sourceChannel = fungibleToken.getString("sourceChannel", ""); + BigInteger timeoutTimestamp = BigInteger.valueOf(fungibleToken.getLong("timeoutTimestamp", 0)); + String memo = fungibleToken.getString("memo", ""); + + JsonObject timeoutHeight = fungibleToken.get("timeoutHeight").asObject(); + Height height = new Height(); + height.setRevisionNumber(BigInteger.valueOf(timeoutHeight.getLong("revisionNumber", 0))); + height.setRevisionHeight(BigInteger.valueOf(timeoutHeight.getLong("latestHeight", 0))); + + Context.require(amount.equals(value), TAG + " : Mismatched amount"); + Context.require(sender.equals(from.toString()), TAG + " : Sender address mismatched"); + Context.require(tokenContracts.get(denomination) == Context.getCaller(), + TAG + " : Sender Token Contract not registered"); + + sendFungibleToken(denomination, amount, sender, receiver, sourcePort, sourceChannel, height, + timeoutTimestamp, memo); + } else { + Context.revert(TAG + " : Unknown method"); + } + + } + + /** + * Sends ICX to the specified receiver via the specified channel and port. + * + * @param receiver the cross chain address of the receiver + * @param sourcePort the source port + * @param sourceChannel the source channel + * @param timeoutHeight the timeout height + * @param timeoutTimestamp the timeout timestamp + * @param memo an optional memo + */ + @Payable + @External + public void sendICX(String receiver, String sourcePort, String sourceChannel, Height timeoutHeight, + BigInteger timeoutTimestamp, @Optional String memo) { + Context.require(Context.getValue().compareTo(BigInteger.ZERO) > 0, + TAG + " : ICX amount should be greater than 0"); + + sendFungibleToken("icx", Context.getValue(), Context.getCaller().toString(), receiver, sourcePort, + sourceChannel, timeoutHeight, timeoutTimestamp, memo); + + } + + /** + * Sends a irc2 token from the sender to the receiver. + * + * @param denomination the denomination of the token to send + * @param amount the amount of the token to send + * @param sender the address of the sender + * @param receiver the cross chain address of the receiver + * @param sourcePort the source port + * @param sourceChannel the source channel + * @param timeoutHeight the timeout height(latest height and revision number) + * @param timeoutTimestamp the timeout timestamp + * @param memo an optional memo for the transaction + */ + private void sendFungibleToken(String denomination, BigInteger amount, String sender, String receiver, + String sourcePort, String sourceChannel, Height timeoutHeight, BigInteger timeoutTimestamp, + @Optional String memo) { + String denomPrefix = getDenomPrefix(sourcePort, sourceChannel); + boolean isSource = !denomination.startsWith(denomPrefix); + + if (!isSource) { + Address tokenContractAddress = getTokenContractAddress(denomination); + Context.call(tokenContractAddress, "burn", amount); + } + + byte[] data = ICS20Lib.marshalFungibleTokenPacketData(denomination, amount, sender, receiver, memo); + + String destPort = destinationPort.get(sourceChannel); + String destChannel = destinationChannel.get(sourceChannel); + + if (destChannel == null || destPort == null) { + Context.revert(TAG + " : Connection not properly Configured"); + } + + BigInteger seq = Context.call(BigInteger.class, ibcHandler.get(), "getNextSequenceSend", sourcePort, + sourceChannel); + + Packet newPacket = new Packet(); + + newPacket.setSequence(seq); + newPacket.setSourcePort(sourcePort); + newPacket.setSourceChannel(sourceChannel); + newPacket.setDestinationPort(destPort); + newPacket.setDestinationChannel(destChannel); + newPacket.setTimeoutHeight(timeoutHeight); + newPacket.setTimeoutTimestamp(timeoutTimestamp); + newPacket.setData(data); + + Context.call(ibcHandler.get(), "sendPacket", newPacket.encode()); + } + + /** + * Handles the reception of a packet + * + * @param packet the byte array representation of the packet to be processed + * @param relayer the address of the relayer + * @return a byte array representing the acknowledgement of the packet + * processing + */ + @External + public byte[] onRecvPacket(byte[] packet, Address relayer) { + onlyIBC(); + Packet packetDb = Packet.decode(packet); + ICS20Lib.FungibleTokenPacketData data; + + try { + data = ICS20Lib.unmarshalFungibleTokenPacketData(packetDb.getData()); + + Context.require(!data.denom.equals(""), TAG + " : ICS20: invalid denomination"); + Context.require(!data.receiver.equals(""), TAG + " : ICS20: invalid receiver address"); + Context.require(!data.sender.equals(""), TAG + " : ICS20: invalid sender address"); + Context.require(data.amount.compareTo(BigInteger.ZERO) > 0, TAG + " : ICS20: invalid amount"); + + } catch (Exception e) { + return ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON; + } + + String denomPrefix = getDenomPrefix(packetDb.getSourcePort(), packetDb.getSourceChannel()); + boolean isSource = data.denom.startsWith(denomPrefix); + + byte[] ack = ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON; + + if (!checkIfReceiverIsAddress(data.receiver)) { + return ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON; + } + + Address receiverAddr = Address.fromString(data.receiver); + + if (isSource) { + String denomOnly = data.denom.substring(denomPrefix.length()); + + if (isNativeAsset(denomOnly)) { + try { + Context.transfer(receiverAddr, data.amount); + } catch (Exception e) { + ack = ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON; + } + } else { + try { + Address tokenContractAddress = getTokenContractAddress(denomOnly); + Context.call(tokenContractAddress, "transfer", receiverAddr, data.amount, data.memo.getBytes()); + } catch (Exception e) { + ack = ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON; + } + } + } else { + denomPrefix = getDenomPrefix(packetDb.getDestinationPort(), packetDb.getDestinationChannel()); + String prefixedDenom = denomPrefix + data.denom; + + try { + Address tokenContractAddress = getTokenContractAddress(prefixedDenom); + Context.call(tokenContractAddress, "mint", receiverAddr, data.amount); + } catch (Exception e) { + ack = ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON; + } + } + + return ack; + } + + /** + * Handles the acknowledgement of a packet. + * + * @param packet the packet being acknowledged + * @param acknowledgement the acknowledgement received + * @param relayer the relayer of the packet + */ + @External + public void onAcknowledgementPacket(byte[] packet, byte[] acknowledgement, Address relayer) { + onlyIBC(); + if (!acknowledgement.equals(ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON)) { + Packet packetDb = Packet.decode(packet); + refundTokens(packetDb); + } + } + + /** + * Handles the timeout of a packet by refunding the tokens associated with the + * packet. + * + * @param packet the encoded packet data + * @param relayer the address of the relayer + */ + @External + public void onTimeoutPacket(byte[] packet, Address relayer) { + Packet packetDb = Packet.decode(packet); + refundTokens(packetDb); + } + + /** + * Initializes the channel opening process. + * + * @param order the order of the channel + * @param connectionHops the connection hops for the channel + * @param portId the port ID for the channel + * @param channelId the channel ID + * @param counterpartyPb the counterparty information + * @param version the version of the channel + */ + @External + public void onChanOpenInit(int order, String[] connectionHops, String portId, String channelId, + byte[] counterpartyPb, String version) { + onlyIBC(); + Context.require(order == Channel.Order.ORDER_UNORDERED, TAG + " : must be unordered"); + Context.require(version.equals(ICS20_VERSION), TAG + " : version should be same with ICS20_VERSION"); + Channel.Counterparty counterparty = Channel.Counterparty.decode(counterpartyPb); + destinationPort.set(channelId, counterparty.getPortId()); + } + + /** + * Channel Opening Process + * + * @param order the order of the channel + * @param connectionHops an array of connection hops + * @param portId the port ID + * @param channelId the channel ID + * @param counterpartyPb the counterparty in protobuf format + * @param version the version + * @param counterPartyVersion the counterparty version + */ + @External + public void onChanOpenTry(int order, String[] connectionHops, String portId, String channelId, + byte[] counterpartyPb, String version, String counterPartyVersion) { + onlyIBC(); + Context.require(order == Channel.Order.ORDER_UNORDERED, TAG + " : must be unordered"); + Context.require(counterPartyVersion.equals(ICS20_VERSION), + TAG + " : version should be same with ICS20_VERSION"); + Channel.Counterparty counterparty = Channel.Counterparty.decode(counterpartyPb); + destinationPort.set(channelId, counterparty.getPortId()); + destinationChannel.set(channelId, counterparty.getChannelId()); + } + + /** + * Handles the acknowledged by the counterparty. + * + * @param portId the identifier of the port on this chain + * @param channelId the identifier of the channel that was opened + * @param counterpartyChannelId the identifier of the channel on the + * counterparty chain + * @param counterPartyVersion the version of the ICS20 protocol used by the + * counterparty + */ + @External + public void onChanOpenAck(String portId, String channelId, String counterpartyChannelId, + String counterPartyVersion) { + onlyIBC(); + Context.require(counterPartyVersion.equals(ICS20_VERSION), + TAG + " : version should be same with ICS20_VERSION"); + destinationChannel.set(channelId, counterpartyChannelId); + } + + /** + * Handles the confirmation of a channel. + * + * @param portId the identifier of the port on this chain + * @param channelId the identifier of the channel that was opened + */ + @External + public void onChanOpenConfirm(String portId, String channelId) { + onlyIBC(); + } + + /** + * Handles the closure of a channel. + * + * @param portId the identifier of the port on this chain + * @param channelId the identifier of the channel that was opened + */ + @External + public void onChanCloseInit(String portId, String channelId) { + Context.revert(TAG + " : Not Allowed"); + } + + /** + * Handles the closing of a channel. + * + * @param portId the identifier of the port on this chain + * @param channelId the identifier of the channel that was opened + */ + @External + public void onChanCloseConfirm(String portId, String channelId) { + onlyIBC(); + } + + /** + * Refunds tokens based on the provided packet. + * + * @param packet the packet containing the token data + */ + private void refundTokens(Packet packet) { + ICS20Lib.FungibleTokenPacketData data = ICS20Lib.unmarshalFungibleTokenPacketData(packet.getData()); + + String denomPrefix = getDenomPrefix(packet.getSourcePort(), packet.getSourceChannel()); + boolean isSource = !data.denom.startsWith(denomPrefix); + + Address sender = Address.fromString(data.sender); + + if (isSource) { + if (isNativeAsset(data.denom)) { + Context.transfer(sender, data.amount); + return; + } + + Address tokenContractAddress = getTokenContractAddress(data.denom); + Context.call(tokenContractAddress, "transfer", sender, data.amount, data.memo.getBytes()); + } else { + Address tokenContractAddress = getTokenContractAddress(data.denom); + Context.call(tokenContractAddress, "mint", sender, data.amount); + } + } + + private static String getDenomPrefix(String port, String channel) { + return port + "/" + channel + "/"; + } + + private void onlyAdmin() { + Context.require(Context.getCaller().equals(admin.get()), TAG + " : Caller is not admin"); + } + + private void onlyIBC() { + Context.require(Context.getCaller().equals(getIBCAddress()), TAG + " : Caller is not IBC Contract"); + } + + private boolean isNativeAsset(String denom) { + return denom.equals("icx"); + } + + private static boolean checkIfReceiverIsAddress(String receiver) { + try { + Address.fromString(receiver); + return true; + } catch (Exception e) { + return false; + } + } + +} diff --git a/contracts/javascore/ics20/src/test/java/ibc/ics20/ICS20TransferTest.java b/contracts/javascore/ics20/src/test/java/ibc/ics20/ICS20TransferTest.java new file mode 100644 index 000000000..b983d424e --- /dev/null +++ b/contracts/javascore/ics20/src/test/java/ibc/ics20/ICS20TransferTest.java @@ -0,0 +1,446 @@ +package ibc.ics20; + +import com.eclipsesource.json.JsonObject; +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; + +import ibc.icon.interfaces.IIBCHandler; +import ibc.icon.interfaces.IIBCHandlerScoreInterface; +import ibc.icon.test.MockContract; +import icon.proto.core.channel.Channel; +import icon.proto.core.channel.Packet; +import icon.proto.core.client.Height; +import ics20.ICS20Lib; + +import java.lang.String; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import score.Address; +import score.Context; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class ICS20TransferTest extends TestBase { + private static final ServiceManager sm = getServiceManager(); + + private static final Account owner = sm.createAccount(); + private static final Account admin = sm.createAccount(); + private static final Account user = sm.createAccount(); + private static final Account sender = sm.createAccount(); + private static final Account relayer = sm.createAccount(); + private static final Address receiver = sm.createAccount().getAddress(); + + private static final Account dest_irc2_token = Account.newScoreAccount(1); + + private static final Account src_irc2_token = Account.newScoreAccount(2); + + // private MockContract token1 = new + // MockContract<>(IRC2ScoreInterface.class, IRC2.class, sm, owner); + + private Score ics20Transfer; + private ICS20Transfer ics20TransferSpy; + private MockContract ibcHandler; + public static final String TAG = "ICS20"; + protected static String port = "transfer"; + protected static int ORDER = Channel.Order.ORDER_UNORDERED; + public static final String ICS20_VERSION = "ics20-1"; + + private final byte[] irc2Bytes = "test".getBytes(); + + @BeforeEach + void setup() throws Exception { + ibcHandler = new MockContract<>(IIBCHandlerScoreInterface.class, IIBCHandler.class, sm, owner); + ics20Transfer = sm.deploy(owner, ICS20Transfer.class, ibcHandler.getAddress(), irc2Bytes); + ics20TransferSpy = (ICS20Transfer) spy(ics20Transfer.getInstance()); + ics20Transfer.setInstance(ics20TransferSpy); + + ics20Transfer.invoke(owner, "setAdmin", admin.getAddress()); + + channelOpenInit("connection-0", "transfer", "channel-0"); + channelOpenAck("channel-0", "channel-1"); + + registerCosmosToken(admin, "transfer/channel-0/dest_irc2_token", "Arch", 18, dest_irc2_token); + ics20Transfer.invoke(admin, "registerIconToken", src_irc2_token.getAddress()); + + } + + @Test + void testGetIBCAddress() { + assertEquals(ibcHandler.getAddress(), ics20Transfer.call("getIBCAddress")); + } + + @Test + void testAdmin() { + assertEquals(admin.getAddress(), ics20Transfer.call("getAdmin")); + + Executable setAdmin = () -> ics20Transfer.invoke(owner, "setAdmin", + owner.getAddress()); + expectErrorMessage(setAdmin, "Reverted(0): ICS20 : Caller is not admin"); + + ics20Transfer.invoke(admin, "setAdmin", owner.getAddress()); + assertEquals(owner.getAddress(), ics20Transfer.call("getAdmin")); + } + + @Test + void testRegisterCosmosToken() { + Executable cosmosToken = () -> ics20Transfer.invoke(user, + "registerCosmosToken", "test", "test", 0); + expectErrorMessage(cosmosToken, "Reverted(0): ICS20 : Caller is not admin"); + + registerCosmosToken(admin, "abc", "ab", 18, dest_irc2_token); + + assertEquals(dest_irc2_token.getAddress(), + ics20Transfer.call("getTokenContractAddress", "abc")); + } + + @Test + void testRegisterIconToken() { + Executable icon = () -> ics20Transfer.invoke(user, "registerIconToken", + src_irc2_token.getAddress()); + expectErrorMessage(icon, "Reverted(0): ICS20 : Caller is not admin"); + + ics20Transfer.invoke(admin, "registerIconToken", + src_irc2_token.getAddress()); + assertEquals(src_irc2_token.getAddress(), + ics20Transfer.call("getTokenContractAddress", + src_irc2_token.getAddress().toString())); + } + + @Test + void testTokenFallbackExceptions() { + byte[] data = "test".getBytes(); + Executable tokenFallback = () -> ics20Transfer.invoke(user, "tokenFallback", + user.getAddress(), BigInteger.ZERO, data); + expectErrorMessage(tokenFallback, "Reverted(0): ICS20 Invalid data: " + + data.toString()); + + byte[] data2 = createByteArray("method", "iconToken", ICX, + sender.getAddress().toString(), admin.getAddress().toString(), "transfer", + "channel-0", BigInteger.ONE, BigInteger.ONE, BigInteger.valueOf(10000), + "memo"); + tokenFallback = () -> ics20Transfer.invoke(user, "tokenFallback", + user.getAddress(), BigInteger.ZERO, data2); + expectErrorMessage(tokenFallback, "Reverted(0): ICS20 : Unknown method"); + + byte[] data3 = createByteArray("sendFungibleTokens", "iconToken", ICX, + sender.getAddress().toString(), admin.getAddress().toString(), "transfer", + "channel-0", BigInteger.ONE, BigInteger.ONE, BigInteger.valueOf(10000), + "memo"); + tokenFallback = () -> ics20Transfer.invoke(user, "tokenFallback", + user.getAddress(), BigInteger.ZERO, data3); + expectErrorMessage(tokenFallback, "Reverted(0): ICS20 : Mismatched amount"); + + byte[] data4 = createByteArray("sendFungibleTokens", "iconToken", ICX, + sender.getAddress().toString(), admin.getAddress().toString(), "transfer", + "channel-0", BigInteger.ONE, BigInteger.ONE, BigInteger.valueOf(10000), + "memo"); + tokenFallback = () -> ics20Transfer.invoke(user, "tokenFallback", + user.getAddress(), ICX, data4); + expectErrorMessage(tokenFallback, "Reverted(0): ICS20 : Sender address mismatched"); + + byte[] data5 = createByteArray("sendFungibleTokens", "iconToken", ICX, + sender.getAddress().toString(), admin.getAddress().toString(), "transfer", + "channel-0", BigInteger.ONE, BigInteger.ONE, BigInteger.valueOf(10000), + "memo"); + tokenFallback = () -> ics20Transfer.invoke(user, "tokenFallback", + sender.getAddress(), ICX, data5); + expectErrorMessage(tokenFallback, "Reverted(0): ICS20 : Sender Token Contract not registered"); + + } + + @Test + void testTokenFallbackSourceToken() { + + byte[] data4 = createByteArray("sendFungibleTokens", + src_irc2_token.getAddress().toString(), ICX, sender.getAddress().toString(), + admin.getAddress().toString(), "transfer", "channel-0", BigInteger.ONE, + BigInteger.ONE, BigInteger.valueOf(10000), "memo"); + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, + Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.call(BigInteger.class, ibcHandler.getAddress(), + "getNextSequenceSend", "transfer", + "channel-0")).thenReturn(BigInteger.ONE); + contextMock.when(() -> Context.call(eq(ibcHandler.getAddress()), + eq("sendPacket"), any())).thenReturn(true); + + ics20Transfer.invoke(src_irc2_token, "tokenFallback", sender.getAddress(), + ICX, data4); + } + + } + + @Test + void testTokenFallbackDestToken() { + + byte[] data4 = createByteArray("sendFungibleTokens", + "transfer/channel-0/dest_irc2_token", ICX, sender.getAddress().toString(), + admin.getAddress().toString(), "transfer", "channel-0", BigInteger.ONE, + BigInteger.ONE, BigInteger.valueOf(10000), "memo"); + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, + Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.call(BigInteger.class, ibcHandler.getAddress(), + "getNextSequenceSend", "transfer", + "channel-0")).thenReturn(BigInteger.ONE); + contextMock.when(() -> Context.call(eq(ibcHandler.getAddress()), + eq("sendPacket"), any())).thenReturn(true); + contextMock.when(() -> Context.call(dest_irc2_token.getAddress(), "burn", + ICX)).thenReturn(true); + + ics20Transfer.invoke(dest_irc2_token, "tokenFallback", sender.getAddress(), + ICX, data4); + } + + } + + @Test + void testSendICX() { + BigInteger amount = BigInteger.TEN.multiply(ICX); + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, + Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.getValue()).thenReturn(amount); + // for the non configured port or channel id + Executable sendICX = () -> ics20Transfer.invoke(sender, "sendICX", + receiver.toString(), "transfer", "channel-1", new Height(), amount, "memo"); + expectErrorMessage(sendICX, "Reverted(0): ICS20 : Connection not properly Configured"); + } + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, + Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.getValue()).thenReturn(amount); + contextMock.when(() -> Context.call(BigInteger.class, ibcHandler.getAddress(), + "getNextSequenceSend", "transfer", + "channel-0")).thenReturn(BigInteger.ONE); + contextMock.when(() -> Context.call(eq(ibcHandler.getAddress()), + eq("sendPacket"), any())).thenReturn(true); + + ics20Transfer.invoke(admin, "registerIconToken", + src_irc2_token.getAddress()); + ics20Transfer.invoke(sender, "sendICX", receiver.toString(), "transfer", + "channel-0", new Height(), amount, "memo"); + } + + } + + @Test + void testOnRecvPacket_icx() { + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.transfer(receiver, ICX)).then(invocationOnMock -> null); + _onRecvPacket(ICX, "transfer/channel-1/icx"); + } + } + + @Test + void testOnRecvPacket_source() { + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock + .when(() -> Context.call(src_irc2_token.getAddress(), "transfer", receiver, ICX, + "memo".getBytes())) + .thenReturn(true); + + _onRecvPacket(ICX, "transfer/channel-1/" + src_irc2_token.getAddress().toString()); + } + + } + + @Test + void testOnRecvPacket_dest() { + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock + .when(() -> Context.call(dest_irc2_token.getAddress(), "mint", receiver, ICX)) + .thenReturn(true); + _onRecvPacket(ICX, "dest_irc2_token"); + } + + } + + @Test + void testOnAcknowledgement_successful() { + + Packet packet = _onRefundPacket(ICX, "src_irc2_token"); + Executable e = () -> ics20Transfer.invoke(admin, "onAcknowledgementPacket", + packet.encode(), ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON, + relayer.getAddress()); + expectErrorMessage(e, "Reverted(0): ICS20 : Caller is not IBC Contract"); + + ics20Transfer.invoke(ibcHandler.account, "onAcknowledgementPacket", + packet.encode(), ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON, + relayer.getAddress()); + + } + + @Test + void testOnAcknowledgement_failure_icx() { + + Packet packet = _onRefundPacket(ICX, "icx"); + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, + Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.transfer(sender.getAddress(), + ICX)).then(invocationOnMock -> null); + ics20Transfer.invoke(ibcHandler.account, "onAcknowledgementPacket", + packet.encode(), ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON, relayer.getAddress()); + } + + } + + @Test + void testOnAcknowledgement_failure_source_token() { + + Packet packet = _onRefundPacket(ICX, src_irc2_token.getAddress().toString()); + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, + Mockito.CALLS_REAL_METHODS)) { + contextMock + .when(() -> Context.call(src_irc2_token.getAddress(), "transfer", sender.getAddress(), ICX, + "memo".getBytes())) + .thenReturn(true); + contextMock.when(() -> Context.call(any(), any(), any())).thenReturn(true); + + ics20Transfer.invoke(ibcHandler.account, "onAcknowledgementPacket", + packet.encode(), ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON, relayer.getAddress()); + } + + } + + @Test + void testOnTimeOutPacket_dest_token() { + + Packet packet = _onRefundPacket(ICX, "transfer/channel-0/dest_irc2_token"); + + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, + Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.call(BigInteger.class, ibcHandler.getAddress(), + "getNextSequenceSend", "transfer", + "channel-0")).thenReturn(BigInteger.ONE); + contextMock.when(() -> Context.call(dest_irc2_token.getAddress(), + "mint", sender.getAddress(), ICX)).thenReturn(true); + + ics20Transfer.invoke(ibcHandler.account, "onTimeoutPacket", packet.encode(), + relayer.getAddress()); + } + } + + void _onRecvPacket(BigInteger amount, String denom) { + String source_channel = "channel-1"; + String dest_channel = "channel-0"; + Packet packet = createPacket(denom, amount, "sender", receiver.toString(), source_channel, dest_channel); + ics20Transfer.invoke(ibcHandler.account, "onRecvPacket", packet.encode(), relayer.getAddress()); + } + + Packet _onRefundPacket(BigInteger amount, String denom) { + String source_channel = "channel-0"; + String dest_channel = "channel-1"; + Packet packet = createPacket(denom, amount, sender.getAddress().toString(), "receiver", source_channel, + dest_channel); + return packet; + } + + private Packet createPacket(String denom, BigInteger amount, String sender, String receiver, String source_channel, + String dest_channel) { + + Height timeOutHeight = new Height(); + timeOutHeight.setRevisionHeight(BigInteger.valueOf(sm.getBlock().getHeight())); + timeOutHeight.setRevisionNumber(BigInteger.ONE); + + String data = "{" + + "\"amount\":\"" + ICX.toString() + "\"," + + "\"denom\":\"" + denom + "\"," + + "\"receiver\":\"" + receiver + "\"," + + "\"sender\":\"" + sender + "\"," + + "\"memo\":\"" + "memo" + "\"" + + "}"; + + Packet packet = new Packet(); + packet.setSequence(BigInteger.ONE); + packet.setSourcePort("transfer"); + packet.setSourceChannel(source_channel); + packet.setDestinationPort("transfer"); + packet.setDestinationChannel(dest_channel); + packet.setTimeoutHeight(timeOutHeight); + packet.setTimeoutTimestamp(BigInteger.valueOf(10000)); + packet.setData(data.getBytes()); + + return packet; + + } + + private void expectErrorMessage(Executable executable, String expectedErrorMessage) { + AssertionError e = assertThrows(AssertionError.class, executable); + assertEquals(expectedErrorMessage, e.getMessage()); + } + + private void registerCosmosToken(Account deployer, String name, String symbol, int decimals, Account token) { + try (MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS)) { + contextMock.when(() -> Context.deploy(irc2Bytes, name, symbol, decimals)).thenReturn(token.getAddress()); + ics20Transfer.invoke(deployer, "registerCosmosToken", name, symbol, decimals); + } + } + + private byte[] createByteArray(String methodName, String denomination, BigInteger amount, String sender, + String receiver, String sourcePort, String sourceChannel, BigInteger latestHeight, + BigInteger revisionNumber, BigInteger timeoutTimestamp, String memo) { + + JsonObject timeoutHeight = new JsonObject() + .add("latestHeight", latestHeight.longValue()) + .add("revisionNumber", revisionNumber.longValue()); + + JsonObject internalParameters = new JsonObject() + .add("denomination", denomination.toString()) + .add("amount", amount.longValue()) + .add("sender", sender.toString()) + .add("receiver", receiver.toString()) + .add("sourcePort", sourcePort.toString()) + .add("sourceChannel", sourceChannel.toString()) + .add("timeoutHeight", timeoutHeight) + .add("timeoutTimestamp", timeoutTimestamp.longValue()) + .add("memo", memo.toString()); + + JsonObject jsonData = new JsonObject() + .add("method", methodName.toString()) + .add("params", internalParameters); + + return jsonData.toString().getBytes(); + } + + public void channelOpenInit(String connectionId, String counterpartyPort, String channelId) { + Channel.Counterparty counterparty = new Channel.Counterparty(); + counterparty.setPortId(counterpartyPort); + counterparty.setChannelId(""); + ics20Transfer.invoke(ibcHandler.account, "onChanOpenInit", ORDER, new String[] { connectionId }, port, + channelId, counterparty.encode(), ICS20_VERSION); + } + + public void channelOpenTry(String connectionId, String counterpartyPort, String channelId, + String counterpartyChannelId) { + Channel.Counterparty counterparty = new Channel.Counterparty(); + counterparty.setPortId(counterpartyPort); + counterparty.setChannelId(counterpartyChannelId); + ics20Transfer.invoke(ibcHandler.account, "onChanOpenTry", ORDER, new String[] { connectionId }, port, channelId, + counterparty.encode(), ICS20_VERSION, ICS20_VERSION); + } + + public void channelOpenAck(String channelId, String counterpartyChannelId) { + ics20Transfer.invoke(ibcHandler.account, "onChanOpenAck", port, channelId, counterpartyChannelId, + ICS20_VERSION); + } + + public void onChanCloseInit(String channelId) { + ics20Transfer.invoke(ibcHandler.account, "onChanCloseInit", port, channelId); + } +} diff --git a/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20App/ICS20Lib.java b/contracts/javascore/lib/src/main/java/ics20/ICS20Lib.java similarity index 65% rename from contracts/javascore/modules/ics20app/src/main/java/ibc/ics20App/ICS20Lib.java rename to contracts/javascore/lib/src/main/java/ics20/ICS20Lib.java index fccbf7815..f7acf6c1b 100644 --- a/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20App/ICS20Lib.java +++ b/contracts/javascore/lib/src/main/java/ics20/ICS20Lib.java @@ -1,12 +1,12 @@ -package ibc.ics20app; +package ics20; -import ibc.ics24.host.IBCCommitment; +import score.annotation.Optional; import ibc.icon.score.util.StringUtil; import java.math.BigInteger; public class ICS20Lib { - public static class PacketData { + public static class FungibleTokenPacketData { public String denom; public String sender; public String receiver; @@ -14,10 +14,8 @@ public static class PacketData { public String memo; } - public static final byte[] SUCCESSFUL_ACKNOWLEDGEMENT_JSON = "{\"result\":\"AQ==\"}".getBytes(); public static final byte[] FAILED_ACKNOWLEDGEMENT_JSON = "{\"error\":\"failed\"}".getBytes(); - public static final byte[] KECCAK256_SUCCESSFUL_ACKNOWLEDGEMENT_JSON = IBCCommitment.keccak256(SUCCESSFUL_ACKNOWLEDGEMENT_JSON); public static final Integer CHAR_SLASH = 0x2f; public static final Integer CHAR_BACKSLASH = 0x5c; public static final Integer CHAR_F = 0x66; @@ -27,7 +25,6 @@ public static class PacketData { public static final Integer CHAR_T = 0x74; public static final Integer CHAR_CLOSING_BRACE = 0x7d; public static final Integer CHAR_M = 0x6d; - private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); private static final int CHAR_DOUBLE_QUOTE = '"'; @@ -41,27 +38,10 @@ static boolean isEscapeNeededString(byte[] bz) { return false; } - public byte[] marshalUnsafeJSON(PacketData data) { - if (data.memo.isEmpty()) { - return marshalJson(data.denom, data.amount, data.sender, data.receiver); - } else { - return marshalJson(data.denom, data.amount, data.sender, data.receiver, data.memo); + public static byte[] marshalFungibleTokenPacketData(String escapedDenom, BigInteger amount, String escapedSender, String escapedReceiver, @Optional String escapedMemo) { + if (escapedMemo == null) { + escapedMemo = ""; } - } - - - public static byte[] marshalJson(String escapedDenom, BigInteger amount, String escapedSender, String escapedReceiver) { - String jsonString = "{" + - "\"amount\":\"" + amount.toString() + "\"," + - "\"denom\":\"" + escapedDenom + "\"," + - "\"receiver\":\"" + escapedReceiver + "\"," + - "\"sender\":\"" + escapedSender + "\"" + - "}"; - - return jsonString.getBytes(); - } - - public static byte[] marshalJson(String escapedDenom, BigInteger amount, String escapedSender, String escapedReceiver, String escapedMemo) { String jsonString = "{" + "\"amount\":\"" + amount.toString() + "\"," + "\"denom\":\"" + escapedDenom + "\"," + @@ -73,7 +53,7 @@ public static byte[] marshalJson(String escapedDenom, BigInteger amount, String return jsonString.getBytes(); } - public static PacketData unmarshalJSON(byte[] packet) { + public static FungibleTokenPacketData unmarshalFungibleTokenPacketData(byte[] packet) { StringBuilder sanitized = new StringBuilder(); String jsonString = new String(packet); @@ -86,7 +66,7 @@ public static PacketData unmarshalJSON(byte[] packet) { String[] jsonParts = StringUtil.split(jsonString, ','); - PacketData data = new PacketData(); + FungibleTokenPacketData data = new FungibleTokenPacketData(); data.amount = new BigInteger(getValue(jsonParts[0])); diff --git a/contracts/javascore/lightclients/tendermint/build.gradle b/contracts/javascore/lightclients/tendermint/build.gradle index 2f82fee7a..5a4133b0a 100644 --- a/contracts/javascore/lightclients/tendermint/build.gradle +++ b/contracts/javascore/lightclients/tendermint/build.gradle @@ -44,6 +44,10 @@ jacocoTestReport { dependsOn ':mockclient:compileJava' dependsOn ':mockclient:test' dependsOn ':score-util:test' + dependsOn ':ics20:compileJava' + dependsOn ':ics20:compileTestJava' + dependsOn ':ics20:test' + dependsOn ':ics20:jacocoTestReport' doNotTrackState("Disable state tracking") reports { diff --git a/contracts/javascore/modules/ics20app/build.gradle b/contracts/javascore/modules/ics20app/build.gradle deleted file mode 100644 index 67f30ffef..000000000 --- a/contracts/javascore/modules/ics20app/build.gradle +++ /dev/null @@ -1,80 +0,0 @@ -version = '0.1.0' - -dependencies { - compileOnly("foundation.icon:javaee-api:$javaeeVersion") - implementation("foundation.icon:javaee-scorex:$scorexVersion") - implementation project(':lib') - implementation project(':score-util') - implementation project(':ibc') - - testImplementation 'com.google.protobuf:protobuf-javalite:3.13.0' - testImplementation 'foundation.icon:javaee-rt:0.9.3' - testImplementation("org.mockito:mockito-core:$mockitoCoreVersion") - testImplementation("org.mockito:mockito-inline:$mockitoCoreVersion") - testImplementation("foundation.icon:javaee-unittest:$javaeeUnittestVersion") - testAnnotationProcessor("foundation.icon:javaee-score-client:$scoreClientVersion") - testImplementation project(':test-lib') - testImplementation("foundation.icon:javaee-score-client:$scoreClientVersion") - testImplementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - testImplementation("foundation.icon:icon-sdk:$iconsdkVersion") - testImplementation("org.junit.jupiter:junit-jupiter-api:$jupiterApiVersion") - testImplementation("org.junit.jupiter:junit-jupiter-params:$jupiterParamsVersion") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterEngineVersion") -} - -test { - useJUnitPlatform() - finalizedBy jacocoTestReport -} - -jacocoTestReport { - dependsOn test - reports { - xml.required = true - csv.required = false - html.outputLocation = layout.buildDirectory.dir('jacocoHtml') - } -} - -tasks.named('compileJava') { - dependsOn(':ibc:optimizedJar') - dependsOn(':score-util:jar') - dependsOn(':lib:jar') -} - -optimizedJar { - mainClassName = 'ibc.ics20app.ICS20TransferBank' - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } -} - - -deployJar { - endpoints { - berlin { - uri = 'https://berlin.net.solidwallet.io/api/v3' - nid = 0x7 - } - lisbon { - uri = 'https://lisbon.net.solidwallet.io/api/v3' - nid = 0x2 - } - local { - uri = 'http://localhost:9082/api/v3' - nid = 0x3 - } - uat { - uri = project.findProperty('uat.host') as String - nid = property('uat.nid') as Integer - // to = "$ics20app"?:null - } - } - keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' - password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' - parameters { -// arg('_ibcHandler', "$ibcCore"?:null) -// arg("_bank","$ics20Bank"?:null) - } -} \ No newline at end of file diff --git a/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20Transfer.java b/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20Transfer.java deleted file mode 100644 index 3e4742c49..000000000 --- a/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20Transfer.java +++ /dev/null @@ -1,214 +0,0 @@ -package ibc.ics20app; - -import ibc.icon.interfaces.IIBCModule; -import ibc.icon.score.util.StringUtil; -import ibc.ics23.commitment.Ops; - -import icon.proto.core.channel.Channel; -import icon.proto.core.channel.Packet; -import score.Address; -import score.Context; -import score.DictDB; -import score.annotation.External; - -import java.math.BigInteger; -import java.util.Arrays; - -import static ibc.ics20app.ICS20TransferBank.bank; - -public abstract class ICS20Transfer implements IIBCModule { - public static final String ICS20_VERSION = "ics20-1"; - public static final Address ZERO_ADDRESS = Address.fromString("hx0000000000000000000000000000000000000000"); - public static final DictDB channelEscrowAddresses = Context.newDictDB("channelEscrowAddresses", Address.class); - protected final DictDB destinationPort = Context.newDictDB("destinationPort", String.class); - protected final DictDB destinationChannel = Context.newDictDB("destinationChannel", String.class); - - @External(readonly = true) - public Address getIBCAddress() { - return ICS20TransferBank.ibcHandler.getOrDefault(ZERO_ADDRESS); - } - - public void onlyIBC() { - Context.require(Context.getCaller().equals(getIBCAddress()), "ICS20App: Caller is not IBC Contract"); - } - - @External(readonly = true) - public String getDestinationPort(String channelId) { - return destinationPort.get(channelId); - } - - @External(readonly = true) - public String getDestinationChannel(String channelId) { - return destinationChannel.get(channelId); - } - - @External - public byte[] onRecvPacket(byte[] packet, Address relayer) { - onlyIBC(); - Packet packetDb = Packet.decode(packet); - ICS20Lib.PacketData data = ICS20Lib.unmarshalJSON(packetDb.getData()); - boolean success = _decodeReceiver(data.receiver); - if (!success) { - return ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON; - } - Address receiver = Address.fromString(data.receiver); - - byte[] denomPrefix = getDenomPrefix(packetDb.getSourcePort(), packetDb.getSourceChannel()); - byte[] denom = data.denom.getBytes(); - - if (denom.length >= denomPrefix.length && Ops.hasPrefix(denom, denomPrefix)) { - byte[] unprefixedDenom = Arrays.copyOfRange(denom, denomPrefix.length, denom.length); - String unprefixedDenomString = new String(unprefixedDenom); - if (unprefixedDenomString.equals("icx")){ - success = _transferICX(receiver, data.amount); - } - else { - success = _transferFrom(getEscrowAddress(packetDb.getDestinationChannel()), receiver, unprefixedDenomString, data.amount); - } - } else { - if (ICS20Lib.isEscapeNeededString(denom)) { - success = false; - } else { - denom = StringUtil.encodePacked(packetDb.getDestinationPort(), "/", packetDb.getDestinationChannel(), "/", data.denom); - String denomText = new String(denom); - success = _mint(receiver, denomText, data.amount); - } - } - - if (success) { - return ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON; - } else { - return ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON; - } - } - - - @External - public void onAcknowledgementPacket(byte[] packet, byte[] acknowledgement, Address relayer) { - onlyIBC(); - Packet packetDb = Packet.decode(packet); - if (!acknowledgement.equals(ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON)) { - refundTokens(ICS20Lib.unmarshalJSON(packetDb.getData()), packetDb.getSourcePort(), packetDb.getSourceChannel()); - } - } - - @External - public void onChanOpenInit(int order, String[] connectionHops, String portId, String channelId, - byte[] counterpartyPb, String version) { - onlyIBC(); - Context.require(order == Channel.Order.ORDER_UNORDERED, "must be unordered"); - Context.require(version.equals(ICS20_VERSION), "version should be same with ICS20_VERSION"); - Channel.Counterparty counterparty = Channel.Counterparty.decode(counterpartyPb); - destinationPort.set(channelId, counterparty.getPortId()); - channelEscrowAddresses.set(channelId, Context.getAddress()); - } - - @External - public void onChanOpenTry(int order, String[] connectionHops, String portId, String channelId, - byte[] counterpartyPb, String version, String counterPartyVersion) { - onlyIBC(); - Context.require(order == Channel.Order.ORDER_UNORDERED, "must be unordered"); - Context.require(counterPartyVersion.equals(ICS20_VERSION), "version should be same with ICS20_VERSION"); - Channel.Counterparty counterparty = Channel.Counterparty.decode(counterpartyPb); - destinationPort.set(channelId, counterparty.getPortId()); - destinationChannel.set(channelId, counterparty.getChannelId()); - channelEscrowAddresses.set(channelId, Context.getAddress()); - } - - @External - public void onChanOpenAck(String portId, String channelId, String counterpartyChannelId, String counterPartyVersion) { - onlyIBC(); - Context.require(counterPartyVersion.equals(ICS20_VERSION), "version should be same with ICS20_VERSION"); - - } - - @External - public void onChanCloseInit(String portId, String channelId) { - Context.revert("Not Allowed"); - } - - @External - public void onTimeoutPacket(byte[] packet, Address relayer) { - Packet packetDb = Packet.decode(packet); - ICS20Lib.PacketData data = ICS20Lib.unmarshalJSON(packetDb.getData()); - refundTokens(data, packetDb.getSourcePort(), packetDb.getSourceChannel()); - } - - @External - public void onChanCloseConfirm(String portId, String channelId) { - onlyIBC(); - Context.println("onChanCloseConfirm"); - } - - @External - public void onChanOpenConfirm(String portId, String channelId) { - onlyIBC(); - Context.println("onChanOpenConfirm"); - } - - - static Address getEscrowAddress(String sourceChannel) { - Address escorw = channelEscrowAddresses.get(sourceChannel); - Context.require(escorw != ZERO_ADDRESS); - return escorw; - } - - @External(readonly = true) - public Address escrowAddress(String sourceChannel){ - return getEscrowAddress(sourceChannel); - } - - private void refundTokens(ICS20Lib.PacketData data, String sourcePort, String sourceChannel) { - byte[] denomPrefix = getDenomPrefix(sourcePort, sourceChannel); - byte[] denom = data.denom.getBytes(); - - if (denom.length >= denomPrefix.length && Ops.hasPrefix(denom, denomPrefix)) { - Context.require(_mint(Address.fromString(data.sender), data.denom, data.amount), "ICS20: mint failed"); - } else { - Context.require(_transferFrom(getEscrowAddress(sourceChannel), Address.fromString(data.sender), data.denom, data.amount), "ICS20: transfer failed"); - } - } - - public static byte[] getDenomPrefix(String port, String channel) { - return StringUtil.encodePacked(port, "/", channel, "/"); - } - - boolean _transferFrom(Address sender, Address receiver, String denom, BigInteger amount) { - Context.call(bank.get(), "transferFrom", sender, receiver, denom, amount); - return true; - } - - private boolean _mint(Address account, String denom, BigInteger amount) { - Context.call(bank.get(), "mint", account, denom, amount); - return true; - } - - boolean _burn(Address account, String denom, BigInteger amount) { - Context.call(bank.get(), "burn", account, denom, amount); - return true; - } - - boolean _transferICX(Address receiver, BigInteger amount) { - Context.require(Context.getBalance(Context.getAddress()).compareTo(amount) >= 0, "ICS20App: insufficient balance for transfer"); - Context.transfer(receiver, amount); - return true; - } - - /** - * @dev _decodeReceiver decodes a hex string to an address. - * `receiver` may be an invalid address format. - */ - protected static boolean _decodeReceiver(String receiver) { - boolean flag; - try { - Address.fromString(receiver); - flag = true; - } catch (Exception e) { - flag = false; - - } - return flag; - } - - -} diff --git a/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20TransferBank.java b/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20TransferBank.java deleted file mode 100644 index b554174ae..000000000 --- a/contracts/javascore/modules/ics20app/src/main/java/ibc/ics20app/ICS20TransferBank.java +++ /dev/null @@ -1,74 +0,0 @@ -package ibc.ics20app; - -import icon.proto.core.channel.Packet; -import icon.proto.core.client.Height; -import score.Address; -import score.Context; -import score.VarDB; -import score.annotation.External; -import score.annotation.Payable; - -import java.math.BigInteger; - -public class ICS20TransferBank extends ICS20Transfer { - public static final VarDB
ibcHandler = Context.newVarDB("ibcHandler", Address.class); - public static final VarDB
bank = Context.newVarDB("bank", Address.class); - - public static final String TAG = "ICS20App"; - - public ICS20TransferBank(Address _ibcHandler, Address _bank) { - if (ibcHandler.get() == null) { - ibcHandler.set(_ibcHandler); - bank.set(_bank); - } - } - - @External(readonly = true) - public Address getBank() { - return bank.getOrDefault(ZERO_ADDRESS); - } - - @External(readonly = true) - public BigInteger getBankBalance() { - return Context.getBalance(Context.getAddress()); - } - - @Payable - @External - public void sendTransfer(String denom, BigInteger amount, String receiver, String sourcePort, String sourceChannel, BigInteger timeoutHeight, BigInteger timeoutRevisionNumber) { - Address caller = Context.getCaller(); - if (denom.equals("icx")) { - Context.require(Context.getValue().compareTo(BigInteger.ZERO) > 0, "ICS20App: icx transfer failed"); - Context.require(Context.getValue().compareTo(amount) == 0, "ICS20App: icx value is not equal to amount sent"); - } else { - byte[] denomPrefix = ICS20Transfer.getDenomPrefix(sourcePort, sourceChannel); - String denomText = new String(denomPrefix); - if (!denom.startsWith(denomText)) { - Context.require(_transferFrom(caller, ICS20Transfer.getEscrowAddress(sourceChannel), denom, amount), "ICS20App: transfer failed"); - } else { - Context.require(_burn(caller, denom, amount), "ICS20App: Burn failed"); - } - } - - Height height = new Height(); - height.setRevisionNumber(timeoutRevisionNumber); - height.setRevisionHeight(timeoutHeight); - - byte[] data = ICS20Lib.marshalJson(denom, amount, caller.toString(), receiver); - - BigInteger seq = (BigInteger) Context.call(ibcHandler.get(), "getNextSequenceSend", sourcePort, sourceChannel); - Packet newPacket = new Packet(); - newPacket.setSequence(seq); - newPacket.setSourcePort(sourcePort); - newPacket.setSourceChannel(sourceChannel); - newPacket.setDestinationPort(destinationPort.get(sourceChannel)); - newPacket.setDestinationChannel(destinationChannel.get(sourceChannel)); - newPacket.setTimeoutHeight(height); - newPacket.setTimeoutTimestamp(BigInteger.ZERO); - newPacket.setData(data); - - Context.call(ibcHandler.get(), "sendPacket", newPacket.encode()); - } - - -} diff --git a/contracts/javascore/modules/ics20app/src/test/java/ibc.ics20app/ICS20TransferTest.java b/contracts/javascore/modules/ics20app/src/test/java/ibc.ics20app/ICS20TransferTest.java deleted file mode 100644 index 237c21894..000000000 --- a/contracts/javascore/modules/ics20app/src/test/java/ibc.ics20app/ICS20TransferTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package ibc.ics20app; - - -import com.iconloop.score.test.Account; -import com.iconloop.score.test.Score; -import com.iconloop.score.test.ServiceManager; -import com.iconloop.score.test.TestBase; -import ibc.icon.structs.messages.MsgChannelOpenInit; -import ibc.ics23.commitment.Ops; -import icon.proto.core.channel.Packet; -import icon.proto.core.client.Height; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import score.Address; -import score.Context; - -import java.math.BigInteger; -import java.util.Arrays; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.Mockito.spy; - -import com.iconloop.score.test.Account; -import com.iconloop.score.test.Score; -import com.iconloop.score.test.ServiceManager; -import com.iconloop.score.test.TestBase; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.function.Executable; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.stubbing.Answer; - - -public class ICS20TransferTest extends TestBase { - public static final Address SYSTEM_ADDRESS = Address.fromString("cx0000000000000000000000000000000000000000"); - public static final Address ICS20Bank = Address.fromString("cx0000000000000000000000000000000000000002"); - public static final Address IBCHandler = Address.fromString("cx0000000000000000000000000000000000000003"); - public static final Address ZERO_ADDRESS = Address.fromString("hx0000000000000000000000000000000000000000"); - - public static final ServiceManager sm = getServiceManager(); - public static final Account owner = sm.createAccount(); - public static final Account testingAccount = sm.createAccount(); - public static final String TAG = "ICS20App"; - public Score ics20App; - ICS20TransferBank ICS20TransferBankSpy; - - public static MockedStatic contextMock; - - @BeforeEach - public void setup() throws Exception { - ics20App = sm.deploy(owner, ICS20TransferBank.class, IBCHandler, ICS20Bank); - - ICS20TransferBank instance = (ICS20TransferBank) ics20App.getInstance(); - ICS20TransferBankSpy = spy(instance); - ics20App.setInstance(ICS20TransferBankSpy); - contextMock.reset(); - } - - @BeforeAll - public static void init(){ - contextMock = Mockito.mockStatic(Context.class, CALLS_REAL_METHODS); - } - - - public void expectErrorMessage(Executable contractCall, String errorMessage) { - AssertionError e = Assertions.assertThrows(AssertionError.class, contractCall); - assertEquals(errorMessage, e.getMessage()); - } - - @Test - void getBank(){ - assertEquals(ICS20Bank, ics20App.call("getBank")); - } - - @Test - void getIBCAddress(){ - assertEquals(IBCHandler, ics20App.call("getIBCAddress")); - } - -// @Test -// void getDestinationPort(){ -// String channelId = "channel-0"; -// assertEquals("transfer", ics20App.call("getDestinationPort", channelId)); -// } -// -// @Test -// void getDestinationChannel(){ -// String channelId = "channel-0"; -// assertEquals("channel-1", ics20App.call("getDestinationChannel", channelId)); -// } - - @Test - void onlyIBCFailure(){ - expectErrorMessage( - () -> ics20App.invoke(testingAccount, "onlyIBC"), - "Reverted(0): ICS20App: Caller is not IBC Contract" - ); - } - - @Test - void onlyIBCSuccess(){ - contextMock.when(Context::getCaller).thenReturn(IBCHandler); - ics20App.invoke(owner, "onlyIBC"); - } - - -// @Test -// void onRecvPacket() { -// BigInteger sequence = BigInteger.ONE; -// String sourcePort = "transfer"; -// String sourceChannel = "channel-0"; -// String destinationPort = "transfer"; -// String destinationChannel = "channel-1"; -// byte[] data = "{\"amount\":\"99000000000000000\",\"denom\":\"stake\",\"receiver\":\"hxb6b5791be0b5ef67063b3c10b840fb81514db2fd\",\"sender\":\"centauri1g5r2vmnp6lta9cpst4lzc4syy3kcj2ljte3tlh\"}".getBytes(); -// BigInteger timeoutTimestamp = BigInteger.ZERO; -// BigInteger timeoutHeight = BigInteger.ZERO; -// -// Height height = new Height(); -// height.setRevisionNumber(timeoutHeight); -// height.setRevisionHeight(timeoutHeight); -// -// Packet packet = new Packet(); -// packet.setSequence(sequence); -// packet.setSourcePort(sourcePort); -// packet.setSourceChannel(sourceChannel); -// packet.setDestinationPort(destinationPort); -// packet.setDestinationChannel(destinationChannel); -// packet.setTimeoutHeight(new Height()); -// packet.setTimeoutTimestamp(timeoutTimestamp); -// packet.setData(data); -// -// byte[] packetBytes = packet.encode(); -// -// contextMock.when(caller()).thenReturn(IBCHandler); -// ics20App.invoke(owner, "onRecvPacket", packetBytes, ZERO_ADDRESS); -// -// } - - public MockedStatic.Verification caller(){ - return () -> Context.getCaller(); - } - - -} \ No newline at end of file diff --git a/contracts/javascore/modules/ics20bank/build.gradle b/contracts/javascore/modules/ics20bank/build.gradle deleted file mode 100644 index 5983f1eca..000000000 --- a/contracts/javascore/modules/ics20bank/build.gradle +++ /dev/null @@ -1,79 +0,0 @@ -version = '0.1.0' - -dependencies { - compileOnly("foundation.icon:javaee-api:$javaeeVersion") - implementation("foundation.icon:javaee-scorex:$scorexVersion") - implementation project(':lib') - implementation project(':score-util') - implementation project(':ibc') - - - testImplementation 'com.google.protobuf:protobuf-javalite:3.13.0' - testImplementation 'foundation.icon:javaee-rt:0.9.3' - testImplementation("org.mockito:mockito-core:$mockitoCoreVersion") - testImplementation("org.mockito:mockito-inline:$mockitoCoreVersion") - testImplementation("foundation.icon:javaee-unittest:$javaeeUnittestVersion") - testAnnotationProcessor("foundation.icon:javaee-score-client:$scoreClientVersion") - testImplementation project(':test-lib') - testImplementation("foundation.icon:javaee-score-client:$scoreClientVersion") - testImplementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - testImplementation("foundation.icon:icon-sdk:$iconsdkVersion") - testImplementation("org.junit.jupiter:junit-jupiter-api:$jupiterApiVersion") - testImplementation("org.junit.jupiter:junit-jupiter-params:$jupiterParamsVersion") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterEngineVersion") -} - -test { - useJUnitPlatform() - finalizedBy jacocoTestReport -} - -jacocoTestReport { - dependsOn test - reports { - xml.required = true - csv.required = false - html.outputLocation = layout.buildDirectory.dir('jacocoHtml') - } -} - -tasks.named('compileJava') { - dependsOn(':ibc:optimizedJar') - dependsOn(':score-util:jar') - dependsOn(':lib:jar') -} - -optimizedJar { - mainClassName = 'ibc.ics20bank.ICS20Bank' - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } -} - - -deployJar { - endpoints { - berlin { - uri = 'https://berlin.net.solidwallet.io/api/v3' - nid = 0x7 - } - lisbon { - uri = 'https://lisbon.net.solidwallet.io/api/v3' - nid = 0x2 - } - local { - uri = 'http://localhost:9082/api/v3' - nid = 0x3 - } - uat { - uri = project.findProperty('uat.host') as String - nid = property('uat.nid') as Integer - // to = "$ics20app"?:null - } - } - keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' - password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' - parameters { - } -} \ No newline at end of file diff --git a/contracts/javascore/modules/ics20bank/src/main/java/ibc/ics20bank/ICS20Bank.java b/contracts/javascore/modules/ics20bank/src/main/java/ibc/ics20bank/ICS20Bank.java deleted file mode 100644 index 3b33f4f59..000000000 --- a/contracts/javascore/modules/ics20bank/src/main/java/ibc/ics20bank/ICS20Bank.java +++ /dev/null @@ -1,113 +0,0 @@ -package ibc.ics20bank; - -import score.*; -import score.annotation.External; - -import java.math.BigInteger; - -import scorex.util.HashMap; - -import java.util.Map; - -public class ICS20Bank { - - public static final String ICS20_VERSION = "ics20-1"; - public static final Address ZERO_ADDRESS = Address.fromString("hx0000000000000000000000000000000000000000"); - - public static final String TAG = "ICS20Bank"; - - private static final Integer ADMIN_ROLE_ID = 1; - private static final Integer OPERATOR_ROLE_ID = 2; - - - // Mapping from token ID to account balances - private final BranchDB> balances = Context.newBranchDB("BALANCES", BigInteger.class); - private final DictDB roles = Context.newDictDB("ROLES", Integer.class); - - - public ICS20Bank() { - if (roles.get(Context.getOwner()) == null) { - setupRole(ADMIN_ROLE_ID, Context.getOwner()); - } - } - - @External - public void setupRole(int role, Address account) { - Context.require(Context.getCaller().equals(Context.getOwner()), "Only owner can set up role"); - roles.set(account, role); - } - - @External - public void setupOperator(Address account) { - setupRole(OPERATOR_ROLE_ID, account); - } - - private boolean hasRole(int role, Address account) { - return (roles.getOrDefault(account, 0) == role); - } - - @External(readonly = true) - public int getRole(Address account) { - return roles.getOrDefault(account, 0); - } - - @External(readonly = true) - public BigInteger balanceOf(Address account, String denom) { - return balances.at(denom).getOrDefault(account, BigInteger.ZERO); - } - - @External - public void transferFrom(Address from, Address to, String denom, BigInteger amount) { - Context.require(to != ZERO_ADDRESS, TAG + ": balance query for the zero address"); - Address caller = Context.getCaller(); - Context.require(from.equals(caller) || hasRole(OPERATOR_ROLE_ID, caller), TAG + ": caller is not owner nor approved"); - Context.require(!from.equals(to), TAG + ": sender and receiver is same"); - BigInteger fromBalance = balanceOf(from, denom); - Context.require(amount.compareTo(BigInteger.ZERO) > 0, TAG + ": transfer amount must be greater than zero"); - Context.require(fromBalance.compareTo(amount) >= 0, TAG + ": insufficient balance for transfer"); - - balances.at(denom).set(from, fromBalance.subtract(amount)); - balances.at(denom).set(to, balanceOf(to, denom).add(amount)); - } - - @External - public void mint(Address account, String denom, BigInteger amount) { - Context.require(hasRole(OPERATOR_ROLE_ID, Context.getCaller()), TAG + ": must have minter role to mint"); - Context.require(account != ZERO_ADDRESS, TAG + ": mint to the zero address"); - Context.require(amount.compareTo(BigInteger.ZERO) > 0, TAG + ": mint amount must be greater than zero"); - _mint(account, denom, amount); - } - - @External - public void burn(Address account, String denom, BigInteger amount) { - Context.require(hasRole(OPERATOR_ROLE_ID, Context.getCaller()), TAG + ": must have burn role to burn"); - Context.require(amount.compareTo(BigInteger.ZERO) > 0, TAG + ": burn amount must be greater than zero"); - _burn(account, denom, amount); - } - - @External - public void deposit(Address tokenContract, BigInteger amount, Address receiver) { - Context.require(tokenContract.isContract(), TAG + ": tokenContract is not a contract"); - Context.call(tokenContract, "transferFrom", Context.getCaller(), Context.getAddress(), amount); - _mint(receiver, tokenContract.toString(), amount); - } - - @External - public void withdraw(Address tokenContract, BigInteger amount) { - Context.require(tokenContract.isContract(), TAG + ": tokenContract is not a contract"); - Address receiver = Context.getCaller(); - _burn(receiver, tokenContract.toString(), amount); - Context.call(tokenContract, "transfer", receiver, amount); - } - - private void _mint(Address account, String denom, BigInteger amount) { - balances.at(denom).set(account, balanceOf(account, denom).add(amount)); - } - - private void _burn(Address account, String denom, BigInteger amount) { - BigInteger accountBalance = balanceOf(account, denom); - Context.require(accountBalance.compareTo(amount) >= 0, TAG + ": burn amount exceeds balance"); - BigInteger newBalance = accountBalance.subtract(amount); - balances.at(denom).set(account, newBalance); - } -} diff --git a/contracts/javascore/modules/ics20bank/src/test/java/ibc/ics20bank/ICS20BankTest.java b/contracts/javascore/modules/ics20bank/src/test/java/ibc/ics20bank/ICS20BankTest.java deleted file mode 100644 index db68c4c94..000000000 --- a/contracts/javascore/modules/ics20bank/src/test/java/ibc/ics20bank/ICS20BankTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package ibc.ics20bank; - - -import com.iconloop.score.test.Account; -import com.iconloop.score.test.Score; -import com.iconloop.score.test.ServiceManager; -import com.iconloop.score.test.TestBase; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import score.Context; - -import java.math.BigInteger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.Mockito.spy; - - -public class ICS20BankTest extends TestBase { - public static final ServiceManager sm = getServiceManager(); - public static final Account owner = sm.createAccount(); - public static final Account testingAccount = sm.createAccount(); - public static final Account testingAccount2 = sm.createAccount(); - public static final String TAG = "ICS20Bank"; - public Score ics20Bank; - ICS20Bank ICS20BankSpy; - - public static MockedStatic contextMock; - - @BeforeEach - public void setup() throws Exception { - ics20Bank = sm.deploy(owner, ICS20Bank.class); - - ICS20Bank instance = (ICS20Bank) ics20Bank.getInstance(); - ICS20BankSpy = spy(instance); - ics20Bank.setInstance(ICS20BankSpy); - ics20Bank.invoke(owner, "setupRole", 1, owner.getAddress()); - ics20Bank.invoke(owner, "setupRole", 2, owner.getAddress()); - contextMock.reset(); - } - - @BeforeAll - public static void init() { - contextMock = Mockito.mockStatic(Context.class, CALLS_REAL_METHODS); - } - - - public void expectErrorMessage(Executable contractCall, String errorMessage) { - AssertionError e = Assertions.assertThrows(AssertionError.class, contractCall); - assertEquals(errorMessage, e.getMessage()); - } - - @Test - void setupRole() { - ics20Bank.invoke(owner, "setupRole", 2, testingAccount.getAddress()); - assertEquals(2, ics20Bank.call("getRole", testingAccount.getAddress())); - } - - @Test - void mint() { - ics20Bank.invoke(owner, "mint", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(100)); - assertEquals(BigInteger.valueOf(100), ics20Bank.call("balanceOf", new Object[]{testingAccount.getAddress(), "testDenom"})); - } - - @Test - void mintNoAccess() { - expectErrorMessage(() -> ics20Bank.invoke(testingAccount, "mint", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(100)), "Reverted(0): ICS20Bank: must have minter role to mint"); - } - - @Test - void mintZeroAmount() { - expectErrorMessage(() -> ics20Bank.invoke(owner, "mint", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(0)), "Reverted(0): ICS20Bank: mint amount must be greater than zero"); - } - - @Test - void burn() { - ics20Bank.invoke(owner, "mint", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(100)); - ics20Bank.invoke(owner, "burn", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(50)); - assertEquals(BigInteger.valueOf(50), ics20Bank.call("balanceOf", new Object[]{testingAccount.getAddress(), "testDenom"})); - } - - @Test - void burnNoAccess() { - expectErrorMessage(() -> ics20Bank.invoke(testingAccount, "burn", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(100)), "Reverted(0): ICS20Bank: must have burn role to burn"); - } - - @Test - void burnGreaterAmount() { - ics20Bank.invoke(owner, "mint", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(100)); - expectErrorMessage(() -> ics20Bank.invoke(owner, "burn", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(150)), "Reverted(0): ICS20Bank: burn amount exceeds balance"); - } - - @Test - void burnZeroAmount() { - expectErrorMessage(() -> ics20Bank.invoke(owner, "burn", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(0)), "Reverted(0): ICS20Bank: burn amount must be greater than zero"); - } - - @Test - void transferFrom() { - ics20Bank.invoke(owner, "mint", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(100)); - ics20Bank.invoke(owner, "transferFrom", testingAccount.getAddress(), owner.getAddress(), "testDenom", BigInteger.valueOf(50)); - assertEquals(BigInteger.valueOf(50), ics20Bank.call("balanceOf", testingAccount.getAddress(), "testDenom")); - assertEquals(BigInteger.valueOf(50), ics20Bank.call("balanceOf", owner.getAddress(), "testDenom")); - } - - @Test - void transferFromSameAddress() { - ics20Bank.invoke(owner, "mint", testingAccount.getAddress(), "testDenom", BigInteger.valueOf(100)); - expectErrorMessage(() -> ics20Bank.invoke(owner, "transferFrom", testingAccount.getAddress(), testingAccount.getAddress(), "testDenom", BigInteger.valueOf(50)), "Reverted(0): ICS20Bank: sender and receiver is same"); - } - - @Test - void transferFromNoAccess() { - expectErrorMessage(() -> ics20Bank.invoke(testingAccount, "transferFrom", owner.getAddress(), testingAccount2.getAddress(), "testDenom", BigInteger.valueOf(50)), "Reverted(0): ICS20Bank: caller is not owner nor approved"); - } - - public MockedStatic.Verification caller() { - return () -> Context.getCaller(); - } - - -} \ No newline at end of file diff --git a/contracts/javascore/settings.gradle b/contracts/javascore/settings.gradle index ea38daa9f..538988c6f 100644 --- a/contracts/javascore/settings.gradle +++ b/contracts/javascore/settings.gradle @@ -6,7 +6,8 @@ include( 'test-lib', 'ibc', 'xcall-connection', -) + 'ics20' + ) include(':tendermint') project(':tendermint').projectDir = file("lightclients/tendermint") @@ -20,10 +21,3 @@ include(':mockapp') project(':mockapp').projectDir = file("modules/mockapp") project(':mockapp').name = "mockapp" -include(':ics20app') -project(':ics20app').projectDir = file("modules/ics20app") -project(':ics20app').name = "ics20app" - -include(':ics20bank') -project(':ics20bank').projectDir = file("modules/ics20bank") -project(':ics20bank').name = "ics20bank" \ No newline at end of file