diff --git a/run_contests.sh b/run_contests.sh index 4979e46ea..e4891cfb1 100755 --- a/run_contests.sh +++ b/run_contests.sh @@ -24,5 +24,7 @@ bash ./contest.sh test/examples/dispatch/storage.json bash ./contest.sh test/examples/dispatch/generic_sum.json bash ./contest.sh test/examples/dispatch/generic_product.json bash ./contest.sh test/examples/dispatch/sum_wide_product.json +bash ./contest.sh test/examples/dispatch/storage_adt_field.json bash ./contest.sh test/examples/dispatch/forloops.json bash ./contest.sh test/examples/dispatch/weth9.json +bash ./contest.sh test/examples/dispatch/multisig.json diff --git a/src/Solcore/Desugarer/DeriveGeneric.hs b/src/Solcore/Desugarer/DeriveGeneric.hs index d97372306..d9d72b635 100644 --- a/src/Solcore/Desugarer/DeriveGeneric.hs +++ b/src/Solcore/Desugarer/DeriveGeneric.hs @@ -10,23 +10,51 @@ deriveGenericTopDecls :: [DataTy] -> [TopDecl Name] -> Either String [TopDecl Na deriveGenericTopDecls localData allDecls | not (genericClassVisible allDecls) = Right allDecls | (n : _) <- conflicts = Left (conflictError n) - | otherwise = Right (allDecls ++ newInsts) + | otherwise = Right (allDecls ++ newInsts ++ storageInsts ++ abiInsts) where excluded = pragmaExcluded allDecls hasInst = existingGenericTypes allDecls + derivable = + [ dt + | dt <- localData, + not (null (dataConstrs dt)), + dataName dt `notElem` excluded, + dataName dt `notElem` hasInst + ] conflicts = [ dataName dt | dt <- localData, dataName dt `elem` hasInst, dataName dt `notElem` excluded ] - newInsts = - [ TInstDef (buildInstance dt) - | dt <- localData, - not (null (dataConstrs dt)), - dataName dt `notElem` excluded, - dataName dt `notElem` hasInst - ] + newInsts = [TInstDef (buildInstance dt) | dt <- derivable] + -- When std.StorageGeneric is in scope, also derive the storage type-class + -- instances so the data type can live in contract storage: StorageSize (the + -- field's slot footprint, for offset arithmetic) and storage(T):CanStore(T) + -- (how a field is loaded/stored). Recursive types are skipped — their slot + -- footprint is unbounded. + -- + -- Storability is decided by CanStore, not StorageType. The derived CanStore + -- decomposes the SOP representation through the structural CanStore instances + -- in std.StorageGeneric, so each leaf is stored via its own CanStore — a + -- fixed leaf via storage(word)/…, a dynamic leaf (memory(bytes)) via + -- storage(bytes). A data type with a dynamic field is therefore genuinely + -- storable, and a field whose type has no CanStore instance fails precisely + -- at the storage site, not while deriving an instance nobody uses. + storageInsts + | storageClassVisible allDecls = + concatMap buildStorageInstances [dt | dt <- derivable, not (isRecursiveData dt)] + | otherwise = [] + -- When std.ABIGeneric is in scope, derive a per-type ABIDecode instance so + -- the contract dispatch can decode ADT-typed method parameters from calldata. + -- ABIAttribs / ABIEncode come for free via the default Generic bridges in + -- ABIGeneric; only ABIDecode needs a concrete per-type instance (its decode + -- returns a result-position type variable a default instance can't + -- monomorphize). The instance delegates to the rep's structural ABIDecode. + abiInsts + | abiClassVisible allDecls = + [TInstDef (buildABIDecode dt) | dt <- derivable, not (isRecursiveData dt)] + | otherwise = [] conflictError n = "type '" ++ show n @@ -42,6 +70,33 @@ genericClassVisible = any isGenericClass isGenericClass (TClassDef cls) = className cls == Name "Generic" isGenericClass _ = False +-- The StorageDeriving marker class is declared in std.StorageGeneric; its +-- presence signals that the storage type classes and their primitive +-- (sum/pair/unit) instances are in scope, so storage instances can be derived. +storageClassVisible :: [TopDecl Name] -> Bool +storageClassVisible = any isMarker + where + isMarker (TClassDef cls) = className cls == Name "StorageDeriving" + isMarker _ = False + +-- The ABIDeriving marker class is declared in std.ABIGeneric; its presence +-- signals that the ABI type classes and their structural instances are in +-- scope, so a per-type ABIDecode instance can be derived. +abiClassVisible :: [TopDecl Name] -> Bool +abiClassVisible = any isMarker + where + isMarker (TClassDef cls) = className cls == Name "ABIDeriving" + isMarker _ = False + +-- A data type is recursive if one of its constructor fields mentions the type +-- itself (directly). Such types have no fixed storage size, so we do not derive +-- storage instances for them. +isRecursiveData :: DataTy -> Bool +isRecursiveData dt = + any selfRef (concatMap constrTy (dataConstrs dt)) + where + selfRef t = dataName dt `elem` tyconNames t + collectDataDefs :: [TopDecl Name] -> [DataTy] collectDataDefs = concatMap go where @@ -197,3 +252,195 @@ buildInstance dt = mainTy = TyCon (dataName dt) (map TyVar (dataParams dt)), instFunctions = [buildFrom dt, buildTo dt] } + +-- Storage instances (StorageSize + CanStore) + +mainTyOf :: DataTy -> Ty +mainTyOf dt = TyCon (dataName dt) (map TyVar (dataParams dt)) + +wordTy :: Ty +wordTy = TyCon (Name "word") [] + +storageTyOf :: Ty -> Ty +storageTyOf t = TyCon (Name "storage") [t] + +proxyTyOf :: Ty -> Ty +proxyTyOf t = TyCon (Name "Proxy") [t] + +proxyExpOf :: Ty -> Exp Name +proxyExpOf t = TyExp (Con (Name "Proxy") []) (proxyTyOf t) + +-- A qualified class-method call, e.g. StorageType.store(...). +methodCall :: String -> String -> [Exp Name] -> Exp Name +methodCall cls method args = Call Nothing (QualName (Name cls) method) args + +buildStorageInstances :: DataTy -> [TopDecl Name] +buildStorageInstances dt = + [ TInstDef (buildStorageSize dt), + TInstDef (buildCanStore dt) + ] + +-- instance => T(params) : StorageSize { +-- function size(x : Proxy(T(params))) -> word { +-- return StorageSize.size(Proxy : Proxy()); +-- } +-- } +buildStorageSize :: DataTy -> Instance Name +buildStorageSize dt = + Instance + { instDefault = False, + instVars = dataParams dt, + instContext = [InCls (Name "StorageSize") (TyVar tv) [] | tv <- dataParams dt], + instName = Name "StorageSize", + paramsTy = [], + mainTy = mainTyOf dt, + instFunctions = [FunDef False sig body] + } + where + sig = + Signature + { sigVars = [], + sigContext = [], + sigName = Name "size", + sigParams = [Typed False (Name "_x") (proxyTyOf (mainTyOf dt))], + sigRetComptime = False, + sigReturn = Just wordTy, + sigPayable = False + } + body = [Return (methodCall "StorageSize" "size" [proxyExpOf (sopRep dt)])] + +-- instance => storage(T(params)) : CanStore(T(params)) { +-- function store(r : storage(T(params)), v : T(params)) -> () { +-- CanStore.store(storage(Typedef.rep(r)) : storage(), Generic.from(v)); +-- } +-- function load(r : storage(T(params))) -> T(params) { +-- let x : = CanStore.load(storage(Typedef.rep(r)) : storage()); +-- return Generic.to(x); +-- } +-- } +-- +-- Storage is expressed entirely through CanStore: the type's SOP representation +-- is stored at the slot via the structural CanStore instances (sum/pair/unit) in +-- std.StorageGeneric, which decompose to each leaf's own CanStore. So a leaf of +-- type memory(bytes) is stored via storage(bytes), and the type stays storable. +-- For parametric types the context carries, per parameter, the field obligations +-- the structural instances need (storage(a):CanStore(a) and a:StorageSize); for a +-- concrete type the context is empty and everything resolves at the leaves. +buildCanStore :: DataTy -> Instance Name +buildCanStore dt = + Instance + { instDefault = False, + instVars = dataParams dt, + instContext = + [InCls (Name "CanStore") (storageTyOf (TyVar tv)) [TyVar tv] | tv <- dataParams dt] + ++ [InCls (Name "StorageSize") (TyVar tv) [] | tv <- dataParams dt], + instName = Name "CanStore", + paramsTy = [mainT], + mainTy = storageTyOf mainT, + instFunctions = [FunDef False storeSig storeBody, FunDef False loadSig loadBody] + } + where + mainT = mainTyOf dt + repT = sopRep dt + -- storage(Typedef.rep(_r)) : storage() + repSlot = TyExp (Con (Name "storage") [methodCall "Typedef" "rep" [Var (Name "_r")]]) (storageTyOf repT) + storeSig = + Signature + { sigVars = [], + sigContext = [], + sigName = Name "store", + sigParams = + [ Typed False (Name "_r") (storageTyOf mainT), + Typed False (Name "_v") mainT + ], + sigRetComptime = False, + sigReturn = Just unitTy, + sigPayable = False + } + storeBody = [StmtExp (methodCall "CanStore" "store" [repSlot, methodCall "Generic" "from" [Var (Name "_v")]])] + loadSig = + Signature + { sigVars = [], + sigContext = [], + sigName = Name "load", + sigParams = [Typed False (Name "_r") (storageTyOf mainT)], + sigRetComptime = False, + sigReturn = Just mainT, + sigPayable = False + } + loadBody = + [ Let False (Name "_x") (Just repT) (Just (methodCall "CanStore" "load" [repSlot])), + Return (methodCall "Generic" "to" [Var (Name "_x")]) + ] + +-- ABIDecoder(ty, reader) +abiDecoderTyOf :: Ty -> Ty -> Ty +abiDecoderTyOf ty reader = TyCon (Name "ABIDecoder") [ty, reader] + +-- instance => ABIDecoder(T(params), reader) : ABIDecode(T(params)) { +-- function decode(ptr : ABIDecoder(T(params), reader), headOffset : word) -> T(params) { +-- match ptr { +-- | ABIDecoder(rdr) => +-- let rep_ptr : ABIDecoder(, reader) = ABIDecoder(rdr); +-- return Generic.to(ABIDecode.decode(rep_ptr, headOffset)); +-- } +-- } +-- } +-- Mirrors std.ABIGeneric's free `decode`, but with the data type fixed in the +-- instance head so Generic.to is monomorphizable. The reader is left generic +-- (its WordReader-ness is the only flat obligation); the rep is decoded in the +-- body through the structural ABIDecode instances. +buildABIDecode :: DataTy -> Instance Name +buildABIDecode dt = + Instance + { instDefault = False, + instVars = dataParams dt ++ [readerTv], + -- Only the reader and, for parametric types, each parameter's decodability + -- are required. The rep's structural ABIDecode is resolved in the body via + -- the sum/pair/unit instances (bottoming out at the parameters / concrete + -- leaves), so we avoid a rep-sized context that would break Patterson. + instContext = + InCls (Name "WordReader") readerTy [] + : [InCls (Name "ABIDecode") (abiDecoderTyOf (TyVar tv) readerTy) [TyVar tv] | tv <- dataParams dt], + instName = Name "ABIDecode", + paramsTy = [mainT], + mainTy = abiDecoderTyOf mainT readerTy, + instFunctions = [FunDef False sig body] + } + where + mainT = mainTyOf dt + repT = sopRep dt + readerTv = TVar (Name "_reader") + readerTy = TyVar readerTv + sig = + Signature + { sigVars = [], + sigContext = [], + sigName = Name "decode", + sigParams = + [ Typed False (Name "_ptr") (abiDecoderTyOf mainT readerTy), + Typed False (Name "_headOffset") wordTy + ], + sigRetComptime = False, + sigReturn = Just mainT, + sigPayable = False + } + body = + [ Match + [Var (Name "_ptr")] + [ ( [PCon (Name "ABIDecoder") [PVar (Name "_rdr")]], + [ Let + False + (Name "_rep_ptr") + (Just (abiDecoderTyOf repT readerTy)) + (Just (Con (Name "ABIDecoder") [Var (Name "_rdr")])), + Return + ( methodCall + "Generic" + "to" + [methodCall "ABIDecode" "decode" [Var (Name "_rep_ptr"), Var (Name "_headOffset")]] + ) + ] + ) + ] + ] diff --git a/std/ABIGeneric.solc b/std/ABIGeneric.solc index 95d450295..ee4450502 100644 --- a/std/ABIGeneric.solc +++ b/std/ABIGeneric.solc @@ -3,6 +3,7 @@ pragma no-bounded-variable-condition ABIAttribs, ABIEncode, ABIDecode; pragma no-coverage-condition ABIDecode; export { + ABIDeriving, encode, decode }; @@ -11,6 +12,15 @@ import std.{*}; import std.opcodes.{mstore}; import std.Generic.{*}; +// Marker class. Importing this module brings ABIDeriving into scope, which is +// the signal DeriveGeneric looks for to auto-derive a per-type ABIDecode +// instance for local data types. ABIAttribs / ABIEncode are provided generically +// via the default Generic bridges below, but ABIDecode cannot be a default +// instance (its decode returns the head variable `a` via Generic.to, a +// result-position type variable the specializer cannot monomorphize), so a +// concrete per-type instance is emitted instead — exactly as for storage. +forall self. class self : ABIDeriving {} + function maxWord(a : word, b : word) -> word { match gtWord(a, b) { | true => return a; diff --git a/std/StorageGeneric.solc b/std/StorageGeneric.solc new file mode 100644 index 000000000..bee021064 --- /dev/null +++ b/std/StorageGeneric.solc @@ -0,0 +1,250 @@ +pragma no-patterson-condition StorageType; +pragma no-bounded-variable-condition StorageType; + +export { + StorageDeriving, + loadGeneric, + storeGeneric +}; + +import std.{*}; +import std.opcodes.{sload, sstore}; +import std.Generic.{*}; + +// Marker class. Importing this module brings StorageDeriving into scope, which +// is the signal DeriveGeneric looks for to auto-derive StorageSize / CanStore +// instances for local data types (alongside their Generic instance). It carries +// no methods — its mere visibility enables storage derivation. +forall self. class self : StorageDeriving {} + +// ─── Storage layout for algebraic data types ───────────────────────────── +// +// This module is the storage analogue of std.ABIGeneric: it teaches the +// StorageSize / StorageType / CanStore classes how to deal with the +// primitive SOP types that `Generic` maps user data types onto +// sum(f, g) with constructors inl / inr (choice / tagged union) +// (f, g) pair (product) +// () unit +// and then bridges every type with a `Generic(rep)` instance to those +// layouts. `Generic` instances are auto-derived for local data types, so +// no per-type boilerplate is needed at the use site. + +function maxWord(a : word, b : word) -> word { + match gtWord(a, b) { + | true => return a; + | false => return b; + } +} + +// ─── StorageSize for the primitive sum(f, g) type ──────────────────────── +// A tagged union occupies one slot for the tag plus enough slots for the +// largest branch: size = 1 + max(size(f), size(g)). +// (StorageSize for () and (a, b) is already provided by std.) + +forall f g . f:StorageSize, g:StorageSize => +instance sum(f, g):StorageSize { + function size(x : Proxy(sum(f, g))) -> word { + let f_sz : word = StorageSize.size(Proxy : Proxy(f)); + let g_sz : word = StorageSize.size(Proxy : Proxy(g)); + return 1 + maxWord(f_sz, g_sz); + } +} + +// ─── StorageType for () ────────────────────────────────────────────────── +// The unit type occupies no slots, so load/store are no-ops. + +instance ():StorageType { + function load(ptr : word) -> () { + return (); + } + function store(ptr : word, value : ()) -> () { + return (); + } +} + +// ─── StorageType for the primitive product (a, b) ──────────────────────── +// Layout: [ptr .. ptr + size(a) - 1] : a +// [ptr + size(a) .. ] : b + +forall a b . a:StorageType, a:StorageSize, b:StorageType => +instance (a, b):StorageType { + function load(ptr : word) -> (a, b) { + let a_sz : word = StorageSize.size(Proxy : Proxy(a)); + let x : a = StorageType.load(ptr); + let y : b = StorageType.load(ptr + a_sz); + return (x, y); + } + function store(ptr : word, value : (a, b)) -> () { + match value { + | (x, y) => + let a_sz : word = StorageSize.size(Proxy : Proxy(a)); + StorageType.store(ptr, x); + StorageType.store(ptr + a_sz, y); + } + } +} + +// ─── StorageType for the primitive sum(f, g) ───────────────────────────── +// Slot layout (static sums): +// [ptr] : tag word (0 = inl, 1 = inr) +// [ptr + 1 .. ] : encoded branch payload + +forall f g . f:StorageType, g:StorageType => +instance sum(f, g):StorageType { + function load(ptr : word) -> sum(f, g) { + let tag : word = sload(ptr); + match tag { + | 0 => + let v : f = StorageType.load(ptr + 1); + return inl(v); + | _ => + let v : g = StorageType.load(ptr + 1); + return inr(v); + } + } + function store(ptr : word, value : sum(f, g)) -> () { + match value { + | inl(v) => + sstore(ptr, 0); + StorageType.store(ptr + 1, v); + | inr(v) => + sstore(ptr, 1); + StorageType.store(ptr + 1, v); + } + } +} + +// ─── Storage layout via CanStore ───────────────────────────────────────── +// +// The structural instances above teach StorageType the fixed-slot encoding of +// the SOP primitives. But StorageType can only describe word-packed types: a +// dynamically-sized field such as memory(bytes) has a StorageSize (one slot, +// Solidity-style) and a CanStore instance (storage(bytes):CanStore(memory(bytes))) +// but NO StorageType instance. Routing an ADT's storage through StorageType +// therefore rejects any data type carrying such a field, even though the field +// is perfectly storable. +// +// So we give CanStore the same structural treatment, decomposing the SOP +// representation and storing each leaf through the leaf's OWN CanStore instance. +// Fixed leaves resolve to storage(word)/storage(uint256)/… (which delegate to +// StorageType); dynamic leaves resolve to storage(bytes)/storage(string). Each +// field occupies StorageSize-many slots, so offsets are computed exactly as in +// the StorageType layout. The slot handle for a value of type `t` is uniformly +// `storage(t)`, which is why the dynamic leaves below are mirrored at that +// handle. + +// The unit type occupies no slots. +instance storage(()) : CanStore(()) { + function store(r : storage(()), v : ()) -> () { + return (); + } + function load(r : storage(())) -> () { + return (); + } +} + +// Product: store `a` at the base slot, `b` size(a) slots later. +forall a b . storage(a):CanStore(a), a:StorageSize, storage(b):CanStore(b) => +instance storage((a, b)) : CanStore((a, b)) { + function store(r : storage((a, b)), v : (a, b)) -> () { + match v { + | (x, y) => + let base : word = Typedef.rep(r); + let a_sz : word = StorageSize.size(Proxy : Proxy(a)); + CanStore.store(storage(base) : storage(a), x); + CanStore.store(storage(base + a_sz) : storage(b), y); + } + } + function load(r : storage((a, b))) -> (a, b) { + let base : word = Typedef.rep(r); + let a_sz : word = StorageSize.size(Proxy : Proxy(a)); + let x : a = CanStore.load(storage(base) : storage(a)); + let y : b = CanStore.load(storage(base + a_sz) : storage(b)); + return (x, y); + } +} + +// Tagged union: slot 0 holds the tag, the branch payload follows. +forall f g . storage(f):CanStore(f), storage(g):CanStore(g) => +instance storage(sum(f, g)) : CanStore(sum(f, g)) { + function store(r : storage(sum(f, g)), v : sum(f, g)) -> () { + let base : word = Typedef.rep(r); + match v { + | inl(x) => + sstore(base, 0); + CanStore.store(storage(base + 1) : storage(f), x); + | inr(y) => + sstore(base, 1); + CanStore.store(storage(base + 1) : storage(g), y); + } + } + function load(r : storage(sum(f, g))) -> sum(f, g) { + let base : word = Typedef.rep(r); + let tag : word = sload(base); + // NOTE: the loaded payload is inlined directly into inl(...) / inr(...) + // rather than bound to a `let x : f` / `let y : g` first. Binding the + // payload to an intermediate of the branch type (f or g) makes the + // compiler infer the *branch* type for the inl/inr application instead + // of the full sum(f, g), so it emits e.g. `inr(y)` and Yul codegen + // rejects it (sum nesting off by one). Inlining matches the working + // ABIGeneric.decode pattern, so inl/inr pick up the full sum(f, g). + match tag { + | 0 => + return inl(CanStore.load(storage(base + 1) : storage(f))); + | _ => + return inr(CanStore.load(storage(base + 1) : storage(g))); + } + } +} + +// Dynamic leaves at the uniform storage(t) handle. std provides the storage(bytes) +// / storage(string) instances (data lives at keccak(slot)); these mirror them at +// the storage(memory(bytes)) / storage(memory(string)) handle the structural +// decomposition asks for, so a memory(bytes) field inside an ADT is storable. +instance storage(memory(bytes)) : CanStore(memory(bytes)) { + function store(r : storage(memory(bytes)), v : memory(bytes)) -> () { + CanStore.store(storage(Typedef.rep(r)) : storage(bytes), v); + } + function load(r : storage(memory(bytes))) -> memory(bytes) { + return CanStore.load(storage(Typedef.rep(r)) : storage(bytes)); + } +} + +instance storage(memory(string)) : CanStore(memory(string)) { + function store(r : storage(memory(string)), v : memory(string)) -> () { + CanStore.store(storage(Typedef.rep(r)) : storage(string), v); + } + function load(r : storage(memory(string))) -> memory(string) { + return CanStore.load(storage(Typedef.rep(r)) : storage(string)); + } +} + +// StorageType / CanStore for an ADT are NOT provided here as blanket bridges. +// +// A `default instance a:StorageType` would have its `load` return the head +// variable `a` via Generic.to — but the specializer cannot monomorphize a +// result-position type variable of a default instance (it is not pinned by the +// arguments), so loads panic. Likewise a tyvar-headed `default a:CanStore(b)` +// is non-functional (accepts any storable b), so contract field access cannot +// infer the stored type from the slot type. +// +// Instead, DeriveGeneric emits a concrete, per-type storage(T):CanStore(T) +// instance (see Solcore.Desugarer.DeriveGeneric) where the data type is fixed +// in the instance head; it delegates to the structural CanStore instances above +// via the type's Generic representation. StorageSize is likewise derived +// per-type for the field layout. + +// ─── Top-level helpers ─────────────────────────────────────────────────── +// Convenience wrappers mirroring std.ABIGeneric's encode / decode: persist or +// read back any 'a' that has a Generic(rep) instance at a raw storage slot. + +forall a rep . a:Generic(rep), rep:StorageType => +function storeGeneric(slot : word, value : a) -> () { + StorageType.store(slot, Generic.from(value)); +} + +forall a rep . a:Generic(rep), rep:StorageType => +function loadGeneric(slot : word) -> a { + let r : rep = StorageType.load(slot); + return Generic.to(r); +} diff --git a/std/dispatch.solc b/std/dispatch.solc index fc45e3636..23234d2dd 100644 --- a/std/dispatch.solc +++ b/std/dispatch.solc @@ -1,5 +1,6 @@ import std.{*}; import std.opcodes.{callvalue, calldatasize, calldataload, shr}; +import std.Generic.{*}; export { ABIString, @@ -61,6 +62,28 @@ instance (a,b):SigString { } } +// A tagged union (the SOP form of an ADT with several constructors). There is no +// standard ABI type for sums, so this signature is structural: the comma-joined +// branch signatures. It makes ADT-typed parameters produce a deterministic +// selector; refine here if a specific on-the-wire sum convention is needed. +forall f g. f:SigString, g: SigString => +instance sum(f,g):SigString { + function sigStr(x:Proxy(sum(f,g))) -> string { + SigString.sigStr( Proxy:Proxy(f) ) + "," + SigString.sigStr( Proxy:Proxy(g) ) + } +} + +// Any data type inherits its ABI signature from its Generic representation, the +// same way ABIAttribs / ABIEncode bridge through Generic in std.ABIGeneric. This +// lets the dispatch take ADT-typed parameters (e.g. a Signature) without a +// hand-written SigString instance per type. +forall a rep. a:Generic(rep), rep:SigString => +default instance a:SigString { + function sigStr(x:Proxy(a)) -> string { + SigString.sigStr( Proxy:Proxy(rep) ) + } +} + forall name f args rets payability. f: invokable(args,rets), name:SigString, args:SigString, rets:SigString => instance Method(name,payability,args,rets,f):SigString { diff --git a/std/std.solc b/std/std.solc index 728c0e100..1be1d24bd 100644 --- a/std/std.solc +++ b/std/std.solc @@ -1750,6 +1750,17 @@ default instance a:CanStore(a) { } } +// bool has no StorageType instance (it is a builtin, not a Typedef(word)), but it +// round-trips through word via frombool / tobool, so it can still be stored. +instance storage(bool):CanStore(bool) { + function store(l:storage(bool), r:bool) -> () { + StorageType.store(Typedef.rep(l), frombool(r)); + } + function load(l:storage(bool)) -> bool { + return tobool(StorageType.load(Typedef.rep(l))); + } +} + forall k v. instance storage(mapping(k,v)):CanStore(storage(mapping(k,v))) { function store(l:storage(mapping(k,v)), r:storage(mapping(k,v))) -> () { @@ -1889,7 +1900,7 @@ instance (storage(mapping(i,a)), i): LValueIdxAccess(storage(a)) { } } -forall i a . a:StorageType, i:Typedef(word) => +forall i a . storage(a):CanStore(a), i:Typedef(word) => instance (storage(mapping(i,a)), i): RValueIdxAccess(a) { function lookup(xi : (storage(mapping(i,a)), i)) -> a { /* @@ -1901,9 +1912,12 @@ instance (storage(mapping(i,a)), i): RValueIdxAccess(a) { } } -forall a. a:StorageType => +// Mapping reads go through CanStore, matching the write side (Assign -> CanStore.store). +// This lets a mapping hold any value with a CanStore instance — including ADTs whose +// fields are dynamic (memory(bytes)) — not just the fixed-slot StorageType primitives. +forall a. storage(a):CanStore(a) => function readStorage(x:storage(a)) -> a { - return StorageType.load(Typedef.rep(x)); + return CanStore.load(x); } /* forall r a. a:StorageType, r: RValueIdxAccess(a) => @@ -1922,9 +1936,9 @@ function lidx( m: storage(mapping(i,a)), x:i) -> storage(a) { return storage(hash2(Typedef.rep(m), Typedef.rep(x))); } -forall i a . i:Typedef(word), a:StorageType => +forall i a . i:Typedef(word), storage(a):CanStore(a) => function ridx( m: storage(mapping(i,a)), x:i) -> a { - return StorageType.load(hash2(Typedef.rep(m), Typedef.rep(x))); + return CanStore.load(storage(hash2(Typedef.rep(m), Typedef.rep(x))) : storage(a)); } // --- Memory Encoding --- diff --git a/test/Cases.hs b/test/Cases.hs index 42fa2cad9..e5bbfbc9f 100644 --- a/test/Cases.hs +++ b/test/Cases.hs @@ -121,7 +121,9 @@ dispatches = runDispatchTest "empty.solc", runDispatchTest "empty_no_constructor.solc", runDispatchTest "generic_product.solc", - runDispatchTest "generic_sum.solc" + runDispatchTest "generic_sum.solc", + runDispatchTest "storage_adt_field.solc", + runDispatchTest "storage_dynamic_field.solc" ] where runDispatchTest file = runTestForFileWith (emptyOption mempty) file "./test/examples/dispatch" diff --git a/test/examples/dispatch/multisig.json b/test/examples/dispatch/multisig.json new file mode 100644 index 000000000..920d035f3 --- /dev/null +++ b/test/examples/dispatch/multisig.json @@ -0,0 +1,232 @@ +{ + "multisig": { + "bytecode": "", + "contract": "Multisig", + "tests": [ + { + "input": { + "comment": "constructor() - deployer becomes signers[0]; count=1, required=1", + "calldata": "", + "value": "0" + }, + "kind": "constructor" + }, + { + "input": { + "comment": "signersCount() -> 1", + "calldata": "e40956b1", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "signersRequired() -> 1", + "calldata": "2d2a7943", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "isSignerOf(A) -> false", + "calldata": "acb0538e00000000000000000000000000000000000000000000000000000000000000aa", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + { + "input": { + "comment": "addSigner(A) -> count 2", + "calldata": "eb12d61e00000000000000000000000000000000000000000000000000000000000000aa", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000002", + "status": "success" + } + }, + { + "input": { + "comment": "isSignerOf(A) -> true", + "calldata": "acb0538e00000000000000000000000000000000000000000000000000000000000000aa", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "addSigner(B) -> count 3", + "calldata": "eb12d61e00000000000000000000000000000000000000000000000000000000000000bb", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000003", + "status": "success" + } + }, + { + "input": { + "comment": "signersCount() -> 3", + "calldata": "e40956b1", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000003", + "status": "success" + } + }, + { + "input": { + "comment": "addSigner(A) again -> reverts SignerAlreadyExists()", + "calldata": "eb12d61e00000000000000000000000000000000000000000000000000000000000000aa", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "12345678", + "status": "failure" + } + }, + { + "input": { + "comment": "signersCount() -> 3 (failed add rolled back)", + "calldata": "e40956b1", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000003", + "status": "success" + } + }, + { + "input": { + "comment": "removeSigner(A) -> count 2 (B swapped into A's slot)", + "calldata": "0e316ab700000000000000000000000000000000000000000000000000000000000000aa", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000002", + "status": "success" + } + }, + { + "input": { + "comment": "isSignerOf(A) -> false", + "calldata": "acb0538e00000000000000000000000000000000000000000000000000000000000000aa", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + { + "input": { + "comment": "isSignerOf(B) -> still true after swap", + "calldata": "acb0538e00000000000000000000000000000000000000000000000000000000000000bb", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "signersCount() -> 2", + "calldata": "e40956b1", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000002", + "status": "success" + } + }, + { + "input": { + "comment": "removeSigner(A) again -> reverts NotASigner()", + "calldata": "0e316ab700000000000000000000000000000000000000000000000000000000000000aa", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "12345678", + "status": "failure" + } + }, + { + "input": { + "comment": "removeSigner(B) -> count 1", + "calldata": "0e316ab700000000000000000000000000000000000000000000000000000000000000bb", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "signersCount() -> 1", + "calldata": "e40956b1", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "removeSigner when only one signer left -> reverts CannotRemoveOnlySigner()", + "calldata": "0e316ab700000000000000000000000000000000000000000000000000000000000000bb", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "12345678", + "status": "failure" + } + }, + { + "input": { + "comment": "signersRequired() -> 1 (required_signatures never changed)", + "calldata": "2d2a7943", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + } + ] + } +} diff --git a/test/examples/dispatch/multisig.solc b/test/examples/dispatch/multisig.solc new file mode 100644 index 000000000..29f9a4fe9 --- /dev/null +++ b/test/examples/dispatch/multisig.solc @@ -0,0 +1,497 @@ +// +// The design of this multisig is fairly simple. +// +// An Operation defines a state change, and OperationStatus defines +// its current status. Each Operation must be approved by enough Signers. +// We store Signers, Operations and OperationStatuses in storage. +// +// An existing Signer can queue, approve, or reject an Operation. Once +// an Operation is approved, anyone can execute it. OperationStatus controls +// it as a state machine. +// +// The states of an Operation: +// - upon creation, called `queue`, the state becomes Approvals(0), where 0 means 0 approvals +// - with `approve` the state increments Approvals(i) to Approvals (i + 1) +// - with `reject` the state changes to Rejected iff the current state is Approvals(i) +// - with `execute` the state changes to Executed iff the current state is Approvals(i) with i >= signers_required +// +// Operations must be executed in strict order. If something becomes Rejected, it must +// still be executed, and execution will mark and skip it. Note that if a transaction +// becomes non-executable for any reason, it can be marked as rejected and skipped. +// +// The second layer is queueWithSignature/approveWithSignature/rejectWithSignature, +// where a signature is passed along and thus the caller is not checked. This +// signature can be multiple options: +// - EIP-2098 compact ECDSA signature, +// - approved hash by target contract, which must be a signer, +// - EIP-1271 contract signature validation, which must be a signer. +// +// The last layer is batching operations. +// +// Optional future improvements: +// - EIP-712 for signing +// - Operation.ChangeSigner -- batched change to replace a given signer +// - Operation.DelegateCall -- it is a security surface, and not neccessarily needed +// - be an EIP-1271 signer +// - gas optimisations +// - Strict sequence vs. re-entracy guard for execute() + +import std.{*}; +import std.dispatch.{*}; +import std.opcodes.{address as address_, calldatasize, mload, caller as caller_}; +import std.ABIGeneric.{*}; +import std.StorageGeneric.{*}; + +function caller() -> address { + return address(caller_()); +} + +data Operation = + AddSigner(address) // Adds a new signer. + | RemoveSigner(address) // Removes an existing signer. + | ChangeSigRequired(uint256) // Change the number of signatures required. + | TransferEth(address, uint256) // Transfers ether. + | TransferToken(address, address, uint256) // Transfers a token. + | Call(address, uint256, memory(bytes)) // Arbitrary calls to an address. + | UnstoredCall(bytes32) // Arbitrary calls to an address, represented by a hash (supplied at execution time). + // It is encoded as [address][value][payload] + | ApproveSignedHash(bytes32) // For interacting as an EIP-1271 signer. + | RevokeSignedHash(bytes32); + +data OperationStatus = + Approvals(uint256) // approval count (TODO: use uint8/uint16 to be realistic) + | Rejected + | Executed; + +data Vote = + None + | Approved + | Rejected; + +data Signature = + ECDSA(bytes32, bytes32) // EIP-2098-style r/s/v (TODO: add chainid/domain) + | Contract(address) // If the hash is approved by the contract. + | EIP1271(address, memory(bytes)); // EIP-1271 signature validation + +data OperationKind = + Queue + | Approve + | Reject; + +data BatchOperation = + Queue(Operation, Signature) + | Approve(uint256, Signature) + | Reject(uint256, Signature) + | Execute(uint256, memory(bytes)); + + +instance Vote:Eq { + function eq(a: Vote, b: Vote) -> bool { + let a_index: word; + match a { + | Vote.None => a_index = 0; + | Vote.Approved => a_index = 1; + | Vote.Rejected => a_index = 2; + } + let b_index: word; + match b { + | Vote.None => b_index = 0; + | Vote.Approved => b_index = 1; + | Vote.Rejected => b_index = 2; + } + return a_index == b_index; + } +} + +contract Multisig { + signers: mapping(uint256, address); // TODO use array() + signers_count: uint256; + signers_required: uint256; + // TODO: Stored by hash -- or should it be by nonce? + operations: mapping(uint256, Operation); // TODO: use array() + operations_count: uint256; + votes: mapping(uint256, mapping(address, Vote)); + status: mapping(uint256, OperationStatus); + nonce: uint256; // Strict ordering. Next executable operation. + approved_signed_hashes: mapping(bytes32, bool); + + constructor() { + // The creator becomes the first signer. + signers[uint256(0)] = caller(); + signers_count = uint256(1); + signers_required = uint256(1); + } + + // Only signers can call this. + public function queue(op: Operation) -> () { + require(isSigner(caller()), Error(0x12345678)); // NotASigner() + perform_queue(op); + } + + function perform_queue(op: Operation) -> () { + // Some basic sanity checks. + match op { + | Operation.AddSigner(signer) => + require(signer != address(0), Error(0x12345678)); // CannotAddZeroAddressAsSigner() + require(signer != address(address_()), Error(0x12345678)); // CannotAddSelfAsSigner() + | Operation.ChangeSigRequired(count) => + require(count >= uint256(1), Error(0x12345678)); // ThresholdBelowMinimum() + | _ => // No extra check needed. + } + + operations[operations_count] = op; + status[operations_count] = OperationStatus.Approvals(uint256(0)); + operations_count += uint256(1); + + // TODO: emit log + } + + // Only signers can call this. + public function approve(nonce_: uint256) -> () { + require(isSigner(caller()), Error(0x12345678)); // NotASigner() + perform_approve(nonce_, caller()); + } + + function perform_approve(nonce_: uint256, signer: address) -> () { + require(nonce_ < operations_count, Error(0x12345678)); // OperationNotFound() + + // TODO: emit log + + match status[nonce_] { + | OperationStatus.Approvals(count) => + require(votes[nonce_][signer] == Vote.None, Error(0x12345678)); // SignerAlreadyApproved() + votes[nonce_][signer] = Vote.Approved; + status[nonce_] = OperationStatus.Approvals(count + uint256(1)); + | _ => revertWithError(Error(0x12345678)); // UnexpectedStatus() + } + } + + function checkSignature(hash: bytes32, signature: Signature) -> address { + match signature { + | Signature.ECDSA(r, s) => + let signer = eip2098_signer(hash, r, s); + require(isSigner(signer), Error(0x12345678)); // NotASigner() + return signer; + | Signature.Contract(contract_) => + require(isSigner(contract_), Error(0x12345678)); // NotASigner() + require(check_contract_hash(contract_, hash), Error(0x12345678)); // HashNotApprovedByTarget() + return contract_; + | Signature.EIP1271(contract_, signature) => + require(isSigner(contract_), Error(0x12345678)); // NotASigner() + require(eip1271_verify(contract_, hash, signature), Error(0x12345678)); // EIP1271VerificationRejected() + return contract_; + } + } + + function create_signature_hash(kind: OperationKind, operation: Operation) -> bytes32 { + // TODO: include domain/chaind information in hash +// return keccak256_(concat(abi_encode(kind), abi_encode(operation))); + // TODO: abi.encode not working yet + return keccak256_(to_bytes(bytes32(1))); + } + + // Anyone can call this. + public function queueWithSignature(operation: Operation, signature: Signature) -> () { + let hash = create_signature_hash(OperationKind.Queue, operation); + + checkSignature(hash, signature); + + perform_queue(operation); + } + + // Anyone can call this. + public function approveWithSignature(nonce_: uint256, signature: Signature) -> () { + let hash = create_signature_hash(OperationKind.Approve, operations[nonce_]); + + let signer = checkSignature(hash, signature); + + perform_approve(nonce_, signer); + } + + // Anyone can call this. + public function rejectWithSignature(nonce_: uint256, signature: Signature) -> () { + let hash = create_signature_hash(OperationKind.Reject, operations[nonce_]); + + let signer = checkSignature(hash, signature); + + perform_reject(nonce_, signer); + } + +/* + public function batch(operations: array(BatchOperation)) -> () { + for (let i = 0; i < operations.length; i += 1) { + match operations[i] { + | Operation.Queue(operation, signature) => queueWithSignature(operation, signature); + | Operation.Approve(nonce_, signature) => approveWithSignature(nonce_, signature); + | Operation.Reject(nonce_, signature) => rejectWithSignature(nonce_, signature); + | Operation.Execute(nonce_, payload) => execute(nonce_, payload); + } + } + } +*/ + // Only signers can call this. + public function reject(nonce_: uint256) -> () { + require(isSigner(caller()), Error(0x12345678)); // NotASigner() + perform_reject(nonce_, caller()); + } + + function perform_reject(nonce_: uint256, signer: address) -> () { + require(nonce_ < operations_count, Error(0x12345678)); // OperationNotFound() + + // TODO: emit log + + match status[nonce_] { + | OperationStatus.Approvals(count) => + status[nonce_] = OperationStatus.Rejected; + votes[nonce_][signer] = Vote.Rejected; + | _ => revertWithError(Error(0x12345678)); // UnexpectedStatus() + } + } + + // Anyone can execute, as long as the status is correct. + // Payload is optional, used in case UnstoredCall is encountered. + public function execute(nonce_: uint256, payload: memory(bytes)) -> () { + // Ensure status. + require(nonce_ < operations_count, Error(0x12345678)); // OperationNotFound() + + // Enforce strict sequence ordering. + require(nonce_ == nonce, Error(0x12345678)); // IncorrectSequence() + match status[nonce_] { + | OperationStatus.Rejected => + nonce += uint256(1); + // Special case for rejections: we operate as a no-op. + return (); + | OperationStatus.Approvals(count) => + require(count >= signers_required, Error(0x12345678)); // NotEnoughApprovals() + nonce += uint256(1); + // Update status. + status[nonce_] = OperationStatus.Executed; + | _ => revertWithError(Error(0x12345678)); // IncorrectStatus(); + } + + // TODO: emit log + + // Execute. + match operations[nonce_] { + | Operation.AddSigner(signer) => add_signer(signer); + | Operation.RemoveSigner(signer) => remove_signer(signer); + | Operation.ChangeSigRequired(count) => + require(count <= signers_count, Error(0x12345678)); // ThresholdExceedsSigners() + signers_required = count; + | Operation.TransferEth(target, amount) => + let ret: word; + let target_ = Typedef.rep(target); + let amount_ = Typedef.rep(amount); + assembly { + ret := call(gas(), target_, amount_, 0, 0, 0, 0) + } + require(tobool(ret), Error(0x12345678)); // EtherTransferFailed() + | Operation.TransferToken(target, token, amount) => + safe_erc20_transfer(token, target, amount); + | Operation.Call(target, value, payload) => + require(arbitrary_call(target, value, payload), Error(0x12345678)); // CallFailed() + | Operation.UnstoredCall(hash) => + require(hash == keccak256_(payload), Error(0x12345678)); // InvalidPayloadSupplied() + let ret: word; + let payload_ = Typedef.rep(payload); + assembly { + let size := mload(payload_) + // Check for minimum length of 64 bytes + if lt(size, 64) { + revert(0, 0) // TODO return proper error + } + let target := mload(add(payload_, 32)) + let value := mload(add(payload_, 64)) + ret := call(gas(), target, value, add(payload_, 96), sub(size, 64), 0, 0) + } + require(tobool(ret), Error(0x12345678)); // UnstoredCallFailed() + | Operation.ApproveSignedHash(hash) => + // Sanity check. + require(!approved_signed_hashes[hash], Error(0x12345678)); // ApprovedSignedHashExist() + approved_signed_hashes[hash] = true; + | Operation.RevokeSignedHash(hash) => + // Sanity check. + require(approved_signed_hashes[hash], Error(0x12345678)); // ApprovedSignedHashDoesNotExist() + approved_signed_hashes[hash] = false; + | _ => unimplemented(); // TODO + } + } + + payable fallback() -> () { + // Accept incoming payments if no selector is hit. + require(calldatasize() == 0, Error(0x12345678)); // UnexpectedEtherTransfer() + + // TODO: emit log + } + + // ERC-1271 receiver + // NOTE: view function. + function isValidSignature(hash: bytes32, signature: memory(bytes)) -> bytes4 { + require(approved_signed_hashes[hash], Error(0x12345678)); // HashNotApproved(); + let signature_ = Typedef.rep(signature); + require(mload(signature_) == 0, Error(0x12345678)); // EmptySignatureExpected() + // TODO: consider pass-through signature checking if the hash is not found (needs passing data and not hash) + return bytes4(0x1626ba7e); + } + + // TODO: these functions should be non-public + + // TODO: this is suboptimal + function isSigner(signer: address) -> bool { + for (let i = uint256(0); i < signers_count; i += uint256(1)) { + if (signers[i] == signer) { + return true; + } + } + return false; + } + + function add_signer(signer: address) -> () { + require(!isSigner(signer), Error(0x12345678)); // SignerAlreadyExists() + signers[signers_count] = signer; + signers_count += uint256(1); + } + + function remove_signer(signer: address) -> () { + require(signers_count > uint256(1), Error(0x12345678)); // CannotRemoveOnlySigner() + for (let i = uint256(0); i < signers_count; i += uint256(1)) { + if (signers[i] == signer) { + // Move last signer into this place. + signers[i] = signers[signers_count - uint256(1)]; + signers_count -= uint256(1); + // Reduce requirement if needed. + if (signers_count < signers_required) { + signers_required = signers_count; + } + return (); + } + } + revertWithError(Error(0x12345678)); // NotASigner() + } + + // --- Test helpers --------------------------------------------------- + // Thin public wrappers used by the integration test suite + // (test/examples/dispatch/multisig.json). They expose the otherwise + // internal add_signer/remove_signer plus a couple of getters so the + // signer bookkeeping can be observed from calldata, without going + // through the full queue/approve/execute governance path. These do NOT + // touch signers_required. + + public function signersCount() -> uint256 { + return signers_count; + } + + public function signersRequired() -> uint256 { + return signers_required; + } + + public function isSignerOf(who: address) -> bool { + return isSigner(who); + } + + public function addSigner(signer: address) -> uint256 { + add_signer(signer); + return signers_count; + } + + public function removeSigner(signer: address) -> uint256 { + remove_signer(signer); + return signers_count; + } +} + +function check_contract_hash(contract__: address, hash: bytes32) -> bool { + let ptr = get_free_memory(); + let contract_ = Typedef.rep(contract__); + let hash_ = Typedef.rep(hash); + let res: word; + let ret: word; + // We assume the [0, 32] scratch space is reserved. + // TODO: add specific error code + assembly { + mstore(ptr, shl(224, 0x12345678)) // IsHashApproved(bytes32) + mstore(add(ptr, 4), hash_) + // Alternative option is ignoring ret, but setting mem[0] to 0. + ret := staticcall(gas(), contract_, ptr, 36, 0, 32) + res := mload(0) + } + return ret == 1 && res == 0x12345678; // Must match the magic. +} + +function eip1271_verify(contract__: address, hash: bytes32, signature: memory(bytes)) -> bool { + let ptr = get_free_memory(); + let contract_ = Typedef.rep(contract__); + let hash_ = Typedef.rep(hash); + let signature_ = Typedef.rep(signature); + let res: word; + let ret: word; + // We assume the [0, 32] scratch space is reserved. + // TODO: add specific error code + assembly { + // TODO: use abi.encode to build this + mstore(ptr, shl(224, 0x1626ba7e)) + mstore(add(ptr, 4), hash_) + mstore(add(ptr, 36), 64) + let size := mload(signature_) + mstore(add(ptr, 68), size) + mcopy(add(ptr, 100), add(signature_, 32), size) + // Alternative option is ignoring ret, but setting mem[0] to 0. + ret := staticcall(gas(), contract_, ptr, add(100, size), 0, 32) + res := mload(0) + } + return ret == 1 && res == 0x1626ba7e; // Must match the magic. +} + +function eip2098_signer(hash: bytes32, r: bytes32, s_: bytes32) -> address { + let s__ = Typedef.rep(s_); + let s: word; + let v: word; + assembly { + s := and(s__, sub(shl(255, 1), 1)) + v := add(shr(255, s__), 27) + } +// let parity = match v { +// | 27 => Even, +// | 28 => Odd, +// } + return ecrecover(hash, uint256(v), r, bytes32(s)); +} + +// Performs a safe transfer of ERC-20 tokens. Makes sure the call succeeded, +// and if the token follows the standard and returns a boolean, that is also true. +function safe_erc20_transfer(token: address, to: address, value: uint256) -> () { + let ptr = get_free_memory(); + let token_ = Typedef.rep(token); + let to_ = Typedef.rep(to); + let value_ = Typedef.rep(value); + assembly { + // Assemble the [selector][address][value] + mstore(ptr, shl(224, 0xa9059cbb)) + mstore(add(ptr, 4), to_) + mstore(add(ptr, 36), value_) + let ret := call(gas(), token_, 0, ptr, 68, 0, 32) + if iszero(ret) { + // Bubble up error. + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + // If the token follows the standard and returns a bool, check it returned true. + // This allows any non-zero value as true, just like OpenZeppelin. + if returndatasize() { + if iszero(mload(0)) { + revert(0, 0) // TODO: use error codes + } + } + } +} + +function arbitrary_call(target: address, value: uint256, payload: memory(bytes)) -> bool { + let target_ = Typedef.rep(target); + let value_ = Typedef.rep(value); + let payload_ = Typedef.rep(payload); + let ret: word; + assembly { + ret := call(gas(), target_, value_, add(payload_, 32), mload(payload_), 0, 0) + } + return tobool(ret); +} diff --git a/test/examples/dispatch/storage_adt_field.json b/test/examples/dispatch/storage_adt_field.json new file mode 100644 index 000000000..fb3a4d7f1 --- /dev/null +++ b/test/examples/dispatch/storage_adt_field.json @@ -0,0 +1,244 @@ +{ + "storage_adt_field": { + "bytecode": "", + "contract": "C", + "tests": [ + { + "input": { + "comment": "constructor() (asserts: Option(uint256)=2, Triple=3, Option(Triple)=4)", + "calldata": "", + "value": "0" + }, + "kind": "constructor" + }, + { + "input": { + "comment": "isSome() -> false (zero-init)", + "calldata": "31f68a08", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + { + "input": { + "comment": "getValue() reverts on None", + "calldata": "20965255", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "failure" + } + }, + { + "input": { + "comment": "setValue(42)", + "calldata": "55241077000000000000000000000000000000000000000000000000000000000000002a", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "success" + } + }, + { + "input": { + "comment": "getValue() -> 42", + "calldata": "20965255", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000002a", + "status": "success" + } + }, + { + "input": { + "comment": "isSome() -> true", + "calldata": "31f68a08", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "setTriple(10,20,30)", + "calldata": "52786e06000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "success" + } + }, + { + "input": { + "comment": "tripleSum() -> 60", + "calldata": "7ba60a20", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000003c", + "status": "success" + } + }, + { + "input": { + "comment": "hasSomeTriple() -> false (zero-init)", + "calldata": "9b5e9a9d", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + { + "input": { + "comment": "someTripleSum() reverts on None", + "calldata": "9650c3bd", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "failure" + } + }, + { + "input": { + "comment": "setSomeTriple(7,8,9)", + "calldata": "421dfc02000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000009", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "success" + } + }, + { + "input": { + "comment": "someTripleSum() -> 24", + "calldata": "9650c3bd", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000018", + "status": "success" + } + }, + { + "input": { + "comment": "hasSomeTriple() -> true", + "calldata": "9b5e9a9d", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000001", + "status": "success" + } + }, + { + "input": { + "comment": "getValue() -> 42 (unaffected by nested writes)", + "calldata": "20965255", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000002a", + "status": "success" + } + }, + { + "input": { + "comment": "tripleSum() -> 60 (unaffected)", + "calldata": "7ba60a20", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000003c", + "status": "success" + } + }, + { + "input": { + "comment": "clearSomeTriple()", + "calldata": "0f0b502e", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "success" + } + }, + { + "input": { + "comment": "hasSomeTriple() -> false after clear", + "calldata": "9b5e9a9d", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "0000000000000000000000000000000000000000000000000000000000000000", + "status": "success" + } + }, + { + "input": { + "comment": "someTripleSum() reverts after clear", + "calldata": "9650c3bd", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "", + "status": "failure" + } + }, + { + "input": { + "comment": "tripleSum() -> 60 (unaffected by clear)", + "calldata": "7ba60a20", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000003c", + "status": "success" + } + }, + { + "input": { + "comment": "getValue() -> 42 (unaffected by clear)", + "calldata": "20965255", + "value": "0" + }, + "kind": "call", + "output": { + "returndata": "000000000000000000000000000000000000000000000000000000000000002a", + "status": "success" + } + } + ] + } +} diff --git a/test/examples/dispatch/storage_adt_field.solc b/test/examples/dispatch/storage_adt_field.solc new file mode 100644 index 000000000..3ef8e8c7f --- /dev/null +++ b/test/examples/dispatch/storage_adt_field.solc @@ -0,0 +1,96 @@ +import std.{*}; +import std.dispatch.{*}; +import std.Generic.{*}; +import std.StorageGeneric.{*}; + +pragma no-patterson-condition; +pragma no-bounded-variable-condition; + +// Algebraic data types used directly as contract storage fields, including a +// nested ADT (Option(Triple)). +// +// - someValue : Option(uint256) (sum, rep sum((), uint256) -> 2 slots) +// - triple : Triple (product, rep (uint256,(uint256,uint256)) -> 3 slots) +// - someTriple : Option(Triple) (sum of product, rep sum((), Triple) -> 4 slots) +// +// Field reads/writes go through the contract field-access machinery, which +// resolves storage(T):CanStore(T) via the per-type instances DeriveGeneric emits +// (Generic + StorageSize + StorageType + CanStore) once std.StorageGeneric is +// imported. The nested case exercises Triple:StorageType being reused inside +// Option(Triple):StorageType. + +data Option(a) = None | Some(a); +data Triple = Triple(uint256, uint256, uint256); + +contract C { + someValue : Option(uint256); + triple : Triple; + someTriple : Option(Triple); + + constructor() { + // sum: 1 tag + max(size (), size uint256) = 1 + 1 = 2 + assert(eqWord(StorageSize.size(Proxy : Proxy(Option(uint256))), 2)); + // product: size uint256 * 3 = 3 + assert(eqWord(StorageSize.size(Proxy : Proxy(Triple)), 3)); + // sum of product: 1 tag + max(size (), size Triple) = 1 + 3 = 4 + assert(eqWord(StorageSize.size(Proxy : Proxy(Option(Triple))), 4)); + } + + public function setValue(v : uint256) -> () { + someValue = Option.Some(v); + } + + public function clearValue() -> () { + someValue = Option.None; + } + + public function getValue() -> uint256 { + match someValue { + | Option.None => revertEmpty(); return uint256(0); + | Option.Some(v) => return v; + } + } + + public function isSome() -> bool { + match someValue { + | Option.None => return false; + | Option.Some(_) => return true; + } + } + + public function setTriple(a : uint256, b : uint256, c : uint256) -> () { + triple = Triple(a, b, c); + } + + public function tripleSum() -> uint256 { + match triple { + | Triple(a, b, c) => return a + b + c; + } + } + + // Nested ADT: Option(Triple). + public function setSomeTriple(a : uint256, b : uint256, c : uint256) -> () { + someTriple = Option.Some(Triple(a, b, c)); + } + + public function clearSomeTriple() -> () { + someTriple = Option.None; + } + + public function someTripleSum() -> uint256 { + match someTriple { + | Option.None => revertEmpty(); return uint256(0); + | Option.Some(t) => + match t { + | Triple(a, b, c) => return a + b + c; + } + } + } + + public function hasSomeTriple() -> bool { + match someTriple { + | Option.None => return false; + | Option.Some(_) => return true; + } + } +} diff --git a/test/examples/dispatch/storage_dynamic_field.solc b/test/examples/dispatch/storage_dynamic_field.solc new file mode 100644 index 000000000..d434ebe85 --- /dev/null +++ b/test/examples/dispatch/storage_dynamic_field.solc @@ -0,0 +1,47 @@ +import std.{*}; +import std.dispatch.{*}; +import std.Generic.{*}; +import std.StorageGeneric.{*}; + +// An ADT carrying a dynamically-sized memory(bytes) field IS storable. The +// derived storage(Blob):CanStore(Blob) decomposes the SOP representation through +// the structural CanStore instances in std.StorageGeneric, and the memory(bytes) +// leaf is stored via storage(bytes) (Solidity-style dynamic bytes, one slot) — +// never through StorageType, which has no instance for memory(bytes). +// +// This mirrors the multisig Operation type, whose Call variant carries a +// memory(bytes) payload. No `pragma no-patterson-condition` is needed: the +// structural CanStore instances satisfy the Patterson / coverage measures on +// their own, and StorageSize already covers memory(bytes). + +data Blob = + NoBlob + | SomeBytes(memory(bytes)); + +contract C { + blob : Blob; + + constructor() { + blob = Blob.NoBlob; + // A dynamic field occupies one slot, so the sum is 1 (tag) + max(0, 1). + assert(eqWord(StorageSize.size(Proxy : Proxy(Blob)), 2)); + } + + public function clear() -> () { + blob = Blob.NoBlob; + } + + // Stores the memory(bytes) payload into the ADT field (round-trips the + // dynamic leaf through storage(bytes)). + public function setBytes(b : memory(bytes)) -> () { + blob = Blob.SomeBytes(b); + } + + // Loads the whole ADT back from storage and inspects its tag. + public function isEmpty() -> bool { + match blob { + | Blob.NoBlob => return true; + | Blob.SomeBytes(_) => return false; + } + } +}