Appearance
Read a value that depends on the caller
Ask stETH to simulate a one-stETH transfer from two different callers, side by side.
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
let wstETH = ethereum:0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
let nobody = ethereum:0x0000000000000000000000000000000000000001;
from stETH as s
| project {
asHolder: s.transfer(nobody, 1e18) with { caller: wstETH },
asNobody: s.transfer(wstETH, 1e18) with { caller: nobody }
}| asHolder | asNobody |
|---|---|
| true | null |
wstETH holds millions of stETH, so the transfer it appears to make succeeds and asHolder is true; nobody holds none, so the transfer it appears to make reverts and asNobody is null. Nothing was sent either way. revert_reason is the language's way to read why a call reverted, and it is not available in the workbench yet.
How it reads
with { caller: wstETH } after a call sets who that one call appears to come from. Nothing is sent and nothing is signed; the contract simply answers as if wstETH had asked. transfer is not a view function, so CQL simulates it, notes the simulation as CQL3908, and no balance moves. The second call carries a different caller, and the two sit in one row because both are made on the same source row.
Any function that reads msg.sender — a balance check, a whitelist, an allowance — answers differently per caller, and this is how you see the difference without holding either key.
Put the option on the source instead — from stETH with { caller: wstETH } as s — and every call through s carries it.
Variations
- Show why. Add
balance: format(s.balanceOf(nobody), 18)to the projection and thenullexplains itself. - Send value with the call.
s.submit(nobody) with { caller: wstETH, value: 1e18 }simulates staking one ETH;submitispayable, sovalueis allowed, and the answer is the shares it would mint.