7. Dynamic Capabilities of SurferMonkey
7.1 Dynamic Multi Smart Contract Calls
In this chapter, we introduce the Dynamic Capabilities of the SurferMonkey SDK, focusing on how these dynamic capabilities enable developers to change specific actions in Zero-Knowledge Proofs (ZKPs) even after assets are locked. This feature is known as Dynamic Multi Smart Contract Calls and allows users to modify the intended blockchain actions dynamically before the ZKP settlement, providing significant flexibility and efficiency.
Overview
The Dynamic Multi Smart Contract Calls feature allows developers to adjust the intended actions for each output transaction even after an asset has been locked. This flexibility is critical in scenarios where user intentions change or technical errors occur in the initial configuration. Key modifications allowed include changes to smart contract function calls, function parameters, target contracts, payload data, and more. However, there are a few constraints to maintain security and privacy.
This feature is especially powerful when used in complex blockchain operations, such as swapping assets or adjusting target protocol parameters. It enables developers to change actions without redeploying the entire transaction, saving time, effort, and gas fees.
Key Problem Solved
Blockchain transactions are traditionally rigid once initiated. The inability to modify actions after locking funds can create limitations in adapting to user needs or unexpected situations. SurferMonkey's dynamic capabilities solve this by allowing adjustments to actions (such as switching a token transfer to a swap) before the ZKP is settled, providing:
- Flexibility: Modify the action based on the user's changing requirements.
- Error Handling: Correct parameter issues or address failed transactions.
- Efficiency: Avoid the need to unlock funds and restart the process.
What You Can and Cannot Change
-
Cannot Change:
- Total Amount Locked
- Amount per child for each output transaction
- Type of Asset Locked
- User EOA (Externally Owned Account)
- Output structure from multi-call to single-call and vice versa
-
Can Change (only related to smartContractCalls fields):
- Smart contract payload data
- Functions being called
- Function parameters
- Target Smart Contracts
- Number of Smart Contract calls within each output
- Payload amount for each call
- Transition from single smart contract call to multiple, and vice versa
Dynamic Use Case Scenario
Imagine a scenario where a user has locked assets into SurferMonkey intending to perform a simple token transfer. However, they later decide that a token swap would be more beneficial. Instead of unlocking the assets and starting over, the Dynamic Multi Smart Contract Call feature allows the developer to modify the child payload to implement the swap instead. This dynamic change saves both time and gas fees while adapting to the user’s changing needs.
SDK API: updatePayloadData()
The core method that allows dynamic modification is updatePayloadData()
. It is used to update the payload fields of a child output transaction without affecting the overall deposit. This enables the modifications mentioned above to be applied seamlessly.
Parameters:
BACKEND_RPC
(string): The backend RPC URL for SurferMonkey.newSmartContractCallOnTargetChild
(array): An array representing the new smart contract call configuration.depositJSON
(object): The original deposit object.targetLeafChild
(number): Index of the output transaction to be modified.
7.2 Configuration Steps
Step 1: Define Input Transaction
Specify the total amount to be locked and define the input transaction as usual. In this example, we assume that the user has already locked the asset and generated a deposit object.
const inputTx = {
AMOUNT_TOTAL: "10000000000000", // Native value in its minimal denomination (wei)
ASSET_ID: "0x0000000000000000000000000000000000000000" // Native asset ID = address(0)
};
Step 2: Obtain Existing Deposit Object
Retrieve the existing deposit object that you intend to modify and that is already locked in the Universal Plugin. For the purpose of this example, assume this object is stored as DEPOSIT
.
const DEPOSIT = "get your stored deposit, which is already locked in SurferMonkey";
Step 3: Define New Smart Contract Calls
Create an updated configuration for the child payload. In this example, we convert the output transaction from a single smart contract call to multiple smart contract calls, changing recipients and amounts.
const targetLeafChild = 0; // Specify the child output transaction to be modified
const newSmartContractCallOnTargetChild = [
{
payloadAmountNative: String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.4), // 40% of total locked amount
targetSC: proxySCAddress, // Target smart contract address
payloadObject: {
functionHeader: "transferEth(address targetAddress, uint256 amount)", // Function header
functionName: "transferEth", // Function name
payloadParmsArr: [RECIPIENT_MODIFIED, String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.4)]
}
},
{
payloadAmountNative: String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.6), // 60% of total locked amount
targetSC: proxySCAddress, // Target smart contract address
payloadObject: {
functionHeader: "transferEth(address targetAddress, uint256 amount)", // Function header
functionName: "transferEth", // Function name
payloadParmsArr: [RECIPIENT_TESTING, String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.6)]
}
}
];
Explanation: Here, we split the child output transaction into two smart contract calls, each targeting different recipients with different amounts. The parameters such as targetSC
, functionHeader
, and functionName
are configured accordingly.
Step 4: Update Payload Data
Use the updatePayloadData()
function from SurferMonkey to update the target child output transaction.
const updatedDeposit = await SurferMonkey.updatePayloadData({
BACKEND_RPC,
newSmartContractCallOnTargetChild,
depositJSON: DEPOSIT,
targetLeafChild
});
Explanation: This function takes the new smart contract calls and updates the specified child transaction. Only the payload fields are modified; the rest of the deposit configuration remains intact.
Step 5: Create the ZKP and Settle
Once the payload data is updated, the next steps are the same as the usual process for creating a proof and settling the ZKP in the blockchain.
const getZKPSignalsObject = {
BACKEND_RPC,
depositJSON: updatedDeposit,
targetLeafChild,
verbose: true // Print ZKP signals
};
const zkpSolidityData = await SurferMonkey.createWithdraw(getZKPSignalsObject);
// Settle ZKP in the Blockchain
const TARGET_MIXER = new ethers.Contract(MIXER_SC_ADDRESS, MIXER_ABI, RELAYER_SIGNER);
const tx = await TARGET_MIXER.withdraw(
zkpSolidityData.a,
zkpSolidityData.b,
zkpSolidityData.c,
zkpSolidityData.Input,
updatedDeposit.depositArr[targetLeafChild].payloadData,
updatedDeposit.depositArr[targetLeafChild].targetSC,
updatedDeposit.depositArr[targetLeafChild].payloadAmountPerCall,
{
value: 0,
gasLimit: 2000000,
maxPriorityFeePerGas: 25000000000,
maxFeePerGas: 27000000000
}
);
Explanation: The ZKP is generated based on the updated payload and then settled on the blockchain, ensuring that the new actions are executed securely and privately.
Full Code Snippet
Below is the full code snippet for dynamically modifying the child payload and settling the updated ZKP in the blockchain:
// 1. Define Input Transaction
const inputTx = {
AMOUNT_TOTAL: "10000000000000", // Native value in its minimal denomination (wei)
ASSET_ID: "0x0000000000000000000000000000000000000000" // Native asset ID = address(0)
};
// Define rest of the parameters accordingly
// **
// * Simulation that you have already locked you deposit.
// **
// 2. Obtain Existing Deposit Object
const DEPOSIT = "get your stored deposit";
// 3. Define New Smart Contract Calls
const targetLeafChild = 0;
const newSmartContractCallOnTargetChild = [
{
payloadAmountNative: String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.4),
targetSC: proxySCAddress,
payloadObject: {
functionHeader: "transferEth(address targetAddress, uint256 amount)",
functionName: "transferEth",
payloadParmsArr: [RECIPIENT_MODIFIED, String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.4)]
}
},
{
payloadAmountNative: String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.6),
targetSC: proxySCAddress,
payloadObject: {
functionHeader: "transferEth(address targetAddress, uint256 amount)",
functionName: "transferEth",
payloadParmsArr: [RECIPIENT_TESTING, String(Number(DEPOSIT.userMessage.outputTxArr[targetLeafChild].amountOutput) * 0.6)]
}
}
];
// 4. Update Payload Data
const updatedDeposit = await SurferMonkey.updatePayloadData({
BACKEND_RPC,
newSmartContractCallOnTargetChild,
depositJSON: DEPOSIT,
targetLeafChild
});
// 5. Create the ZKP and Settle
const getZKPSignalsObject = {
BACKEND_RPC,
depositJSON: updatedDeposit,
targetLeafChild,
verbose: true
};
const zkpSolidityData = await SurferMonkey.createWithdraw(getZKPSignalsObject);
const TARGET_MIXER = new ethers.Contract(MIXER_SC_ADDRESS, MIXER_ABI, RELAYER_SIGNER);
const tx = await TARGET_MIXER.withdraw(
zkpSolidityData.a,
zkpSolidityData.b,
zkpSolidityData.c,
zkpSolidityData.Input,
updatedDeposit.depositArr[targetLeafChild].payloadData,
updatedDeposit.depositArr[targetLeafChild].targetSC,
updatedDeposit.depositArr[targetLeafChild].payloadAmountPerCall,
{
value: 0,
gasLimit: 2000000,
maxPriorityFeePerGas: 25000000000,
maxFeePerGas: 27000000000
}
);
Summary
The Dynamic Multi Smart Contract Call feature of SurferMonkey provides powerful flexibility in blockchain interactions, allowing developers to modify actions after locking assets but before ZKP settlement. This capability enhances user experience, adaptability, and efficiency, making SurferMonkey an effective tool for managing evolving transaction needs without compromising privacy or security.