4.2 Quick Integration: ERC20 Assets
Overview of Core Functions
The SurferMonkey SDK simplifies the process of executing privacy-compliant blockchain transactions by providing three core functions: getInstitutionMembershipProof()
, createDeposit()
, and createWithdraw()
. These functions abstract the complexity of interacting with the blockchain and ensure that the entire process remains secure and compliant.
-
getInstitutionMembershipProof()
: Generates the User Institution Membership Proof (userMerkleProof
). This cryptographic proof verifies that the user is part of the institution's whitelist. The institution backend handles this step to preserve user privacy while providing the necessary proof for subsequent transactions. -
createDeposit()
: Generates a deposit object, which includes data required in subsequent stages of the transaction. It also automatically creates a ZKP Leaf Verification, which is essential to lock the ERC20 asset into the Universal Plugin. -
createWithdraw()
: Generates a Withdraw Zero-Knowledge Proof (ZKP) to maintain transaction privacy. After creating a deposit, this function ensures the legitimacy of the action to withdraw the funds from the Universal Plugin in a privacy-preserving way.
Treat the createDeposit()
response with the same level of security as your private keys:
- Do not share any information from the response.
- Do not modify any parameters. The SDK and the ZKP Leaf Verification are designed to ensure security, consistency, and error handling. Altering any value from the
createDeposit()
response could compromise the transaction, potentially resulting in the loss of your funds.
Blockchain Flow Overview (ERC20)
The diagram in Figure 6 illustrates the flow of an ERC20 token transaction involving both deposit and withdrawal phases.
- Deposit Phase: Before the deposit, Alice approves the ERC20 asset, allowing the Universal Plugin to lock the tokens. She then deposits the approved ERC20 tokens into the Universal Plugin.
- Withdraw Phase: Alice sends a Zero-Knowledge Proof (ZKP) to the Relayer. The Relayer initiates the withdrawal through the Mixer, which unlocks the ERC20 funds from the Universal Plugin. The funds are then transferred to the Proxy, which sends the tokens to Bob through an agnostic transfer call.
Figure 6. - ERC20 transfer blockchain flow
Step-by-Step Minimal Integration Example (ERC20)
This step-by-step guide illustrates a minimal setup for an ERC20 token transaction using the SurferMonkey SDK:
-
Import the SDK
Start by importing the SurferMonkey SDK and other required libraries in your JavaScript file.
const SurferMonkey = require('@surfermonkey/sdk_babyulu'); // SurferMonkey SDK
const ethers = require('ethers');
const USDC_ABI = require('ERC20_ABI.json'); -
Define Input Transaction Parameters
Create the input transaction object to specify the amount and asset details.
const inputTx = {
AMOUNT_TOTAL: "1000000", // Amount to be locked (6 decimals for USDC)
ASSET_ID: "0xUSDCAddress" // ERC20 asset ID (USDC contract address)
}; -
Define Output Transaction Parameters
Define how the locked assets will be distributed in this output transaction.
infoFor ERC20 Transfers, specify the
targetSC
as the ERC20 Token Contract address. Use the following function details:- Function Header: "
function transfer(address recipient, uint256 amount)
" - Function Name: "
transfer
"
const outputTxArr = [
{
amountOutput: "1000000",
smartContractCalls: [
{
payloadAmountNative: "0", // Native amount ignored for ERC20
targetSC: inputTx.ASSET_ID, // USDC contract address
payloadObject: {
functionHeader: "function transfer(address recipient, uint256 amount)",
functionName: "transfer",
payloadParmsArr: ["0xRecipientAddress", "1000000"]
}
}
]
}
]; - Function Header: "
-
Create the User Message
Prepare the user message configuration that will be passed to the
createDeposit()
function.warningThe
userEOA
must be included in the whitelist Merkle Tree by the institution admin. Failure to do so will prevent the deposit transaction from being processed successfully.// Get User Institution Merkle Proof from the backend
const userMerkleProof = await SurferMonkey.getInstitutionMembershipProof({
BACKEND_RPC,
userEOA: "0xYourEvmAddressHere",
institutionLeaves: ["0xValidUserAddress1", "0xValidUserAddress2", "0xYourEvmAddressHere"]
});
// Create the user message
const userMessage = {
userEOA: "0xYourEvmAddressHere",
inputTx: inputTx,
outputTxArr: outputTxArr,
numberTxOut: 1,
pubKey: ["PubKeyString[0]", "PubKeyString[1]"],
chainID: "ChainIdentifier",
userMerkleProof: userMerkleProof
}; -
Approve ERC20 Token for Universal Plugin
ERC20 assets require an approval step before locking them into the Universal Plugin. Approve the plugin to spend the specified token amount on your behalf.
const USER_SIGNER = new ethers.Wallet(PRIV_KEY, PROVIDER);
const ERC20_SC = new ethers.Contract(inputTx.ASSET_ID, USDC_ABI, USER_SIGNER);
const approveTx = await ERC20_SC.approve("0xUniversalPluginAddress", inputTx.AMOUNT_TOTAL, {
maxPriorityFeePerGas: Number(25000000000),
maxFeePerGas: Number(27000000000)
});
await approveTx.wait(); // Wait for transaction to get minted -
Create Deposit Object and Lock Funds
Call the
createDeposit()
function to generate the data structures and ZKP required to lock the funds into the Universal Plugin, then submit the deposit to the blockchain via the Universal Plugindeposit()
function.warningFor ERC20 deposits, msg.value must be set to 0 or not be used at all; do not send native value otherwise, the transaction will revert.
const deposit = await SurferMonkey.createDeposit({
BACKEND_RPC,
userMessage,
verbose: true
});
// Lock funds into Universal Plugin
const UniversalPlugin_SC = new ethers.Contract("0xUniversalPluginAddress", UP_ABI, USER_SIGNER);
const submitDeposit = await UniversalPlugin_SC.Deposit(
deposit.SOLIDITY_DATA.a,
deposit.SOLIDITY_DATA.b,
deposit.SOLIDITY_DATA.c,
deposit.SOLIDITY_DATA.Input,
{
maxPriorityFeePerGas: Number(25000000000),
maxFeePerGas: Number(27000000000),
gasLimit: Number(5142880)
}
);
await submitDeposit.wait(); -
Generate Withdraw Zero-Knowledge Proof
Generate the Zero-Knowledge Proof (ZKP) by calling
createWithdraw()
after creating and submitting the deposit.infoThe
targetLeafChild
parameter specifies the index of the output transaction withinoutputTxArr
that you intend to withdraw from the Universal Plugin.const getZKPSignalsObject = {
BACKEND_RPC,
depositJSON: deposit,
targetLeafChild: 0, // Use the first output transaction
verbose: true
};
const zkpSolidityData = await SurferMonkey.createWithdraw(getZKPSignalsObject);
console.log("ZKP generated:", zkpSolidityData); -
Submit Zero-Knowledge Proof to Blockchain
Use ethers.js or any blockchain library to submit the generated ZKP to the blockchain.
warningUse a different Relayer address for Withdraw than the one used for Deposit to maintain privacy. Alternatively, use the SurferMonkey Relayer for enhanced anonymity.
const TARGET_MIXER = new ethers.Contract("0xMixerAddress", MIXER_ABI, RELAYER_SIGNER);
const tx = await TARGET_MIXER.withdraw(
zkpSolidityData.a,
zkpSolidityData.b,
zkpSolidityData.c,
zkpSolidityData.Input,
deposit.depositArr[0].payloadData,
deposit.depositArr[0].targetSC,
deposit.depositArr[0].payloadAmountPerCall,
{
value: 0,
maxPriorityFeePerGas: Number(25000000000),
maxFeePerGas: Number(27000000000),
gasLimit: Number(5142880)
}
);
console.log("ZKP transaction submitted:", tx.hash);
🎉 Congrats on Completing the ERC20 Integration! 🎉
You're now ready to conduct privacy-compliant blockchain transactions with ERC20 assets using SurferMonkey. 🚀✨
Full integration ERC20 example code
// Full Script: Minimal Integration Example for ERC20 Asset
// Import the SDK
const SurferMonkey = require('@surfermonkey/sdk_babyulu'); // SurferMonkey SDK
const ethers = require('ethers');
const UP_ABI = require('UniversalPlugin.json');
const MIXER_ABI = require('SurferMonkeyMixer.json');
const USDC_ABI = require('USDC.json');
const PRIV_KEY = "0xYourPrivateKeyHere";
const PROVIDER = new ethers.providers.JsonRpcProvider("https://your-blockchain-rpc-url.com");
const USER_SIGNER = new ethers.Wallet(PRIV_KEY, PROVIDER);
const ALICE = "0xYourEvmAddressHere";
// Define Input Transaction Parameters
const inputTx = {
AMOUNT_TOTAL: "100", // Lock amount of 0.0001 USDC with 6 decimals. String in uint256
ASSET_ID: "0xUSDCContractAddress" // USDC ERC20 Address. String
};
// Define Output Transaction Parameters
const outputTxArr = [
{
amountOutput: inputTx.AMOUNT_TOTAL, // Total amount of tokens to be consumed on this output Tx
smartContractCalls: [
{
payloadAmountNative: "0", // Amount of native asset used in on this call. Ignored in ERC20
targetSC: inputTx.ASSET_ID, // Target smart contract address for the transaction to be called
payloadObject: {
functionHeader: "function transfer(address recipient, uint256 amount)",
functionName: "transfer",
payloadParmsArr: ["0xRecipientAddress", inputTx.AMOUNT_TOTAL] // Recipient address and amount
}
}
]
}
];
// Create Deposit and Lock Funds into the Universal Plugin
async function createDepositAndLockFunds() {
try {
// Get User Institution Merkle Proof from the backend
const userMerkleProof = await SurferMonkey.getInstitutionMembershipProof({
BACKEND_RPC: "https://your-surfermonkey-backend-rpc-url.com",
userEOA: ALICE,
institutionLeaves: ["0xValidUserAddress1", "0xValidUserAddress2", ALICE]
});
// Create the User Message
const userMessage = {
userEOA: ALICE,
inputTx: inputTx,
outputTxArr: outputTxArr,
numberTxOut: 1,
pubKey: ["PubKeyString[0]", "PubKeyString[1]"],
chainID: "ChainIdentifier",
userMerkleProof: userMerkleProof
};
// Create Deposit Object
const BACKEND_RPC = "https://your-surfermonkey-backend-rpc-url.com";
const deposit = await SurferMonkey.createDeposit({ BACKEND_RPC, userMessage, verbose: true });
console.log("DEPOSIT:", deposit);
// Extra Step - Approve ERC20 Tokens to be Locked
const ERC20_SMART_CONTRACT = new ethers.Contract(inputTx.ASSET_ID, USDC_ABI, USER_SIGNER);
const approveTx = await ERC20_SMART_CONTRACT.approve("0xUniversalPluginAddress", inputTx.AMOUNT_TOTAL, {
maxPriorityFeePerGas: Number(25000000000),
maxFeePerGas: Number(27000000000),
gasLimit: Number(5142880)
});
console.log("Wait for transaction ERC20 approve to get minted...", approveTx.hash);
await approveTx.wait(); // Wait for transaction to get minted
// Lock funds into SurferMonkey Universal Plugin
const UniversalPlugin_SC = new ethers.Contract("0xUniversalPluginAddress", UP_ABI, USER_SIGNER);
const submitDeposit = await UniversalPlugin_SC.Deposit(
deposit.SOLIDITY_DATA.a,
deposit.SOLIDITY_DATA.b,
deposit.SOLIDITY_DATA.c,
deposit.SOLIDITY_DATA.Input,
{
maxPriorityFeePerGas: Number(25000000000),
maxFeePerGas: Number(27000000000),
gasLimit: Number(5142880)
}
);
console.log("Deposit transaction submitted:", submitDeposit.hash);
await submitDeposit.wait();
// Wait for 10 seconds for blockchain probabilistic finality
await new Promise(resolve => setTimeout(resolve, 10000));
// Generate Zero-Knowledge Proof
const targetLeafChild = 0; // Which child output we want to use
const getZKPSignalsObject = {
BACKEND_RPC,
depositJSON: deposit,
targetLeafChild,
verbose: true
};
const zkpSolidityData = await SurferMonkey.createWithdraw(getZKPSignalsObject);
console.log("ZKP generated:", zkpSolidityData);
// Submit Zero-Knowledge Proof to Blockchain
const RELAYER_SIGNER = new ethers.Wallet("0xRelayerPrivateKeyHere", PROVIDER);
const TARGET_MIXER = new ethers.Contract("0xMixerAddress", MIXER_ABI, RELAYER_SIGNER);
const tx = await TARGET_MIXER.withdraw(
zkpSolidityData.a,
zkpSolidityData.b,
zkpSolidityData.c,
zkpSolidityData.Input,
deposit.depositArr[targetLeafChild].payloadData,
deposit.depositArr[targetLeafChild].targetSC,
deposit.depositArr[targetLeafChild].payloadAmountPerCall,
{
value: 0,
maxPriorityFeePerGas: Number(25000000000),
maxFeePerGas: Number(27000000000),
gasLimit: Number(5142880)
}
);
console.log("ZKP transaction submitted:", tx.hash);
} catch (error) {
console.error("Error during integration:", error);
}
}
// Execute the function
createDepositAndLockFunds();