Smart accounts let developers define custom rules that determine when an account is allowed to sign. Traditionally, this validation lives on-chain, in a smart contract. In this tutorial, we’ll build the same kind of programmable validation—but instead of deploying contracts, we’ll implement a private smart contract that runs as a Cubist Confidential Cloud Function (C2F). This gives you a smart account with programmable rules while simultaneously keeping your signing logic private.
What we’re building
We’ll implement a smart account that enforces ERC-20 transfer limits. Our private smart contract will: parse an EVM transaction, decode ERC-20 transfers, look up the allowed maximum spend for a given token, and determine whether the account is allowed to sign. This example works for EVM chains, but you can use your favorite transaction parsing library to re-create it for the chain of your choice—Solana, Sui, or even chains without native smart contract support, like Bitcoin or Zcash.
How private smart accounts work with CubeSigner + Cubist C2F
In this example, we use a key stored in CubeSigner, a hardware-backed key management system, as our EOA. Then, to turn this EOA into a smart account, we attach a Confidential Cloud Function with spending rules to the key; the cloud function encodes our ERC-20 spending limit. Together, the key and the cloud function are a private smart contract wallet.
Tutorial: Enforcing ERC-20 transfer limits with a private smart account
Below, you'll implement the core of the smart account: the private validation logic that parses an EVM transaction and decides whether the account is permitted to sign.
1. Set up a new project for the C2F function
Create a new binary with cargo:
cargo new --bin erc-limit
cd erc-limitNavigate to the erc-limit directory, and execute the rest of the instructions in this tutorial from there.
To compile your C2F function, you'll need to add the C2F SDK as a dependency; contact us to be added to the git repo. You'll also need to compile your project to WebAssembly, which is what C2F executes. Add the following to your Cargo config (.cargo/config.toml):
[build]
target = "wasm32-wasip2"2. Store the per-token limits as private secrets
First, before writing our C2F code, we'll configure the per-token transfer limits by creating a secret called ERC20_VALUE_LIMITS.
This secret will store the value limits for each token address in a map. Let's say we only want to allow interactions with the UCDC ERC-20 token on Avalanche testnet, with transfers of 1,000,000 (0xf4240) or less:
cs org policy-secrets set { "0x5425890298aed601595a70AB815c96711a31Bc65": "0xf4240" }This command assumes you have access to the CubeSigner CLI; contact us if you do not.
3. Retrieve the limit for a given token
Next, we’ll write a function, value_limit_for, that retrieves the ERC-20 transfer limit for a given token. Open up src/main.rs and add the following code:
use std::collections::HashMap;
use cubist_policy_sdk::config;
use ethers_core::types::{Address, U256};
/// Return the value limit for the given ERC-20 contract, if one exists.
fn value_limit_for(contract: &Address) -> Result<U256> {
// Fetch the limits secret
let limits = config::get("ERC20_VALUE_LIMITS")?
.ok_or("ERC-20 value limits not found")?;
let mut limits: HashMap<Address, U256> = serde_json::from_str(&limits)?;
let limit = limits
.remove(contract)
.ok_or(format!("EVM contract not recognized: {contract}"))?;
Ok(limit)
}4. Extract transfer amount
To implement our ERC-20 transfer limit, we need to compare the private limit to the amount of the transfer being attempted. We will examine the EVM sign request and parse the transfer method call data in the below parse_transfer_value function:
use ethers_core::abi::{FunctionExt as _, parse_abi};
/// Parse the transaction data as an ERC-20 "transfer" function.
fn parse_transfer_value(sign_request: &EvmSignRequest) -> Result<U256> {
// Get the ABI for the transfer function.
let mut abi = parse_abi(&[
"function transfer(address _to, uint256 _value) public returns (bool success)",
])
.unwrap();
let func = abi.functions.remove("transfer").unwrap().pop().unwrap();
// Get the transaction data
let data = sign_request
.tx
.data()
.ok_or("transaction data missing")?
.iter()
.as_slice();
// Check the selector.
if data[0..4] != func.selector() {
Err("Invalid 'transfer' call: invalid selector")?;
}
// Try to decode the input from the data.
let mut inputs = func
.decode_input(&data[4..])
.map_err(|err| format!("Invalid 'transfer' call: {err}"))?;
// Get the transfer value
let value = inputs
.pop()
.unwrap()
.into_uint()
.ok_or("Invalid 'transfer' call: invalid amount")?;
Ok(value)
}5. Implement the smart account validation logic: check the transfer value against the limit
This function is the private smart account's validation logic. It lives in the main function in src/main.rs. If it returns AccessDecision::Allow, our key will sign the transaction; if it returns AccessDecision::Deny, our key will refuse to sign the transaction.
use cubist_policy_sdk::{AccessDecision, AccessRequest, error::Result, policy};
use ethers_core::types::NameOrAddress;
/// Only allow ERC-20 `transfer` calls to known
/// contracts, and limits each transfer to a given amount.
#[policy]
async fn main(req: AccessRequest) -> Result<AccessDecision> {
// Get the sign request, assuming it's an EVM transaction.
let tx = req.evm_sign_request()?;
// Get the value limit from the policy secrets.
let contract_address = tx
.tx
.to()
.and_then(NameOrAddress::as_address)
.ok_or("to address missing")?;
let value_limit = value_limit_for(contract_address)?;
let value = parse_transfer_value(&tx)?;
// Enforce the rule
if value > value_limit {
Ok(AccessDecision::Deny(format!(
"Transfer value '{value}' is above the limit '{value_limit}'"
)))
} else {
Ok(AccessDecision::Allow)
}
}6. Deploy the private smart contract
Now that we've written our private smart contract, we can build it with:
cargo build --releaseAnd we can upload that code as a Confidential Cloud Function:
cs c2f create --type wasm --name "erc_limit" target/wasm32-wasip2/release/erc-limit.wasmFinally, to attach our smart account logic to a specific EOA, we do the following:
cs key set-policy -k $KEY_ID --policy '"erc_limit/latest"'The smart account will now enforce ERC-20 transfer limits privately, per-signature.
Beyond ERC-20 limits: Why private smart accounts matter
This approach gives you the ergonomics of smart accounts without deploying smart contracts or exposing your signing rules publicly. As in this example, you can write rules that interact with private data without revealing that data. You can also interact with external applications (e.g., transaction monitoring services, compliance/risk systems, etc.), interact with data on different (and/or multiple) chains, and use any existing Rust library you like. This makes it dramatically easier to, for example, manage a fleet of (private) smart contracts across many chains.
Keep reading
- Review the C2F Press Release.
- Read the C2F for Private Smart Contracts and Verifiable Off-Chain Code blog post.
- Dive into the C2F whitepaper to learn more about the system architecture.
- Learn how Cubist customer Squid is using C2F to replace on-chain smart contracts and GMP protocols.
- Discover C2F for writing and enforcing cross-chain wallet policies.