Skip to content

Commit 0e13e0d

Browse files
WIP
1 parent ace6fd2 commit 0e13e0d

7 files changed

Lines changed: 569 additions & 10 deletions

File tree

consensus-client-it/build.sbt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,11 @@ libraryDependencies ++= Seq(
1717
).map(_ % Test)
1818

1919
Test / sourceGenerators += Def.task {
20-
val generateSourcesFromContracts = Seq("Bridge", "StandardBridge", "ERC20")
20+
val generateSourcesFromContracts = Seq("Bridge", "StandardBridge", "ERC20", "TERC20")
2121
val contractSources = baseDirectory.value / ".." / "contracts" / "eth"
2222
val compiledDir = contractSources / "target"
2323
// --silent to bypass garbage "Counting objects" git logs
24-
s"forge build --silent --config-path ${contractSources / "foundry.toml"} --contracts " +
25-
s"${contractSources / "src" / "utils" / "TERC20.sol"} " +
26-
s"${contractSources / "src" / "StandardBridge.sol"} " +
27-
s"${contractSources / "src" / "Bridge.sol"} " +
28-
s"${contractSources / "src" / "UnitsMintableERC20.sol"}" !
24+
s"forge build --silent --config-path ${contractSources / "foundry.toml"} ${contractSources / "src"}" !
2925

3026
generateSourcesFromContracts.foreach { contract =>
3127
val json = Json.parse(new FileInputStream(compiledDir / s"$contract.sol" / s"$contract.json"))
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package units
2+
3+
import com.wavesplatform.common.utils.EitherExt2.explicitGet
4+
import com.wavesplatform.state.IntegerDataEntry
5+
import com.wavesplatform.transaction.smart.InvokeScriptTransaction
6+
import com.wavesplatform.transaction.{Asset, TxHelpers}
7+
import monix.execution.atomic.AtomicInt
8+
import org.web3j.protocol.core.DefaultBlockParameterName
9+
import org.web3j.protocol.core.methods.response.{EthSendTransaction, TransactionReceipt}
10+
import org.web3j.tx.RawTransactionManager
11+
import org.web3j.tx.gas.DefaultGasProvider
12+
import units.docker.EcContainer
13+
import units.el.{BridgeMerkleTree, E2CTopics, Erc20Client, TERC20Client}
14+
import units.eth.EthAddress
15+
16+
import scala.jdk.OptionConverters.RichOptional
17+
18+
class FailedTransfersTestSuite1 extends BaseDockerTestSuite {
19+
private val clRecipient = clRichAccount1
20+
private val elSender = elRichAccount1
21+
private val elSenderAddress = elRichAddress1
22+
23+
private val issueAssetDecimals = 8.toByte
24+
private lazy val issueAsset = chainContract.getRegisteredAsset(1) // 0 is WAVES
25+
26+
private val userAmount = BigDecimal("1")
27+
28+
private val gasProvider = new DefaultGasProvider
29+
private lazy val txnManager = new RawTransactionManager(ec1.web3j, elSender, EcContainer.ChainId, 20, 2000)
30+
private lazy val wwaves = new Erc20Client(ec1.web3j, WWavesAddress, txnManager, gasProvider)
31+
private lazy val terc20 = new Erc20Client(ec1.web3j, TErc20Address, txnManager, gasProvider)
32+
private lazy val terc20client = new TERC20Client(ec1.web3j, TErc20Address, txnManager, gasProvider)
33+
34+
"Mining continues after 2 equivalent transfers: 1 successful and 1 failed" in {
35+
val currNonce =
36+
AtomicInt(ec1.web3j.ethGetTransactionCount(elSenderAddress.hex, DefaultBlockParameterName.PENDING).send().getTransactionCount.intValueExact())
37+
def nextNonce: Int = currNonce.getAndIncrement()
38+
39+
val nativeE2CAmount = UnitsConvert.toAtomic(userAmount * 2, NativeTokenElDecimals)
40+
val issuedE2CAmount = UnitsConvert.toAtomic(userAmount * 2, TErc20Decimals)
41+
val burnE2CAmount = UnitsConvert.toAtomic(userAmount, TErc20Decimals)
42+
val leftoverE2CAmount = UnitsConvert.toAtomic(userAmount, TErc20Decimals)
43+
val wavesE2CAmount = UnitsConvert.toAtomic(userAmount, WwavesDecimals)
44+
45+
step("Send allowances")
46+
List(
47+
terc20.sendApprove(StandardBridgeAddress, issuedE2CAmount, nextNonce),
48+
wwaves.sendApprove(StandardBridgeAddress, wavesE2CAmount, nextNonce)
49+
).foreach(waitFor)
50+
51+
step("Initiate E2C transfers")
52+
val e2cNativeTxn = nativeBridge.sendSendNative(elSender, clRecipient.toAddress, nativeE2CAmount, nextNonce)
53+
val e2cIssuedTxn = standardBridge.sendBridgeErc20(elSender, TErc20Address, clRecipient.toAddress, issuedE2CAmount, nextNonce)
54+
val e2cWavesTxn = standardBridge.sendBridgeErc20(elSender, WWavesAddress, clRecipient.toAddress, wavesE2CAmount, nextNonce)
55+
56+
chainContract.waitForEpoch(waves1.api.height() + 1) // Bypass rollbacks
57+
val e2cReceipts = List(e2cNativeTxn, e2cIssuedTxn, e2cWavesTxn).map { txn =>
58+
eventually {
59+
val hash = txn.getTransactionHash
60+
withClue(s"$hash: ") {
61+
ec1.web3j.ethGetTransactionReceipt(hash).send().getTransactionReceipt.toScala.value
62+
}
63+
}
64+
}
65+
66+
withClue("E2C should be on same height, can't continue the test: ") {
67+
val e2cHeights = e2cReceipts.map(_.getBlockNumber.intValueExact()).toSet
68+
e2cHeights.size shouldBe 1
69+
}
70+
71+
val e2cBlockHash = BlockHash(e2cReceipts.head.getBlockHash)
72+
log.debug(s"Block with e2c transfers: $e2cBlockHash")
73+
74+
val e2cLogsInBlock = ec1.engineApi
75+
.getLogs(e2cBlockHash, List(NativeBridgeAddress, StandardBridgeAddress))
76+
.explicitGet()
77+
.filter(_.topics.intersect(E2CTopics).nonEmpty)
78+
79+
withClue("We have logs for all transactions: ") {
80+
e2cLogsInBlock.size shouldBe e2cReceipts.size
81+
}
82+
83+
step(s"Wait block $e2cBlockHash with transfers on contract")
84+
val e2cBlockConfirmationHeight = eventually {
85+
chainContract.getBlock(e2cBlockHash).value.height
86+
}
87+
88+
step(s"Wait for block $e2cBlockHash ($e2cBlockConfirmationHeight) finalization")
89+
eventually {
90+
val currFinalizedHeight = chainContract.getFinalizedBlock.height
91+
step(s"Current finalized height: $currFinalizedHeight")
92+
currFinalizedHeight should be >= e2cBlockConfirmationHeight
93+
}
94+
95+
step("Broadcast withdrawAsset transactions")
96+
val recipientAssetBalanceBefore = clRecipientAssetBalance
97+
98+
def mkE2CWithdrawTxn(transferIndex: Int, asset: Asset, amount: BigDecimal, decimals: Byte): InvokeScriptTransaction =
99+
ChainContract.withdrawAsset(
100+
sender = clRecipient,
101+
blockHash = e2cBlockHash,
102+
merkleProof = BridgeMerkleTree.mkTransferProofs(e2cLogsInBlock, transferIndex).explicitGet().reverse,
103+
transferIndexInBlock = transferIndex,
104+
amount = UnitsConvert.toWavesAtomic(amount, decimals),
105+
asset = asset
106+
)
107+
108+
val e2cWithdrawTxns = List(
109+
mkE2CWithdrawTxn(0, chainContract.nativeTokenId, userAmount * 2, NativeTokenClDecimals),
110+
mkE2CWithdrawTxn(1, issueAsset, userAmount * 2, issueAssetDecimals),
111+
mkE2CWithdrawTxn(2, Asset.Waves, userAmount, WavesDecimals)
112+
)
113+
114+
e2cWithdrawTxns.foreach(waves1.api.broadcast)
115+
e2cWithdrawTxns.foreach(txn => waves1.api.waitForSucceeded(txn.id()))
116+
117+
withClue("Assets received after E2C: ") {
118+
withClue("Issued asset: the balance was initially sufficient on CL") {
119+
val balanceAfter = clRecipientAssetBalance
120+
balanceAfter shouldBe (recipientAssetBalanceBefore + UnitsConvert.toWavesAtomic(userAmount * 2, issueAssetDecimals))
121+
}
122+
withClue("Issued asset: the StandardBridge balance was initially sufficient on EL") {
123+
terc20.getBalance(StandardBridgeAddress) shouldBe issuedE2CAmount
124+
}
125+
}
126+
127+
step("Reduce Standard Bridge issued asset balance to make C2E transfer fail")
128+
waitForTxn(terc20client.sendBurn(StandardBridgeAddress, burnE2CAmount.bigInteger, nextNonce))
129+
130+
withClue("Assert Standard Bridge issued asset balance after reducing") {
131+
terc20.getBalance(StandardBridgeAddress) shouldBe leftoverE2CAmount
132+
}
133+
134+
step("Initiate C2E transfers")
135+
val c2eRecipientAddress = EthAddress.unsafeFrom("0xAAAA00000000000000000000000000000000AAAA")
136+
137+
def mkC2ETransferTxn(asset: Asset, decimals: Byte): InvokeScriptTransaction =
138+
ChainContract.transfer(
139+
clRecipient,
140+
c2eRecipientAddress,
141+
asset,
142+
UnitsConvert.toWavesAtomic(userAmount, decimals)
143+
)
144+
145+
val c2eTransferTxns = List(
146+
mkC2ETransferTxn(Asset.Waves, WavesDecimals),
147+
mkC2ETransferTxn(chainContract.nativeTokenId, NativeTokenClDecimals),
148+
mkC2ETransferTxn(issueAsset, issueAssetDecimals),
149+
mkC2ETransferTxn(issueAsset, issueAssetDecimals),
150+
mkC2ETransferTxn(chainContract.nativeTokenId, NativeTokenClDecimals)
151+
)
152+
153+
c2eTransferTxns.foreach(waves1.api.broadcast)
154+
val c2eTransferTxnResults = c2eTransferTxns.map(txn => waves1.api.waitForSucceeded(txn.id()))
155+
156+
withClue("C2E should be on same height, can't continue the test: ") {
157+
val c2eHeights = c2eTransferTxnResults.map(_.height).toSet
158+
c2eHeights.size shouldBe 1
159+
}
160+
161+
withClue("Assets received after C2E: ") {
162+
eventually {
163+
withClue("Issued asset: the sender balance has been reduced even though the transfer has failed") {
164+
val balanceAfter = clRecipientAssetBalance
165+
balanceAfter shouldBe recipientAssetBalanceBefore
166+
}
167+
168+
withClue("Issued asset: the transfer has failed") {
169+
terc20.getBalance(c2eRecipientAddress) shouldBe leftoverE2CAmount
170+
}
171+
172+
withClue("Native token: the other transfers have succeeded") {
173+
val balanceAfter = ec1.web3j.ethGetBalance(c2eRecipientAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance
174+
BigInt(balanceAfter) shouldBe nativeE2CAmount
175+
}
176+
177+
withClue("WAVES: the other transfers have succeeded") {
178+
wwaves.getBalance(c2eRecipientAddress) shouldBe wavesE2CAmount
179+
}
180+
}
181+
}
182+
183+
step("Mining continues")
184+
val clHeightAfterTransfers = waves1.api.height()
185+
val elHeightAfterTransfers = ec1.web3j.ethBlockNumber().send().getBlockNumber.longValueExact()
186+
187+
withClue("CL height grows") {
188+
waves1.api.waitForHeight(clHeightAfterTransfers + 2)
189+
}
190+
withClue("EL height grows") {
191+
chainContract.waitForHeight(elHeightAfterTransfers + 2)
192+
}
193+
194+
// step("Sender can get their funds back from a failed transfer using a chain contract method")
195+
// TODO: implement
196+
}
197+
198+
private def clRecipientAssetBalance: Long = waves1.api.balance(clRecipient.toAddress, issueAsset)
199+
200+
private def waitForTxn(txnResult: EthSendTransaction): TransactionReceipt = eventually {
201+
ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value
202+
}
203+
204+
override def beforeAll(): Unit = {
205+
super.beforeAll()
206+
deploySolidityContracts()
207+
208+
step("Enable token transfers")
209+
val activationEpoch = waves1.api.height() + 1
210+
waves1.api.broadcastAndWait(
211+
ChainContract.enableTokenTransfersWithWaves(
212+
StandardBridgeAddress,
213+
WWavesAddress,
214+
activationEpoch = activationEpoch
215+
)
216+
)
217+
218+
step("Set strict C2E transfers feature activation epoch")
219+
waves1.api.broadcastAndWait(
220+
TxHelpers.dataEntry(
221+
chainContractAccount,
222+
IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch)
223+
)
224+
)
225+
226+
step("Wait for features activation")
227+
waves1.api.waitForHeight(activationEpoch)
228+
229+
step("Register asset")
230+
waves1.api.broadcastAndWait(ChainContract.issueAndRegister(TErc20Address, TErc20Decimals, "TERC20", "Test ERC20 token", issueAssetDecimals))
231+
eventually {
232+
standardBridge.isRegistered(TErc20Address, ignoreExceptions = true) shouldBe true
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)