Skip to content

Commit 621c6a1

Browse files
Refactor native token transfers to use deposited transactions
1 parent 3a9b0a6 commit 621c6a1

File tree

15 files changed

+667
-26
lines changed

15 files changed

+667
-26
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package units
2+
3+
import com.wavesplatform.transaction.TxHelpers
4+
import com.wavesplatform.state.IntegerDataEntry
5+
import org.web3j.crypto.Credentials
6+
import org.web3j.protocol.core.DefaultBlockParameterName
7+
import units.eth.EthAddress
8+
9+
class C2ENativeTokenTransfersViaDepositsTestSuite extends BaseDockerTestSuite {
10+
private val clSender = clRichAccount1
11+
private val elReceiver = elRichAccount1
12+
private val elReceiverAddress = EthAddress.unsafeFrom(elReceiver.getAddress)
13+
14+
private val userAmount = 1
15+
private val clAmount = UnitsConvert.toUnitsInWaves(userAmount)
16+
private val elAmount = UnitsConvert.toWei(userAmount)
17+
18+
"C2E native token transfers via deposited transactions" in {
19+
def getElBalance: BigInt = ec1.web3j.ethGetBalance(elReceiverAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance
20+
val elBalanceBefore = getElBalance
21+
step(s"elBalanceBefore: $elBalanceBefore")
22+
23+
waves1.api.broadcastAndWait(
24+
ChainContract.transfer(
25+
sender = clSender,
26+
destElAddress = elReceiverAddress,
27+
asset = chainContract.nativeTokenId,
28+
amount = clAmount
29+
)
30+
)
31+
32+
withClue("Expected amount: ") {
33+
eventually {
34+
val elBalanceAfter = getElBalance
35+
step(s"elBalanceAfter: $elBalanceAfter")
36+
val expectedBalanceAfter = elBalanceBefore + elAmount
37+
UnitsConvert.toUser(elBalanceAfter, NativeTokenElDecimals) shouldBe UnitsConvert.toUser(expectedBalanceAfter, NativeTokenElDecimals)
38+
}
39+
}
40+
}
41+
42+
override def beforeAll(): Unit = {
43+
44+
super.beforeAll()
45+
deploySolidityContracts()
46+
47+
step("Enable token transfers")
48+
val activationEpoch = waves1.api.height() + 1
49+
waves1.api.broadcastAndWait(
50+
ChainContract.enableTokenTransfersWithWaves(
51+
StandardBridgeAddress,
52+
WWavesAddress,
53+
activationEpoch = activationEpoch
54+
)
55+
)
56+
waves1.api.waitForHeight(activationEpoch)
57+
58+
step("Set native token transfers via deposits feature activation epoch")
59+
val featureActivationEpoch = waves1.api.height() + 2
60+
val txRes = waves1.api.broadcastAndWait(
61+
TxHelpers.dataEntry(
62+
chainContractAccount,
63+
IntegerDataEntry("nativeTokenDepositTransfersActivationEpoch", featureActivationEpoch)
64+
)
65+
)
66+
67+
step("Wait for feature activation")
68+
waves1.api.waitForHeight(featureActivationEpoch)
69+
70+
step("Prepare: issue tokens on chain contract and transfer to a user")
71+
waves1.api.broadcastAndWait(
72+
TxHelpers.reissue(
73+
asset = chainContract.nativeTokenId,
74+
sender = chainContractAccount,
75+
amount = clAmount
76+
)
77+
)
78+
waves1.api.broadcastAndWait(
79+
TxHelpers.transfer(
80+
from = chainContractAccount,
81+
to = clSender.toAddress,
82+
amount = clAmount,
83+
asset = chainContract.nativeTokenId
84+
)
85+
)
86+
}
87+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package units
2+
3+
import com.wavesplatform.common.utils.EitherExt2
4+
import com.wavesplatform.common.utils.EitherExt2.explicitGet
5+
import com.wavesplatform.state.IntegerDataEntry
6+
import com.wavesplatform.transaction.TxHelpers
7+
import com.wavesplatform.transaction.Asset
8+
import com.wavesplatform.transaction.Asset.IssuedAsset
9+
import com.wavesplatform.transaction.smart.InvokeScriptTransaction
10+
import monix.execution.atomic.AtomicInt
11+
import org.web3j.protocol.core.DefaultBlockParameterName
12+
import org.web3j.protocol.core.methods.response.{EthSendTransaction, TransactionReceipt}
13+
import org.web3j.tx.RawTransactionManager
14+
import org.web3j.tx.gas.DefaultGasProvider
15+
import org.web3j.utils.Convert
16+
import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees
17+
import units.docker.EcContainer
18+
import units.el.{BridgeMerkleTree, E2CTopics, Erc20Client}
19+
import units.eth.EthAddress
20+
21+
import scala.jdk.OptionConverters.RichOptional
22+
23+
class MultipleTransfersViaDepositsTestSuite extends BaseDockerTestSuite {
24+
private val clAssetOwner = clRichAccount2
25+
private val clRecipient = clRichAccount1
26+
private val elSender = elRichAccount1
27+
private val elSenderAddress = elRichAddress1
28+
29+
private val issueAssetDecimals = 8.toByte
30+
private lazy val issueAsset = chainContract.getRegisteredAsset(1) // 0 is WAVES
31+
32+
private val userAmount = BigDecimal("1")
33+
34+
private val tenGwei = BigInt(Convert.toWei("10", Convert.Unit.GWEI).toBigIntegerExact)
35+
36+
private val gasProvider = new DefaultGasProvider
37+
private lazy val txnManager = new RawTransactionManager(ec1.web3j, elSender, EcContainer.ChainId, 20, 2000)
38+
private lazy val wwaves = new Erc20Client(ec1.web3j, WWavesAddress, txnManager, gasProvider)
39+
private lazy val terc20 = new Erc20Client(ec1.web3j, TErc20Address, txnManager, gasProvider)
40+
41+
"Checking balances in E2C2E transfers" in {
42+
val currNonce =
43+
AtomicInt(ec1.web3j.ethGetTransactionCount(elSenderAddress.hex, DefaultBlockParameterName.PENDING).send().getTransactionCount.intValueExact())
44+
def nextNonce: Int = currNonce.getAndIncrement()
45+
46+
val nativeE2CAmount = UnitsConvert.toAtomic(userAmount, NativeTokenElDecimals)
47+
val issuedE2CAmount = UnitsConvert.toAtomic(userAmount, TErc20Decimals)
48+
val wavesE2CAmount = UnitsConvert.toAtomic(userAmount, WwavesDecimals)
49+
50+
step("Send allowances")
51+
List(
52+
terc20.sendApprove(StandardBridgeAddress, issuedE2CAmount, nextNonce),
53+
wwaves.sendApprove(StandardBridgeAddress, wavesE2CAmount, nextNonce)
54+
).foreach(waitFor)
55+
56+
step("Initiate E2C transfers")
57+
val e2cNativeTxn = nativeBridge.sendSendNative(elSender, clRecipient.toAddress, nativeE2CAmount, nextNonce)
58+
val e2cIssuedTxn = standardBridge.sendBridgeErc20(elSender, TErc20Address, clRecipient.toAddress, issuedE2CAmount, nextNonce)
59+
val e2cWavesTxn = standardBridge.sendBridgeErc20(elSender, WWavesAddress, clRecipient.toAddress, wavesE2CAmount, nextNonce)
60+
61+
chainContract.waitForEpoch(waves1.api.height() + 1) // Bypass rollbacks
62+
val e2cReceipts = List(e2cIssuedTxn, e2cIssuedTxn, e2cWavesTxn).map { txn =>
63+
eventually {
64+
val hash = txn.getTransactionHash
65+
withClue(s"$hash: ") {
66+
ec1.web3j.ethGetTransactionReceipt(hash).send().getTransactionReceipt.toScala.value
67+
}
68+
}
69+
}
70+
71+
withClue("E2C should be on same height, can't continue the test: ") {
72+
val e2cHeights = e2cReceipts.map(_.getBlockNumber.intValueExact()).toSet
73+
e2cHeights.size shouldBe 1
74+
}
75+
76+
val e2cBlockHash = BlockHash(e2cReceipts.head.getBlockHash)
77+
log.debug(s"Block with e2c transfers: $e2cBlockHash")
78+
79+
val e2cLogsInBlock = ec1.engineApi
80+
.getLogs(e2cBlockHash, List(NativeBridgeAddress, StandardBridgeAddress), Nil)
81+
.explicitGet()
82+
.filter(_.topics.intersect(E2CTopics).nonEmpty)
83+
84+
withClue("We have logs for all transactions: ") {
85+
e2cLogsInBlock.size shouldBe e2cReceipts.size
86+
}
87+
88+
step(s"Wait block $e2cBlockHash with transfers on contract")
89+
val e2cBlockConfirmationHeight = eventually {
90+
chainContract.getBlock(e2cBlockHash).value.height
91+
}
92+
93+
step(s"Wait for block $e2cBlockHash ($e2cBlockConfirmationHeight) finalization")
94+
eventually {
95+
val currFinalizedHeight = chainContract.getFinalizedBlock.height
96+
step(s"Current finalized height: $currFinalizedHeight")
97+
currFinalizedHeight should be >= e2cBlockConfirmationHeight
98+
}
99+
100+
step("Broadcast withdrawAsset transactions")
101+
val recipientWavesBalanceBefore = clRecipientWavesBalance
102+
val recipientAssetBalanceBefore = clRecipientAssetBalance
103+
val recipientNativeTokenBalanceBefore = clRecipientNativeTokenBalance
104+
105+
def mkE2CWithdrawTxn(transferIndex: Int, asset: Asset, decimals: Byte): InvokeScriptTransaction = ChainContract.withdrawAsset(
106+
sender = clRecipient,
107+
blockHash = e2cBlockHash,
108+
merkleProof = BridgeMerkleTree.mkTransferProofs(e2cLogsInBlock, transferIndex).explicitGet().reverse,
109+
transferIndexInBlock = transferIndex,
110+
amount = UnitsConvert.toWavesAtomic(userAmount, decimals),
111+
asset = asset
112+
)
113+
114+
val e2cWithdrawTxns = List(
115+
mkE2CWithdrawTxn(0, chainContract.nativeTokenId, NativeTokenClDecimals),
116+
mkE2CWithdrawTxn(1, issueAsset, issueAssetDecimals),
117+
mkE2CWithdrawTxn(2, Asset.Waves, WavesDecimals)
118+
)
119+
120+
e2cWithdrawTxns.foreach(waves1.api.broadcast)
121+
e2cWithdrawTxns.foreach(txn => waves1.api.waitForSucceeded(txn.id()))
122+
123+
withClue("Assets received after E2C: ") {
124+
withClue("Native token: ") {
125+
val balanceAfter = clRecipientNativeTokenBalance
126+
balanceAfter shouldBe (recipientNativeTokenBalanceBefore + UnitsConvert.toWavesAtomic(userAmount, NativeTokenClDecimals))
127+
}
128+
129+
withClue("Issued asset: ") {
130+
val balanceAfter = clRecipientAssetBalance
131+
balanceAfter shouldBe (recipientAssetBalanceBefore + UnitsConvert.toWavesAtomic(userAmount, issueAssetDecimals))
132+
}
133+
134+
withClue("WAVES: ") {
135+
val balanceAfter = clRecipientWavesBalance
136+
val fee = DefaultFees.ChainContract.withdrawFee * e2cWithdrawTxns.size
137+
balanceAfter shouldBe (recipientWavesBalanceBefore + UnitsConvert.toWavesAtomic(userAmount, WavesDecimals) - fee)
138+
}
139+
}
140+
141+
step("Initiate C2E transfers")
142+
val c2eRecipientAddress = EthAddress.unsafeFrom("0xAAAA00000000000000000000000000000000AAAA")
143+
144+
def mkC2ETransferTxn(asset: Asset, decimals: Byte): InvokeScriptTransaction = ChainContract.transfer(
145+
clRecipient,
146+
c2eRecipientAddress,
147+
asset,
148+
UnitsConvert.toWavesAtomic(userAmount, decimals)
149+
)
150+
151+
val c2eTransferTxns = List(
152+
mkC2ETransferTxn(chainContract.nativeTokenId, NativeTokenClDecimals),
153+
mkC2ETransferTxn(issueAsset, issueAssetDecimals),
154+
mkC2ETransferTxn(Asset.Waves, WavesDecimals)
155+
)
156+
157+
c2eTransferTxns.foreach(waves1.api.broadcast)
158+
val c2eTransferTxnResults = c2eTransferTxns.map(txn => waves1.api.waitForSucceeded(txn.id()))
159+
160+
withClue("C2E should be on same height, can't continue the test: ") {
161+
val c2eHeights = c2eTransferTxnResults.map(_.height).toSet
162+
c2eHeights.size shouldBe 1
163+
}
164+
165+
withClue("Assets received after C2E: ") {
166+
eventually {
167+
withClue("Native token: ") {
168+
val balanceAfter = ec1.web3j.ethGetBalance(c2eRecipientAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance
169+
BigInt(balanceAfter) shouldBe nativeE2CAmount
170+
}
171+
172+
withClue("Issued asset: ") {
173+
terc20.getBalance(c2eRecipientAddress) shouldBe issuedE2CAmount
174+
}
175+
176+
withClue("WAVES: ") {
177+
wwaves.getBalance(c2eRecipientAddress) shouldBe wavesE2CAmount
178+
}
179+
}
180+
}
181+
}
182+
183+
private def clRecipientWavesBalance: Long = waves1.api.balance(clRecipient.toAddress, Asset.Waves)
184+
private def clRecipientAssetBalance: Long = waves1.api.balance(clRecipient.toAddress, issueAsset)
185+
private def clRecipientNativeTokenBalance: Long = waves1.api.balance(clRecipient.toAddress, chainContract.nativeTokenId)
186+
187+
override def beforeAll(): Unit = {
188+
super.beforeAll()
189+
deploySolidityContracts()
190+
191+
step("Enable token transfers")
192+
val activationEpoch = waves1.api.height() + 1
193+
waves1.api.broadcastAndWait(
194+
ChainContract.enableTokenTransfersWithWaves(
195+
StandardBridgeAddress,
196+
WWavesAddress,
197+
activationEpoch = activationEpoch
198+
)
199+
)
200+
waves1.api.waitForHeight(activationEpoch)
201+
202+
step("Set native token transfers via deposits feature activation epoch")
203+
val featureActivationEpoch = waves1.api.height() + 2
204+
val txRes = waves1.api.broadcastAndWait(
205+
TxHelpers.dataEntry(
206+
chainContractAccount,
207+
IntegerDataEntry("nativeTokenDepositTransfersActivationEpoch", featureActivationEpoch)
208+
)
209+
)
210+
step("Wait for feature activation")
211+
waves1.api.waitForHeight(featureActivationEpoch)
212+
213+
step("Register asset")
214+
waves1.api.broadcastAndWait(ChainContract.issueAndRegister(TErc20Address, TErc20Decimals, "TERC20", "Test ERC20 token", issueAssetDecimals))
215+
eventually {
216+
standardBridge.isRegistered(TErc20Address, ignoreExceptions = true) shouldBe true
217+
}
218+
}
219+
}

contracts/eth/src/StandardBridge.sol

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ contract StandardBridge {
2929
uint256 amount
3030
);
3131

32+
event ETHBridgeFinalized(
33+
address indexed elTo,
34+
uint256 amount
35+
);
36+
3237
event RegistryUpdated(address[] addedTokens, uint8[] addedTokenExponents, address[] removedTokens);
3338

3439
/// @notice Ensures that the caller is an empty address.
@@ -145,6 +150,24 @@ contract StandardBridge {
145150
_emitERC20BridgeFinalized(_localToken, _from, _to, _amount);
146151
}
147152

153+
/// @notice Finalizes a native token bridge on this chain. Can only be triggered by the
154+
/// Chain contract on the remote chain.
155+
/// @param _to Address of the receiver.
156+
/// @param _amount Amount of the native token being bridged.
157+
function finalizeBridgeETH(
158+
address _to,
159+
uint256 _amount
160+
) public payable onlyMiner {
161+
require(
162+
msg.value == _amount,
163+
"StandardBridge: amount sent does not match amount required"
164+
);
165+
require(_to != address(this), "StandardBridge: cannot send to self");
166+
167+
Address.sendValue(payable(_to), _amount);
168+
emit ETHBridgeFinalized(_to, _amount);
169+
}
170+
148171
/// @notice Emits the ERC20BridgeFinalized event and if necessary the appropriate legacy
149172
/// event when an ERC20 bridge is initiated to the other chain.
150173
/// @param _localToken Address of the ERC20 on this chain.

local-network/deploy/deploy.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
os.path.join(contracts_dir, "waves", "src", "main.ride"), "r", encoding="utf-8"
4444
) as file:
4545
source = file.read()
46-
r = network.cl_chain_contract.setScript(source, txFee=5_400_000)
46+
r = network.cl_chain_contract.setScript(source, txFee=7_000_000)
4747
waves.force_success(log, r, "Can not set the chain contract script")
4848

4949
if not network.cl_chain_contract.isContractSetup():
@@ -184,6 +184,25 @@
184184
while pw.height() < activation_height:
185185
sleep(3)
186186

187+
key = "nativeTokenDepositTransfersActivationEpoch"
188+
if len(network.cl_chain_contract.getData(regex=key)) == 0:
189+
log.info("Activation of native token transfers via deposits...")
190+
activation_height = pw.height() + 2
191+
enable_native_token_transfers_via_deposits_txn = (
192+
network.cl_chain_contract.oracleAcc.dataTransaction(
193+
[{"type": "integer", "key": key, "value": activation_height}]
194+
)
195+
)
196+
waves.force_success(
197+
log,
198+
enable_native_token_transfers_via_deposits_txn,
199+
"Could not enable native token transfers via deposits",
200+
wait=True,
201+
)
202+
log.info("Waiting for activation of native token transfers via deposits")
203+
while pw.height() < activation_height:
204+
sleep(3)
205+
187206
log.info(f"StandardBridge address: {network.bridges.standard_bridge.contract_address}")
188207
log.info(f"ERC20 token address: {network.el_test_erc20.contract_address}")
189208
# Issues and registers asset under the hood

0 commit comments

Comments
 (0)