#![cfg(feature = "wallet")]
use crate::soliditylite::asm::op;
use std::collections::HashMap;
use sha3::{Digest, Keccak256};
pub type Word = [u8; 32];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExecError {
Revert(Vec<u8>),
UnknownOpcode(u8),
StackUnderflow,
BadJumpDest(usize),
OutOfGas,
}
pub type ExecResult = Result<Vec<u8>, ExecError>;
#[derive(Debug, Clone, Default)]
pub struct Contract {
pub code: Vec<u8>,
pub storage: HashMap<Word, Word>,
}
#[derive(Debug, Clone, Default)]
pub struct CallEnv {
pub caller: [u8; 20],
pub timestamp: u64,
pub number: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogEntry {
pub topics: Vec<Word>,
pub data: Vec<u8>,
}
const STEP_BUDGET: usize = 1_000_000;
impl Contract {
pub fn deploy(init_code: &[u8], env: &CallEnv) -> Result<Contract, ExecError> {
let mut c = Contract { code: Vec::new(), storage: HashMap::new() };
let runtime = run(init_code, &[], env, &mut c.storage, &mut Vec::new())?;
c.code = runtime;
Ok(c)
}
pub fn call(&mut self, calldata: &[u8], env: &CallEnv) -> ExecResult {
let code = self.code.clone();
run(&code, calldata, env, &mut self.storage, &mut Vec::new())
}
pub fn call_logs(&mut self, calldata: &[u8], env: &CallEnv) -> Result<(Vec<u8>, Vec<LogEntry>), ExecError> {
let code = self.code.clone();
let mut logs = Vec::new();
let ret = run(&code, calldata, env, &mut self.storage, &mut logs)?;
Ok((ret, logs))
}
pub fn sload(&self, slot: &Word) -> Word {
self.storage.get(slot).copied().unwrap_or([0u8; 32])
}
}
pub fn calldata(selector: [u8; 4], args: &[Word]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 32 * args.len());
out.extend_from_slice(&selector);
for a in args {
out.extend_from_slice(a);
}
out
}
pub fn word(v: u64) -> Word {
let mut w = [0u8; 32];
w[24..].copy_from_slice(&v.to_be_bytes());
w
}
pub fn addr_word(a: &[u8; 20]) -> Word {
let mut w = [0u8; 32];
w[12..].copy_from_slice(a);
w
}
pub fn word_to_u64(w: &Word) -> u64 {
let mut b = [0u8; 8];
b.copy_from_slice(&w[24..]);
u64::from_be_bytes(b)
}
fn add256(a: &Word, b: &Word) -> Word {
let mut out = [0u8; 32];
let mut carry = 0u16;
for i in (0..32).rev() {
let v = a[i] as u16 + b[i] as u16 + carry;
out[i] = (v & 0xFF) as u8;
carry = v >> 8;
}
out
}
fn sub256(a: &Word, b: &Word) -> Word {
let mut out = [0u8; 32];
let mut borrow = 0i16;
for i in (0..32).rev() {
let v = a[i] as i16 - b[i] as i16 - borrow;
if v < 0 {
out[i] = (v + 256) as u8;
borrow = 1;
} else {
out[i] = v as u8;
borrow = 0;
}
}
out
}
struct Vm<'a> {
code: &'a [u8],
calldata: &'a [u8],
env: &'a CallEnv,
storage: &'a mut HashMap<Word, Word>,
logs: &'a mut Vec<LogEntry>,
stack: Vec<Word>,
memory: Vec<u8>,
pc: usize,
}
fn run(
code: &[u8],
calldata: &[u8],
env: &CallEnv,
storage: &mut HashMap<Word, Word>,
logs: &mut Vec<LogEntry>,
) -> ExecResult {
let mut vm = Vm {
code,
calldata,
env,
storage,
logs,
stack: Vec::new(),
memory: Vec::new(),
pc: 0,
};
vm.exec()
}
impl Vm<'_> {
fn pop(&mut self) -> Result<Word, ExecError> {
self.stack.pop().ok_or(ExecError::StackUnderflow)
}
fn ensure_mem(&mut self, off: usize, len: usize) {
let end = off.saturating_add(len);
if end > self.memory.len() {
self.memory.resize(end, 0);
}
}
fn mstore(&mut self, off: usize, w: &Word) {
self.ensure_mem(off, 32);
self.memory[off..off + 32].copy_from_slice(w);
}
fn mload(&mut self, off: usize) -> Word {
self.ensure_mem(off, 32);
let mut w = [0u8; 32];
w.copy_from_slice(&self.memory[off..off + 32]);
w
}
fn calldataword(&self, off: usize) -> Word {
let mut w = [0u8; 32];
for (i, byte) in w.iter_mut().enumerate() {
let src = off.wrapping_add(i);
if src < self.calldata.len() {
*byte = self.calldata[src];
}
}
w
}
fn exec(&mut self) -> ExecResult {
let mut steps = 0usize;
loop {
steps += 1;
if steps > STEP_BUDGET {
return Err(ExecError::OutOfGas);
}
if self.pc >= self.code.len() {
return Ok(Vec::new());
}
let opc = self.code[self.pc];
match opc {
o if (op::PUSH1..=op::PUSH1 + 31).contains(&o) => {
let n = (o - op::PUSH1) as usize + 1;
let start = self.pc + 1;
let mut w = [0u8; 32];
for i in 0..n {
let b = self.code.get(start + i).copied().unwrap_or(0);
w[32 - n + i] = b;
}
self.stack.push(w);
self.pc += 1 + n;
}
op::POP => {
self.pop()?;
self.pc += 1;
}
op::DUP1 => {
let top = *self.stack.last().ok_or(ExecError::StackUnderflow)?;
self.stack.push(top);
self.pc += 1;
}
op::DUP2 => {
let v = *self.stack.iter().rev().nth(1).ok_or(ExecError::StackUnderflow)?;
self.stack.push(v);
self.pc += 1;
}
op::DUP3 => {
let v = *self.stack.iter().rev().nth(2).ok_or(ExecError::StackUnderflow)?;
self.stack.push(v);
self.pc += 1;
}
op::SWAP1 => {
let n = self.stack.len();
if n < 2 {
return Err(ExecError::StackUnderflow);
}
self.stack.swap(n - 1, n - 2);
self.pc += 1;
}
op::ADD => {
let a = self.pop()?;
let b = self.pop()?;
self.stack.push(add256(&a, &b));
self.pc += 1;
}
op::SUB => {
let a = self.pop()?;
let b = self.pop()?;
self.stack.push(sub256(&a, &b));
self.pc += 1;
}
op::MUL => {
let a = word_to_u128(&self.pop()?);
let b = word_to_u128(&self.pop()?);
self.stack.push(u128_to_word(a.wrapping_mul(b)));
self.pc += 1;
}
op::DIV => {
let a = word_to_u128(&self.pop()?);
let b = word_to_u128(&self.pop()?);
self.stack.push(u128_to_word(if b == 0 { 0 } else { a / b }));
self.pc += 1;
}
op::MOD => {
let a = word_to_u128(&self.pop()?);
let b = word_to_u128(&self.pop()?);
self.stack.push(u128_to_word(if b == 0 { 0 } else { a % b }));
self.pc += 1;
}
op::LT => {
let a = self.pop()?;
let b = self.pop()?;
self.stack.push(bool_word(cmp256(&a, &b).is_lt()));
self.pc += 1;
}
op::GT => {
let a = self.pop()?;
let b = self.pop()?;
self.stack.push(bool_word(cmp256(&a, &b).is_gt()));
self.pc += 1;
}
op::EQ => {
let a = self.pop()?;
let b = self.pop()?;
self.stack.push(bool_word(a == b));
self.pc += 1;
}
op::ISZERO => {
let a = self.pop()?;
self.stack.push(bool_word(a == [0u8; 32]));
self.pc += 1;
}
op::SHR => {
let shift = word_to_u128(&self.pop()?);
let value = self.pop()?;
self.stack.push(shr256(&value, shift));
self.pc += 1;
}
op::AND => {
let a = self.pop()?;
let b = self.pop()?;
let mut out = [0u8; 32];
for i in 0..32 {
out[i] = a[i] & b[i];
}
self.stack.push(out);
self.pc += 1;
}
op::KECCAK256 => {
let off = word_to_usize(&self.pop()?);
let len = word_to_usize(&self.pop()?);
self.ensure_mem(off, len);
let digest = Keccak256::digest(&self.memory[off..off + len]);
let mut w = [0u8; 32];
w.copy_from_slice(&digest);
self.stack.push(w);
self.pc += 1;
}
op::MSTORE => {
let off = word_to_usize(&self.pop()?);
let val = self.pop()?;
self.mstore(off, &val);
self.pc += 1;
}
0x51 => {
let off = word_to_usize(&self.pop()?);
let w = self.mload(off);
self.stack.push(w);
self.pc += 1;
}
op::SLOAD => {
let slot = self.pop()?;
let v = self.storage.get(&slot).copied().unwrap_or([0u8; 32]);
self.stack.push(v);
self.pc += 1;
}
op::SSTORE => {
let slot = self.pop()?;
let val = self.pop()?;
if val == [0u8; 32] {
self.storage.remove(&slot);
} else {
self.storage.insert(slot, val);
}
self.pc += 1;
}
op::CALLDATASIZE => {
self.stack.push(word(self.calldata.len() as u64));
self.pc += 1;
}
op::CALLDATALOAD => {
let off = word_to_usize(&self.pop()?);
let w = self.calldataword(off);
self.stack.push(w);
self.pc += 1;
}
op::CALLER => {
self.stack.push(addr_word(&self.env.caller));
self.pc += 1;
}
op::TIMESTAMP => {
self.stack.push(word(self.env.timestamp));
self.pc += 1;
}
op::NUMBER => {
self.stack.push(word(self.env.number));
self.pc += 1;
}
op::CODECOPY => {
let dest = word_to_usize(&self.pop()?);
let src = word_to_usize(&self.pop()?);
let len = word_to_usize(&self.pop()?);
self.ensure_mem(dest, len);
for i in 0..len {
let b = self.code.get(src + i).copied().unwrap_or(0);
self.memory[dest + i] = b;
}
self.pc += 1;
}
op::CALLDATACOPY => {
let dest = word_to_usize(&self.pop()?);
let src = word_to_usize(&self.pop()?);
let len = word_to_usize(&self.pop()?);
self.ensure_mem(dest, len);
for i in 0..len {
let b = self.calldata.get(src.wrapping_add(i)).copied().unwrap_or(0);
self.memory[dest + i] = b;
}
self.pc += 1;
}
op::JUMP => {
let dest = word_to_usize(&self.pop()?);
self.jump(dest)?;
}
op::JUMPI => {
let dest = word_to_usize(&self.pop()?);
let cond = self.pop()?;
if cond != [0u8; 32] {
self.jump(dest)?;
} else {
self.pc += 1;
}
}
op::JUMPDEST => {
self.pc += 1;
}
op::RETURN => {
let off = word_to_usize(&self.pop()?);
let len = word_to_usize(&self.pop()?);
self.ensure_mem(off, len);
return Ok(self.memory[off..off + len].to_vec());
}
op::REVERT => {
let off = word_to_usize(&self.pop()?);
let len = word_to_usize(&self.pop()?);
self.ensure_mem(off, len);
return Err(ExecError::Revert(self.memory[off..off + len].to_vec()));
}
o if (op::LOG0..=op::LOG4).contains(&o) => {
let ntopics = (o - op::LOG0) as usize;
let off = word_to_usize(&self.pop()?);
let len = word_to_usize(&self.pop()?);
let mut topics = Vec::with_capacity(ntopics);
for _ in 0..ntopics {
topics.push(self.pop()?);
}
self.ensure_mem(off, len);
let data = self.memory[off..off + len].to_vec();
self.logs.push(LogEntry { topics, data });
self.pc += 1;
}
other => return Err(ExecError::UnknownOpcode(other)),
}
}
}
fn jump(&mut self, dest: usize) -> Result<(), ExecError> {
if self.code.get(dest) != Some(&op::JUMPDEST) {
return Err(ExecError::BadJumpDest(dest));
}
self.pc = dest;
Ok(())
}
}
fn bool_word(b: bool) -> Word {
let mut w = [0u8; 32];
if b {
w[31] = 1;
}
w
}
fn cmp256(a: &Word, b: &Word) -> std::cmp::Ordering {
a.iter().cmp(b.iter())
}
fn shr256(value: &Word, shift: u128) -> Word {
if shift >= 256 {
return [0u8; 32];
}
let shift = shift as usize;
let byte_shift = shift / 8;
let bit_shift = shift % 8;
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
let src = i as isize - byte_shift as isize;
if src >= 0 {
*byte = value[src as usize];
}
}
if bit_shift > 0 {
let mut carry = 0u8;
for byte in out.iter_mut() {
let new_carry = *byte << (8 - bit_shift);
*byte = (*byte >> bit_shift) | carry;
carry = new_carry;
}
}
out
}
fn word_to_u128(w: &Word) -> u128 {
let mut b = [0u8; 16];
b.copy_from_slice(&w[16..]);
u128::from_be_bytes(b)
}
fn u128_to_word(v: u128) -> Word {
let mut w = [0u8; 32];
w[16..].copy_from_slice(&v.to_be_bytes());
w
}
fn word_to_usize(w: &Word) -> usize {
word_to_u64(w) as usize
}
#[cfg(test)]
mod tests {
use super::*;
use crate::soliditylite::compile;
fn deploy_src(src: &str) -> Contract {
let art = compile(src).expect("source must compile");
Contract::deploy(&art.init_code, &CallEnv::default()).expect("deploy must succeed")
}
fn sel(sig: &str) -> [u8; 4] {
crate::registry::selector(sig)
}
#[test]
fn add_sub_wrap_mod_2_256() {
let max = [0xffu8; 32];
let one = word(1);
assert_eq!(add256(&max, &one), [0u8; 32], "max + 1 wraps to 0");
assert_eq!(sub256(&[0u8; 32], &one), max, "0 - 1 wraps to max");
}
#[test]
fn shr_by_224_extracts_a_selector() {
let mut w = [0u8; 32];
w[0..4].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
let shifted = shr256(&w, 224);
assert_eq!(&shifted[28..], &[0xde, 0xad, 0xbe, 0xef]);
assert!(shifted[..28].iter().all(|&b| b == 0));
}
#[test]
fn bootstrap_const_getter_returns_42() {
let mut c = deploy_src(
"facet C { function get() external view returns (uint256) { return 42; } }",
);
let ret = c.call(&calldata(sel("get()"), &[]), &CallEnv::default()).unwrap();
assert_eq!(word_to_u64(&ret.as_slice().try_into().unwrap()), 42);
}
#[test]
fn bootstrap_unknown_selector_reverts() {
let mut c = deploy_src(
"facet C { function get() external view returns (uint256) { return 42; } }",
);
let err = c.call(&calldata([0x00, 0x00, 0x00, 0x00], &[]), &CallEnv::default()).unwrap_err();
assert_eq!(err, ExecError::Revert(Vec::new()));
}
#[test]
fn bootstrap_tally_storage_write_round_trips() {
const SRC: &str = "facet Tally { uint256 n; \
function bump() external { n = n + 1; } \
function get() external view returns (uint256) { return n; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
let read = |c: &mut Contract| {
word_to_u64(&c.call(&calldata(sel("get()"), &[]), &env).unwrap().as_slice().try_into().unwrap())
};
assert_eq!(read(&mut c), 0, "n starts at 0");
c.call(&calldata(sel("bump()"), &[]), &env).unwrap();
c.call(&calldata(sel("bump()"), &[]), &env).unwrap();
c.call(&calldata(sel("bump()"), &[]), &env).unwrap();
assert_eq!(read(&mut c), 3, "three bumps → 3");
}
#[test]
fn bootstrap_ledger_mapping_per_caller() {
const SRC: &str = "facet Ledger { mapping(address => uint256) bal; \
function add(uint256 amt) external { bal[msg.sender] = bal[msg.sender] + amt; } \
function balanceOf(address who) external view returns (uint256) { return bal[who]; } }";
let mut c = deploy_src(SRC);
let alice = CallEnv { caller: [0x11; 20], ..Default::default() };
let bob = CallEnv { caller: [0x22; 20], ..Default::default() };
c.call(&calldata(sel("add(uint256)"), &[word(10)]), &alice).unwrap();
c.call(&calldata(sel("add(uint256)"), &[word(5)]), &alice).unwrap();
c.call(&calldata(sel("add(uint256)"), &[word(99)]), &bob).unwrap();
let bal = |c: &mut Contract, who: &[u8; 20]| {
word_to_u64(
&c.call(&calldata(sel("balanceOf(address)"), &[addr_word(who)]), &CallEnv::default())
.unwrap()
.as_slice()
.try_into()
.unwrap(),
)
};
assert_eq!(bal(&mut c, &[0x11; 20]), 15, "alice = 10 + 5");
assert_eq!(bal(&mut c, &[0x22; 20]), 99, "bob = 99");
assert_eq!(bal(&mut c, &[0x33; 20]), 0, "an unseen caller = 0");
}
#[test]
fn bootstrap_require_guard_reverts_on_false() {
const SRC: &str = "facet C { uint256 total; \
function incrementBy(uint256 n) external { require(n > 0, \"zero\"); require(n <= 100, \"big\"); total = total + n; } \
function get() external view returns (uint256) { return total; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
assert_eq!(
c.call(&calldata(sel("incrementBy(uint256)"), &[word(0)]), &env).unwrap_err(),
ExecError::Revert(Vec::new())
);
assert_eq!(
c.call(&calldata(sel("incrementBy(uint256)"), &[word(101)]), &env).unwrap_err(),
ExecError::Revert(Vec::new())
);
c.call(&calldata(sel("incrementBy(uint256)"), &[word(7)]), &env).unwrap();
let total =
word_to_u64(&c.call(&calldata(sel("get()"), &[]), &env).unwrap().as_slice().try_into().unwrap());
assert_eq!(total, 7);
}
#[test]
fn bootstrap_arithmetic_precedence() {
const SRC: &str = "facet Math { \
function poly(uint256 x) external pure returns (uint256) { return x + x * x; } \
function fee(uint256 amount, uint256 rate) external pure returns (uint256) { return amount * rate / 10000; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
let poly = word_to_u64(
&c.call(&calldata(sel("poly(uint256)"), &[word(3)]), &env).unwrap().as_slice().try_into().unwrap(),
);
assert_eq!(poly, 12, "3 + 3*3 = 12 (precedence)");
let fee = word_to_u64(
&c.call(&calldata(sel("fee(uint256,uint256)"), &[word(1_000_000), word(250)]), &env)
.unwrap()
.as_slice()
.try_into()
.unwrap(),
);
assert_eq!(fee, 25_000, "1_000_000 * 250 / 10000");
}
#[test]
fn bootstrap_dynamic_array_push_index_length() {
const SRC: &str = "facet Stack { uint256 total; uint256[] xs; \
function push(uint256 v) external { xs.push(v); total = total + 1; } \
function set(uint256 i, uint256 v) external { xs[i] = v; } \
function get(uint256 i) external view returns (uint256) { return xs[i]; } \
function size() external view returns (uint256) { return xs.length; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
let size = |c: &mut Contract| {
word_to_u64(&c.call(&calldata(sel("size()"), &[]), &env).unwrap().as_slice().try_into().unwrap())
};
let get = |c: &mut Contract, i: u64| {
word_to_u64(
&c.call(&calldata(sel("get(uint256)"), &[word(i)]), &env).unwrap().as_slice().try_into().unwrap(),
)
};
assert_eq!(size(&mut c), 0);
c.call(&calldata(sel("push(uint256)"), &[word(11)]), &env).unwrap();
c.call(&calldata(sel("push(uint256)"), &[word(22)]), &env).unwrap();
assert_eq!(size(&mut c), 2, "two pushes → length 2");
assert_eq!(get(&mut c, 0), 11);
assert_eq!(get(&mut c, 1), 22);
c.call(&calldata(sel("set(uint256,uint256)"), &[word(0), word(99)]), &env).unwrap();
assert_eq!(get(&mut c, 0), 99, "set(0,99) overwrites in place");
}
#[test]
fn bootstrap_emit_produces_a_log() {
const SRC: &str = "facet C { event E(address indexed who, uint256 amt); \
function f(uint256 n) external { emit E(msg.sender, n); } }";
let art = compile(SRC).unwrap();
let mut c = Contract::deploy(&art.init_code, &CallEnv::default()).unwrap();
let env = CallEnv { caller: [0xAB; 20], ..Default::default() };
let (_, logs) = c.call_logs(&calldata(sel("f(uint256)"), &[word(42)]), &env).unwrap();
assert_eq!(logs.len(), 1, "one log");
let log = &logs[0];
assert_eq!(log.topics.len(), 2, "topic0 + indexed who");
assert_eq!(log.topics[0], super::super::codegen::event_topic0("E(address,uint256)"));
assert_eq!(log.topics[1], addr_word(&[0xAB; 20]), "topic1 is the caller");
assert_eq!(log.data, word(42).to_vec(), "the data region holds n = 42");
}
const POP_DELETE_SRC: &str = "facet Stack { uint256[] xs; \
function push(uint256 v) external { xs.push(v); } \
function pop() external { xs.pop(); } \
function clear(uint256 i) external { delete xs[i]; } \
function get(uint256 i) external view returns (uint256) { return xs[i]; } \
function size() external view returns (uint256) { return xs.length; } }";
#[test]
fn pop_decrements_length_and_zeroes_the_removed_element() {
let mut c = deploy_src(POP_DELETE_SRC);
let env = CallEnv::default();
let size = |c: &mut Contract| {
word_to_u64(&c.call(&calldata(sel("size()"), &[]), &env).unwrap().as_slice().try_into().unwrap())
};
let get = |c: &mut Contract, i: u64| {
word_to_u64(
&c.call(&calldata(sel("get(uint256)"), &[word(i)]), &env).unwrap().as_slice().try_into().unwrap(),
)
};
for v in [11u64, 22, 33] {
c.call(&calldata(sel("push(uint256)"), &[word(v)]), &env).unwrap();
}
assert_eq!(size(&mut c), 3);
c.call(&calldata(sel("pop()"), &[]), &env).unwrap();
assert_eq!(size(&mut c), 2, "pop → length 2");
assert_eq!(get(&mut c, 0), 11, "remaining elements persist");
assert_eq!(get(&mut c, 1), 22);
c.call(&calldata(sel("push(uint256)"), &[word(44)]), &env).unwrap();
assert_eq!(size(&mut c), 3, "re-push → length 3");
assert_eq!(get(&mut c, 2), 44, "the reused slot holds the new value");
c.call(&calldata(sel("pop()"), &[]), &env).unwrap();
c.call(&calldata(sel("pop()"), &[]), &env).unwrap();
c.call(&calldata(sel("pop()"), &[]), &env).unwrap();
assert_eq!(size(&mut c), 0, "popped to empty");
}
#[test]
fn popped_element_slot_reads_zero() {
let mut c = deploy_src(POP_DELETE_SRC);
let env = CallEnv::default();
c.call(&calldata(sel("push(uint256)"), &[word(11)]), &env).unwrap();
c.call(&calldata(sel("push(uint256)"), &[word(22)]), &env).unwrap();
c.call(&calldata(sel("pop()"), &[]), &env).unwrap();
let elem1 = word_to_u64(
&c.call(&calldata(sel("get(uint256)"), &[word(1)]), &env).unwrap().as_slice().try_into().unwrap(),
);
assert_eq!(elem1, 0, "the popped element slot reads zero");
}
#[test]
fn delete_zeroes_element_in_place_keeping_length() {
let mut c = deploy_src(POP_DELETE_SRC);
let env = CallEnv::default();
let size = |c: &mut Contract| {
word_to_u64(&c.call(&calldata(sel("size()"), &[]), &env).unwrap().as_slice().try_into().unwrap())
};
let get = |c: &mut Contract, i: u64| {
word_to_u64(
&c.call(&calldata(sel("get(uint256)"), &[word(i)]), &env).unwrap().as_slice().try_into().unwrap(),
)
};
for v in [11u64, 22, 33] {
c.call(&calldata(sel("push(uint256)"), &[word(v)]), &env).unwrap();
}
c.call(&calldata(sel("clear(uint256)"), &[word(1)]), &env).unwrap();
assert_eq!(size(&mut c), 3, "delete does NOT change the length");
assert_eq!(get(&mut c, 0), 11, "neighbor before is intact");
assert_eq!(get(&mut c, 1), 0, "the deleted element is zeroed");
assert_eq!(get(&mut c, 2), 33, "neighbor after is intact");
}
#[test]
fn pop_and_delete_interleave() {
let mut c = deploy_src(POP_DELETE_SRC);
let env = CallEnv::default();
let get = |c: &mut Contract, i: u64| {
word_to_u64(
&c.call(&calldata(sel("get(uint256)"), &[word(i)]), &env).unwrap().as_slice().try_into().unwrap(),
)
};
let size = |c: &mut Contract| {
word_to_u64(&c.call(&calldata(sel("size()"), &[]), &env).unwrap().as_slice().try_into().unwrap())
};
for v in [1u64, 2, 3, 4] {
c.call(&calldata(sel("push(uint256)"), &[word(v)]), &env).unwrap();
}
c.call(&calldata(sel("clear(uint256)"), &[word(0)]), &env).unwrap(); c.call(&calldata(sel("pop()"), &[]), &env).unwrap(); assert_eq!(size(&mut c), 3);
assert_eq!(get(&mut c, 0), 0);
assert_eq!(get(&mut c, 1), 2);
assert_eq!(get(&mut c, 2), 3);
}
#[test]
fn delete_clears_the_actual_storage_slot() {
let mut c = deploy_src(POP_DELETE_SRC);
let env = CallEnv::default();
for v in [11u64, 22, 33] {
c.call(&calldata(sel("push(uint256)"), &[word(v)]), &env).unwrap();
}
let base = storage_base("stack");
let elem1 = array_elem_slot(&base, 1);
assert_ne!(c.sload(&elem1), [0u8; 32], "before delete the slot holds 22");
c.call(&calldata(sel("clear(uint256)"), &[word(1)]), &env).unwrap();
assert_eq!(c.sload(&elem1), [0u8; 32], "after delete the storage slot is zero");
}
fn storage_base(facet_name_lower: &str) -> Word {
let preimage = format!("localharness.{facet_name_lower}.storage.v1");
let mut w = [0u8; 32];
w.copy_from_slice(&Keccak256::digest(preimage.as_bytes()));
w
}
fn array_elem_slot(slot: &Word, index: u64) -> Word {
let base: Word = Keccak256::digest(slot).into();
super::add256(&base, &word(index))
}
#[test]
fn bootstrap_const_string_return_abi_encoding() {
let mut c = deploy_src(
"facet Meta { function name() external pure returns (string) { return \"claude\"; } }",
);
let ret = c.call(&calldata(sel("name()"), &[]), &CallEnv::default()).unwrap();
assert_eq!(word_to_u64(&ret[0..32].try_into().unwrap()), 0x20);
let len = word_to_u64(&ret[32..64].try_into().unwrap()) as usize;
assert_eq!(len, 6);
assert_eq!(&ret[64..64 + len], b"claude");
}
fn decode_abi_dynamic(ret: &[u8]) -> Vec<u8> {
assert!(ret.len() >= 64, "a dynamic return is at least offset+length words");
assert_eq!(word_to_u64(&ret[0..32].try_into().unwrap()), 0x20, "ABI offset word must be 0x20");
let len = word_to_u64(&ret[32..64].try_into().unwrap()) as usize;
assert!(ret.len() >= 64 + len, "the return holds the full {len}-byte payload");
ret[64..64 + len].to_vec()
}
fn calldata_dynamic_arg(selector: [u8; 4], data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&selector);
out.extend_from_slice(&word(0x20)); out.extend_from_slice(&word(data.len() as u64)); out.extend_from_slice(data); let pad = (32 - data.len() % 32) % 32; out.extend(std::iter::repeat_n(0u8, pad));
out
}
#[test]
fn dynamic_storage_short_string_round_trips() {
const SRC: &str = "facet Note { string s; \
function set() external { s = \"claude\"; } \
function get() external view returns (string) { return s; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
let ret = c.call(&calldata(sel("get()"), &[]), &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), b"", "unset string reads empty");
c.call(&calldata(sel("set()"), &[]), &env).unwrap();
let slot = storage_base("note"); let raw = c.sload(&slot);
assert_eq!(&raw[..6], b"claude", "short data is left-aligned in the slot");
assert_eq!(raw[31], 12, "low byte = len*2 (6*2)");
let ret = c.call(&calldata(sel("get()"), &[]), &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), b"claude");
}
#[test]
fn dynamic_storage_31_byte_string_is_short() {
const S: &str = "0123456789012345678901234567890"; assert_eq!(S.len(), 31);
let src = format!(
"facet Note {{ string s; function set() external {{ s = \"{S}\"; }} \
function get() external view returns (string) {{ return s; }} }}"
);
let mut c = deploy_src(&src);
let env = CallEnv::default();
c.call(&calldata(sel("set()"), &[]), &env).unwrap();
let raw = c.sload(&storage_base("note"));
assert_eq!(raw[31], 62, "31-byte string is SHORT (low byte = 31*2 = 62, even)");
let ret = c.call(&calldata(sel("get()"), &[]), &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), S.as_bytes());
}
#[test]
fn dynamic_storage_long_string_round_trips() {
const S: &str = "this string is forty bytes long, yes sir";
assert_eq!(S.len(), 40);
let src = format!(
"facet Note {{ string s; function set() external {{ s = \"{S}\"; }} \
function get() external view returns (string) {{ return s; }} }}"
);
let mut c = deploy_src(&src);
let env = CallEnv::default();
c.call(&calldata(sel("set()"), &[]), &env).unwrap();
let slot = storage_base("note");
assert_eq!(word_to_u64(&c.sload(&slot)), 40 * 2 + 1, "long header = len*2 + 1");
let d0 = array_elem_slot(&slot, 0);
let d1 = array_elem_slot(&slot, 1);
assert_eq!(&c.sload(&d0)[..], &S.as_bytes()[..32], "first 32 data bytes");
assert_eq!(&c.sload(&d1)[..8], &S.as_bytes()[32..], "trailing 8 data bytes");
let ret = c.call(&calldata(sel("get()"), &[]), &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), S.as_bytes());
}
#[test]
fn dynamic_storage_32_byte_string_is_long() {
const S: &str = "01234567890123456789012345678901"; assert_eq!(S.len(), 32);
let src = format!(
"facet Note {{ string s; function set() external {{ s = \"{S}\"; }} \
function get() external view returns (string) {{ return s; }} }}"
);
let mut c = deploy_src(&src);
let env = CallEnv::default();
c.call(&calldata(sel("set()"), &[]), &env).unwrap();
assert_eq!(word_to_u64(&c.sload(&storage_base("note"))), 32 * 2 + 1, "32-byte → LONG header 65");
let ret = c.call(&calldata(sel("get()"), &[]), &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), S.as_bytes());
}
#[test]
fn dynamic_storage_bytes_round_trips() {
const SRC: &str = "facet Blob { bytes b; \
function set() external { b = \"raw\"; } \
function get() external view returns (bytes) { return b; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
c.call(&calldata(sel("set()"), &[]), &env).unwrap();
let ret = c.call(&calldata(sel("get()"), &[]), &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), b"raw");
}
#[test]
fn dynamic_param_echo_short_string() {
const SRC: &str =
"facet E { function echo(string s) external pure returns (string) { return s; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
let s = b"hello world";
let cd = calldata_dynamic_arg(sel("echo(string)"), s);
let ret = c.call(&cd, &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), s, "the echoed string matches the input");
}
#[test]
fn dynamic_param_echo_long_string() {
const SRC: &str =
"facet E { function echo(string s) external pure returns (string) { return s; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
let s = b"this is a string longer than thirty-two bytes for sure!!";
assert!(s.len() > 32);
let cd = calldata_dynamic_arg(sel("echo(string)"), s);
let ret = c.call(&cd, &env).unwrap();
assert_eq!(decode_abi_dynamic(&ret), s);
}
#[test]
fn dynamic_param_echo_empty_string() {
const SRC: &str =
"facet E { function echo(string s) external pure returns (string) { return s; } }";
let mut c = deploy_src(SRC);
let cd = calldata_dynamic_arg(sel("echo(string)"), b"");
let ret = c.call(&cd, &CallEnv::default()).unwrap();
assert_eq!(decode_abi_dynamic(&ret), b"", "an empty string echoes empty");
}
#[test]
fn dynamic_param_echo_bytes() {
const SRC: &str =
"facet E { function echo(bytes b) external pure returns (bytes) { return b; } }";
let mut c = deploy_src(SRC);
let payload = [0x00u8, 0xde, 0xad, 0x00, 0xbe, 0xef];
let cd = calldata_dynamic_arg(sel("echo(bytes)"), &payload);
let ret = c.call(&cd, &CallEnv::default()).unwrap();
assert_eq!(decode_abi_dynamic(&ret), payload, "raw bytes (with NULs) echo verbatim");
}
#[test]
fn dynamic_param_echo_after_a_static_arg() {
const SRC: &str = "facet E { \
function echo(uint256 n, string s) external pure returns (string) { return s; } }";
let mut c = deploy_src(SRC);
let s = b"second-arg string";
let mut cd = Vec::new();
cd.extend_from_slice(&sel("echo(uint256,string)"));
cd.extend_from_slice(&word(7)); cd.extend_from_slice(&word(0x40)); cd.extend_from_slice(&word(s.len() as u64));
cd.extend_from_slice(s);
let pad = (32 - s.len() % 32) % 32;
cd.extend(std::iter::repeat_n(0u8, pad));
let ret = c.call(&cd, &CallEnv::default()).unwrap();
assert_eq!(decode_abi_dynamic(&ret), s);
}
#[test]
fn dynamic_storage_and_param_echo_coexist() {
const SRC: &str = "facet Mix { string s; \
function set() external { s = \"stored-value-that-is-long-enough-to-spill\"; } \
function get() external view returns (string) { return s; } \
function echo(string x) external pure returns (string) { return x; } }";
let mut c = deploy_src(SRC);
let env = CallEnv::default();
c.call(&calldata(sel("set()"), &[]), &env).unwrap();
let stored = c.call(&calldata(sel("get()"), &[]), &env).unwrap();
assert_eq!(decode_abi_dynamic(&stored), b"stored-value-that-is-long-enough-to-spill");
let echoed = c.call(&calldata_dynamic_arg(sel("echo(string)"), b"echoed"), &env).unwrap();
assert_eq!(decode_abi_dynamic(&echoed), b"echoed");
}
}