Skip to content

Commit b2c9d2e

Browse files
committed
std: add instances for contract dispatch desugaring
This adds the core datatype definitions and class / instance definitions that allow us to desugar a surface level `contract` definition into a single entrypoint that dispatches to the contracts methods based on it's function selector hash. An example desugaring can be seen at the end of the file. Still to do: - receive() - move callvalue check to the top level if all methods are not payable - `public` / `external`
1 parent 01caec8 commit b2c9d2e

File tree

3 files changed

+318
-69
lines changed

3 files changed

+318
-69
lines changed

std/dispatch.solc

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import std;
2+
3+
pragma no-patterson-condition RunDispatch, ExecMethod;
4+
5+
// --- Preliminaries ---
6+
7+
data Bool = True | False;
8+
data Proxy(a) = Proxy;
9+
10+
// --- Core Data Types ---
11+
12+
// A contract contains a tuple of methods and a single fallback
13+
// TODO: implement receive()
14+
data Contract(methods, fallback) = Contract(methods,fallback);
15+
16+
// A method contains an implementation (fn) as well as it's name and type signature
17+
data Method(name, payability, args, rets, fn) = Method(name, payability, args, rets, fn);
18+
19+
// Contains the implementation for the fallback (fn) as well as it's type signature
20+
data Fallback(payability, args, rets, fn) = Fallback(payability, args, rets, fn);
21+
22+
// --- Method Selectors ---
23+
24+
// For each method in a contract the compiler generates a unique type and
25+
// produces a `Selector` instance for that type that returns the selector hash
26+
forall nm . class nm:Selector {
27+
function hash(prx: Proxy(nm)) -> word;
28+
}
29+
30+
// Method has a Selector if its name has a Selector
31+
forall name payability args rets fn . name:Selector => instance Method(name,payability,args,rets,fn):Selector {
32+
function hash(prx: Proxy(Method(name,payability,args,rets,fn))) -> word {
33+
return Selector.hash(Proxy : Proxy(name));
34+
}
35+
}
36+
37+
// --- Method Execution ---
38+
39+
// Describes how to execute a given method / fallback
40+
forall ty . class ty:ExecMethod {
41+
function exec(x: ty);
42+
}
43+
44+
// If fn matches the provided args/ret types, then we can execute any method
45+
forall name payability args rets fn
46+
. fn:invokable(args,rets)
47+
, args:ABIAttribs
48+
, rets:ABIAttribs
49+
, ABIDecoder(args,CalldataWordReader):ABIDecode(args)
50+
, rets:ABIEncode
51+
, Method(name,payability,Proxy(args),Proxy(rets),fn):MethodLevelCallvalueCheck
52+
=> instance Method(name,payability,Proxy(args),Proxy(rets),fn):ExecMethod {
53+
function exec(m : Method(name,payability,Proxy(args),Proxy(rets),fn)) -> () {
54+
match m {
55+
| Method(nm,payability,pargs,prets,fn) =>
56+
// check callvalue
57+
MethodLevelCallvalueCheck.checkCallvalue(Proxy : Proxy(Method(name,payability,Proxy(args),Proxy(rets),fn)));
58+
59+
// check we have enough calldata for the head of args
60+
let hdsz = ABIAttribs.headSize(pargs);
61+
assembly {
62+
if lt(sub(calldatasize(), 4), hdsz) {
63+
revert(0,0);
64+
};
65+
}
66+
67+
// TODO: calldatasize checks for dynamic types
68+
69+
// abi decode args from calldata
70+
let ptr : calldata(bytes) = calldata(4);
71+
72+
// TODO: this needs entirely too many type annotations
73+
let args : args = abi_decode(ptr, pargs, Proxy : Proxy(CalldataWordReader));
74+
75+
// call fn with args
76+
// TODO: why are type annotations needed here?
77+
let rets : rets = fn(args);
78+
79+
// abi encode rets to memory
80+
let ptr = abi_encode(rets);
81+
82+
// TODO: this is broken for dynamically sized types...
83+
let retSz : word = ABIAttribs.headSize(prets);
84+
let start : word = Typedef.rep(ptr);
85+
86+
assembly {
87+
return(start,retSz)
88+
}
89+
}
90+
}
91+
}
92+
93+
// If fn matches the provided args/ret types, then we can execute any fallback
94+
forall payability args rets fn
95+
. fn:invokable(args,rets)
96+
, args:ABIAttribs
97+
, rets:ABIAttribs
98+
, Fallback(payability,Proxy(args),Proxy(rets),fn):MethodLevelCallvalueCheck
99+
=> instance Fallback(payability,Proxy(args),Proxy(rets),fn):ExecMethod {
100+
function exec(fb : Fallback(payability, Proxy(args),Proxy(rets),fn)) -> () {
101+
match fb {
102+
| Fallback(payability, args, rets, fn) =>
103+
// check callvalue
104+
MethodLevelCallvalueCheck.checkCallvalue(Proxy : Proxy(Fallback(payability, Proxy(args),Proxy(rets),fn)));
105+
106+
// check we have enough calldata for the head of args
107+
// abi decode args from calldata
108+
// call fn with args
109+
// abi encode rets to memory
110+
// returndata copy encoded returns
111+
// evm return
112+
return ();
113+
}
114+
}
115+
}
116+
117+
// --- Method Dispatch ---
118+
119+
// For a given tuple of methods this executes the method specified by the first four bytes of calldata
120+
forall ty callvalueCheckStatus . class ty:RunDispatch {
121+
function go(methods : ty) -> ();
122+
}
123+
124+
// We can dispatch to a single executable method with a known selector
125+
// TODO: do we need this instance?
126+
forall m . m:ExecMethod, m:Selector => instance m:RunDispatch {
127+
function go(method : m) -> () {
128+
match selector_matches(Proxy : Proxy(m)) {
129+
| True => ExecMethod.exec(method);
130+
| False => return ();
131+
}
132+
}
133+
}
134+
135+
// We can dispatch to a tuple of executable methods with a known selector
136+
forall n m . n:ExecMethod, n:Selector, m:ExecMethod, m:Selector => instance (n,m):RunDispatch {
137+
function go(methods : (n,m)) -> () {
138+
match methods {
139+
| (method_n, method_m) =>
140+
match selector_matches(Proxy : Proxy(n)) {
141+
| True => ExecMethod.exec(method_n);
142+
| False => match selector_matches(Proxy : Proxy(m)) {
143+
| True => ExecMethod.exec(method_m);
144+
| False => return ();
145+
}
146+
}
147+
}
148+
}
149+
}
150+
151+
// Recursive instance
152+
forall n m . n:ExecMethod, n:Selector, m:RunDispatch => instance (n,m):RunDispatch {
153+
function go(methods : (n,m)) -> () {
154+
match methods {
155+
| (method_n, rest) =>
156+
match selector_matches(Proxy : Proxy(n)) {
157+
| True => ExecMethod.exec(method_n);
158+
| False => RunDispatch.go(rest);
159+
}
160+
}
161+
}
162+
}
163+
164+
// TODO: we only wanna do the calldataload once
165+
// Given evidence of a name with a known selector, we can check if it matches the selector in the first four bytes of calldata
166+
forall name . name:Selector => function selector_matches(prx : Proxy(name)) -> Bool {
167+
let hash = Selector.hash(prx);
168+
let res : word;
169+
assembly {
170+
let sel := shr(224, calldataload(0));
171+
res := eq(sel, hash);
172+
}
173+
match res {
174+
| 0 => return False;
175+
| _ => return True;
176+
}
177+
}
178+
179+
// --- Callvalue Checks ---
180+
181+
data Payable;
182+
data NonPayable;
183+
184+
forall ty . class ty:MethodLevelCallvalueCheck {
185+
function checkCallvalue(pty : Proxy(ty));
186+
}
187+
188+
// no callvalue check for Payable methods
189+
forall name args ret fn . instance Method(name,Proxy(Payable),args,ret,fn):MethodLevelCallvalueCheck {
190+
function checkCallvalue(prx : Proxy(Method(name,Proxy(Payable),args,ret,fn))) -> () { }
191+
}
192+
forall args ret fn . instance Fallback(Proxy(Payable),args,ret,fn):MethodLevelCallvalueCheck {
193+
function checkCallvalue(prx : Proxy(Fallback(Proxy(Payable),args,ret,fn))) -> () { }
194+
}
195+
196+
// NonPayable methods revert if passed value
197+
forall name args ret fn . instance Method(name,Proxy(NonPayable),args,ret,fn):MethodLevelCallvalueCheck {
198+
function checkCallvalue(prx : Proxy(Method(name,Proxy(NonPayable),args,ret,fn))) -> () {
199+
ensureNoCallvalue();
200+
}
201+
}
202+
203+
forall name args ret fn . instance Fallback(Proxy(NonPayable),args,ret,fn):MethodLevelCallvalueCheck {
204+
function checkCallvalue(prx : Proxy(Fallback(Proxy(NonPayable),args,ret,fn))) -> () {
205+
ensureNoCallvalue();
206+
}
207+
}
208+
209+
function ensureNoCallvalue() -> () {
210+
assembly {
211+
if gt(callvalue(), 0) {
212+
mstore(0, 0x1);
213+
revert(0, 32);
214+
}
215+
}
216+
}
217+
218+
// --- Contract Execution ---
219+
220+
// Describes how to execute a given contract
221+
forall c . class c:RunContract {
222+
function exec(v : c) -> ();
223+
}
224+
225+
// If we have a dispatch for the contracts methods, and we know how to execute it's fallback, then we can define an entrypoint
226+
forall methods fallback . methods:RunDispatch, fallback:ExecMethod => instance Contract(methods, fallback):RunContract {
227+
function exec(c : Contract(methods, fallback)) -> () {
228+
match c {
229+
| Contract(ms, fb) =>
230+
231+
// TODO: if all methods are non payable then we should life the callvalue check here
232+
233+
// check that we have at least 4 bytes of calldata
234+
let haveSelector : word;
235+
assembly {
236+
haveSelector := lt(3, calldatasize());
237+
}
238+
239+
match haveSelector {
240+
| 0 => assembly {
241+
mstore(0,0xff)
242+
revert(0,1)
243+
}
244+
| _ =>
245+
// set free memory pointer to the output of memoryguard
246+
// https://docs.soliditylang.org/en/v0.8.30/yul.html#memoryguard
247+
// TODO: we will need to consider immutables here at some point...
248+
assembly { mstore(0x40, memoryguard(128)); }
249+
250+
// dispatch to method based on selector
251+
RunDispatch.go(ms);
252+
// run fallback if no methods matched
253+
ExecMethod.exec(fb);
254+
}
255+
}
256+
}
257+
}
258+
259+
// --- Manually Desugared Example ---
260+
261+
// compiler generated
262+
263+
function revert_handler() -> () {
264+
assembly { revert(0,0) }
265+
}
266+
267+
data C_Add2_Selector = C_Add2_Selector;
268+
269+
instance C_Add2_Selector:Selector {
270+
function hash(prx: Proxy(C_Add2_Selector)) -> word {
271+
// This would be keccak256("add2(uint256,uint256)") >> 224
272+
// Compiler computes this at compile time
273+
return 0x29fcda33; // placeholder value
274+
}
275+
}
276+
277+
// transform
278+
279+
contract C {
280+
function add2(x : uint256, y : uint256) -> uint256 {
281+
return Add.add(x,y);
282+
}
283+
284+
function main() -> word {
285+
let c = Contract(
286+
Method(C_Add2_Selector, Proxy : Proxy(NonPayable), Proxy : Proxy((uint256,uint256)), Proxy : Proxy(uint256), add2),
287+
Fallback(Proxy : Proxy(NonPayable), Proxy : Proxy(()), Proxy : Proxy(()),revert_handler)
288+
);
289+
290+
RunContract.exec(c);
291+
return 0;
292+
}
293+
}

0 commit comments

Comments
 (0)