diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java b/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java index 595871949..28ba89cd6 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java @@ -45,6 +45,7 @@ public class Names { public final static String BURNER = "Balanced-ICON Burner"; public final static String SAVINGS = "Balanced Savings"; public final static String TRICKLER = "Balanced Trickler"; + public final static String WICX = "Wrapped ICX"; public final static String SPOKE_ASSET_MANAGER = "Balanced Spoke Asset Manager"; public final static String SPOKE_XCALL_MANAGER = "Balanced Spoke XCall Manager"; diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java b/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java index bc08e7ced..af54615e4 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java @@ -44,6 +44,7 @@ public class Versions { public final static String BURNER = "v1.0.0"; public final static String SAVINGS = "v1.0.0"; public final static String TRICKLER = "v1.0.0"; + public final static String WICX = "v1.0.1"; public final static String SPOKE_ASSET_MANAGER = "v1.0.2"; public final static String SPOKE_XCALL_MANAGER = "v1.0.1"; diff --git a/settings.gradle b/settings.gradle index 05fce79a5..d96dacb1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,6 +58,9 @@ include 'test-lib' include(':WorkerToken') project(':WorkerToken').projectDir = file("token-contracts/WorkerToken") +include(':WICX') +project(':WICX').projectDir = file("token-contracts/WICX") + include(':Router') project(':Router').projectDir = file("core-contracts/Router") diff --git a/token-contracts/WICX/build.gradle b/token-contracts/WICX/build.gradle new file mode 100644 index 000000000..ccb1a73dd --- /dev/null +++ b/token-contracts/WICX/build.gradle @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * 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. + */ + +import network.balanced.score.dependencies.* + +plugins { + id 'java' +} + +version '1.0.0' + +repositories { + mavenCentral() +} + +dependencies { + compileOnly Dependencies.javaeeApi + implementation project(':score-lib') + + testImplementation Dependencies.javaeeUnitTest + testImplementation Dependencies.junitJupiter + testRuntimeOnly Dependencies.junitJupiterEngine + testImplementation project(':test-lib') + testImplementation Dependencies.mockitoCore + testImplementation Dependencies.mockitoInline +} + +deployJar { + endpoints { + sejong { + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + } + 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 + } + mainnet { + uri = 'https://ctz.solidwallet.io/api/v3' + nid = 0x1 + to = "cx3975b43d260fb8ec802cef6e60c2f4d07486f11d" + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg("_governance", Addresses.mainnet.governance) + } +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} +optimizedJar { + mainClassName = 'network.balanced.score.tokens.wicx.WICX' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} \ No newline at end of file diff --git a/token-contracts/WICX/src/main/java/network/balanced/score/tokens/wicx/PayableIRC2Base.java b/token-contracts/WICX/src/main/java/network/balanced/score/tokens/wicx/PayableIRC2Base.java new file mode 100644 index 000000000..127d7351d --- /dev/null +++ b/token-contracts/WICX/src/main/java/network/balanced/score/tokens/wicx/PayableIRC2Base.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * 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 network.balanced.score.tokens.wicx; + +import network.balanced.score.lib.interfaces.tokens.IRC2; +import score.Address; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; + +public class PayableIRC2Base implements IRC2 { + + private final static String NAME = "name"; + private final static String SYMBOL = "symbol"; + private final static String DECIMALS = "decimals"; + private final static String TOTAL_SUPPLY = "total_supply"; + private final static String BALANCES = "balances"; + + static final Address ZERO_ADDRESS = new Address(new byte[Address.LENGTH]); + + private final VarDB name = Context.newVarDB(NAME, String.class); + private final VarDB symbol = Context.newVarDB(SYMBOL, String.class); + private final VarDB decimals = Context.newVarDB(DECIMALS, BigInteger.class); + private final VarDB totalSupply = Context.newVarDB(TOTAL_SUPPLY, BigInteger.class); + protected final DictDB balances = Context.newDictDB(BALANCES, BigInteger.class); + + protected PayableIRC2Base(String _tokenName, String _symbolName, @Optional BigInteger _decimals) { + if (this.name.get() == null) { + _decimals = _decimals == null ? BigInteger.valueOf(18L) : _decimals; + Context.require(_decimals.compareTo(BigInteger.ZERO) >= 0, "Decimals cannot be less than zero"); + + this.name.set(ensureNotEmpty(_tokenName)); + this.symbol.set(ensureNotEmpty(_symbolName)); + this.decimals.set(_decimals); + } + } + + @EventLog(indexed = 3) + public void Transfer(Address _from, Address _to, BigInteger _value, byte[] _data) { + } + + private String ensureNotEmpty(String str) { + Context.require(str != null && !str.trim().isEmpty(), "str is null or empty"); + assert str != null; + return str.trim(); + } + + @External(readonly = true) + public String name() { + return name.get(); + } + + @External(readonly = true) + public String symbol() { + return symbol.get(); + } + + @External(readonly = true) + public BigInteger decimals() { + return decimals.get(); + } + + @External(readonly = true) + public BigInteger totalSupply() { + return totalSupply.getOrDefault(BigInteger.ZERO); + } + + @External(readonly = true) + public BigInteger balanceOf(Address _owner) { + return balances.getOrDefault(_owner, BigInteger.ZERO); + } + + @External + @Payable + public void transfer(Address _to, BigInteger _value, @Optional byte[] _data) { + transfer(Context.getCaller(), _to, _value, _data); + } + + protected void transfer(Address _from, Address _to, BigInteger _value, byte[] _data) { + Context.require(_value.compareTo(BigInteger.ZERO) >= 0, this.name.get() + ": _value needs to be positive"); + Context.require(balanceOf(_from).compareTo(_value) >= 0, this.name.get() + ": Insufficient balance"); + + this.balances.set(_from, balanceOf(_from).subtract(_value)); + this.balances.set(_to, balanceOf(_to).add(_value)); + + byte[] dataBytes = (_data == null) ? "None".getBytes() : _data; + Transfer(_from, _to, _value, dataBytes); + + if (_to.isContract()) { + Context.call(_to, "tokenFallback", _from, _value, dataBytes); + } + } + + protected void mint(Address owner, BigInteger amount) { + Context.require(!ZERO_ADDRESS.equals(owner), this.name.get() + ": Owner address cannot be zero address"); + Context.require(amount.compareTo(BigInteger.ZERO) >= 0, this.name.get() + ": Amount needs to be positive"); + + totalSupply.set(totalSupply().add(amount)); + balances.set(owner, balanceOf(owner).add(amount)); + Transfer(ZERO_ADDRESS, owner, amount, "mint".getBytes()); + } + + protected void burn(Address owner, BigInteger amount) { + Context.require(!ZERO_ADDRESS.equals(owner), this.name.get() + ": Owner address cannot be zero address"); + Context.require(amount.compareTo(BigInteger.ZERO) >= 0, this.name.get() + ": Amount needs to be positive"); + Context.require(balanceOf(owner).compareTo(amount) >= 0, this.name.get() + ": Insufficient Balance"); + + balances.set(owner, balanceOf(owner).subtract(amount)); + totalSupply.set(totalSupply().subtract(amount)); + Transfer(owner, ZERO_ADDRESS, amount, "burn".getBytes()); + } +} diff --git a/token-contracts/WICX/src/main/java/network/balanced/score/tokens/wicx/WICX.java b/token-contracts/WICX/src/main/java/network/balanced/score/tokens/wicx/WICX.java new file mode 100644 index 000000000..fdea83795 --- /dev/null +++ b/token-contracts/WICX/src/main/java/network/balanced/score/tokens/wicx/WICX.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022-2023 Balanced.network. + * + * 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 network.balanced.score.tokens.wicx; + +import network.balanced.score.lib.utils.Names; +import network.balanced.score.lib.utils.Versions; +import network.balanced.score.lib.utils.BalancedAddressManager; +import score.Address; +import score.Context; +import score.VarDB; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; + +public class WICX extends PayableIRC2Base { + + private static final String TOKEN_NAME = Names.WICX; + private static final String SYMBOL_NAME = "wICX"; + private static final BigInteger DECIMALS = BigInteger.valueOf(18); + private static final String VERSION = "version"; + + private final VarDB currentVersion = Context.newVarDB(VERSION, String.class); + + public WICX(Address _governance) { + super(TOKEN_NAME, SYMBOL_NAME, DECIMALS); + if (BalancedAddressManager.getAddressByName(Names.GOVERNANCE) == null) { + BalancedAddressManager.setGovernance(_governance); + } + if (currentVersion.getOrDefault("").equals(Versions.WICX)) { + Context.revert("Can't Update same version of code"); + } + currentVersion.set(Versions.WICX); + } + + @External(readonly = true) + public String version() { + return currentVersion.getOrDefault(""); + } + + @Override + @Payable + @External + public void transfer(Address _to, BigInteger _value, @Optional byte[] _data) { + Address from = Context.getCaller(); + BigInteger deposit = getICXDeposit(); + if (deposit.compareTo(BigInteger.ZERO) > 0) { + mint(from, deposit); + } + super.transfer(_to, _value, _data); + + if (!_to.isContract()) { + burn(_to, _value); + Context.transfer(_to, _value); + } + } + + @External + public void unwrap(BigInteger amount) { + Address from = Context.getCaller(); + burn(from, amount); + Context.transfer(from, amount); + } + + @Payable + public void fallback() { + Address from = Context.getCaller(); + BigInteger deposit = getICXDeposit(); + if (deposit.compareTo(BigInteger.ZERO) > 0) { + mint(from, deposit); + } + } + + BigInteger getICXDeposit() { + return getICXDeposit(); + } + +} diff --git a/token-contracts/WICX/src/test/java/network/balanced/score/tokens/wicx/WICXTest.java b/token-contracts/WICX/src/test/java/network/balanced/score/tokens/wicx/WICXTest.java new file mode 100644 index 000000000..38eee7af2 --- /dev/null +++ b/token-contracts/WICX/src/test/java/network/balanced/score/tokens/wicx/WICXTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * 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 network.balanced.score.tokens.wicx; + +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 network.balanced.score.lib.test.VarargAnyMatcher; +import network.balanced.score.lib.test.mock.MockBalanced; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import score.Address; +import score.Context; + +import java.math.BigInteger; +import java.util.List; + +import static network.balanced.score.lib.test.UnitTest.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +class WICXTest extends TestBase { + + private static final ServiceManager sm = getServiceManager(); + private static final Account owner = sm.createAccount(); + private static MockBalanced mockBalanced; + private static Account governanceScore; + + private static Score wICX; + private static WICX wICXSpy; + private static final Account user = sm.createAccount(); + + @BeforeEach + void setup() throws Exception { + mockBalanced = new MockBalanced(sm, owner); + governanceScore = mockBalanced.governance.account; + wICX = sm.deploy(owner, WICX.class, governanceScore.getAddress()); + + wICXSpy = (WICX) spy(wICX.getInstance()); + wICX.setInstance(wICXSpy); + } + + @Test + void testTransferToEOA() { + Account user2 = sm.createAccount(); + BigInteger icxSent = BigInteger.valueOf(500); + doReturn(icxSent).when(wICXSpy).getICXDeposit(); + wICX.getAccount().addBalance("ICX", icxSent); + + // Execute transfer function + wICX.invoke(user, "transfer", user2.getAddress(), icxSent, new byte[0]); + + // Verify mint is called if Context.getValue() > 0 + verify(wICXSpy).mint(user.getAddress(), icxSent); + + // Verify super.transfer is called + verify(wICXSpy).transfer(user2.getAddress(), icxSent, new byte[0]); + + // Since `user1` is an EOA, it should call burn and transfer ICX + verify(wICXSpy).burn(user2.getAddress(), icxSent); + assertEquals(user2.getBalance(), icxSent); + + } + + @Test + void testTransferToContract() { + BigInteger icxSent = BigInteger.valueOf(500); + doReturn(icxSent).when(wICXSpy).getICXDeposit(); + wICX.getAccount().addBalance("ICX", icxSent); + + // Execute transfer function + wICX.invoke(user, "transfer", mockBalanced.dex.getAddress(), icxSent, new byte[0]); + + // Verify mint is called if Context.getValue() > 0 + verify(wICXSpy).mint(user.getAddress(), icxSent); + + // Verify super.transfer is called + verify(wICXSpy).transfer(mockBalanced.dex.getAddress(), icxSent, new byte[0]); + + assertEquals(wICX.call("balanceOf", mockBalanced.dex.getAddress()), icxSent); + assertEquals(mockBalanced.dex.account.getBalance(), BigInteger.ZERO); + assertEquals(wICX.getAccount().getBalance(), icxSent); + + } + + @Test + void testFallback() { + BigInteger icxSent = BigInteger.valueOf(500); + doReturn(icxSent).when(wICXSpy).getICXDeposit(); + wICX.getAccount().addBalance("ICX", icxSent); + + // Execute transfer function + wICX.invoke(user, "fallback"); + + // Assert + assertEquals(wICX.call("balanceOf", user.getAddress()), icxSent); + assertEquals(wICX.getAccount().getBalance(), icxSent); + + } + + @Test + void testUnwrap() { + BigInteger icxSent = BigInteger.valueOf(500); + BigInteger unwrapAmount = BigInteger.valueOf(200); + + doReturn(icxSent).when(wICXSpy).getICXDeposit(); + wICX.getAccount().addBalance("ICX", icxSent); + + // Execute transfer function + wICX.invoke(user, "fallback"); + wICX.invoke(user, "unwrap", unwrapAmount); + + assertEquals(wICX.call("balanceOf", user.getAddress()), icxSent.subtract(unwrapAmount)); + assertEquals(user.getBalance(), unwrapAmount); + assertEquals(wICX.getAccount().getBalance(), icxSent.subtract(unwrapAmount)); + } + +}