right-leftCasting

Explicit Casting Only

Shielded types and their unshielded counterparts do not support implicit casting. You must always cast explicitly. This is a deliberate design decision -- every conversion between shielded and unshielded types is a potential information boundary, and the compiler requires you to be explicit about crossing it.

uint256 publicNumber = 100;

// Implicit casting -- will NOT compile
suint256 shielded = publicNumber; // Error

// Explicit casting -- correct
suint256 shielded = suint256(publicNumber); // OK

This applies to all shielded types: suint, sint, sbool, saddress, and sbytes.

bool flag = true;
sbool shieldedFlag = sbool(flag);          // OK

address addr = msg.sender;
saddress shieldedAddr = saddress(addr);    // OK

int256 signed = -42;
sint256 shieldedSigned = sint256(signed);  // OK

bytes32 hash = keccak256("secret");
sbytes32 shieldedHash = sbytes32(hash);    // OK

Shielding Values (Unshielded to Shielded)

When you cast from an unshielded type to a shielded type, you are "shielding" the value -- moving it from the public domain into confidential storage/computation.

circle-exclamation

If you need the value to be confidential from the start, it should arrive as encrypted calldata via a Seismic transaction (type 0x4A), not be cast from a public variable.

Unshielding Values (Shielded to Unshielded)

When you cast from a shielded type to an unshielded type, you are "unshielding" the value -- making it publicly visible.

circle-exclamation

This is sometimes necessary (e.g., returning a value from a view function or interfacing with a non-shielded contract), but you should be deliberate about when and why you do it.

Casting saddress to Payable

saddress payable is a valid type, but it does not unlock any extra operations — .transfer(), .send(), and .balance are all blocked on shielded addresses regardless of payability. The payable marker exists for type-system consistency (e.g., contracts with receive() payable convert to saddress payable), not for sending ETH.

To actually send ETH to a shielded address, you must unshield it first:

circle-exclamation

You can also convert in the other direction:

Size Casting Between Shielded Integers

You can cast between different sizes of shielded integers, just as you can with regular Solidity integers:

The same rules that apply to regular Solidity integer casting apply here:

  • Widening (smaller to larger): Always safe, the value is preserved.

  • Narrowing (larger to smaller): May truncate the value if it exceeds the target type's range.

You can also cast between signed and unsigned shielded integers:

Common Patterns

Returning values from view functions

Since shielded types cannot be returned from public or external functions, you must unshield them first:

circle-info

Returning an unshielded value from a view function makes it visible to the caller. If the caller should only see their own data, use access control and signed reads to ensure only authorized users can query it.

Interfacing with non-shielded contracts

When calling a contract that expects unshielded types, cast at the call boundary:

Shielding input from encrypted calldata

The only way to introduce a value that is private from the start is through encrypted calldata, where the value is never visible in plaintext on-chain:

Security Implications

Every cast between shielded and unshielded types is a potential information leak point. Keep these principles in mind:

  1. Unshielded-to-shielded casts expose the input value in the trace. If the value was meant to be secret from the start, use encrypted calldata instead.

  2. Shielded-to-unshielded casts expose the output value in the trace. Only unshield when you intend the value to become public.

  3. Minimize casts. The fewer times you cross the shielded/unshielded boundary, the smaller your attack surface.

  4. Audit cast points. During security review, identify every cast in your contract and verify that the information exposure at each point is intentional and acceptable.

Last updated