Cross-Contract Calls
Cross Contract calls are the primary method for RAILGUN to access external DeFi protocols and smart contracts. Any number of ordered contract calls can be wired together and executed against a private balance. These cross-contract interactions occur in the generalized RAILGUN Relay Adapt contract, which facilitates a number of serial multi-calls in a single block.
First (1) an unshield is executed against the private balance, moving tokens temporarily into the Relay Adapt contract. Then (2) a multi-call processes any number of contract calls against the balance. Finally, (3) the result is shielded back into the user’s private balance. This all occurs in a single block and one transaction.
For example, Railway Wallet has integrated 0x API to facilitate private swaps. This is done through a two serial cross-contract calls: 1st the exact swap token amount is approved for spending, 2nd the 0x token swap is performed. The contract spend approval is a failsafe operation on each swap transaction. It ensures that the swap amount is guaranteed to be approved during the execution block.
In the case of a contract call failure, the entire call with revert.
Please see our guide on RAILGUN Cookbook, and check the verified recipes that are available in the Cookbook SDK (Github).
The enclosed Recipes in the Cookbook contain structured calls that integrate directly into Wallet SDK. Contributors are working on verified Recipes for DEX Swaps, Asset Vaults, Liquidity Provisioning (LP), Stablecoin Minting, Liquid Staking and more.
crossContractCallsSerialized:
{to, data, value}
fields make up our array of crossContractCalls, which interact with external smart contracts through a series of ordered calls.relayAdaptUnshieldERC20Amounts: The exact amounts that should be unshielded to process in this transaction. Make sure only to unshield the exact token amount that is required, as there is an unshield fee associated with this value (like any unshield transaction).
relayAdaptShieldERC20Addresses: A list of token addresses that are the result of the cross contract calls, to shield into private balance after the calls are complete. If its balance is 0, nothing will be shielded. These are the only tokens that will be shielded back, so make sure the list is complete. Any tokens that are not listed in this array and shielded during the transaction will not be recoverable.
// Tokens to swap through 0x API.
const sellToken = {
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
amount: BigInt('0x100000000'),
}
const buyTokenAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'; // USDT
// Amount of sell token will be modified by initial unshield, fee of 0.25%.
const sellTokenAmountAfterUnshieldFee =
sellToken.amount * (10000n - unshieldFeeBasisPoints) / 10000n;
// Approve the exact amount to swap with 0x.
const erc20 = new Contract(sellToken.address, erc20ABI, provider);
const transactionApprove0x = await erc20.populateTransaction.approve(
quote.allowanceTarget,
sellTokenAmountAfterUnshieldFee,
);
// Fetch quote data from 0x API.
const quote = await get0xAPIQuote(
sellToken.address,
buyTokenAddress,
sellTokenAmountAfterUnshieldFee,
);
const transaction0xSwap = {
to: quote.to,
value: quote.value,
data: quote.data,
};
const crossContractCalls: ContractTransaction[] = [
transactionApprove0x,
transaction0xSwap,
];
const relayAdaptUnshieldERC20Amounts = [sellToken];
// In case there's slippage or dust leftover,
// make sure to shield the sellToken as well as buyToken.
const relayAdaptShieldERC20Recipients: RailgunERC20Recipient = [{
tokenAddress: sellToken.address,
recipientAddress: railgunWallet.getAddress(),
},
{
tokenAddress: buyTokenAddress,
recipientAddress: railgunWallet.getAddress(),
}];
By default, you should assume that the Relay Adapt contract is not approved to spend any tokens. Because anyone can execute transactions against this contract, a user could potentially reset all spend allowances to 0 in any transaction.
However, because we execute cross-contract transactions inside a multi-call, we're safe from outside interactions during the execution block.
It's recommended to always approve the exact amount that you require in your transaction, as the initial step of the cross-contract calls. (As in the example above).
Because of the unshield fee, the amount of tokens in the transaction will be 0.25% less than the amount unshielded. This is very important if transacting a specific value, like for a swap.
For example, if we want to swap 100 DAI for wETH, the transaction will look like this:
- 1.Unshield 100 DAI (
relayAdaptUnshieldERC20Amounts
), with a 0.25 DAI fee. - 2.Cross contract call: approve 99.75 DAI for swap spending.
- 3.Cross contract call: swap 99.75 DAI for wETH.
- 4.Shield wETH and any leftover DAI dust into private balance (
relayAdaptShieldERC20Addresses
).
We suggest using the RAILGUN Cookbook SDK for this process, which automatically calculates the unshield fees when you write your cross-contract calls as a Recipe.
//
// See Example code above to generate required fields:
// - crossContractCallsSerialized
// - relayAdaptUnshieldERC20Amounts
// - relayAdaptShieldERC20Addresses
//
// Database encryption key. Keep this very safe.
const encryptionKey = '...';
// Gas price, used to calculate Relayer Fee iteratively.
const originalGasDetails: TransactionGasDetails = {
evmGasType: EVMGasType.Type2, // Depends on the chain (BNB uses type 0)
gasEstimate: 0n, // Always 0, we don't have this yet.
maxFeePerGas: BigInt('0x100000'), // Current gas Max Fee
maxPriorityFeePerGas: BigInt('0x010000'), // Current gas Max Priority Fee
}
// Token Fee for selected Relayer.
const feeTokenDetails: FeeTokenDetails = {
tokenAddress: selectedTokenFeeAddress,
feePerUnitGas: selectedRelayer.feePerUnitGas,
}
// Minimum gas limit for the cross-contract call.
// Set an appropriate minimum for your call (1.6M - 3.2M depending on the call).
// If this is set too low, the gas estimate can revert.
const minGasLimit = BigInt(2_400_000);
// Whether to use a Relayer or self-signing wallet.
// true for self-signing, false for Relayer.
const sendWithPublicWallet = false;
const {gasEstimate} = await gasEstimateForUnprovenCrossContractCalls(
NetworkName.Ethereum,
railgunWalletID,
encryptionKey,
relayAdaptUnshieldERC20Amounts,
relayAdaptUnshieldNFTAmounts,
relayAdaptShieldERC20Recipients,
relayAdaptShieldNFTRecipients,
crossContractCalls,
originalGasDetails,
feeTokenDetails,
sendWithPublicWallet,
minGasLimit,
);
// See "Unshields ERC-20 tokens" for an example of other required fields.
// Minimum gas price, only required for relayed transaction.
const overallBatchMinGasPrice: Optional<bigint> = BigInt('0x10000');
await generateCrossContractCallsProof(
NetworkName.Ethereum,
railgunWalletID,
encryptionKey,
relayAdaptUnshieldERC20Amounts,
relayAdaptUnshieldNFTAmounts,
relayAdaptShieldERC20Recipients,
relayAdaptShieldNFTRecipients,
crossContractCallsSerialized,
relayerFeeTokenAmountRecipient,
sendWithPublicWallet,
overallBatchMinGasPrice,
minGasLimit,
progressCallback,
);
// NOTE: Must follow proof generation.
// Use the exact same parameters as proof or this will throw invalid error.
// Gas to use for the transaction.
const gasDetails: TransactionGasDetails = {
evmGasType: EVMGasType.Type2, // Depends on the chain (BNB uses type 0)
gasEstimate: BigInt('0x0100'), // Output from gasEstimateForShield
maxFeePerGas: BigInt('0x100000'), // Current gas Max Fee
maxPriorityFeePerGas: BigInt('0x010000'), // Current gas Max Priority Fee
}
const { transaction } = await populateProvedCrossContractCalls(
NetworkName.Ethereum,
railgunWalletID,
relayAdaptUnshieldERC20Amounts,
relayAdaptUnshieldNFTAmounts,
relayAdaptShieldERC20Recipients,
relayAdaptShieldNFTRecipients,
crossContractCalls,
relayerFeeTokenAmountRecipient,
sendWithPublicWallet,
overallBatchMinGasPrice,
gasDetails,
);
// Send transaction to Relayer, or self-sign with any wallet.
Last modified 1mo ago