Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
49baa0e
Add storage support for ADTs via Generic (std.StorageGeneric)
claude Jun 24, 2026
fb88300
Test an ADT used directly as a contract storage field
claude Jun 24, 2026
93d285c
Make ADT storage-field test a runnable dispatch test; add Triple + si…
claude Jun 24, 2026
ce7ed70
Fix phantom type variable in StorageGeneric CanStore default
claude Jun 24, 2026
6655ac1
Add functional per-type CanStore instances for ADT storage fields
claude Jun 24, 2026
aa43025
Auto-derive StorageSize/CanStore for ADTs in DeriveGeneric
claude Jun 24, 2026
323ee7e
Simplify
axic Jun 24, 2026
66dd311
f
axic Jun 24, 2026
4a716b4
Derive concrete per-type StorageType instead of a default bridge
claude Jun 24, 2026
193a5d1
Test nested ADT in storage: someTriple : Option(Triple)
claude Jun 24, 2026
d6db35f
Add initial layout
axic Jun 3, 2026
40612d5
Add more complex structure
axic Jun 3, 2026
3186b7f
Signatures + batching
axic Jun 3, 2026
7a5eba4
Add approval tracking
axic Jun 3, 2026
d32c148
Parity
axic Jun 3, 2026
a016e62
implement ChangeSigRequired
axic Jun 3, 2026
1d961bb
Implement TransferEth
axic Jun 3, 2026
4ca9781
Implement UnstoredCall
axic Jun 3, 2026
0b6160a
Add sanity check for ChangeSigRequired
axic Jun 3, 2026
449cf4e
Refactor checkSignature
axic Jun 3, 2026
615fdd1
Implement batch
axic Jun 3, 2026
3ffa04c
Add queueWithSignature
axic Jun 3, 2026
6c7dc27
Refactor internals
axic Jun 3, 2026
a6efbbf
Pass correct signer to perform_approve
axic Jun 3, 2026
6469483
Add sanity check to AddSigner
axic Jun 3, 2026
87c43c9
Remove useless nonce
axic Jun 3, 2026
fefbd9f
Rename approvals to vote and store rejections
axic Jun 3, 2026
68e7dda
Add ecdsa malleability todo
axic Jun 3, 2026
a0b216c
Add a special version of nonce
axic Jun 3, 2026
84c2e42
Use match
axic Jun 3, 2026
88f1657
Encode UnstoredCall properly
axic Jun 3, 2026
d997aeb
Add safe_erc20_transfer helper
axic Jun 3, 2026
cbd950c
Implement TransferToken
axic Jun 3, 2026
978991f
Implement Call
axic Jun 3, 2026
29d5737
Add malleability check
axic Jun 3, 2026
f71072a
Add create_signature_hash helper
axic Jun 3, 2026
175ce72
Add dummy isValidSignature
axic Jun 3, 2026
897358a
Implement ApproveSignedHash
axic Jun 3, 2026
4f810db
Add RevokeSignedHash
axic Jun 3, 2026
594fa73
Document execute/reject logic
axic Jun 3, 2026
69c3ac3
Reject incoming transfer in non-receive case
axic Jun 3, 2026
65e5f82
Change Pending/Approved into Approvals
axic Jun 3, 2026
b083b40
Compilation fixes
axic Jun 19, 2026
2d7c7f9
Compilation fixes
axic Jun 19, 2026
4dbe274
Typesystem bug
axic Jun 19, 2026
6ee3d34
Add public/private keyword
axic Jun 19, 2026
f586496
Import caller from std.opcodes
axic Jun 19, 2026
cb07fc0
Remove ecrecover (its in std now)
axic Jun 19, 2026
2da7e30
f
axic Jun 24, 2026
f8d8a74
DeriveGeneric: skip storage derivation for non-storable types
claude Jun 24, 2026
0e3ce57
DeriveGeneric: gate storage instances on StorageType/CanStore, not a …
claude Jun 24, 2026
e43d904
Store ADTs through CanStore so dynamic fields (memory(bytes)) work
claude Jun 24, 2026
3e2f92e
Route mapping value reads through CanStore (matching writes)
claude Jun 24, 2026
0ad8d60
Fix caller
axic Jun 24, 2026
0dc840a
Explicit matches
axic Jun 24, 2026
5fd81bb
Add Vote:Eq
axic Jun 24, 2026
fb2794a
DeriveGeneric: derive per-type ABIDecode for ADTs
claude Jun 24, 2026
0093579
SigString: bridge ADTs via Generic + add sum instance
claude Jun 24, 2026
0763ab2
Use uint256() types for literals
axic Jun 24, 2026
9ab6feb
Use uint256() types for literals
axic Jun 24, 2026
6b465d8
Exhaustive match
axic Jun 24, 2026
74b6b6c
Rename to .solc
axic Jun 24, 2026
8ab7e30
Add multisig integration test suite (add_signer/remove_signer)
axic Jun 24, 2026
7d6625e
std: fix sum CanStore.load emitting inl/inr at wrong sum nesting
claude Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions run_contests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
263 changes: 255 additions & 8 deletions src/Solcore/Desugarer/DeriveGeneric.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 <ctx> => T(params) : StorageSize {
-- function size(x : Proxy(T(params))) -> word {
-- return StorageSize.size(Proxy : Proxy(<rep>));
-- }
-- }
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 <ctx> => storage(T(params)) : CanStore(T(params)) {
-- function store(r : storage(T(params)), v : T(params)) -> () {
-- CanStore.store(storage(Typedef.rep(r)) : storage(<rep>), Generic.from(v));
-- }
-- function load(r : storage(T(params))) -> T(params) {
-- let x : <rep> = CanStore.load(storage(Typedef.rep(r)) : storage(<rep>));
-- 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(<rep>)
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 <ctx> => 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(<rep>, 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")]]
)
]
)
]
]
10 changes: 10 additions & 0 deletions std/ABIGeneric.solc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma no-bounded-variable-condition ABIAttribs, ABIEncode, ABIDecode;
pragma no-coverage-condition ABIDecode;

export {
ABIDeriving,
encode,
decode
};
Expand All @@ -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;
Expand Down
Loading
Loading