How to Transform Your Smart Contract into a Bubble Smart Contract?
Convert a public Solidity contract into a confidential one: encrypted handles, MPC logic, and explicit ACL permissions.
Turning a public Solidity contract into a Bubble-enabled, privacy-preserving one boils down to three themes: represent data as encrypted handles, move secret-dependent logic into MPC, and use the SDK/User-Interactor for encrypt/decrypt at the edges. Follow this checklist.
1. Identify private state & I/O
Decide which fields/parameters/results must be private (e.g., balance, bid, price, health data). These become encrypted instead of plaintext.
- Public → keep as is.
- Private → store as encrypted values and operate via MPC. Inputs are transformed to
Inputtext™,while other contract' variables are transformed togarbledtexttm
2. Replace plaintext types with encrypted types
Where you previously had uint256, store an encrypted type.
For inputs, use Inputtext™, for other contract's parameter, use garbledtexttm (a handle - ID/pointer to encrypted data).
Keep a clear naming convention:
// Before
mapping(address => uint256) public balance;
// After (handle to encrypted balance)
mapping(address => gtUint256) private balanceHandle; // handle, not valueInputs are created/encrypted off-chain using the Bubble SDK and passed to the contract as
Inputtext™(encrypted) inputs.
// Before (clear input)
function transfer(address to, uint256 value) public virtual returns (bool)
// After (encrypted input)
function transfer(address _to, itUint256 calldata _it) public returns (gtBool)3. Remove on-chain control flow over secrets
You can’t branch/loop on ciphertext. Replace:
- Simple “if” on secret → use
mux(encrypted select) to choose between encrypted values without revealing the condition. - Complex logic / arithmetic / comparisons → use asynchronous MPC:
- Request decrypt (using the Decryption caller
requestDecryptionfunction). - Wait for callback (Bubble calls your contract).
- Continue with the now-public result (only when truly needed).
- Request decrypt (using the Decryption caller
Example (compare + conditional update)
Before (public):
if (a > b) { x = a - b; } else { x = b - a; }After (private):
// MPC computes cmp = (a > b) as an encrypted boolean.
gtBool cmpHandle = MPCCore.gt(aHandle, bHandle)
// Use mux to select without revealing which branch:
uint256 xHandle = mux(cmpHandle, MPCCore.sub(aHandle, bHandle), MPCCore.sub(bHandle, aHandle));
// xHandle is still encrypted; store/pass it as a handle.4. Redesign functions into request → callback flow (when needed)
Split secret-dependent operations into two transactions:
- Tx1 (request): store request state, call
DecryptionCaller.requestDecryption.... to store request data and emit requestDecryption event. - Tx2 (callback): callback function receives the result, verifies request status using the DecryptionCaller and updates state.
function runValidateCiphertext(itUint8 calldata it) public {
gtUint8 ciphertext = MpcCore.validateCiphertext(it);
uint256[] memory arr = new uint256[](1);
arr[0] = gtUint8.unwrap(ciphertext);
requestDecryption(arr, this.callbackValidateCiphertext.selector);
}
function callbackValidateCiphertext(uint256 decryptID, bytes[] calldata output, bytes[] calldata signatures) public verifyCallback(decryptID, output, signatures){
// Verify inputs
ciphertextResult = abi.decode(output[0], (uint8));
// Continue working using the decryption result...
}5. Grant permissions
When your contract creates a new encrypted value (a handle), it must explicitly specify who is allowed to use that value. By default, no one, not even the contract, has permission to operate on a handle unless it is granted. Therefore, after producing a new encrypted result, your contract should call MpcCore.permitThis(handle) to allow itself to use the handle in future computations. If the handle represents data owned by a user (e.g., a private balance), also grant the user access with MpcCore.permit(handle, userAddress). This ensures the user can later authorize computations or decrypt results if needed. In cases where the permission should be temporary, a transient permission can be granted instead, limiting access to a single operation or a specific time window. Proper permissioning is essential for securely controlling who can interact with encrypted values in your Bubble-enabled contract.
(gtUint128 x, gtUint128 y) = MpcCore.oprfMint(key, q);
MpcCore.permitThis(x);
MpcCore.permitThis(y);Detailed explanations on permit functions can be found in the ACL section.
Mini ERC-20 Example (Public → Bubble)
Transfer (public)
mapping(address account => uint256) private _balances;
function transfer(address to, uint256 amount) external {
address owner = msg.sender;
uint256 fromBalance = _balances[owner];
_balances[owner] = fromBalance - amount;
_balances[to] += amount;
emit Transfer(owner, to, amount);
return true;
}Transfer (Bubble, private balances)
mapping(address => gtUint256) private balances;
function transfer(address _to, itUint256 calldata _it) public returns (gtBool) {
address owner = msg.sender;
gtUint256 value = MpcCore.validateCiphertext(_it);
MpcCore.permitTransient(value, owner); // Give transient permission the the sender, so the contractTransfer check will pass
// Check if the sender is permitted to use the _value handle
require(MpcCore.isSenderPermitted(_value));
gtUint256 fromBalance = balanceOf(owner);
gtUint256 ToBalance = balanceOf(_to)
(gtUint256 newFromBalance, gtUint256 newToBalance, gtBool result) = MpcCore.transfer(fromBalance, toBalance, _value);
// Store the new balances handles in the mapping
balances[owner] = newFromBalance;
balances[_to] = newToBalance;
// Permit the contract and the _from user to use the balance handle
MpcCore.permitThis(newFromBalance);
MpcCore.permit(newFromBalance, owner);
// Permit the contract and the _to to use the balance handle
MpcCore.permitThis(newToBalance);
MpcCore.permit(newToBalance, _to);
emit Transfer(owner, _to);
return result;
}Quick Migration Checklist
- Mark which fields/params/results must be private.
- Switch those to handles (no plaintext on-chain).
- Substitute secret-dependent operations with MPC functionalities.
- Replace secret-dependent branches with
muxor async MPC callbacks. - Grant access to result handles.
That’s the essence: keep secrets encrypted end-to-end, push secret logic into Bubble operations (using mux for simple selection), and structure your contract around request → MPC → callback.