From 2aa0a64349b36e10e5af600d22fedf61c9d59fe6 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 23 May 2025 14:37:26 +0200 Subject: [PATCH] first implementation of token minting and approval --- tests/waku_rln_relay/utils_onchain.nim | 238 +++++++++++++++++++++++-- 1 file changed, 220 insertions(+), 18 deletions(-) diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index 7116a0796..a433ad9d1 100644 --- a/tests/waku_rln_relay/utils_onchain.nim +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -81,14 +81,157 @@ proc getForgePath*(): string = forgePath = joinPath(forgePath, ".foundry/bin/forge") return $forgePath -proc deployTestToken*(): Future[string] {.async.} = +contract(ERC20Token): + proc approve(spender: Address, amount: UInt256) + proc allowance(owner: Address, spender: Address): UInt256 {.view.} + proc balanceOf(account: Address): UInt256 {.view.} + proc mint(recipient: Address, amount: UInt256) + +proc getTokenBalance*( + web3: Web3, tokenAddress: Address, account: Address +): Future[UInt256] {.async.} = + let token = web3.contractSender(ERC20Token, tokenAddress) + return await token.balanceOf(account).call() + +proc ethToWei(eth: UInt256): UInt256 = + eth * 1000000000000000000.u256 + +proc sendMintCall*( + web3: Web3, + accountFrom: Address, + tokenAddress: Address, + recipientAddress: Address, + amountTokens: UInt256, + recipientBalanceBeforeExpectedTokens: Option[UInt256] = none(UInt256), +): Future[TxHash] {.async.} = + # Create mint transaction + let mintSelector = "0x40c10f19" # Pad the address and amount to 32 bytes each + let addressHex = recipientAddress.toHex() + let paddedAddress = addressHex.align(64, '0') + + let amountHex = amountTokens.toHex() + let amountWithout0x = + if amountHex.startsWith("0x"): + amountHex[2 .. ^1] + else: + amountHex + let paddedAmount = amountWithout0x.align(64, '0') # Create the call data + let mintCallData = mintSelector & paddedAddress & paddedAmount # Get gas price + let gasPrice = int(await web3.provider.eth_gasPrice()) + + # Create the transaction + var tx: TransactionArgs + tx.`from` = Opt.some(accountFrom) + tx.to = Opt.some(tokenAddress) + tx.value = Opt.some(0.u256) # No ETH is sent for token operations + tx.gasPrice = Opt.some(Quantity(gasPrice)) + tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData)) + + debug "Sending mint call" + # Send the transaction + let txHash = await web3.send(tx) + + let balanceOfSelector = "0x70a08231" + let balanceCallData = balanceOfSelector & paddedAddress + + # Wait a bit for transaction to be mined + await sleepAsync(500.milliseconds) + + var balanceCallTx: TransactionArgs + balanceCallTx.to = Opt.some(tokenAddress) + balanceCallTx.data = Opt.some(byteutils.hexToSeqByte(balanceCallData)) + + # Make the call to get updated balance + let balanceResponse = await web3.provider.eth_call(balanceCallTx, "latest") + + let balanceHexStr = "0x" & byteutils.toHex(balanceResponse) + let balanceAfterTokens = stint.parse(balanceHexStr, UInt256, 16) + let balanceAfterExpectedTokens = + recipientBalanceBeforeExpectedTokens.get() + amountTokens + debug "token balance after minting", balanceAfterTokens = balanceAfterTokens + assert balanceAfterTokens == balanceAfterExpectedTokens, + fmt"Token balance is {balanceAfterTokens} but expected {balanceAfterExpectedTokens}" + + let balance3 = await web3.provider.eth_getBalance(recipientAddress, "latest") + debug "sendMintCall: after mint recipientAddress account balance: ", + recipientAddress = recipientAddress, balance = balance3 + + return txHash + +proc checkApproval*( + web3: Web3, tokenAddress: Address, owner: Address, spender: Address +): Future[UInt256] {.async.} = + let token = web3.contractSender(ERC20Token, tokenAddress) + let allowance = await token.allowance(owner, spender).call() + debug "Current allowance", owner = owner, spender = spender, allowance = allowance + return allowance + +proc sendApproveCall*( + web3: Web3, + accountFrom: Address, + privateKey: keys.PrivateKey, + tokenAddress: Address, + spender: Address, + amountWei: UInt256, +): Future[TxHash] {.async.} = + # # ERC20 approve function signature: approve(address spender, uint256 amount) + # # Create the contract call data + # # Method ID for approve(address,uint256) is 0x095ea7b3 + + # Temporarily set the private key + let oldPrivateKey = web3.privateKey + web3.privateKey = Opt.some(privateKey) + web3.lastKnownNonce = Opt.none(Quantity) # Reset nonce tracking + + # Create approve transaction + let approveSelector = "0x095ea7b3" + + let addressHex = spender.toHex() # Already without 0x + let paddedAddress = addressHex.align(64, '0') + let amountHex = amountWei.toHex() + # let amountWithout0x = + # if amountHex.startsWith("0x"): + # amountHex[2 .. ^1] + # else: + # amountHex + let paddedAmount = amountHex.align(64, '0') + let approveCallData = approveSelector & paddedAddress & paddedAmount + + # Get gas price + let gasPrice = int(await web3.provider.eth_gasPrice()) + + # Create the transaction + var tx: TransactionArgs + tx.`from` = Opt.some(accountFrom) + tx.to = Opt.some(tokenAddress) + tx.value = Opt.some(0.u256) + tx.gasPrice = Opt.some(Quantity(gasPrice)) + tx.gas = Opt.some(Quantity(100000)) # Add gas limit + tx.data = Opt.some(byteutils.hexToSeqByte(approveCallData)) + tx.chainId = Opt.some(Quantity(CHAIN_ID)) + + debug "Sending approve call with transaction", tx = tx + + try: + # Send will automatically sign because privateKey is set + let txHash = await web3.send(tx) + return txHash + except CatchableError as e: + error "Failed to send approve transaction", error = e.msg + raise e + finally: + # Always restore the old private key + web3.privateKey = oldPrivateKey + +proc deployTestToken*( + pk: keys.PrivateKey, acc: Address, web3: Web3 +): Future[Address] {.async.} = ## Executes a Foundry forge script that deploys the a token contract (ERC-20) used for testing. This is a prerequisite to enable the contract deployment and this token contract address needs to be minted and approved for the accounts that need to register membership with the contract ## submodulePath: path to the submodule containing contract deploy scripts # All RLN related tests should be run from the root directory of the project let submodulePath = "./vendor/waku-rlnv2-contract" - let PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" let forgePath = getForgePath() debug "Forge path", forgePath @@ -124,7 +267,7 @@ proc deployTestToken*(): Future[string] {.async.} = # Deploy TestToken contract let forgeCmdTestToken = - fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {PRIVATE_KEY} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" + fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {pk} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" let (outputDeployTestToken, exitCodeDeployTestToken) = execCmdEx(forgeCmdTestToken) trace "Executed forge command to deploy TestToken contract", output = outputDeployTestToken @@ -144,9 +287,51 @@ proc deployTestToken*(): Future[string] {.async.} = ##TODO: raise exception here? let testTokenAddress = testTokenAddressRes.get() debug "Address of the TestToken contract", testTokenAddress - putEnv("TOKEN_ADDRESS", testTokenAddress) + debug "TestToken contract deployer account details", account = acc, pk = pk - return testTokenAddress + let testTokenAddressBytes = hexToByteArray[20](testTokenAddress) + let testTokenAddressAddress = Address(testTokenAddressBytes) + + return testTokenAddressAddress + +proc approveAndVerify*( + web3: Web3, + accountFrom: Address, + privateKey: keys.PrivateKey, + tokenAddress: Address, + spender: Address, + amountWei: UInt256, +): Future[bool] {.async.} = + debug "Starting approval process", + owner = accountFrom, + tokenAddress = tokenAddress, + spender = spender, + amount = amountWei + + # Send approval + let txHash = await sendApproveCall( + web3, accountFrom, privateKey, tokenAddress, spender, amountWei + ) + + debug "Approval transaction sent", txHash = txHash + + # Wait for transaction to be mined + let receipt = await web3.getMinedTransactionReceipt(txHash) + debug "Transaction mined", status = receipt.status, blockNumber = receipt.blockNumber + + # Check if status is present and successful + if receipt.status.isNone or receipt.status.get != 1.Quantity: + error "Approval transaction failed" + return false + + # Give it a moment for the state to settle + await sleepAsync(100.milliseconds) + + # Check allowance after mining + let allowanceAfter = await checkApproval(web3, tokenAddress, accountFrom, spender) + debug "Allowance after approval", amount = allowanceAfter + + return allowanceAfter >= amountWei proc executeForgeContractDeployScripts*(): Future[Address] {.async.} = ## Executes a set of foundry forge scripts required to deploy the RLN contract and returns the deployed proxy contract address @@ -223,10 +408,10 @@ proc executeForgeContractDeployScripts*(): Future[Address] {.async.} = let wakuRlnV2AddressRes = getContractAddressFromDeployScriptOutput(outputDeployWakuRln) if wakuRlnV2AddressRes.isErr(): - error "Failed to get LinearPriceCalculator contract address from deploy script output" + error "Failed to get WakuRlnV2 contract address from deploy script output" ##TODO: raise exception here? let wakuRlnV2Address = wakuRlnV2AddressRes.get() - debug "Address of the LinearPriceCalculator contract", wakuRlnV2Address + debug "Address of the WakuRlnV2 contract", wakuRlnV2Address putEnv("WAKURLNV2_ADDRESS", wakuRlnV2Address) # Deploy Proxy contract @@ -321,7 +506,7 @@ proc sendEthTransfer*( let balanceBeforeWei = await web3.provider.eth_getBalance(accountTo, "latest") let balanceBeforeExpectedWei = accountToBalanceBeforeExpectedWei.get() assert balanceBeforeWei == balanceBeforeExpectedWei, - fmt"Balance is {balanceBeforeWei} but expected {balanceBeforeExpectedWei}" + fmt"Balance is {balanceBeforeWei} before transfer but expected {balanceBeforeExpectedWei}" let gasPrice = int(await web3.provider.eth_gasPrice()) @@ -334,17 +519,17 @@ proc sendEthTransfer*( # TODO: handle the error if sending fails let txHash = await web3.send(tx) + # Wait a bit for transaction to be mined + await sleepAsync(2000.milliseconds) + if doBalanceAssert: let balanceAfterWei = await web3.provider.eth_getBalance(accountTo, "latest") let balanceAfterExpectedWei = accountToBalanceBeforeExpectedWei.get() + amountWei assert balanceAfterWei == balanceAfterExpectedWei, - fmt"Balance is {balanceAfterWei} but expected {balanceAfterExpectedWei}" + fmt"Balance is {balanceAfterWei} after transfer but expected {balanceAfterExpectedWei}" return txHash -proc ethToWei(eth: UInt256): UInt256 = - eth * 1000000000000000000.u256 - proc createEthAccount*( ethAmount: UInt256 = 1000.u256 ): Future[(keys.PrivateKey, Address)] {.async.} = @@ -449,24 +634,41 @@ proc setupOnchainGroupManager*( let rlnInstance = rlnInstanceRes.get() - let testTokenAddress = await deployTestToken() - putEnv("TOKEN_ADDRESS", testTokenAddress) - - let contractAddress = await executeForgeContractDeployScripts() # connect to the eth client let web3 = await newWeb3(ethClientUrl) let accounts = await web3.provider.eth_accounts() - web3.defaultAccount = accounts[0] + web3.defaultAccount = accounts[1] let (privateKey, acc) = createEthAccount(web3) # we just need to fund the default account # the send procedure returns a tx hash that we don't use, hence discard discard await sendEthTransfer( - web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256) + web3, web3.defaultAccount, acc, ethToWei(5000.u256), some(0.u256) ) + let testTokenAddress = await deployTestToken(privateKey, acc, web3) + putEnv("TOKEN_ADDRESS", testTokenAddress.toHex()) + + discard await sendMintCall( + web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256), some(0.u256) + ) + let contractAddress = await executeForgeContractDeployScripts() + + let approvalSuccess = await approveAndVerify( + web3, + acc, # owner + privateKey, + testTokenAddress, # ERC20 token address + contractAddress, # spender - the proxy contract that will spend the tokens + ethToWei(500.u256), + ) + + # Also check the token balance + let tokenBalance = await getTokenBalance(web3, testTokenAddress, acc) + debug "Token balance before register", owner = acc, balance = tokenBalance + let manager = OnchainGroupManager( ethClientUrls: @[ethClientUrl], ethContractAddress: $contractAddress,