From 6f89a7a8d7f4809b620d72bcc6c1f0e8f8836252 Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Wed, 4 Nov 2026 20:09:12 -0500 Subject: [PATCH 1/8] Version contract and address variables so we can mutate them while using contracts in labels. --- src/TypeChecker.java | 4 +- src/ast/AnnAssign.java | 39 +----- src/ast/Assert.java | 4 +- src/ast/Assign.java | 41 +++++++ src/ast/Atomic.java | 9 ++ src/ast/Call.java | 51 +++++--- src/ast/ClosureCreation.java | 26 +--- src/ast/Contract.java | 37 ++++++ src/ast/EndorseIfStatement.java | 17 ++- src/ast/Expression.java | 9 ++ src/ast/FlowsToExp.java | 4 +- src/ast/For.java | 6 + src/ast/FunctionDef.java | 12 +- src/ast/FunctionSig.java | 20 ++- src/ast/GuardBlock.java | 2 +- src/ast/If.java | 8 +- src/ast/Name.java | 8 ++ src/ast/New.java | 23 ++-- src/ast/StateVariableDeclaration.java | 60 ++++----- src/ast/Try.java | 9 ++ src/ast/While.java | 5 + src/typecheck/ComplexLabel.java | 7 ++ src/typecheck/Label.java | 3 + src/typecheck/PrimitiveLabel.java | 28 ++++- src/typecheck/Utils.java | 21 +++- src/typecheck/VarSym.java | 16 ++- src/typecheck/VisitEnv.java | 116 +++++++++++++++++- .../ifcTypechecking/FinalFieldInit.scif | 13 ++ .../NonFinalPrincipalLabel_W01.scif | 10 ++ .../ifcTypechecking/TrustFactBranch_W01.scif | 16 +++ .../TrustFactFieldCall_W01.scif | 17 +++ .../ifcTypechecking/TrustFactKept.scif | 21 ++++ .../ifcTypechecking/TrustFactLoop_W01.scif | 17 +++ .../TrustFactReassign_W01.scif | 14 +++ .../ifcTypechecking/ValueNowTrusted.scif | 12 ++ .../FieldLabelNonFinal_W01.scif | 6 + .../FinalFieldAssignInMethod_W01.scif | 6 + .../FinalFieldInitNested_W01.scif | 11 ++ .../regularTypechecking/FinalFieldInitOK.scif | 6 + .../FinalFieldInitTwice_W01.scif | 5 + .../FinalFieldUninitialized_W01.scif | 5 + .../FinalLocalReassign_W01.scif | 8 ++ .../FinalParamReassign_W01.scif | 7 ++ ...Final_W01.scif => TrustQueryNonFinal.scif} | 2 +- test/src/TestIfcTypechecking.java | 8 ++ test/src/TestRegularTypechecking.java | 10 +- 46 files changed, 615 insertions(+), 164 deletions(-) create mode 100644 test/contracts/ifcTypechecking/FinalFieldInit.scif create mode 100644 test/contracts/ifcTypechecking/NonFinalPrincipalLabel_W01.scif create mode 100644 test/contracts/ifcTypechecking/TrustFactBranch_W01.scif create mode 100644 test/contracts/ifcTypechecking/TrustFactFieldCall_W01.scif create mode 100644 test/contracts/ifcTypechecking/TrustFactKept.scif create mode 100644 test/contracts/ifcTypechecking/TrustFactLoop_W01.scif create mode 100644 test/contracts/ifcTypechecking/TrustFactReassign_W01.scif create mode 100644 test/contracts/ifcTypechecking/ValueNowTrusted.scif create mode 100644 test/contracts/regularTypechecking/FieldLabelNonFinal_W01.scif create mode 100644 test/contracts/regularTypechecking/FinalFieldAssignInMethod_W01.scif create mode 100644 test/contracts/regularTypechecking/FinalFieldInitNested_W01.scif create mode 100644 test/contracts/regularTypechecking/FinalFieldInitOK.scif create mode 100644 test/contracts/regularTypechecking/FinalFieldInitTwice_W01.scif create mode 100644 test/contracts/regularTypechecking/FinalFieldUninitialized_W01.scif create mode 100644 test/contracts/regularTypechecking/FinalLocalReassign_W01.scif create mode 100644 test/contracts/regularTypechecking/FinalParamReassign_W01.scif rename test/contracts/regularTypechecking/{TrustQueryNonFinal_W01.scif => TrustQueryNonFinal.scif} (80%) diff --git a/src/TypeChecker.java b/src/TypeChecker.java index 6c5e8856..0a266045 100644 --- a/src/TypeChecker.java +++ b/src/TypeChecker.java @@ -320,8 +320,10 @@ private static boolean ifcTypecheck(ContractFile contractFile, VisitEnv env, for (Constraint c : trustCons) System.err.println("DBGTRUST[" + methodName + "] " + c.toSherrlocFmt(true)); for (Sym s : env.principalSet()) System.err.println("DBGCTOR[" + methodName + "] " + s.toSHErrLocFmt()); for (String a : env.rigidAtoms()) System.err.println("DBGATOM[" + methodName + "] " + a); + for (String v : env.versionNames()) System.err.println("DBGVER[" + methodName + "] " + v); } - SherrlocDiagnoser sherrlocDiagnoser = Utils.createDiagnoser(env.principalSet(), trustCons, cons, + SherrlocDiagnoser sherrlocDiagnoser = Utils.createDiagnoser(env.principalSet(), + env.versionNames(), trustCons, cons, true, contractSym, env.rigidAtoms(), env.atomAssumptions()); boolean result; try { diff --git a/src/ast/AnnAssign.java b/src/ast/AnnAssign.java index 5dcb2f52..4a00db72 100644 --- a/src/ast/AnnAssign.java +++ b/src/ast/AnnAssign.java @@ -151,41 +151,12 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic SLCNameVarLbl = varSym.labelNameSLC(); // // logger.debug(varSym.typeSym.getName()); - // If the declared variable is a principal (final address/contract), add it to the principal list - if ((varSym.isFinal && - (varSym.typeSym instanceof ContractSym || varSym.typeSym.getName().equals(Utils.ADDRESS_TYPE))) - // || varSym.typeSym.name().equals(Utils.PRINCIPAL_TYPE)) Invalid to declare a non-global principal - ) { + // A principal-typed variable starts out naming the principal it is initialized to. + if (varSym.isPrincipalTyped()) { env.addPrincipal(varSym); - - VarSym equalPrincipal = null; - boolean correctInit = true; - if (value instanceof Name) { // assigned as another variable - // Nothing to check - equalPrincipal = env.getVar(((Name) value).id); - } else if (value instanceof Call call && call.isCast(env)) { // assigned as another contract - // check if it is a cast to a final address variable - Expression arg = call.getArgAt(0); - if (arg instanceof Name) { - VarSym sym = env.getVar(((Name) arg).id); - if (!sym.isFinal) { - correctInit = false; - } else { - equalPrincipal = sym; - } - } else { - correctInit = false; - } - } else if (!isBuiltIn) { - correctInit = false; - } - - if (correctInit) { - // add equivalence assumption to the trust set - env.checkTrustEqual(SLCNameVar, equalPrincipal.toSHErrLocFmt(), location, - "New principal declaration"); - } else { - throw new SemanticException("A final address/Contract must be initialized to another final address/Contract: " + id, location); + String init = value == null ? null : value.principalName(env); + if (init != null) { + env.assumeSamePrincipal(SLCNameVar, init); } } diff --git a/src/ast/Assert.java b/src/ast/Assert.java index 1e430903..5287302b 100644 --- a/src/ast/Assert.java +++ b/src/ast/Assert.java @@ -35,8 +35,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic VarSym l = env.getVar(left.id), r = env.getVar(right.id); // // logger.debug(l.toString()); // // logger.debug(r.toString()); - Inequality hypo = new Inequality(l.toSHErrLocFmt(), Relation.LEQ, - r.toSHErrLocFmt()); + Inequality hypo = new Inequality(env.principalName(l), Relation.LEQ, + env.principalName(r)); env.hypothesis().add(hypo); } diff --git a/src/ast/Assign.java b/src/ast/Assign.java index b17d9c0d..c8458f42 100644 --- a/src/ast/Assign.java +++ b/src/ast/Assign.java @@ -16,15 +16,46 @@ public class Assign extends Statement { Expression target; Expression value; + /** Whether this assignment is a constructor's initialization of a final field. */ + private boolean initializesFinalField = false; public Assign(Expression target, Expression value) { this.target = target; this.value = value; } + /** The names of the variables assigned by assignments within {@code nodes}. */ + static java.util.Set assignedNames(List nodes) { + java.util.Set names = new java.util.HashSet<>(); + for (Node node : nodes) { + collectAssignedNames(node, names); + } + return names; + } + + private static void collectAssignedNames(Node node, java.util.Set names) { + if (node instanceof Assign a && a.target instanceof Name n) { + names.add(n.id); + } + for (Node child : node.children()) { + if (child != null) { + collectAssignedNames(child, names); + } + } + } + + /** Marks this assignment as a constructor's initialization of a final field. */ + void markFinalFieldInit() { + initializesFinalField = true; + } + @Override public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { ScopeContext now = new ScopeContext(this, parent); + if (target instanceof Name n && env.getCurSym(n.id) instanceof VarSym v && v.isFinal + && !(initializesFinalField && v.isGlobal())) { + throw new SemanticException("cannot assign to final variable " + n.id, location); + } if (target instanceof Attribute att && att.value.getVarInfo(env) != null && att.value.getVarInfo(env).typeSym instanceof typecheck.ClosureTypeSym && att.attr.id.equals(typecheck.Utils.CLOSURE_TARGET_MEMBER)) { @@ -83,6 +114,16 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic value.location, "Closure value flowing into the assignment target must subtype its slot"); } + // The target names the assigned principal from here on. + if (target instanceof Name tgtName && env.getVar(tgtName.id) instanceof VarSym tgtSym + && tgtSym.isPrincipalTyped()) { + String source = value.principalName(env); + String dest = tgtSym.isMutablePrincipal() ? env.newVersion(tgtSym) : tgtSym.toSHErrLocFmt(); + if (source != null) { + env.assumeSamePrincipal(dest, source); + } + } + typecheck.Utils.contextFlow(env, vo.psi.getNormalPath().c, endContext, value.location); // env.outContext = endContext; diff --git a/src/ast/Atomic.java b/src/ast/Atomic.java index 3bc9db7f..7e0d92e0 100644 --- a/src/ast/Atomic.java +++ b/src/ast/Atomic.java @@ -247,6 +247,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic //env.psi = newPsi; PathOutcome psi = new PathOutcome(); PathOutcome input = new PathOutcome(); + Map versionsBefore = env.saveVersions(); env.incScopeLayer(); PathOutcome so = new PathOutcome(new PsiUnit(beginContext)); Utils.genConsStmtsWithException(body, env, so, psi, tail_position); @@ -269,7 +270,13 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic //Context cTry = env.outContext; //env.psi = oldPsi; + // A handler may be entered from any point in the body. + List> exitVersions = new ArrayList<>(); + exitVersions.add(env.saveVersions()); + java.util.Set assignedInBody = ast.Assign.assignedNames(body); for (ExceptHandler h : handlers) { + env.restoreVersions(versionsBefore); + env.havoc(assignedInBody); env.incScopeLayer(); //env.inContext = new Context(h.getHandlerPcLabelName(), beginContext.lambda); ExceptionTypeSym expSym = env.getExp(h.name()); @@ -278,10 +285,12 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.inContext = new Context(expUnit.c.pc, beginContext.lambda); PathOutcome ho = h.genConsVisit(env, tail_position); psi.join(ho); + exitVersions.add(env.saveVersions()); } env.decScopeLayer(); // cTry = new Context(Utils.makeJoin(cTry.pc, env.outContext.outPcName), Utils.makeJoin(cTry.lambda, env.outContext.lockName)); } + env.mergeVersions(exitVersions); Utils.contextFlow(env, psi.getNormalPath().c, endContext, location); diff --git a/src/ast/Call.java b/src/ast/Call.java index bcc8ea1a..67821a67 100644 --- a/src/ast/Call.java +++ b/src/ast/Call.java @@ -278,6 +278,18 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S @Override public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + ExpOutcome result = callIFConstraints(env, tail_position); + // A call may assign to any field, so fields hold new principals afterwards. + boolean cast = value instanceof Name n && (env.getCurSym(n.id) instanceof InterfaceSym + || env.getCurSym(n.id) instanceof BuiltinTypeSym); + if (!cast) { + env.havocFields(); + } + return result; + } + + private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) + throws SemanticException { //TODO: Assuming value is a Name for now Context beginContext = env.inContext; // (pc, lambda) Context endContext = new Context(Utils.getLabelNamePc(toSHErrLocFmt()), @@ -285,6 +297,8 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) Map dependentLabelMapping = new HashMap<>(); List argValueLabelNames = new ArrayList<>(); + // The principal each argument holds when it is evaluated, or null. + List argPrincipalNames = new ArrayList<>(); PathOutcome psi = new PathOutcome(new PsiUnit(endContext)); @@ -294,6 +308,7 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) ao = arg.genIFConstraints(env, false); psi.joinExe(ao.psi); argValueLabelNames.add(ao.valueLabelName); + argPrincipalNames.add(arg.principalName(env)); env.inContext = Utils.genNewContextAndConstraints(env, false, ao.psi.getNormalPath().c, beginContext.lambda, arg.nextPcSHL(), arg.location); @@ -415,17 +430,12 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) if (binder == null) { continue; } - Expression principalExp = args.get(i); - if (principalExp instanceof Call cast && cast.isCast(env)) { - principalExp = cast.getArgAt(0); - } - VarSym actual = principalExp instanceof Name an ? env.getVar(an.id) : null; - if (actual != null && actual.isPrincipalVar()) { - dependentLabelMapping.put(binder.toSHErrLocFmt(), - actual.toSHErrLocFmt()); + String actual = argPrincipalNames.get(i); + if (actual != null) { + dependentLabelMapping.put(binder.toSHErrLocFmt(), actual); } else if (cs.referencesBinder(binder)) { throw new SemanticException( - "must use a final address/contract for the " + "must use an address or contract variable for the " + Utils.ordNumString(i + 1) + " invoke argument: the closure's labels depend on it", args.get(i).location); @@ -620,16 +630,8 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) for (int i = 0; i < args.size(); ++i) { Expression arg = args.get(i); VarSym argSym = funcSym.parameters.get(i); - if (argSym.isPrincipalVar()) { - - if (arg instanceof Name) { - VarSym valueSym = (VarSym) env.getVar(((Name) arg).id); - if (valueSym.isPrincipalVar()) { -// System.err.println("dependent " + argSym.typeSym + " -> " + valueSym.toSHErrLocFmt()); - dependentLabelMapping.put(argSym.toSHErrLocFmt(), valueSym.toSHErrLocFmt()); - } - } - // TODO: Do we throw exception if arg is not a Name? + if (argSym.isPrincipalTyped() && argPrincipalNames.get(i) != null) { + dependentLabelMapping.put(argSym.toSHErrLocFmt(), argPrincipalNames.get(i)); } // env.prevContext = prevContext = tmp; @@ -980,6 +982,17 @@ public boolean typeMatch(Expression expression) { return true; } + @Override + public String principalName(VisitEnv env) { + if (args.size() == 1 && isCast(env)) { + String type = ((Name) value).id; + if (env.containsContract(type) || type.equals(Utils.ADDRESS_TYPE)) { + return args.get(0).principalName(env); + } + } + return null; + } + /** * Check if a type cast */ diff --git a/src/ast/ClosureCreation.java b/src/ast/ClosureCreation.java index 30c61299..f5b9b98e 100644 --- a/src/ast/ClosureCreation.java +++ b/src/ast/ClosureCreation.java @@ -222,16 +222,10 @@ private ExpOutcome bindIFConstraints(VisitEnv env, PathOutcome psi, // A slot filled with a principal becomes that principal // everywhere the result's labels named the slot. - Expression principalExp = a; - if (a instanceof Call cast && cast.isCast(env)) { - principalExp = cast.getArgAt(0); - } VarSym binder = i < binders.size() ? binders.get(i) : null; - if (binder != null && principalExp instanceof Name an) { - VarSym valueSym = env.getVar(an.id); - if (valueSym != null && valueSym.isPrincipalVar()) { - bindMapping.put(binder.toSHErrLocFmt(), valueSym.toSHErrLocFmt()); - } + String principal = a.principalName(env); + if (binder != null && principal != null) { + bindMapping.put(binder.toSHErrLocFmt(), principal); } ExpOutcome ao = a.genIFConstraints(env, false); @@ -357,17 +351,9 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) VarSym argSym = funcSym.parameters.get(i); - Expression principalExp = a; - if (a instanceof Call cast && cast.isCast(env)) { - principalExp = cast.getArgAt(0); - } - - if (argSym.isPrincipalVar() && principalExp instanceof Name an) { - VarSym valueSym = env.getVar(an.id); - if (valueSym != null && valueSym.isPrincipalVar()) { - dependentLabelMapping.put(argSym.toSHErrLocFmt(), - valueSym.toSHErrLocFmt()); - } + String principal = a.principalName(env); + if (argSym.isPrincipalTyped() && principal != null) { + dependentLabelMapping.put(argSym.toSHErrLocFmt(), principal); } ExpOutcome ao = a.genIFConstraints(env, false); diff --git a/src/ast/Contract.java b/src/ast/Contract.java index ae551b92..0924f756 100644 --- a/src/ast/Contract.java +++ b/src/ast/Contract.java @@ -177,6 +177,8 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S def.genTypeConstraints(env, now); } + markFinalFieldInits(); + for (FunctionDef fDef : methodDeclarations) { fDef.genTypeConstraints(env, now); } @@ -185,6 +187,41 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S return now; } + /** + * Checks that each final field, other than a built-in one, is assigned exactly once by a + * top-level statement of a constructor (this contract's own, or a superclass constructor + * inherited as a method), and marks those assignments as final-field initializations. + */ + private void markFinalFieldInits() throws SemanticException { + Map finalFields = new HashMap<>(); + for (StateVariableDeclaration dec : varDeclarations) { + if (dec.isFinal() && !dec.isBuiltIn()) { + finalFields.put(dec.name().id, dec); + } + } + Set initialized = new HashSet<>(); + for (FunctionDef fDef : methodDeclarations) { + if (!fDef.isConstructor() && !Utils.isSuperConstructor(fDef.getName())) continue; + for (Statement stmt : fDef.body) { + if (stmt instanceof Assign a && a.target instanceof Name n + && finalFields.containsKey(n.id)) { + if (!initialized.add(n.id)) { + throw new SemanticException("final field " + n.id + + " is initialized more than once", a.location); + } + a.markFinalFieldInit(); + } + } + } + for (Map.Entry e : finalFields.entrySet()) { + if (!initialized.contains(e.getKey())) { + throw new SemanticException("final field " + e.getKey() + + " is not initialized by a top-level statement of the constructor", + e.getValue().location); + } + } + } + @Override public void globalInfoVisit(InterfaceSym contractSym) throws SemanticException { // contractSym.name = contractName; diff --git a/src/ast/EndorseIfStatement.java b/src/ast/EndorseIfStatement.java index 79f0927c..f2a58799 100644 --- a/src/ast/EndorseIfStatement.java +++ b/src/ast/EndorseIfStatement.java @@ -75,8 +75,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // // logger.debug(l.toString()); // // logger.debug(r.toString()); createdHypo = true; - Inequality hypo = new Inequality(l.toSHErrLocFmt(), Relation.LEQ, - r.toSHErrLocFmt()); + Inequality hypo = new Inequality(env.principalName(l), Relation.LEQ, + env.principalName(r)); env.hypothesis().add(hypo); // // logger.debug("testing label"); @@ -96,6 +96,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic if (ifStatement.body.size() > 0 || ifStatement.orelse.size() > 0) { Utils.genSequenceConstraints(env, beginContext.lambda, IfTestNormalPc, IfTestNormalLambda, IfNamePcAfter, ifStatement.test.location); } + Map versionsBefore = env.saveVersions(); env.incScopeLayer(); Label fromLabel = env.toLabel(from); @@ -116,9 +117,9 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic newSym.ifl = toLabel; env.checkEqual(newSym.labelNameSLC(), newSym.labelValueSLC(), location, "Variable " + newSym.getName() + " may be endorsed incorrectly"); - if (sym.isPrincipalVar()) { - env.checkTrustEqual(sym.toSHErrLocFmt(), newSym.toSHErrLocFmt(), location, - "final variable " + newSym.getName() + " may be endorsed incorrectly"); + if (sym.isPrincipalTyped()) { + env.addPrincipal(newSym); + env.assumeSamePrincipal(env.principalName(sym), newSym.toSHErrLocFmt()); } try { env.addVar(newSym.getName(), newSym); @@ -157,6 +158,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // // logger.debug("finished if branch"); //System.err.println("finished if branch"); // env.prevContext.lambda = curContext.lambda; + Map versionsThen = env.saveVersions(); + env.restoreVersions(versionsBefore); env.incScopeLayer(); // index = 0; PathOutcome elseo = toOutCome.psi; @@ -173,6 +176,10 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // // env.prevContext.lambda = rightContext.lambda; // } env.decScopeLayer(); + env.mergeVersions(List.of(versionsThen, env.saveVersions())); + // Assignments in the then branch go to the endorsed copies, which share storage + // with the original variables. + env.havoc(Assign.assignedNames(ifStatement.body)); ifo.join(elseo); // env.cons.add(new Constraint(new Inequality(IfNameLock, Relation.REQ, Utils.joinLabels(leftContext.lambda, rightContext.lambda)), env.hypothesis, location)); diff --git a/src/ast/Expression.java b/src/ast/Expression.java index 309301a6..b634c190 100644 --- a/src/ast/Expression.java +++ b/src/ast/Expression.java @@ -34,6 +34,15 @@ public VarSym getVarInfo(NTCEnv env) throws SemanticException { public abstract ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException; + /** + * If this expression is a principal-typed variable, or a cast of one to a contract or + * address type, the SHErrLoc name of the principal it holds at the current program point. + * Otherwise null. + */ + public String principalName(VisitEnv env) { + return null; + } + /* Check whether there are non-exception-free calls inside the expression. If so, create temporary variables and statements to eventually evaluate the expression. diff --git a/src/ast/FlowsToExp.java b/src/ast/FlowsToExp.java index f3b88dd7..f9d39f37 100644 --- a/src/ast/FlowsToExp.java +++ b/src/ast/FlowsToExp.java @@ -82,9 +82,9 @@ public boolean typeMatch(Expression expression) { public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { ScopeContext now = new ScopeContext(this, parent); for (Name operand : List.of((Name) lhs, (Name) rhs)) { - if (!(env.getCurSym(operand.id) instanceof VarSym v) || !v.isPrincipalVar()) { + if (!(env.getCurSym(operand.id) instanceof VarSym v) || !v.isPrincipalTyped()) { throw new SemanticException( - "dynamic trust queries can only happen between final address variables: " + "dynamic trust queries can only happen between address or contract variables: " + operand.id, location); } } diff --git a/src/ast/For.java b/src/ast/For.java index 7688fab9..25ab212b 100644 --- a/src/ast/For.java +++ b/src/ast/For.java @@ -61,6 +61,10 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.incScopeLayer(); PathOutcome newVarOut = newVars.IFCVisit(env, false); beginContext = newVarOut.getNormalPath().c; + // The loop head is reached from before the loop and from the end of an iteration. + java.util.Set assigned = Assign.assignedNames(body); + assigned.addAll(Assign.assignedNames(iter)); + env.havoc(assigned); ExpOutcome to = test.genIFConstraints(env, false); String ifNameTest = to.valueLabelName; @@ -96,6 +100,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic loc = s.location; } + // The loop exits from its head or from a break in the body. + env.havoc(assigned); env.decScopeLayer(); return ifo; diff --git a/src/ast/FunctionDef.java b/src/ast/FunctionDef.java index bd382dc4..e76fb30f 100644 --- a/src/ast/FunctionDef.java +++ b/src/ast/FunctionDef.java @@ -115,13 +115,17 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_info) throws SemanticExce if (!returnVoid()) { addBuiltInResult(env.curSymTab, scopeContext); } - VarSym sender = env.getVar(Utils.LABEL_SENDER); - env.checkEqual(sender.labelNameSLC(), sender.labelValueSLC(), location, - "Label of sender may be incorrect"); + List builtInNames = new ArrayList<>(List.of(Utils.LABEL_SENDER, Utils.LABEL_PAYVALUE)); + builtInNames.addAll(Utils.BUILTIN_INMETHOD_VARS.keySet()); + for (String builtInName : builtInNames) { + VarSym builtIn = env.getVar(builtInName); + env.checkEqual(builtIn.labelNameSLC(), builtIn.labelValueSLC(), location, + "Label of " + builtInName + " may be incorrect"); + } for (Entry entry: env.curSymTab.getVars().entrySet()) { VarSym varSym = entry.getValue(); - if (varSym.isFinal && (varSym.typeSym instanceof ContractSym || varSym.typeSym.getName().equals(Utils.ADDRESS_TYPE))) { + if (varSym.isPrincipalTyped()) { env.addPrincipal(varSym); } } diff --git a/src/ast/FunctionSig.java b/src/ast/FunctionSig.java index 0927196c..992339e5 100644 --- a/src/ast/FunctionSig.java +++ b/src/ast/FunctionSig.java @@ -409,10 +409,13 @@ public boolean isBuiltIn() { protected void addBuiltInVars(SymTab curSymTab, ScopeContext now) { - // final address{TOP} sender; - // The address is trusted data because the runtime authenticates it; - // the contract it names is not thereby trusted. + // final address{TOP} sender; final uint{TOP} value; final uint{TOP} now; + // These values are established by the runtime, so they are trusted data. + // The contract that sender names is not thereby trusted. try { + PrimitiveLabel labelTop = new PrimitiveLabel( + (VarSym) curSymTab.lookup(typecheck.Utils.LABEL_TOP), + typecheck.Utils.BUILTIN_LOCATION); VarSym varSender = new VarSym( typecheck.Utils.LABEL_SENDER, @@ -424,31 +427,26 @@ protected void addBuiltInVars(SymTab curSymTab, ScopeContext now) { true, true ); - PrimitiveLabel labelSender = new PrimitiveLabel(varSender, typecheck.Utils.BUILTIN_LOCATION); - varSender.setLabel(new PrimitiveLabel( - (VarSym) curSymTab.lookup(typecheck.Utils.LABEL_TOP), - typecheck.Utils.BUILTIN_LOCATION)); + varSender.setLabel(labelTop); curSymTab.add(typecheck.Utils.LABEL_SENDER, varSender); - // final uint{sender} value; curSymTab.add(typecheck.Utils.LABEL_PAYVALUE, new VarSym( typecheck.Utils.LABEL_PAYVALUE, (TypeSym) curSymTab.lookup(typecheck.Utils.BuiltinType2ID(BuiltInT.UINT)), - labelSender, + labelTop, typecheck.Utils.BUILTIN_LOCATION, now, true, true, true )); - // final uint{sender} value; // other built-in vars for (Entry entry : typecheck.Utils.BUILTIN_INMETHOD_VARS.entrySet()) { String varName = entry.getKey(); String typeName = entry.getValue(); - VarSym varSym = new VarSym(varName, (TypeSym) curSymTab.lookup(typeName), labelSender, + VarSym varSym = new VarSym(varName, (TypeSym) curSymTab.lookup(typeName), labelTop, typecheck.Utils.BUILTIN_LOCATION, now, true, true, true); curSymTab.add(varName, varSym); } diff --git a/src/ast/GuardBlock.java b/src/ast/GuardBlock.java index 291c5e7b..81a5253a 100644 --- a/src/ast/GuardBlock.java +++ b/src/ast/GuardBlock.java @@ -54,7 +54,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // env.prevContext.lambda = newLockLabel; - Label label = env.curContractSym().newLabel(l); + Label label = env.toLabel(l); String guardLabel = label.toSHErrLocFmt(); env.checkFlow(Utils.meetLabels(guardLabel, curContext.lambda), beginContext.lambda, diff --git a/src/ast/If.java b/src/ast/If.java index 3e29926c..ad5d8cd9 100644 --- a/src/ast/If.java +++ b/src/ast/If.java @@ -90,8 +90,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // // logger.debug(l.toString()); // // logger.debug(r.toString()); createdHypo = true; - Inequality hypo = new Inequality(l.toSHErrLocFmt(), Relation.LEQ, - r.toSHErrLocFmt()); + Inequality hypo = new Inequality(env.principalName(l), Relation.LEQ, + env.principalName(r)); env.hypothesis().add(hypo); // logger.debug("testing label"); @@ -107,6 +107,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // env.cons.add(new Constraint(new Inequality(IfTestNormalPc, IfNamePcAfter), env.hypothesis(), test.location, env.curContractSym().getName(), // Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION)); } + Map versionsBefore = env.saveVersions(); env.incScopeLayer(); // Context leftContext = new Context(curContext), rightContext = new Context(curContext); @@ -138,6 +139,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // logger.debug("finished if branch"); //System.err.println("finished if branch"); // env.prevContext.lambda = curContext.lambda; + Map versionsThen = env.saveVersions(); + env.restoreVersions(versionsBefore); env.incScopeLayer(); index = 0; PathOutcome elseo = to.psi; @@ -154,6 +157,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // env.prevContext.lambda = rightContext.lambda; } env.decScopeLayer(); + env.mergeVersions(List.of(versionsThen, env.saveVersions())); ifo.join(elseo); // env.cons.add(new Constraint(new Inequality(IfNameLock, Relation.REQ, Utils.joinLabels(leftContext.lambda, rightContext.lambda)), env.hypothesis, location)); diff --git a/src/ast/Name.java b/src/ast/Name.java index 752c76e0..3a5975ba 100644 --- a/src/ast/Name.java +++ b/src/ast/Name.java @@ -62,6 +62,14 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) { return new ExpOutcome(ifNameRtn, new PathOutcome(new PsiUnit(env.inContext))); } + @Override + public String principalName(VisitEnv env) { + if (env.getCurSym(id) instanceof VarSym v && v.isPrincipalTyped()) { + return env.principalName(v); + } + return null; + } + @Override public compile.ast.Expression solidityCodeGen(List result, CompileEnv code) { return new SingleVar(id); diff --git a/src/ast/New.java b/src/ast/New.java index 74ac4f3c..ea7507bf 100644 --- a/src/ast/New.java +++ b/src/ast/New.java @@ -100,6 +100,16 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S @Override public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + ExpOutcome result = newIFConstraints(env, tail_position); + // A contract's constructor may call back and assign to any field, so fields hold + // new principals afterwards. + if (isConstructor) { + env.havocFields(); + } + return result; + } + + private ExpOutcome newIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { if (!isConstructor) { // creating a struct var returns a join of its arg values Context beginContext = env.inContext; @@ -136,10 +146,12 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S PathOutcome psi = new PathOutcome(new PsiUnit(endContext)); ExpOutcome ao = null; + List argPrincipalNames = new ArrayList<>(); for (Expression arg : constructor_call.args) { ao = arg.genIFConstraints(env, false); psi.joinExe(ao.psi); argValueLabelNames.add(ao.valueLabelName); + argPrincipalNames.add(arg.principalName(env)); env.inContext = new Context( Utils.joinLabels(ao.psi.getNormalPath().c.pc, beginContext.pc), beginContext.lambda); @@ -171,15 +183,8 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S for (int i = 0; i < constructor_call.args.size(); ++i) { Expression arg = constructor_call.args.get(i); VarSym argSym = funcSym.parameters.get(i); - if (argSym.isPrincipalVar()) { - - if (arg instanceof Name) { - VarSym valueSym = (VarSym) env.getVar(((Name) arg).id); - if (valueSym.isPrincipalVar()) { - dependentLabelMapping.put(argSym.toSHErrLocFmt(), - valueSym.toSHErrLocFmt()); - } - } + if (argSym.isPrincipalTyped() && argPrincipalNames.get(i) != null) { + dependentLabelMapping.put(argSym.toSHErrLocFmt(), argPrincipalNames.get(i)); } // env.prevContext = prevContext = tmp; diff --git a/src/ast/StateVariableDeclaration.java b/src/ast/StateVariableDeclaration.java index ff9f3dce..83c46094 100644 --- a/src/ast/StateVariableDeclaration.java +++ b/src/ast/StateVariableDeclaration.java @@ -91,8 +91,16 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S if (value != null) { ScopeContext v = value.genTypeConstraints(env, now); env.addCons(tgt.genTypeConstraints(v, Relation.LEQ, env, location)); - } else if (isFinal && !isBuiltin) { - throw new SemanticException("final variable " + name.id + " not initialized", location); + } + // The label must mean the same thing wherever the field is used, so it may not + // name a principal that can change. + if (env.getCurSym(name.id) instanceof VarSym field && field.ifl != null) { + for (VarSym p : field.ifl.principalSyms()) { + if (p.isMutablePrincipal()) { + throw new SemanticException("the label of field " + name.id + + " names " + p.getName() + ", which is not final", field.ifl.location()); + } + } } return now; } @@ -127,45 +135,14 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic SLCNameVar = varSym.toSHErrLocFmt(); SLCNameVarLbl = varSym.labelNameSLC(); // logger.debug(varSym.typeSym.getName()); - if ((varSym.isFinal && - (varSym.typeSym instanceof ContractSym || varSym.typeSym.getName().equals(Utils.ADDRESS_TYPE)))) { + // A field initialized here starts out naming the principal it is initialized to. + // Only built-in fields have initializers; others are initialized in a constructor. + if (varSym.isPrincipalTyped()) { env.addPrincipal(varSym); - - VarSym equalPrincipal = null; - boolean correctInit = true; - if (value instanceof Name) { // assigned as another variable - // Nothing to check - equalPrincipal = env.getVar(((Name) value).id); - assert equalPrincipal != null : ((Name) value).id; - } else if (value instanceof Call && ((Call) value).isCast(env)) { // assigned as another contract - // check if it is a cast to a final address variable - Call cast = (Call) value; - Expression arg = cast.getArgAt(0); - if (arg instanceof Name) { - VarSym sym = env.getVar(((Name) arg).id); - if (!sym.isFinal) { - correctInit = false; - } else { - equalPrincipal = sym; - } - } else { - correctInit = false; - } - } else { - correctInit = false; - } - - - if (correctInit) { - // add equivalence assumption to the trust set - env.checkTrustEqual(SLCNameVar, equalPrincipal.toSHErrLocFmt(), location, - "New principal declaration"); - } else if (!isBuiltin) { - throw new SemanticException("A final address/Contract must be initialized to another final address/Contract: " + id, - location); + String init = value == null ? null : value.principalName(env); + if (init != null) { + env.assumeSamePrincipal(SLCNameVar, init); } - - } else if (varSym.typeSym.getName().equals(Utils.PRINCIPAL_TYPE)) { env.addPrincipal(varSym); } @@ -226,6 +203,11 @@ public Name name() { return name; } + /** Whether this field is final. */ + public boolean isFinal() { + return isFinal; + } + public boolean isBuiltIn() { return isBuiltin; } diff --git a/src/ast/Try.java b/src/ast/Try.java index 5323359a..38a02ace 100644 --- a/src/ast/Try.java +++ b/src/ast/Try.java @@ -159,6 +159,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic //env.psi = newPsi; PathOutcome psi = new PathOutcome(); PathOutcome input = new PathOutcome(); + Map versionsBefore = env.saveVersions(); env.incScopeLayer(); PathOutcome so = new PathOutcome(new PsiUnit(beginContext)); Utils.genConsStmtsWithException(body, env, so, psi, false); @@ -188,7 +189,13 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic //Context cTry = env.outContext; //env.psi = oldPsi; + // A handler may be entered from any point in the body. + List> exitVersions = new ArrayList<>(); + exitVersions.add(env.saveVersions()); + java.util.Set assignedInBody = ast.Assign.assignedNames(body); for (ExceptHandler h : handlers) { + env.restoreVersions(versionsBefore); + env.havoc(assignedInBody); env.incScopeLayer(); //env.inContext = new Context(h.getHandlerPcLabelName(), beginContext.lambda); ExceptionTypeSym expSym = env.getExp(h.name()); @@ -198,10 +205,12 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // env.inContext = new Context(expUnit.c.pc, beginContext.lambda); PathOutcome ho = h.genConsVisit(env, tail_position); psi.join(ho); + exitVersions.add(env.saveVersions()); } env.decScopeLayer(); // cTry = new Context(Utils.makeJoin(cTry.pc, env.outContext.outPcName), Utils.makeJoin(cTry.lambda, env.outContext.lockName)); } + env.mergeVersions(exitVersions); typecheck.Utils.contextFlow(env, psi.getNormalPath().c, endContext, location); diff --git a/src/ast/While.java b/src/ast/While.java index 49c262f2..b6f5a8c6 100644 --- a/src/ast/While.java +++ b/src/ast/While.java @@ -46,6 +46,9 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic Context endContext = new Context(Utils.getLabelNamePc(toSHErrLocFmt()), Utils.getLabelNameLock(toSHErrLocFmt())); + // The loop head is reached from before the loop and from the end of the body. + java.util.Set assigned = Assign.assignedNames(body); + env.havoc(assigned); ExpOutcome to = test.genIFConstraints(env, false); String ifNameTest = to.valueLabelName; @@ -75,6 +78,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // } env.decScopeLayer(); + // The loop exits from its head or from a break in the body. + env.havoc(assigned); return ifo; diff --git a/src/typecheck/ComplexLabel.java b/src/typecheck/ComplexLabel.java index 6c8aecbe..b339b2c7 100644 --- a/src/typecheck/ComplexLabel.java +++ b/src/typecheck/ComplexLabel.java @@ -16,6 +16,13 @@ public List principalLeaves() { return leaves; } + @Override + public List principalSyms() { + List syms = new ArrayList<>(left.principalSyms()); + syms.addAll(right.principalSyms()); + return syms; + } + public ComplexLabel(Label left, IfOperator op, Label right, CodeLocation location) { super(location); this.left = left; diff --git a/src/typecheck/Label.java b/src/typecheck/Label.java index dc6284fd..b658e27f 100644 --- a/src/typecheck/Label.java +++ b/src/typecheck/Label.java @@ -22,6 +22,9 @@ protected Label(CodeLocation location) { /** The local names of this label's principal leaves (flattening joins). */ abstract public List principalLeaves(); + /** The variables naming this label's principal leaves (flattening joins). */ + abstract public List principalSyms(); + public CodeLocation location() { return location; } diff --git a/src/typecheck/PrimitiveLabel.java b/src/typecheck/PrimitiveLabel.java index c895f51e..e1d6c0fb 100644 --- a/src/typecheck/PrimitiveLabel.java +++ b/src/typecheck/PrimitiveLabel.java @@ -5,33 +5,49 @@ public class PrimitiveLabel extends Label { VarSym sym; + /** The version of {@code sym} this label names, or null if it names {@code sym} itself. */ + private final String version; @Override public List principalLeaves() { return List.of(sym.getName()); } + @Override + public List principalSyms() { + return List.of(sym); + } + + /** A label naming the principal held by {@code sym}. */ public PrimitiveLabel(VarSym sym, CodeLocation location) { + this(sym, null, location); + } + + /** + * A label naming {@code version}, a version of the principal variable {@code sym}. + * If {@code version} is null, the label names {@code sym} itself. + */ + public PrimitiveLabel(VarSym sym, String version, CodeLocation location) { super(location); assert sym != null; this.sym = sym; + this.version = version; } @Override public String toSHErrLocFmt() { - return sym.toSHErrLocFmt(); + return version != null ? version : sym.toSHErrLocFmt(); } @Override public String toSHErrLocFmt(String origin, String substitution) { - if (sym.toSHErrLocFmt().equals(origin)) { - return substitution; - } - return sym.toSHErrLocFmt(); + String str = toSHErrLocFmt(); + return str.equals(origin) ? substitution : str; } + @Override public String toSHErrLocFmt(Map mapping) { - String str = sym.toSHErrLocFmt(); + String str = toSHErrLocFmt(); return mapping.getOrDefault(str, str); } } diff --git a/src/typecheck/Utils.java b/src/typecheck/Utils.java index 92e4db43..7e9cf4d1 100644 --- a/src/typecheck/Utils.java +++ b/src/typecheck/Utils.java @@ -271,11 +271,17 @@ public static String BuiltinType2ID(BuiltInT type) { public static SherrlocDiagnoser createDiagnoser(Set constructors, List assumptions, List constraints, boolean isIFC, InterfaceSym contractSym) { - return createDiagnoser(constructors, assumptions, constraints, isIFC, contractSym, - java.util.Set.of(), List.of()); + return createDiagnoser(constructors, java.util.Set.of(), assumptions, constraints, isIFC, + contractSym, java.util.Set.of(), List.of()); } - public static SherrlocDiagnoser createDiagnoser(Set constructors, List assumptions, + /** + * A diagnoser for {@code constraints} under {@code assumptions}. The principals named by + * {@code constructors} and {@code principalNames} are constants rather than variables to + * solve for; in IFC mode, each lies between the top and bottom principals. + */ + public static SherrlocDiagnoser createDiagnoser(Set constructors, + Set principalNames, List assumptions, List constraints, boolean isIFC, InterfaceSym contractSym, Set rigidAtoms, List atomAssumptions) { @@ -317,6 +323,15 @@ public static SherrlocDiagnoser createDiagnoser(Set constructors, diagnoser.addAssumedInequality(diagnoser.createEqualityConstraint( diagnoser.createElement(top.toSHErrLocFmt(), sherrloc.constraint.ast.Position.EmptyPosition()), diagnoser.createElement(contractSym.thisSym().toSHErrLocFmt(), sherrloc.constraint.ast.Position.EmptyPosition()))); + for (String x : principalNames) { + diagnoser.defineConstructor(x, 0); + diagnoser.addAssumedInequality(diagnoser.createGreaterThanConstraint( + diagnoser.createElement(bot.toSHErrLocFmt(), sherrloc.constraint.ast.Position.EmptyPosition()), + diagnoser.createElement(x, sherrloc.constraint.ast.Position.EmptyPosition()))); + diagnoser.addAssumedInequality(diagnoser.createLessThanConstraint( + diagnoser.createElement(top.toSHErrLocFmt(), sherrloc.constraint.ast.Position.EmptyPosition()), + diagnoser.createElement(x, sherrloc.constraint.ast.Position.EmptyPosition()))); + } } if (isIFC) { for (String atom : rigidAtoms) { diff --git a/src/typecheck/VarSym.java b/src/typecheck/VarSym.java index ac8ae79e..82fd2174 100644 --- a/src/typecheck/VarSym.java +++ b/src/typecheck/VarSym.java @@ -133,8 +133,22 @@ public void setLabel(Label label) { ifl = label; } + /** Whether this variable holds an address or a contract reference, and so names a principal. */ + public boolean isPrincipalTyped() { + return typeSym instanceof InterfaceSym || typeSym.getName().equals(Utils.ADDRESS_TYPE); + } + + /** Whether this variable is final and names a principal. */ public boolean isPrincipalVar() { - return isFinal && (typeSym instanceof ContractSym || typeSym instanceof InterfaceSym || typeSym.getName().equals(Utils.ADDRESS_TYPE)); + return isFinal && isPrincipalTyped(); + } + + /** + * Whether this variable names a principal that can change during execution, so that + * different occurrences of it may name different principals. + */ + public boolean isMutablePrincipal() { + return !isFinal && isPrincipalTyped(); } @Override diff --git a/src/typecheck/VisitEnv.java b/src/typecheck/VisitEnv.java index 3e35ee85..e122afbb 100644 --- a/src/typecheck/VisitEnv.java +++ b/src/typecheck/VisitEnv.java @@ -35,6 +35,15 @@ public class VisitEnv { public Map sigReq = new HashMap<>(); public Map> programMap; + /** + * The current version of each mutable principal variable that has been versioned in + * the current method, keyed by the variable's SHErrLoc name. A variable absent from + * the map is at its initial version, which is named by the variable itself. + */ + private Map versions = new HashMap<>(); + private final Set versionNames = new java.util.LinkedHashSet<>(); + private int versionCount = 0; + private final Set rigidAtoms = new java.util.LinkedHashSet<>(); private final List atomAssumptions = new ArrayList<>(); @@ -216,6 +225,102 @@ public void addPrincipal(VarSym varSym) { principalSet.add(varSym); } + /** + * The SHErrLoc name of the principal that {@code v} holds at the current program point. + * For a variable that is not a mutable principal, this is the variable's own name. + */ + public String principalName(VarSym v) { + String base = v.toSHErrLocFmt(); + return versions.getOrDefault(base, base); + } + + /** + * Starts a new version of the mutable principal variable {@code v}, about which nothing + * is known, and returns its SHErrLoc name. + */ + public String newVersion(VarSym v) { + assert v.isMutablePrincipal() : v.getName(); + return newVersion(v.toSHErrLocFmt()); + } + + private String newVersion(String base) { + String version = base + "..v" + (++versionCount); + versions.put(base, version); + versionNames.add(version); + return version; + } + + /** + * Assumes that the principals named {@code a} and {@code b} are the same, as when one + * variable is assigned the principal held by another. + */ + public void assumeSamePrincipal(String a, String b) { + addAtomAssumption(a, b); + } + + /** The SHErrLoc names of all versions created so far. Each names a principal. */ + public Set versionNames() { + return versionNames; + } + + /** The current versions, for restoring with {@link #restoreVersions}. */ + public Map saveVersions() { + return new HashMap<>(versions); + } + + /** Makes {@code saved} the current versions. */ + public void restoreVersions(Map saved) { + versions = new HashMap<>(saved); + } + + /** + * Makes the current versions those at a point where the control-flow paths ending in + * {@code branches} merge. A variable whose version differs among the branches gets a + * new version (a phi-version), about which nothing is known. + */ + public void mergeVersions(List> branches) { + Set keys = new java.util.HashSet<>(); + for (Map b : branches) keys.addAll(b.keySet()); + Map merged = new HashMap<>(); + for (String base : keys) { + Set seen = new java.util.HashSet<>(); + for (Map b : branches) seen.add(b.getOrDefault(base, base)); + if (seen.size() == 1) { + String only = seen.iterator().next(); + if (!only.equals(base)) merged.put(base, only); + } else { + merged.put(base, base + "..v" + (++versionCount)); + versionNames.add(merged.get(base)); + } + } + versions = merged; + } + + /** + * Gives a new version to each mutable principal variable visible here whose name is in + * {@code names}, and to each mutable principal field of the current contract. + */ + public void havoc(Set names) { + for (String name : names) { + if (curSymTab.lookup(name) instanceof VarSym v && v.isMutablePrincipal()) { + newVersion(v); + } + } + havocFields(); + } + + /** + * Gives a new version to each mutable principal field of the current contract, as + * needed after any call, which may assign to fields. + */ + public void havocFields() { + for (VarSym v : curContractSym.symTab.getVars().values()) { + if (v.isMutablePrincipal()) { + newVersion(v); + } + } + } + public VarSym thisSym() { return curContractSym.thisSym(); } @@ -237,11 +342,15 @@ public void setCurFuncSym(FuncSym funcSym) { curFuncSym = funcSym; } + /** + * Resolves {@code ifl} at the current program point: a mutable principal variable in it + * names the version it holds here. + */ public Label toLabel(IfLabel ifl) { if (ifl instanceof PrimitiveIfLabel) { - VarSym label = (VarSym) getVar(((PrimitiveIfLabel) ifl).value().id); - if (label == null) return null; - return new PrimitiveLabel(label, ifl.getLocation()); + VarSym label = getVar(((PrimitiveIfLabel) ifl).value().id); + return new PrimitiveLabel(label, + label.isMutablePrincipal() ? principalName(label) : null, ifl.getLocation()); } else if (ifl instanceof ComplexIfLabel) { return new ComplexLabel(toLabel(((ComplexIfLabel) ifl).getLeft()), ((ComplexIfLabel) ifl).getOp(), @@ -262,6 +371,7 @@ public void enterNewContract() { } public void enterNewMethod(String name) { + versions = new HashMap<>(); cons = new ArrayList<>(); trustCons = new ArrayList<>(); conMap.put(name, cons); diff --git a/test/contracts/ifcTypechecking/FinalFieldInit.scif b/test/contracts/ifcTypechecking/FinalFieldInit.scif new file mode 100644 index 00000000..fade468e --- /dev/null +++ b/test/contracts/ifcTypechecking/FinalFieldInit.scif @@ -0,0 +1,13 @@ +// A final field initialized in the constructor names a fixed principal in every method. +contract FinalFieldInit { + final address{this} owner; + uint{this} secret; + + constructor() { super(); owner = sender; } + + public void f{this}(uint{owner} v) { + if (owner => this) { + secret = v; + } + } +} diff --git a/test/contracts/ifcTypechecking/NonFinalPrincipalLabel_W01.scif b/test/contracts/ifcTypechecking/NonFinalPrincipalLabel_W01.scif new file mode 100644 index 00000000..c8d87289 --- /dev/null +++ b/test/contracts/ifcTypechecking/NonFinalPrincipalLabel_W01.scif @@ -0,0 +1,10 @@ +// x is not final, but it still names a principal: data trusted by it is not trusted by this. +contract NonFinalPrincipalLabel_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} x, uint{x} v) { + secret = v; + } +} diff --git a/test/contracts/ifcTypechecking/TrustFactBranch_W01.scif b/test/contracts/ifcTypechecking/TrustFactBranch_W01.scif new file mode 100644 index 00000000..add68c02 --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustFactBranch_W01.scif @@ -0,0 +1,16 @@ +// a may be reassigned on one branch, so after the merge {a} names an unknown principal. +contract TrustFactBranch_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a, address{this} b, bool{this} c) { + if (a => this) { + if (c) { + a = b; + } + uint{a} w = 1; + secret = w; + } + } +} diff --git a/test/contracts/ifcTypechecking/TrustFactFieldCall_W01.scif b/test/contracts/ifcTypechecking/TrustFactFieldCall_W01.scif new file mode 100644 index 00000000..301fa751 --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustFactFieldCall_W01.scif @@ -0,0 +1,17 @@ +// A call may reassign the field owner, so after it {owner} names an unknown principal. +contract TrustFactFieldCall_W01 { + uint{this} secret; + address{this} owner; + + constructor() { super(); owner = sender; } + + public void g{this}() { } + + public void f{this}() { + if (owner => this) { + g(); + uint{owner} w = 1; + secret = w; + } + } +} diff --git a/test/contracts/ifcTypechecking/TrustFactKept.scif b/test/contracts/ifcTypechecking/TrustFactKept.scif new file mode 100644 index 00000000..1e4eb451 --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustFactKept.scif @@ -0,0 +1,21 @@ +// A trust test on a variable holds for its uses until the variable is reassigned. +contract TrustFactKept { + uint{this} secret; + address{this} owner; + + constructor() { super(); owner = sender; } + + public void local{this}(address{this} a) { + if (a => this) { + uint{a} w = 1; + secret = w; + } + } + + public void field{this}() { + if (owner => this) { + uint{owner} w = 1; + secret = w; + } + } +} diff --git a/test/contracts/ifcTypechecking/TrustFactLoop_W01.scif b/test/contracts/ifcTypechecking/TrustFactLoop_W01.scif new file mode 100644 index 00000000..c9bdabcb --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustFactLoop_W01.scif @@ -0,0 +1,17 @@ +// a may be reassigned in the loop, so after the loop {a} names an unknown principal. +contract TrustFactLoop_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a, address{this} b, uint{this} n) { + if (a => this) { + while (n > 0) { + a = b; + n = n - 1; + } + uint{a} w = 1; + secret = w; + } + } +} diff --git a/test/contracts/ifcTypechecking/TrustFactReassign_W01.scif b/test/contracts/ifcTypechecking/TrustFactReassign_W01.scif new file mode 100644 index 00000000..7b8d0b72 --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustFactReassign_W01.scif @@ -0,0 +1,14 @@ +// After a is reassigned, {a} names the new principal, which the trust test did not cover. +contract TrustFactReassign_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a, address{this} b) { + if (a => this) { + a = b; + uint{a} w = 1; + secret = w; + } + } +} diff --git a/test/contracts/ifcTypechecking/ValueNowTrusted.scif b/test/contracts/ifcTypechecking/ValueNowTrusted.scif new file mode 100644 index 00000000..bf1e140d --- /dev/null +++ b/test/contracts/ifcTypechecking/ValueNowTrusted.scif @@ -0,0 +1,12 @@ +// The ether sent with a call and the block timestamp are trusted data. +contract ValueNowTrusted { + uint{this} paid; + uint{this} paidAt; + + constructor() { super(); } + + public void pay{sender -> this; any}() { + paid = value; + paidAt = now; + } +} diff --git a/test/contracts/regularTypechecking/FieldLabelNonFinal_W01.scif b/test/contracts/regularTypechecking/FieldLabelNonFinal_W01.scif new file mode 100644 index 00000000..92660ec4 --- /dev/null +++ b/test/contracts/regularTypechecking/FieldLabelNonFinal_W01.scif @@ -0,0 +1,6 @@ +// A field's label may name only final variables. +contract FieldLabelNonFinal_W01 { + address owner; + uint{owner} amount; + constructor() { super(); } +} diff --git a/test/contracts/regularTypechecking/FinalFieldAssignInMethod_W01.scif b/test/contracts/regularTypechecking/FinalFieldAssignInMethod_W01.scif new file mode 100644 index 00000000..19b4088c --- /dev/null +++ b/test/contracts/regularTypechecking/FinalFieldAssignInMethod_W01.scif @@ -0,0 +1,6 @@ +// Only the constructor initializes a final field. +contract FinalFieldAssignInMethod_W01 { + final address owner; + constructor() { super(); owner = sender; } + public void f() { owner = sender; } +} diff --git a/test/contracts/regularTypechecking/FinalFieldInitNested_W01.scif b/test/contracts/regularTypechecking/FinalFieldInitNested_W01.scif new file mode 100644 index 00000000..252452bc --- /dev/null +++ b/test/contracts/regularTypechecking/FinalFieldInitNested_W01.scif @@ -0,0 +1,11 @@ +// A final field is initialized by a top-level statement of the constructor. +contract FinalFieldInitNested_W01 { + final address owner; + bool flag; + constructor() { + super(); + if (flag) { + owner = sender; + } + } +} diff --git a/test/contracts/regularTypechecking/FinalFieldInitOK.scif b/test/contracts/regularTypechecking/FinalFieldInitOK.scif new file mode 100644 index 00000000..5683c41f --- /dev/null +++ b/test/contracts/regularTypechecking/FinalFieldInitOK.scif @@ -0,0 +1,6 @@ +// A final field may be initialized by the constructor and named in another field's label. +contract FinalFieldInitOK { + final address owner; + uint{owner} amount; + constructor() { super(); owner = sender; } +} diff --git a/test/contracts/regularTypechecking/FinalFieldInitTwice_W01.scif b/test/contracts/regularTypechecking/FinalFieldInitTwice_W01.scif new file mode 100644 index 00000000..0528dfd3 --- /dev/null +++ b/test/contracts/regularTypechecking/FinalFieldInitTwice_W01.scif @@ -0,0 +1,5 @@ +// A final field is initialized exactly once. +contract FinalFieldInitTwice_W01 { + final address owner; + constructor() { super(); owner = sender; owner = this; } +} diff --git a/test/contracts/regularTypechecking/FinalFieldUninitialized_W01.scif b/test/contracts/regularTypechecking/FinalFieldUninitialized_W01.scif new file mode 100644 index 00000000..34d60660 --- /dev/null +++ b/test/contracts/regularTypechecking/FinalFieldUninitialized_W01.scif @@ -0,0 +1,5 @@ +// A final field must be initialized by the constructor. +contract FinalFieldUninitialized_W01 { + final address owner; + constructor() { super(); } +} diff --git a/test/contracts/regularTypechecking/FinalLocalReassign_W01.scif b/test/contracts/regularTypechecking/FinalLocalReassign_W01.scif new file mode 100644 index 00000000..9c8f9e45 --- /dev/null +++ b/test/contracts/regularTypechecking/FinalLocalReassign_W01.scif @@ -0,0 +1,8 @@ +// A final local cannot be reassigned. +contract FinalLocalReassign_W01 { + constructor() { super(); } + public void f() { + final uint a = 1; + a = 2; + } +} diff --git a/test/contracts/regularTypechecking/FinalParamReassign_W01.scif b/test/contracts/regularTypechecking/FinalParamReassign_W01.scif new file mode 100644 index 00000000..b712aabf --- /dev/null +++ b/test/contracts/regularTypechecking/FinalParamReassign_W01.scif @@ -0,0 +1,7 @@ +// A final parameter cannot be reassigned. +contract FinalParamReassign_W01 { + constructor() { super(); } + public void f(final address a) { + a = sender; + } +} diff --git a/test/contracts/regularTypechecking/TrustQueryNonFinal_W01.scif b/test/contracts/regularTypechecking/TrustQueryNonFinal.scif similarity index 80% rename from test/contracts/regularTypechecking/TrustQueryNonFinal_W01.scif rename to test/contracts/regularTypechecking/TrustQueryNonFinal.scif index ebd628fe..55c0e7e1 100644 --- a/test/contracts/regularTypechecking/TrustQueryNonFinal_W01.scif +++ b/test/contracts/regularTypechecking/TrustQueryNonFinal.scif @@ -1,5 +1,5 @@ // A dynamic trust query between non-final addresses. -contract TrustQueryNonFinal_W01 { +contract TrustQueryNonFinal { address a; address b; bool t; diff --git a/test/src/TestIfcTypechecking.java b/test/src/TestIfcTypechecking.java index 78754652..30f55bd7 100644 --- a/test/src/TestIfcTypechecking.java +++ b/test/src/TestIfcTypechecking.java @@ -53,6 +53,9 @@ public class TestIfcTypechecking { "ifcTypechecking/ClosurePcExStronger", "ifcTypechecking/ClosureLockWeaken", "ifcTypechecking/SenderTrustedAddress", + "ifcTypechecking/ValueNowTrusted", + "ifcTypechecking/TrustFactKept", + "ifcTypechecking/FinalFieldInit", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -101,6 +104,11 @@ void testPositive(String contractName) throws Exception { "ifcTypechecking/ClosureBindArgLow_W01", "ifcTypechecking/ClosureBindBoundDependent_W01", "ifcTypechecking/ClosureLockOverclaim_W01", + "ifcTypechecking/TrustFactReassign_W01", + "ifcTypechecking/TrustFactBranch_W01", + "ifcTypechecking/TrustFactLoop_W01", + "ifcTypechecking/TrustFactFieldCall_W01", + "ifcTypechecking/NonFinalPrincipalLabel_W01", // "examples/ERC20", }) void testNegative(String contractName) throws Exception { diff --git a/test/src/TestRegularTypechecking.java b/test/src/TestRegularTypechecking.java index 70552834..00e76627 100644 --- a/test/src/TestRegularTypechecking.java +++ b/test/src/TestRegularTypechecking.java @@ -49,6 +49,8 @@ public class TestRegularTypechecking { "regularTypechecking/ClosureNoOuterLabelOK", "regularTypechecking/ClosureJoinPcEx", "regularTypechecking/ClosureBinderRegularOK", + "regularTypechecking/FinalFieldInitOK", + "regularTypechecking/TrustQueryNonFinal", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -118,7 +120,6 @@ void testNegative(String contractName) { "regularTypechecking/AttributeNoMember_W01", "regularTypechecking/EmitArity_W01", "regularTypechecking/ThrowArity_W01", - "regularTypechecking/TrustQueryNonFinal_W01", "regularTypechecking/MethodAsValue_W01", "regularTypechecking/CatchUnknownException_W01", "regularTypechecking/CallReceiverUndefined_W01", @@ -134,6 +135,13 @@ void testNegative(String contractName) { "regularTypechecking/ThrowUndeclared_W01", "regularTypechecking/SubscriptNonCollection_W01", "regularTypechecking/LabelNonPrincipalVar_W01", + "regularTypechecking/FinalFieldUninitialized_W01", + "regularTypechecking/FinalFieldInitTwice_W01", + "regularTypechecking/FinalFieldInitNested_W01", + "regularTypechecking/FinalFieldAssignInMethod_W01", + "regularTypechecking/FinalLocalReassign_W01", + "regularTypechecking/FinalParamReassign_W01", + "regularTypechecking/FieldLabelNonFinal_W01", }) void testReportedError(String contractName) throws Exception { URL input = ClassLoader.getSystemResource(contractName + ".scif"); From 91bcd4942d75642bf0a877323fdb6527bb6ae15c Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Thu, 5 Nov 2026 06:25:29 -0500 Subject: [PATCH 2/8] Fix the typing rules for endorse and emit and treat interfaces as principals. --- src/ast/Arg.java | 2 +- src/ast/Emit.java | 50 ++++++++++++++++++- src/ast/Endorse.java | 26 +++------- .../applications/ERC20_nodepmap.scif | 6 +-- .../ifcTypechecking/EmitArgLow_W01.scif | 10 ++++ .../ifcTypechecking/EmitDependentLow_W01.scif | 10 ++++ test/contracts/ifcTypechecking/EmitIFC.scif | 16 ++++++ .../ifcTypechecking/EmitPcLow_W01.scif | 12 +++++ .../ifcTypechecking/EndorseFromLabel_W01.scif | 10 ++++ .../ifcTypechecking/EndorseLabels.scif | 10 ++++ .../ifcTypechecking/EndorsePcLow_W01.scif | 11 ++++ .../ifcTypechecking/EndorseToLabel_W01.scif | 10 ++++ test/contracts/ifcTypechecking/IOracle.scif | 3 ++ .../InterfaceLocalPrincipal.scif | 15 ++++++ .../InterfaceLocalPrincipal_W01.scif | 13 +++++ .../multiContract/DexibleWithEvents.scif | 2 +- test/src/TestIfcTypechecking.java | 10 ++++ 17 files changed, 190 insertions(+), 26 deletions(-) create mode 100644 test/contracts/ifcTypechecking/EmitArgLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/EmitDependentLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/EmitIFC.scif create mode 100644 test/contracts/ifcTypechecking/EmitPcLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/EndorseFromLabel_W01.scif create mode 100644 test/contracts/ifcTypechecking/EndorseLabels.scif create mode 100644 test/contracts/ifcTypechecking/EndorsePcLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/EndorseToLabel_W01.scif create mode 100644 test/contracts/ifcTypechecking/IOracle.scif create mode 100644 test/contracts/ifcTypechecking/InterfaceLocalPrincipal.scif create mode 100644 test/contracts/ifcTypechecking/InterfaceLocalPrincipal_W01.scif diff --git a/src/ast/Arg.java b/src/ast/Arg.java index 80717008..e57f6eb4 100644 --- a/src/ast/Arg.java +++ b/src/ast/Arg.java @@ -117,7 +117,7 @@ public void genConsVisit(VisitEnv env, boolean tail_position) throws SemanticExc if (annotation.label() != null) { varSym.setLabel(env.toLabel(annotation.label())); } - if (varSym.typeSym instanceof InterfaceSym || varSym.typeSym.getName().equals(Utils.ADDRESS_TYPE)) { + if (varSym.isPrincipalTyped()) { env.addPrincipal(varSym); } } diff --git a/src/ast/Emit.java b/src/ast/Emit.java index 2ea8a4c5..0040cbb9 100644 --- a/src/ast/Emit.java +++ b/src/ast/Emit.java @@ -7,7 +7,9 @@ import typecheck.sherrlocUtils.Relation; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class Emit extends Statement { @@ -83,10 +85,54 @@ public List solidityCodeGen(CompileEnv code) { @Override public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws SemanticException { - // TODO placeholder + Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); - return new PathOutcome(new PsiUnit(endContext)); + EventTypeSym eventTypeSym = (EventTypeSym) env.getCurSym(((Name) event.value).id); + + // Evaluate the arguments from left to right. + PathOutcome psi = new PathOutcome(new PsiUnit(beginContext)); + List argValueLabelNames = new ArrayList<>(); + List argPrincipalNames = new ArrayList<>(); + for (Expression arg : event.args) { + ExpOutcome ao = arg.genIFConstraints(env, false); + psi.joinExe(ao.psi); + argValueLabelNames.add(ao.valueLabelName); + argPrincipalNames.add(arg.principalName(env)); + env.inContext = Utils.genNewContextAndConstraints(env, false, + ao.psi.getNormalPath().c, beginContext.lambda, arg.nextPcSHL(), arg.location); + } + + // A parameter label naming another parameter names the principal passed for it. + Map dependentLabelMapping = new HashMap<>(); + for (int i = 0; i < event.args.size(); i++) { + VarSym param = eventTypeSym.parameters().get(i); + if (param.isPrincipalTyped() && argPrincipalNames.get(i) != null) { + dependentLabelMapping.put(param.toSHErrLocFmt(), argPrincipalNames.get(i)); + } + } + + // Each argument, and the control flow that emits it, must be trusted at the + // parameter's label, as for the arguments of a call. + String ifNamePc = env.inContext.pc; + for (int i = 0; i < event.args.size(); i++) { + Expression arg = event.args.get(i); + String paramLabel = eventTypeSym.parameters().get(i).ifl.toSHErrLocFmt(dependentLabelMapping); + env.checkFlow(argValueLabelNames.get(i), paramLabel, arg.location, + "Input to the " + Utils.ordNumString(i + 1) + + " argument of the event must be trusted enough"); + env.checkFlow(ifNamePc, paramLabel, arg.location, + "Current control flow must be trusted to emit the " + + Utils.ordNumString(i + 1) + " argument of the event"); + } + + Utils.contextFlow(env, env.inContext, endContext, location); + if (!tail_position) { + env.checkFlow(endContext.lambda, beginContext.lambda, location, + Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION); + } + psi.setNormalPath(endContext); + return psi; } @Override diff --git a/src/ast/Endorse.java b/src/ast/Endorse.java index a6e6254f..7ede5270 100644 --- a/src/ast/Endorse.java +++ b/src/ast/Endorse.java @@ -30,23 +30,19 @@ public Endorse(Expression value, IfLabel from, IfLabel to) { } public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { + from.genTypeConstraints(env, parent); + to.genTypeConstraints(env, parent); return value.genTypeConstraints(env, parent); } @Override public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { - //TODO: change to conditioned form - Context beginContext = env.inContext; - Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), - typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); ExpOutcome vo = value.genIFConstraints(env, tail_position); String ifNameValue = vo.valueLabelName; String ifNameRtn = scopeContext.getSHErrLocName() + "." + "endorse" + location.toString(); - String fromLabel = fromNameSLC(); - //from.toSherrlocFmt(scopeContext); - String toLabel = toNameSLC(); - //to.toSherrlocFmt(scopeContext); + String fromLabel = env.toLabel(from).toSHErrLocFmt(); + String toLabel = env.toLabel(to).toSHErrLocFmt(); env.checkFlow(ifNameValue, fromLabel, location, "The expression must be trusted to be endorsed"); @@ -54,10 +50,9 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S env.checkFlow(toLabel, ifNameRtn, location, "The integrity level of this expression would be endorsed"); - //env.outContext = endContext; - - env.checkTrustedBy(beginContext.pc, endContext.pc, location, - "The control flow of this expression would be endorsed"); + // Code can endorse data only up to the integrity of its own control flow. + env.checkFlow(vo.psi.getNormalPath().c.pc, toLabel, location, + "Control flow must be trusted at the label this expression is endorsed to"); // if (value instanceof Name n && env.getVar(n.id) != null // && env.getVar(n.id).typeSym instanceof ClosureTypeSym cs) { @@ -84,13 +79,6 @@ public compile.ast.Expression solidityCodeGen(List result, CompileEnv return value.solidityCodeGen(result, code); } - private String fromNameSLC() { - return scopeContext + ".endorse" + "." + "from" + from.location; - } - private String toNameSLC() { - return scopeContext + ".endorse" + "." + "to" + from.location; - } - @Override public boolean typeMatch(Expression expression) { return expression instanceof Endorse && diff --git a/test/contracts/applications/ERC20_nodepmap.scif b/test/contracts/applications/ERC20_nodepmap.scif index 3b03d614..7986f447 100644 --- a/test/contracts/applications/ERC20_nodepmap.scif +++ b/test/contracts/applications/ERC20_nodepmap.scif @@ -50,9 +50,9 @@ contract ERC20 { } public void approveFrom{from->this}(final address from, address spender, uint val) { - address _from = endorse(from, sender->this); - address _to = endorse(spender, sender->this); - uint _val = endorse(val, sender->this); + address _from = endorse(from, from->this); + address _to = endorse(spender, from->this); + uint _val = endorse(val, from->this); _allowances[from][spender] = val; } diff --git a/test/contracts/ifcTypechecking/EmitArgLow_W01.scif b/test/contracts/ifcTypechecking/EmitArgLow_W01.scif new file mode 100644 index 00000000..0e355b31 --- /dev/null +++ b/test/contracts/ifcTypechecking/EmitArgLow_W01.scif @@ -0,0 +1,10 @@ +// An event parameter labeled {this} (the default) cannot carry untrusted data. +contract EmitArgLow_W01 { + event Paid(address to, uint amount); + + constructor() { super(); } + + public void f{this}(address{this} to, uint{any} amount) { + emit Paid(to, amount); + } +} diff --git a/test/contracts/ifcTypechecking/EmitDependentLow_W01.scif b/test/contracts/ifcTypechecking/EmitDependentLow_W01.scif new file mode 100644 index 00000000..d233e722 --- /dev/null +++ b/test/contracts/ifcTypechecking/EmitDependentLow_W01.scif @@ -0,0 +1,10 @@ +// amount is labeled by who, so data trusted only by sender cannot be reported as trusted by this. +contract EmitDependentLow_W01 { + event Claimed(address who, uint{who} amount); + + constructor() { super(); } + + public void g{sender -> this; any}(uint{sender} amount) { + emit Claimed(address(this), amount); + } +} diff --git a/test/contracts/ifcTypechecking/EmitIFC.scif b/test/contracts/ifcTypechecking/EmitIFC.scif new file mode 100644 index 00000000..2cf45f1a --- /dev/null +++ b/test/contracts/ifcTypechecking/EmitIFC.scif @@ -0,0 +1,16 @@ +// Emitting trusted data under trusted control flow; a parameter label may name +// another parameter, which stands for the principal passed for it. +contract EmitIFC { + event Paid(address to, uint amount); + event Claimed(address who, uint{who} amount); + + constructor() { super(); } + + public void f{this}(address{this} to, uint{this} amount) { + emit Paid(to, amount); + } + + public void g{sender -> this; any}(uint{sender} amount) { + emit Claimed(sender, amount); + } +} diff --git a/test/contracts/ifcTypechecking/EmitPcLow_W01.scif b/test/contracts/ifcTypechecking/EmitPcLow_W01.scif new file mode 100644 index 00000000..3007fa2e --- /dev/null +++ b/test/contracts/ifcTypechecking/EmitPcLow_W01.scif @@ -0,0 +1,12 @@ +// Emitting under untrusted control flow tells observers something untrusted. +contract EmitPcLow_W01 { + event Paid(address to, uint amount); + + constructor() { super(); } + + public void f{this}(address{this} to, uint{this} amount, bool{any} c) { + if (c) { + emit Paid(to, amount); + } + } +} diff --git a/test/contracts/ifcTypechecking/EndorseFromLabel_W01.scif b/test/contracts/ifcTypechecking/EndorseFromLabel_W01.scif new file mode 100644 index 00000000..f2fb3099 --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorseFromLabel_W01.scif @@ -0,0 +1,10 @@ +// The endorsed value must flow to the from-label: v is not trusted by sender. +contract EndorseFromLabel_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(uint{any} v) { + secret = endorse(v, sender -> this); + } +} diff --git a/test/contracts/ifcTypechecking/EndorseLabels.scif b/test/contracts/ifcTypechecking/EndorseLabels.scif new file mode 100644 index 00000000..f87af71b --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorseLabels.scif @@ -0,0 +1,10 @@ +// An endorsement raises a value from its from-label to its to-label. +contract EndorseLabels { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(uint{any} v) { + secret = endorse(v, any -> this); + } +} diff --git a/test/contracts/ifcTypechecking/EndorsePcLow_W01.scif b/test/contracts/ifcTypechecking/EndorsePcLow_W01.scif new file mode 100644 index 00000000..90298bf7 --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorsePcLow_W01.scif @@ -0,0 +1,11 @@ +// Code under untrusted control flow cannot endorse data up to this, +// even if the result is used only where untrusted data may go. +contract EndorsePcLow_W01 { + constructor() { super(); } + + public void f{this}(uint{any} v, bool{any} c) { + if (c) { + uint{any} w = endorse(v, any -> this); + } + } +} diff --git a/test/contracts/ifcTypechecking/EndorseToLabel_W01.scif b/test/contracts/ifcTypechecking/EndorseToLabel_W01.scif new file mode 100644 index 00000000..2c309721 --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorseToLabel_W01.scif @@ -0,0 +1,10 @@ +// The result of an endorsement is trusted only at its to-label. +contract EndorseToLabel_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(uint{any} v) { + secret = endorse(v, any -> sender); + } +} diff --git a/test/contracts/ifcTypechecking/IOracle.scif b/test/contracts/ifcTypechecking/IOracle.scif new file mode 100644 index 00000000..3d45433d --- /dev/null +++ b/test/contracts/ifcTypechecking/IOracle.scif @@ -0,0 +1,3 @@ +interface IOracle { + public uint price{this}(); +} diff --git a/test/contracts/ifcTypechecking/InterfaceLocalPrincipal.scif b/test/contracts/ifcTypechecking/InterfaceLocalPrincipal.scif new file mode 100644 index 00000000..d6b9da37 --- /dev/null +++ b/test/contracts/ifcTypechecking/InterfaceLocalPrincipal.scif @@ -0,0 +1,15 @@ +import "./IOracle.scif"; +// An interface-typed local names the principal it is initialized to. +contract InterfaceLocalPrincipal { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a) { + if (a => this) { + final IOracle o = IOracle(a); + uint{o} w = 1; + secret = w; + } + } +} diff --git a/test/contracts/ifcTypechecking/InterfaceLocalPrincipal_W01.scif b/test/contracts/ifcTypechecking/InterfaceLocalPrincipal_W01.scif new file mode 100644 index 00000000..e0083966 --- /dev/null +++ b/test/contracts/ifcTypechecking/InterfaceLocalPrincipal_W01.scif @@ -0,0 +1,13 @@ +import "./IOracle.scif"; +// An interface-typed local names a principal, so data it trusts is not trusted by this. +contract InterfaceLocalPrincipal_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a) { + final IOracle o = IOracle(a); + uint{o} w = 1; + secret = w; + } +} diff --git a/test/contracts/multiContract/DexibleWithEvents.scif b/test/contracts/multiContract/DexibleWithEvents.scif index e6d6bedd..6ad05e3c 100644 --- a/test/contracts/multiContract/DexibleWithEvents.scif +++ b/test/contracts/multiContract/DexibleWithEvents.scif @@ -18,7 +18,7 @@ interface IExchange { contract Dexible { exception FailedSwap(); - event Swap(IERC20 tokenIn, IERC20 tokenOut, IExchange router, uint amount); + event Swap(IERC20{any} tokenIn, IERC20{any} tokenOut, IExchange{any} router, uint{any} amount); constructor() { super(); } diff --git a/test/src/TestIfcTypechecking.java b/test/src/TestIfcTypechecking.java index 30f55bd7..4ce90648 100644 --- a/test/src/TestIfcTypechecking.java +++ b/test/src/TestIfcTypechecking.java @@ -56,6 +56,9 @@ public class TestIfcTypechecking { "ifcTypechecking/ValueNowTrusted", "ifcTypechecking/TrustFactKept", "ifcTypechecking/FinalFieldInit", + "ifcTypechecking/InterfaceLocalPrincipal", + "ifcTypechecking/EndorseLabels", + "ifcTypechecking/EmitIFC", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -109,6 +112,13 @@ void testPositive(String contractName) throws Exception { "ifcTypechecking/TrustFactLoop_W01", "ifcTypechecking/TrustFactFieldCall_W01", "ifcTypechecking/NonFinalPrincipalLabel_W01", + "ifcTypechecking/InterfaceLocalPrincipal_W01", + "ifcTypechecking/EndorseFromLabel_W01", + "ifcTypechecking/EndorseToLabel_W01", + "ifcTypechecking/EndorsePcLow_W01", + "ifcTypechecking/EmitArgLow_W01", + "ifcTypechecking/EmitPcLow_W01", + "ifcTypechecking/EmitDependentLow_W01", // "examples/ERC20", }) void testNegative(String contractName) throws Exception { From da6e7482e60aa130fd6631fc1a2b18f93a1b0e1b Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Thu, 5 Nov 2026 16:06:20 -0500 Subject: [PATCH 3/8] Instantiate callee labels per polarity, check casts and new against the leaky semantics, and pick up SHErrLoc's hypothesis-soundness fixes. --- sherrloc | 2 +- src/ast/AnnAssign.java | 10 +- src/ast/Call.java | 70 ++++---- src/ast/New.java | 21 ++- src/typecheck/VisitEnv.java | 9 + src/typecheck/sherrlocUtils/Inequality.java | 43 ++++- .../applications/EthCrossChainManager.scif | 51 +++--- .../contracts/applications/Uniswap_ERC20.scif | 28 ++-- .../applications/Uniswap_ERC20_noe.scif | 28 ++-- .../applications/Uniswap_ERC777_noe.scif | 28 ++-- .../CallLendsAuthority_W01.scif | 12 ++ .../ifcTypechecking/CallTrustedReference.scif | 13 ++ .../CastUnknownContract_W01.scif | 10 ++ .../ClosureCrossContractBothThis.scif | 32 ++-- ...ClosureCrossContractUntrustedSink_W01.scif | 38 +++++ test/contracts/ifcTypechecking/IVault.scif | 3 + .../ifcTypechecking/JoinLabelPrincipal.scif | 12 ++ .../JoinLabelPrincipal_W01.scif | 10 ++ .../ifcTypechecking/NewEndorseTail.scif | 13 ++ test/contracts/ifcTypechecking/NewLocked.scif | 16 ++ .../ifcTypechecking/NewNonTail_W01.scif | 15 ++ test/contracts/ifcTypechecking/NewTail.scif | 13 ++ .../ifcTypechecking/NewUntrusted_W01.scif | 13 ++ .../TrustTestAfterBranch_W01.scif | 14 ++ .../TrustTestBeforeBranch_W01.scif | 13 ++ .../ifcTypechecking/TrustTestTwoBranches.scif | 14 ++ .../TrustTestTwoBranches_W01.scif | 15 ++ .../multiContract/EthCrossChainManager.scif | 155 ------------------ test/src/TestIfcTypechecking.java | 15 ++ 29 files changed, 433 insertions(+), 283 deletions(-) create mode 100644 test/contracts/ifcTypechecking/CallLendsAuthority_W01.scif create mode 100644 test/contracts/ifcTypechecking/CallTrustedReference.scif create mode 100644 test/contracts/ifcTypechecking/CastUnknownContract_W01.scif create mode 100644 test/contracts/ifcTypechecking/ClosureCrossContractUntrustedSink_W01.scif create mode 100644 test/contracts/ifcTypechecking/IVault.scif create mode 100644 test/contracts/ifcTypechecking/JoinLabelPrincipal.scif create mode 100644 test/contracts/ifcTypechecking/JoinLabelPrincipal_W01.scif create mode 100644 test/contracts/ifcTypechecking/NewEndorseTail.scif create mode 100644 test/contracts/ifcTypechecking/NewLocked.scif create mode 100644 test/contracts/ifcTypechecking/NewNonTail_W01.scif create mode 100644 test/contracts/ifcTypechecking/NewTail.scif create mode 100644 test/contracts/ifcTypechecking/NewUntrusted_W01.scif create mode 100644 test/contracts/ifcTypechecking/TrustTestAfterBranch_W01.scif create mode 100644 test/contracts/ifcTypechecking/TrustTestBeforeBranch_W01.scif create mode 100644 test/contracts/ifcTypechecking/TrustTestTwoBranches.scif create mode 100644 test/contracts/ifcTypechecking/TrustTestTwoBranches_W01.scif delete mode 100644 test/contracts/multiContract/EthCrossChainManager.scif diff --git a/sherrloc b/sherrloc index 12589003..82e16e83 160000 --- a/sherrloc +++ b/sherrloc @@ -1 +1 @@ -Subproject commit 125890034012022280d595bcb6740795dd38b687 +Subproject commit 82e16e83887c1158fe5cf48cd8909cc42cc1640d diff --git a/src/ast/AnnAssign.java b/src/ast/AnnAssign.java index 4a00db72..6bd07d98 100644 --- a/src/ast/AnnAssign.java +++ b/src/ast/AnnAssign.java @@ -151,13 +151,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic SLCNameVarLbl = varSym.labelNameSLC(); // // logger.debug(varSym.typeSym.getName()); - // A principal-typed variable starts out naming the principal it is initialized to. if (varSym.isPrincipalTyped()) { env.addPrincipal(varSym); - String init = value == null ? null : value.principalName(env); - if (init != null) { - env.assumeSamePrincipal(SLCNameVar, init); - } } String ifNamePc = Utils.getLabelNamePc(scopeContext.getSHErrLocName()); @@ -172,6 +167,11 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic if (value != null) { env.inContext = beginContext; ExpOutcome valueOutcome = value.genIFConstraints(env, scopeContext.isContractLevel()); + // A principal-typed variable starts out naming the principal it is initialized to. + String init = varSym.isPrincipalTyped() ? value.principalName(env) : null; + if (init != null) { + env.assumeSamePrincipal(SLCNameVar, init); + } env.checkFlow(valueOutcome.valueLabelName, SLCNameVarLbl, value.location, "Integrity of the value being assigned must be trusted to allow this assignment"); diff --git a/src/ast/Call.java b/src/ast/Call.java index 67821a67..04a21ef4 100644 --- a/src/ast/Call.java +++ b/src/ast/Call.java @@ -329,6 +329,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) InterfaceSym externalContractSym = null; VarSym externalTargetSym = null; String ifContRtn = null; + String receiverPrincipal = null; // the principal the receiver holds, if it is a variable if (!(value instanceof Name)) { if (!(value instanceof Attribute att)) { @@ -344,6 +345,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) vo = att.value.genIFConstraints(env, false); psi.joinExe(vo.psi); ifContRtn = vo.valueLabelName; // a..lbl + receiverPrincipal = att.value.principalName(env); //TODO: assuming a's depth is 1 String varName = ((Name) att.value).id; // a @@ -569,6 +571,14 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) } String ifNameArgValue = argValueLabelNames.get(0); + if (env.containsContract(funcName)) { + // A contract reference is trusted no more than the contract it + // names, which is the principal the cast address holds. + String principal = argPrincipalNames.get(0); + ifNameArgValue = principal != null + ? Utils.joinLabels(ifNameArgValue, principal) + : env.curContractSym().any().toSHErrLocFmt(); + } Utils.contextFlow(env, psi.getNormalPath().c, endContext, args.get(0).location); // env.outContext = endContext; @@ -611,42 +621,47 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) env.inContext = Utils.genNewContextAndConstraints(env, false, co.getNormalPath().c, beginContext.lambda, callSpec.nextPcSHL(), callSpec.location); } - // if external call and the target address is final, make callee.this equal to the target address - if (externalCall) { - if (externalTargetSym.isFinal) { - dependentLabelMapping.put( - externalContractSym.thisSym().toSHErrLocFmt(), - externalTargetSym.toSHErrLocFmt()); - } else { - dependentLabelMapping.put( - externalContractSym.thisSym().toSHErrLocFmt(), - ifContRtn); // XXX is this correct? Seems like this is the label of the contract value. + // A principal parameter of the callee names the principal passed for it. + for (int i = 0; i < args.size(); ++i) { + VarSym argSym = funcSym.parameters.get(i); + if (argSym.isPrincipalTyped() && argPrincipalNames.get(i) != null) { + dependentLabelMapping.put(argSym.toSHErrLocFmt(), argPrincipalNames.get(i)); } } + // The callee's labels are instantiated differently in input and output positions. + // Its inputs (argument labels, external pc) are demands it makes of this caller, + // so there the callee's `this` is the callee itself: the principal the receiver + // holds. This contract can meet those demands only with what it trusts itself, + // since it is the top principal here. Its outputs (return label, post-call pc, + // lock label) are promises trusted only as much as the callee, which acts for the + // label of the reference used to call it, so there its `this` becomes that label. + Map inputMapping = new HashMap<>(dependentLabelMapping); + Map outputMapping = new HashMap<>(dependentLabelMapping); + if (externalCall) { + String calleeThis = externalContractSym.thisSym().toSHErrLocFmt(); + inputMapping.put(calleeThis, receiverPrincipal != null + ? receiverPrincipal : env.thisSym().toSHErrLocFmt()); + outputMapping.put(calleeThis, ifContRtn); + } + // Generate constraints on arguments -// System.err.println("Call: " + funcName); for (int i = 0; i < args.size(); ++i) { Expression arg = args.get(i); VarSym argSym = funcSym.parameters.get(i); - if (argSym.isPrincipalTyped() && argPrincipalNames.get(i) != null) { - dependentLabelMapping.put(argSym.toSHErrLocFmt(), argPrincipalNames.get(i)); - } - - // env.prevContext = prevContext = tmp; String ifNameArgValue = argValueLabelNames.get(i); // argument's label name Label ifArgLabel = funcSym.getLabelArg(i); // parameter's label assert ifArgLabel != null : argSym.getName(); - env.checkFlow(ifNameArgValue, ifArgLabel.toSHErrLocFmt(dependentLabelMapping), + env.checkFlow(ifNameArgValue, ifArgLabel.toSHErrLocFmt(inputMapping), arg.location, "Input to the " + Utils.ordNumString(i + 1) + " argument must be trusted enough"); // arglbl => paramlbl - env.checkFlow(ifNamePc, ifArgLabel.toSHErrLocFmt(dependentLabelMapping), arg.location, + env.checkFlow(ifNamePc, ifArgLabel.toSHErrLocFmt(inputMapping), arg.location, "Current control flow must be trusted to feed the " + Utils.ordNumString(i + 1) + "-th argument value"); // pc => paramlbl if (argSym.typeSym instanceof ClosureTypeSym paramCs) { - ClosureCreation.checkFlowInto(arg, paramCs, dependentLabelMapping, env, arg.location, + ClosureCreation.checkFlowInto(arg, paramCs, inputMapping, env, arg.location, "Closure value flowing into the " + Utils.ordNumString(i + 1) + " argument must subtype its slot"); } } @@ -654,16 +669,15 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // Generate pre-call and post-call constraints and contexts if (externalCall) { -// String tem = ((Attribute) value).value.toSHErrLocFmt(); - env.checkFlow(ifContRtn, ifFuncCallPcBefore.toSHErrLocFmt(dependentLabelMapping), + env.checkFlow(ifContRtn, ifFuncCallPcBefore.toSHErrLocFmt(inputMapping), location, "Target contract must be trusted to call this method"); // target address..lbl => pc_ex } PathOutcome expPsi = new PathOutcome(new PsiUnit(new Context( - Utils.joinLabels(psi.getNormalPath().c.pc, funcSym.endPc().toSHErrLocFmt(dependentLabelMapping)), + Utils.joinLabels(psi.getNormalPath().c.pc, funcSym.endPc().toSHErrLocFmt(outputMapping)), // funcSym.getLabelNameCallGamma() - ifFuncGammaLock.toSHErrLocFmt(dependentLabelMapping) + ifFuncGammaLock.toSHErrLocFmt(outputMapping) ))); // post-call psi = (current normal path pc ⊔ function return label, gamma) for (Entry exp : funcSym.exceptions.entrySet()) { @@ -681,7 +695,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) expPsi.set(curSym, new PsiUnit( new Context( Utils.joinLabels(expLabelName, funcSym.externalPcSLC()), - ifFuncGammaLock.toSHErrLocFmt(dependentLabelMapping)), + ifFuncGammaLock.toSHErrLocFmt(outputMapping)), true) ); // exception-path post-call psi = (function exception-path pc ⊔ pc_ex, gamma) // TODO: should pc label get joined by pc_ex, or psi.normal.pc? @@ -693,7 +707,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) //TODO - env.checkFlow(ifNamePc, ifFuncCallPcBefore.toSHErrLocFmt(dependentLabelMapping), location, + env.checkFlow(ifNamePc, ifFuncCallPcBefore.toSHErrLocFmt(inputMapping), location, "Current control flow must be trusted to call this method"); // current pc => pc_ex // pc_2 dropped // env.cons.add(new Constraint(new Inequality(ifFuncCallPcBefore.toSHErrLocFmt(dependentLabelMapping), @@ -705,7 +719,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) if (externalCall) { // target address..lbl ⊔ gamma == post-call lambda env.checkEqual( - Utils.joinLabels(ifContRtn, ifFuncGammaLock.toSHErrLocFmt(dependentLabelMapping)), + Utils.joinLabels(ifContRtn, ifFuncGammaLock.toSHErrLocFmt(outputMapping)), endContext.lambda, location, "Calling this function does not respect static reentrancy locks"); } @@ -726,11 +740,11 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // Finalize - String ifNameFuncRtnValue = funcSym.rtn.toSHErrLocFmt(dependentLabelMapping); + String ifNameFuncRtnValue = funcSym.rtn.toSHErrLocFmt(outputMapping); // realLabels if (funcSym.returnType instanceof ClosureTypeSym retCs) { - this.realLabels = retCs.renderLabels(dependentLabelMapping); + this.realLabels = retCs.renderLabels(outputMapping); } psi.joinExe(expPsi); diff --git a/src/ast/New.java b/src/ast/New.java index ea7507bf..143ad589 100644 --- a/src/ast/New.java +++ b/src/ast/New.java @@ -28,6 +28,8 @@ public class New extends Expression { Call constructor_call; boolean isConstructor = false; + /** The principal of the contract this expression creates, once IFC-visited. */ + private String createdPrincipal = null; public New(Call constructor_call) { this.constructor_call = constructor_call; @@ -109,6 +111,11 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S return result; } + @Override + public String principalName(VisitEnv env) { + return createdPrincipal; + } + private ExpOutcome newIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { if (!isConstructor) { // creating a struct var returns a join of its arg values @@ -175,6 +182,12 @@ private ExpOutcome newIFConstraints(VisitEnv env, boolean tail_position) throws ifNamePc = Utils.getLabelNamePc(scopeContext.getSHErrLocName()); dependentLabelMapping.put(funcSym.sender().toSHErrLocFmt(), env.thisSym().toSHErrLocFmt()); + // The new contract is a principal about which nothing is known; in the + // constructor's labels, its `this` is that principal. + createdPrincipal = env.freshPrincipal(toSHErrLocFmt() + "..new"); + typecheck.InterfaceSym createdSym = + env.getContract(((Name) constructor_call.value).id); + dependentLabelMapping.put(createdSym.thisSym().toSHErrLocFmt(), createdPrincipal); ifFuncCallPcBefore = funcSym.externalPc(); ifFuncCallPcAfter = funcSym.internalPc(); @@ -239,11 +252,13 @@ private ExpOutcome newIFConstraints(VisitEnv env, boolean tail_position) throws typecheck.Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION); } - String ifNameFuncRtnValue = funcSym.rtn.toSHErrLocFmt(dependentLabelMapping); - // String ifNameFuncRtnLock = funcSym.getLabelNameRtnLock(); + // Creating a contract does not make it trusted: the reference is trusted + // no more than the new contract, and no more than the control flow that + // decided to create it. + String ifNameRtn = Utils.joinLabels(ifNamePc, createdPrincipal); psi.joinExe(expPsi); - return new ExpOutcome(ifNameFuncRtnValue, psi); + return new ExpOutcome(ifNameRtn, psi); } } diff --git a/src/typecheck/VisitEnv.java b/src/typecheck/VisitEnv.java index e122afbb..b8f3084d 100644 --- a/src/typecheck/VisitEnv.java +++ b/src/typecheck/VisitEnv.java @@ -258,6 +258,15 @@ public void assumeSamePrincipal(String a, String b) { addAtomAssumption(a, b); } + /** + * Registers {@code name} as a principal about which nothing is known, such as a + * newly created contract, and returns it. + */ + public String freshPrincipal(String name) { + versionNames.add(name); + return name; + } + /** The SHErrLoc names of all versions created so far. Each names a principal. */ public Set versionNames() { return versionNames; diff --git a/src/typecheck/sherrlocUtils/Inequality.java b/src/typecheck/sherrlocUtils/Inequality.java index eaad2e0c..b9fa1cdf 100644 --- a/src/typecheck/sherrlocUtils/Inequality.java +++ b/src/typecheck/sherrlocUtils/Inequality.java @@ -4,6 +4,8 @@ import sherrloc.diagnostic.SherrlocDiagnoser; import java.util.List; +import sherrloc.constraint.ast.Element; +import sherrloc.constraint.ast.Position; // a <= b means a flows to b // that is, a >= b in terms of integrity level @@ -57,8 +59,8 @@ public String toSherrlocFmt() { } public sherrloc.constraint.ast.Inequality toSherrlocInequality(SherrlocDiagnoser diagnoser) { - var e = diagnoser.createElement(lhs, sherrloc.constraint.ast.Position.EmptyPosition()); - var o = diagnoser.createElement(rhs, sherrloc.constraint.ast.Position.EmptyPosition()); + var e = toElement(lhs, diagnoser); + var o = toElement(rhs, diagnoser); if (relation == Relation.EQ) { return diagnoser.createEqualityConstraint(e, o); @@ -68,4 +70,41 @@ public sherrloc.constraint.ast.Inequality toSherrlocInequality(SherrlocDiagnoser return diagnoser.createGreaterThanConstraint(e, o); } } + + /** + * The element denoted by {@code label}, a name or a join or meet written as + * {@code (l ⊔ r)} or {@code (l ⊓ r)}. Each name resolves to the constructor or + * variable that {@code diagnoser} knows by that name. + */ + private static Element toElement(String label, SherrlocDiagnoser diagnoser) { + // A composite string handed to the diagnoser whole would be parsed with + // empty tables, turning every name inside it into a fresh variable. + if (label.startsWith("(") && label.endsWith(")")) { + String inner = label.substring(1, label.length() - 1); + int depth = 0; + for (int i = 0; i < inner.length(); i++) { + char c = inner.charAt(i); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth < 0) { + break; // the outer parentheses do not enclose the whole label + } + } else if (depth == 0 && (c == '⊔' || c == '⊓') + && i > 0 && inner.charAt(i - 1) == ' ' && i + 1 < inner.length() + && inner.charAt(i + 1) == ' ') { + List operands = List.of( + toElement(inner.substring(0, i - 1), diagnoser), + toElement(inner.substring(i + 2), diagnoser)); + return c == '⊔' ? diagnoser.createJoinListElement(operands) + : diagnoser.createMeetListElement(operands); + } + } + if (depth == 0) { + return toElement(inner, diagnoser); + } + } + return diagnoser.createElement(label, Position.EmptyPosition()); + } } diff --git a/test/contracts/applications/EthCrossChainManager.scif b/test/contracts/applications/EthCrossChainManager.scif index d4b4de7d..95e6a3e3 100644 --- a/test/contracts/applications/EthCrossChainManager.scif +++ b/test/contracts/applications/EthCrossChainManager.scif @@ -3,22 +3,22 @@ import "./IECCUtils.scif"; import "./IEthCrossChainData.scif"; contract EthCrossChainManager { - address EthCrossChainDataAddress; - address EthECCUtilsAddress; + final IEthCrossChainData{this} eccd; + final IECCUtils{this} cutils; constructor(address _eccd, address _cutils) { - EthCrossChainDataAddress = endorse(_eccd, sender->this); - EthECCUtilsAddress = endorse(_cutils, sender->this); + eccd = endorse(IEthCrossChainData(_eccd), (sender ⨆ _eccd) -> this); + cutils = endorse(IECCUtils(_cutils), (sender ⨆ _cutils) -> this); super(); } public void initGenesisBlock(Header@IECCUtils header, bytes pubKeyList) { - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); address[] keepers = eccd.getCurEpochConPubKeys(); assert(keepers.length() == 0); - // Parse header and convit the public keys into nextBookKeeper and compare it with header.nextBookKeeper to verify the validity of signature + // Parse header and convit the public keys into nextBookKeeper + // and compare it with header.nextBookKeeper to verify the + // validity of signature address nextBookkeeper = cutils.verifyPubkey(keepers); endorse([header], sender->this) when (header.nextBookkeeper == nextBookkeeper) { @@ -31,8 +31,6 @@ contract EthCrossChainManager { } public void changeBookKeeper(Header@IECCUtils header, address[] polyChainBKs, bytes sigList) { - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); // Make sure rawHeader.height is higher than recorded current epoch start height uint curEpochStartHeight = eccd.getCurEpochStartHeight(); @@ -42,12 +40,14 @@ contract EthCrossChainManager { (header.nextBookkeeper != address(0)) && (cutils.verifySig(header, sigList, polyChainBKs, polyChainBKs.length() - (polyChainBKs.length() - 1) / 3))) { - // Convert pubKeyList into ethereum address format and make sure the compound address from the converted ethereum addresses - // equals passed in header.nextBooker + // Convert pubKeyList into ethereum address format and make + // sure the compound address from the converted ethereum + // addresses equals passed in header.nextBooker address nextBookKeeper = cutils.verifyPubkey(polyChainBKs); assert(header.nextBookkeeper == nextBookKeeper); - // update current epoch start height of Poly chain and current epoch consensus peers book keepers addresses + // update current epoch start height of Poly chain and current + // epoch consensus peers book keepers addresses eccd.putCurEpochStartHeight(header.height); eccd.putCurEpochConPubKeys(polyChainBKs); } else { @@ -56,8 +56,6 @@ contract EthCrossChainManager { } public void crossChain(uint toChainId, address toContract, bytes txData) { - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); // To help differentiate two txs, the ethTxHashIndex is increasing automatically uint txHashIndex = eccd.getEthTxHashIndex(); @@ -70,11 +68,15 @@ contract EthCrossChainManager { eccd.putEthTxHash(cutils.keccak256(rawParam)); } - public void verifyHeaderAndExecuteTx(bytes proof, Header@IECCUtils header, bytes headerProof, Header@IECCUtils curHeader, bytes headerSig) { - // Load ehereum cross chain data contract - // Get stored consensus public key bytes of current poly chain epoch and deserialize Poly chain consensus public key bytes to address[] - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); + public void verifyHeaderAndExecuteTx{sender -> this; any}(bytes proof, + Header@IECCUtils header, + bytes headerProof, + Header@IECCUtils curHeader, + bytes headerSig) + { + // Load Ethereum cross-chain data contract. + // Get stored consensus public key bytes of current poly chain epoch + // and deserialize Poly chain consensus public key bytes to address[] assert eccd => this; assert cutils => this; @@ -94,7 +96,8 @@ contract EthCrossChainManager { assert(cutils.getHeaderHash(header) == proveValue); } - // Parse the toMerkleValue struct and make sure the tx has not been processed, then mark this tx as processed + // Parse the toMerkleValue struct and make sure the tx has not been + // processed, then mark this tx as processed. ToMerkleValue@IECCUtils toMerkleValue = cutils.merkleProve(proof, header.crossStatesRoot); assert(!eccd.checkIfFromChainTxExist(toMerkleValue.fromChainID, toMerkleValue.txHash)); eccd.markFromChainTxExist(toMerkleValue.fromChainID, toMerkleValue.txHash); @@ -102,10 +105,12 @@ contract EthCrossChainManager { // Ethereum ChainId is 2, we need to check the transaction is for Ethereum network assert(toMerkleValue.toChainId == 2); - // Obtain the targeting contract, so that Ethereum cross chain manager contract can trigger the executation of cross chain tx on Ethereum side + // Obtain the targeting contract, so that Ethereum cross chain manager + // contract can trigger the execution of cross chain tx on Ethereum + // side address toContract = toMerkleValue.toContract; - IPolyCallee callee = IPolyCallee(toContract); + IPolyCallee{toContract} callee = IPolyCallee(toContract); callee.polyCallback(sender, toMerkleValue.args, toMerkleValue.fromContract, toMerkleValue.fromChainID); } -} \ No newline at end of file +} diff --git a/test/contracts/applications/Uniswap_ERC20.scif b/test/contracts/applications/Uniswap_ERC20.scif index fddfc887..75e4a6e3 100644 --- a/test/contracts/applications/Uniswap_ERC20.scif +++ b/test/contracts/applications/Uniswap_ERC20.scif @@ -9,7 +9,7 @@ contract Uniswap extends ERC20 { constructor(address token_addr, bytes _name, bytes _symbol) { assert (address(token) == address(0) && token_addr != address(0)); - token = endorse(IERC20(token_addr), sender->this); + token = endorse(IERC20(token_addr), (sender ⨆ token_addr) -> this); super(endorse(_name, sender->this), endorse(_symbol, sender->this)); } @@ -206,9 +206,9 @@ contract Uniswap extends ERC20 { return tokenToEthOutput(endorse(eth_bought, sender->this), max_tokens, deadline, sender, endorse(recipient, sender->this)); } - uint tokenToTokenInput(uint tokens_sold, uint{any} min_tokens_bought, uint{any} min_eth_bought, uint{any} deadline, address buyer, address recipient, address exchange_addr) { + uint tokenToTokenInput(uint tokens_sold, uint{any} min_tokens_bought, uint{any} min_eth_bought, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { assert ((deadline >= now && tokens_sold > 0) && (min_tokens_bought > 0 && min_eth_bought > 0)); - assert (exchange_addr != address(this) && exchange_addr != address(0)); + assert (address(exchange) != address(this) && address(exchange) != address(0)); uint token_reserve = token.balanceOf(address(this)); uint eth_bought = getInputPrice(tokens_sold, token_reserve, balance(address(this))); uint wei_bought = eth_bought; @@ -222,25 +222,23 @@ contract Uniswap extends ERC20 { assert false; } } - Uniswap exchange = Uniswap(exchange_addr); uint tokens_bought = exchange.ethToTokenTransferInput(min_tokens_bought, deadline, recipient){value = wei_bought}; return tokens_bought; } public uint tokenToTokenSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToTokenTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); } - uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, address exchange_addr) { + uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { assert (deadline >= now && (tokens_bought > 0 && max_eth_sold > 0)); - assert (exchange_addr != address(this) && exchange_addr != address(0)); - Uniswap exchange = Uniswap(exchange_addr); + assert (address(exchange) != address(this) && address(exchange) != address(0)); uint eth_bought = exchange.getEthToTokenOutputPrice(tokens_bought); uint token_reserve = token.balanceOf(address(this)); uint tokens_sold = getOutputPrice(eth_bought, token_reserve, balance(address(this))); @@ -260,30 +258,30 @@ contract Uniswap extends ERC20 { public uint tokenToTokenSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToTokenTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToExchangeSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address exchange_addr) { - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address exchange_addr) { assert (recipient != address(this)); - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address exchange_addr) { - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address exchange_addr) { assert (recipient != address(this)); - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), any -> this)); } public uint getEthToTokenInputPrice(uint eth_sold) { diff --git a/test/contracts/applications/Uniswap_ERC20_noe.scif b/test/contracts/applications/Uniswap_ERC20_noe.scif index 6102a5e2..ddc07804 100644 --- a/test/contracts/applications/Uniswap_ERC20_noe.scif +++ b/test/contracts/applications/Uniswap_ERC20_noe.scif @@ -9,7 +9,7 @@ contract Uniswap extends ERC20 { constructor(address token_addr, bytes _name, bytes _symbol) { assert (address(token) == address(0) && token_addr != address(0)); - token = endorse(IERC20(token_addr), sender->this); + token = endorse(IERC20(token_addr), (sender ⨆ token_addr) -> this); super(endorse(_name, sender->this), endorse(_symbol, sender->this)); } @@ -170,9 +170,9 @@ contract Uniswap extends ERC20 { return tokenToEthOutput(endorse(eth_bought, sender->this), max_tokens, deadline, sender, endorse(recipient, sender->this)); } - uint tokenToTokenInput(uint tokens_sold, uint{any} min_tokens_bought, uint{any} min_eth_bought, uint{any} deadline, address buyer, address recipient, address exchange_addr) { + uint tokenToTokenInput(uint tokens_sold, uint{any} min_tokens_bought, uint{any} min_eth_bought, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { assert ((deadline >= now && tokens_sold > 0) && (min_tokens_bought > 0 && min_eth_bought > 0)); - assert (exchange_addr != address(this) && exchange_addr != address(0)); + assert (address(exchange) != address(this) && address(exchange) != address(0)); uint token_reserve = token.balanceOf(address(this)); uint eth_bought = getInputPrice(tokens_sold, token_reserve, balance(address(this))); uint wei_bought = eth_bought; @@ -180,25 +180,23 @@ contract Uniswap extends ERC20 { atomic { token.transferFrom(buyer, address(this), tokens_sold); } - Uniswap exchange = Uniswap(exchange_addr); uint tokens_bought = exchange.ethToTokenTransferInput(min_tokens_bought, deadline, recipient){value = wei_bought}; return tokens_bought; } public uint tokenToTokenSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToTokenTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); } - uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, address exchange_addr) { + uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { assert (deadline >= now && (tokens_bought > 0 && max_eth_sold > 0)); - assert (exchange_addr != address(this) && exchange_addr != address(0)); - Uniswap exchange = Uniswap(exchange_addr); + assert (address(exchange) != address(this) && address(exchange) != address(0)); uint eth_bought = exchange.getEthToTokenOutputPrice(tokens_bought); uint token_reserve = token.balanceOf(address(this)); uint tokens_sold = getOutputPrice(eth_bought, token_reserve, balance(address(this))); @@ -212,30 +210,30 @@ contract Uniswap extends ERC20 { public uint tokenToTokenSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToTokenTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToExchangeSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address exchange_addr) { - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address exchange_addr) { assert (recipient != address(this)); - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address exchange_addr) { - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address exchange_addr) { assert (recipient != address(this)); - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), any -> this)); } public uint getEthToTokenInputPrice(uint eth_sold) { diff --git a/test/contracts/applications/Uniswap_ERC777_noe.scif b/test/contracts/applications/Uniswap_ERC777_noe.scif index 3271ab55..05fe05fc 100644 --- a/test/contracts/applications/Uniswap_ERC777_noe.scif +++ b/test/contracts/applications/Uniswap_ERC777_noe.scif @@ -7,7 +7,7 @@ contract Uniswap extends ERC20 { constructor(address token_addr, bytes _name, bytes _symbol) { assert (address(token) == address(0) && token_addr != address(0)); - token = endorse(IERC777(token_addr), sender->this); + token = endorse(IERC777(token_addr), (sender ⨆ token_addr) -> this); super(endorse(_name, sender->this), endorse(_symbol, sender->this)); } @@ -176,9 +176,9 @@ contract Uniswap extends ERC20 { return tokenToEthOutput(endorse(eth_bought, sender->this), max_tokens, deadline, sender, endorse(recipient, sender->this)); } - uint tokenToTokenInput(uint tokens_sold, uint{any} min_tokens_bought, uint{any} min_eth_bought, uint{any} deadline, address buyer, address recipient, address exchange_addr) { + uint tokenToTokenInput(uint tokens_sold, uint{any} min_tokens_bought, uint{any} min_eth_bought, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { assert ((deadline >= now && tokens_sold > 0) && (min_tokens_bought > 0 && min_eth_bought > 0)); - assert (exchange_addr != address(this) && exchange_addr != address(0)); + assert (address(exchange) != address(this) && address(exchange) != address(0)); uint token_reserve = token.balanceOf(address(this)); uint eth_bought = getInputPrice(tokens_sold, token_reserve, balance(address(this))); uint wei_bought = eth_bought; @@ -188,25 +188,23 @@ contract Uniswap extends ERC20 { token.transferFrom(buyer, address(this), tokens_sold); } } - Uniswap exchange = Uniswap(exchange_addr); uint tokens_bought = exchange.ethToTokenTransferInput(min_tokens_bought, deadline, recipient){value = wei_bought}; return tokens_bought; } public uint tokenToTokenSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToTokenTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); } - uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, address exchange_addr) { + uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { assert (deadline >= now && (tokens_bought > 0 && max_eth_sold > 0)); - assert (exchange_addr != address(this) && exchange_addr != address(0)); - Uniswap exchange = Uniswap(exchange_addr); + assert (address(exchange) != address(this) && address(exchange) != address(0)); uint eth_bought = exchange.getEthToTokenOutputPrice(tokens_bought); uint token_reserve = token.balanceOf(address(this)); uint tokens_sold = getOutputPrice(eth_bought, token_reserve, balance(address(this))); @@ -222,30 +220,30 @@ contract Uniswap extends ERC20 { public uint tokenToTokenSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToTokenTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address token_addr) { address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); } public uint tokenToExchangeSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address exchange_addr) { - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address exchange_addr) { assert (recipient != address(this)); - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address exchange_addr) { - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), any -> this)); } public uint tokenToExchangeTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address exchange_addr) { assert (recipient != address(this)); - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(exchange_addr, sender->this)); + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), any -> this)); } public uint getEthToTokenInputPrice(uint eth_sold) { diff --git a/test/contracts/ifcTypechecking/CallLendsAuthority_W01.scif b/test/contracts/ifcTypechecking/CallLendsAuthority_W01.scif new file mode 100644 index 00000000..a9c9812e --- /dev/null +++ b/test/contracts/ifcTypechecking/CallLendsAuthority_W01.scif @@ -0,0 +1,12 @@ +import "./IVault.scif"; +// The callee demands data and control flow trusted by itself. A caller vouches +// for them with its own authority, so it must trust them itself, whoever chose +// the reference: otherwise anyone could use relay to set a price in a vault +// that trusts this contract. +contract CallLendsAuthority_W01 { + constructor() { super(); } + + public void relay{any}(IVault{any} v, uint{any} p) { + v.setPrice(p); + } +} diff --git a/test/contracts/ifcTypechecking/CallTrustedReference.scif b/test/contracts/ifcTypechecking/CallTrustedReference.scif new file mode 100644 index 00000000..3a605101 --- /dev/null +++ b/test/contracts/ifcTypechecking/CallTrustedReference.scif @@ -0,0 +1,13 @@ +import "./IOracle.scif"; +// Endorsing a reference states that the contract it names is trusted; the +// results of calls through it are then trusted as much as the reference. +contract CallTrustedReference { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a) { + IOracle{this} o = endorse(IOracle(a), a -> this); + secret = o.price(); + } +} diff --git a/test/contracts/ifcTypechecking/CastUnknownContract_W01.scif b/test/contracts/ifcTypechecking/CastUnknownContract_W01.scif new file mode 100644 index 00000000..025c2d71 --- /dev/null +++ b/test/contracts/ifcTypechecking/CastUnknownContract_W01.scif @@ -0,0 +1,10 @@ +import "./IOracle.scif"; +// A reference is trusted no more than the contract it names: a trusted address +// may still name an untrusted contract. +contract CastUnknownContract_W01 { + constructor() { super(); } + + public void f{this}(address{this} a) { + IOracle{this} o = IOracle(a); + } +} diff --git a/test/contracts/ifcTypechecking/ClosureCrossContractBothThis.scif b/test/contracts/ifcTypechecking/ClosureCrossContractBothThis.scif index 541e3e51..6166110f 100644 --- a/test/contracts/ifcTypechecking/ClosureCrossContractBothThis.scif +++ b/test/contracts/ifcTypechecking/ClosureCrossContractBothThis.scif @@ -2,26 +2,14 @@ // // Caller builds a closure value of type // (fun {this -> this; this} () -> void) {this} -// where every `this` resolves, in Caller's scope, to Caller's contract -// identity. The closure value flows out via `Sink.accept` whose -// parameter type uses the same syntactic shape, with `this` resolving -// in Sink's scope to Sink's contract identity. The two `this`'s are -// syntactically the same but logically different principals. -// -// IFC accepts the contract — and that acceptance is correct for the -// single-hop case. The reason: SCIF's normal external-call -// dependent-label substitution rewrites the parameter slot's -// `Sink.this` to the receiver's label (Caller's `sink` field, which -// defaults to {this} = Caller.this). After substitution, the closure's -// inner labels (Caller.this) and the parameter slot's substituted -// labels (Caller.this) are equal, so the variance constraints emitted -// by the closure-argument hook in Call.genIFConstraints reduce to -// trivially true. -// -// This test exists to lock in that single-hop behavior. Multi-hop -// (Caller → Intermediate → Other) is the remaining open case: the -// substitution chain breaks after the first hop. See G2 in -// IMPLEMENTATION.md. +// where every `this` is Caller. It passes the closure to Sink.accept, +// whose parameter type has the same syntax but where every `this` is +// Sink: Sink promises to invoke the closure with control flow trusted +// by Sink. At the call, Sink's `this` is the principal the receiver +// `sink` holds, so the closure fits the slot only if Sink acts for +// Caller. Handing Sink a closure that demands Caller-trusted control +// flow delegates Caller's authority, so Caller checks that it trusts +// Sink before doing so. interface IT { closurable public void op{this -> this}(); } @@ -42,7 +30,9 @@ contract Caller { public void forward{this -> this; this}() { (fun {this -> this; this} () -> void) {this} c = closure t.op(); - sink.accept(c); + if (sink => this) { + sink.accept(c); + } } } diff --git a/test/contracts/ifcTypechecking/ClosureCrossContractUntrustedSink_W01.scif b/test/contracts/ifcTypechecking/ClosureCrossContractUntrustedSink_W01.scif new file mode 100644 index 00000000..bfec02f6 --- /dev/null +++ b/test/contracts/ifcTypechecking/ClosureCrossContractUntrustedSink_W01.scif @@ -0,0 +1,38 @@ +// Caller hands Sink a closure that demands control flow trusted by +// Caller, but Sink promises only control flow trusted by Sink. Without +// knowing that Sink acts for Caller, this delegates Caller's authority. +interface IT { + closurable public void op{this -> this}(); +} + +interface ISink { + public void accept{this -> this; this}( + (fun {this -> this; this} () -> void) {this} c + ); +} + +contract Caller { + IT t; + ISink sink; + + constructor() { + super(); + } + + public void forward{this -> this; this}() { + (fun {this -> this; this} () -> void) {this} c = closure t.op(); + sink.accept(c); + } +} + +contract Sink { + constructor() { + super(); + } + + public void accept{this -> this; this}( + (fun {this -> this; this} () -> void) {this} c + ) { + c.invoke(); + } +} diff --git a/test/contracts/ifcTypechecking/IVault.scif b/test/contracts/ifcTypechecking/IVault.scif new file mode 100644 index 00000000..a6be1d55 --- /dev/null +++ b/test/contracts/ifcTypechecking/IVault.scif @@ -0,0 +1,3 @@ +interface IVault { + public void setPrice{this}(uint{this} p); +} diff --git a/test/contracts/ifcTypechecking/JoinLabelPrincipal.scif b/test/contracts/ifcTypechecking/JoinLabelPrincipal.scif new file mode 100644 index 00000000..cf8db4ff --- /dev/null +++ b/test/contracts/ifcTypechecking/JoinLabelPrincipal.scif @@ -0,0 +1,12 @@ +// Once a is known to be trusted, data labeled with a join of a and this is trusted. +contract JoinLabelPrincipal { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a, uint{a ⨆ this} x) { + if (a => this) { + secret = x; + } + } +} diff --git a/test/contracts/ifcTypechecking/JoinLabelPrincipal_W01.scif b/test/contracts/ifcTypechecking/JoinLabelPrincipal_W01.scif new file mode 100644 index 00000000..b0ac42b0 --- /dev/null +++ b/test/contracts/ifcTypechecking/JoinLabelPrincipal_W01.scif @@ -0,0 +1,10 @@ +// A principal inside a join is the principal itself, not something the solver may choose. +contract JoinLabelPrincipal_W01 { + uint{this} secret; + + constructor() { super(); } + + public void f{this}(address{this} a, uint{a ⨆ this} x) { + secret = x; + } +} diff --git a/test/contracts/ifcTypechecking/NewEndorseTail.scif b/test/contracts/ifcTypechecking/NewEndorseTail.scif new file mode 100644 index 00000000..88e3f33b --- /dev/null +++ b/test/contracts/ifcTypechecking/NewEndorseTail.scif @@ -0,0 +1,13 @@ +// A trusted control flow can endorse a reference to a contract it has just +// created. +contract Child { + constructor() { super(); } +} + +contract NewEndorseTail { + constructor() { super(); } + + public Child{this} f{this}() { + return endorse(new Child(), any -> this); + } +} diff --git a/test/contracts/ifcTypechecking/NewLocked.scif b/test/contracts/ifcTypechecking/NewLocked.scif new file mode 100644 index 00000000..305bea1b --- /dev/null +++ b/test/contracts/ifcTypechecking/NewLocked.scif @@ -0,0 +1,16 @@ +// Creating a contract under a dynamic lock need not be the last operation. +contract Child { + constructor() { super(); } +} + +contract NewLocked { + uint{this} count; + constructor() { super(); } + + public void f{this}() { + lock(this) { + Child{any} c = new Child(); + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/NewNonTail_W01.scif b/test/contracts/ifcTypechecking/NewNonTail_W01.scif new file mode 100644 index 00000000..914b3bea --- /dev/null +++ b/test/contracts/ifcTypechecking/NewNonTail_W01.scif @@ -0,0 +1,15 @@ +// Creating a contract runs code with the new contract's authority, which +// nothing trusts. Without a dynamic lock, it must be the last operation. +contract Child { + constructor() { super(); } +} + +contract NewNonTail_W01 { + uint{this} count; + constructor() { super(); } + + public void f{this}() { + Child{any} c = new Child(); + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/NewTail.scif b/test/contracts/ifcTypechecking/NewTail.scif new file mode 100644 index 00000000..0eab08e8 --- /dev/null +++ b/test/contracts/ifcTypechecking/NewTail.scif @@ -0,0 +1,13 @@ +// A reference to a newly created contract is trusted by nobody, and can be +// returned as such. +contract Child { + constructor() { super(); } +} + +contract NewTail { + constructor() { super(); } + + public Child{any} f{this}() { + return new Child(); + } +} diff --git a/test/contracts/ifcTypechecking/NewUntrusted_W01.scif b/test/contracts/ifcTypechecking/NewUntrusted_W01.scif new file mode 100644 index 00000000..2edb77fa --- /dev/null +++ b/test/contracts/ifcTypechecking/NewUntrusted_W01.scif @@ -0,0 +1,13 @@ +// A newly created contract is a principal that nothing trusts, so a reference +// to it cannot be returned as trusted. +contract Child { + constructor() { super(); } +} + +contract NewUntrusted_W01 { + constructor() { super(); } + + public Child{this} f{this}() { + return new Child(); + } +} diff --git a/test/contracts/ifcTypechecking/TrustTestAfterBranch_W01.scif b/test/contracts/ifcTypechecking/TrustTestAfterBranch_W01.scif new file mode 100644 index 00000000..351656a1 --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustTestAfterBranch_W01.scif @@ -0,0 +1,14 @@ +// The trust test justifies the flow inside its branch only; the same flow +// after the branch must still be rejected. +contract TrustTestAfterBranch_W01 { + uint{this} secret; + uint{this} other; + constructor() { super(); } + + public void f{this}(address{this} a, uint{a} x) { + if (a => this) { + other = x; + } + secret = x; + } +} diff --git a/test/contracts/ifcTypechecking/TrustTestBeforeBranch_W01.scif b/test/contracts/ifcTypechecking/TrustTestBeforeBranch_W01.scif new file mode 100644 index 00000000..95e9446f --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustTestBeforeBranch_W01.scif @@ -0,0 +1,13 @@ +// The same flow before the branch is not justified by the trust test. +contract TrustTestBeforeBranch_W01 { + uint{this} secret; + uint{this} other; + constructor() { super(); } + + public void f{this}(address{this} a, uint{a} x) { + secret = x; + if (a => this) { + other = x; + } + } +} diff --git a/test/contracts/ifcTypechecking/TrustTestTwoBranches.scif b/test/contracts/ifcTypechecking/TrustTestTwoBranches.scif new file mode 100644 index 00000000..2848cf41 --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustTestTwoBranches.scif @@ -0,0 +1,14 @@ +// Both flows are justified by the trust tests that guard them. +contract TrustTestTwoBranches { + uint{this} secret; + constructor() { super(); } + + public void f{this}(address{this} a, uint{a} x) { + if (a => this) { + secret = x; + } + if (a => this) { + secret = x + 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/TrustTestTwoBranches_W01.scif b/test/contracts/ifcTypechecking/TrustTestTwoBranches_W01.scif new file mode 100644 index 00000000..bc240350 --- /dev/null +++ b/test/contracts/ifcTypechecking/TrustTestTwoBranches_W01.scif @@ -0,0 +1,15 @@ +// Each branch's flow is checked under its own trust test: the test on b +// does not justify the flow of data trusted by a. +contract TrustTestTwoBranches_W01 { + uint{this} secret; + constructor() { super(); } + + public void f{this}(address{this} a, address{this} b, uint{a} x) { + if (a => this) { + secret = x; + } + if (b => this) { + secret = x; + } + } +} diff --git a/test/contracts/multiContract/EthCrossChainManager.scif b/test/contracts/multiContract/EthCrossChainManager.scif deleted file mode 100644 index 7c6b3e8a..00000000 --- a/test/contracts/multiContract/EthCrossChainManager.scif +++ /dev/null @@ -1,155 +0,0 @@ -interface IPolyCallee { - // called by PolyNetManager -// public void polyCallBack{any}(bytes args, bytes _fromContractAddr, uint _fromChainId); -// public void polyCallBack{origin}(final address origin, bytes args, bytes _fromContractAddr, uint _fromChainId); - public void polyCallback{this}(final address origin, bytes{origin} args, bytes _fromContractAddr, uint _fromChainId); -} -interface IECCUtils { - struct Header { - uint version; - uint chainId; - uint timestamp; - uint height; - uint consensusData; - uint prevBlockHash; - uint transactionsRoot; - uint crossStatesRoot; - uint blockRoot; - bytes consensusPayload; - address nextBookkeeper; - } - struct ToMerkleValue { - uint txHash; // cross chain txhash - uint fromChainID; - uint crossChainId; - bytes fromContract; - uint toChainId; - address toContract; - bytes args; - } - public bool verifySig{this}(Header{any} header, bytes{any} sigList, address[]{any} keepers, uint{any} m); - public address verifyPubkey{this}(address[]{any} keepers); - public bytes encodeParam{this}(uint{any} paramTxHash, address{any} _this, address{any} _sender, uint{any} toChainId, address{any} toContract, bytes{any} txData); - public uint keccak256{this}(bytes{any} data); - public uint merkleProveValue{this}(bytes{any} proof, uint{any} root); - public ToMerkleValue merkleProve{this}(bytes{any} proof, uint{any} root); - public uint getHeaderHash{this}(Header{any} header); -} -interface IEthCrossChainData { - public address[] getCurEpochConPubKeys(); - public void putCurEpochConPubKeys{this}(address[] keepers); - public uint getCurEpochStartHeight(); - public uint getEthTxHashIndex(); - public void putCurEpochStartHeight{this}(uint height); - public void putEthTxHash{this}(uint hash); - public bool checkIfFromChainTxExist(uint fromChainID, uint txHash); - public void markFromChainTxExist{this}(uint fromChainID, uint txHash); -} -contract EthCrossChainManager { - - address EthCrossChainDataAddress; - address EthECCUtilsAddress; - - constructor(address _eccd, address _cutils) { - EthCrossChainDataAddress = endorse(_eccd, sender->this); - EthECCUtilsAddress = endorse(_cutils, sender->this); - super(); - } - - public void initGenesisBlock(Header@IECCUtils header, bytes pubKeyList) { - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); - address[] keepers = eccd.getCurEpochConPubKeys(); - assert(keepers.length() == 0); - - // Parse header and convit the public keys into nextBookKeeper and compare it with header.nextBookKeeper to verify the validity of signature - address nextBookkeeper = cutils.verifyPubkey(keepers); - endorse([header], sender->this) - when (header.nextBookkeeper == nextBookkeeper) { - // Record current epoch start height and public keys (by storing them in address format) - eccd.putCurEpochStartHeight(header.height); - eccd.putCurEpochConPubKeys(keepers); - } else { - assert(false); - } - } - - public void changeBookKeeper(Header@IECCUtils header, address[] polyChainBKs, bytes sigList) { - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); - // Make sure rawHeader.height is higher than recorded current epoch start height - uint curEpochStartHeight = eccd.getCurEpochStartHeight(); - - // Verify signature of rawHeader comes from pubKeyList - endorse([header, polyChainBKs], sender -> this) - when ((header.height > curEpochStartHeight) && - (header.nextBookkeeper != address(0)) && - (cutils.verifySig(header, sigList, polyChainBKs, polyChainBKs.length() - (polyChainBKs.length() - 1) / 3))) { - - // Convert pubKeyList into ethereum address format and make sure the compound address from the converted ethereum addresses - // equals passed in header.nextBooker - address nextBookKeeper = cutils.verifyPubkey(polyChainBKs); - assert(header.nextBookkeeper == nextBookKeeper); - - // update current epoch start height of Poly chain and current epoch consensus peers book keepers addresses - eccd.putCurEpochStartHeight(header.height); - eccd.putCurEpochConPubKeys(polyChainBKs); - } else { - assert(false); - } - } - - public void crossChain(uint toChainId, address toContract, bytes txData) { - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); - // To help differentiate two txs, the ethTxHashIndex is increasing automatically - uint txHashIndex = eccd.getEthTxHashIndex(); - - uint paramTxHash = txHashIndex; - - // Construct the makeTxParam, and put the hash info storage, to help provide proof of tx existence - bytes rawParam = cutils.encodeParam(paramTxHash, address(this), sender, toChainId, toContract, txData); - - // Must save it in the storage to be included in the proof to be verified. - eccd.putEthTxHash(cutils.keccak256(rawParam)); - } - - public void verifyHeaderAndExecuteTx(bytes proof, Header@IECCUtils header, bytes headerProof, Header@IECCUtils curHeader, bytes headerSig) { - // Load ehereum cross chain data contract - // Get stored consensus public key bytes of current poly chain epoch and deserialize Poly chain consensus public key bytes to address[] - final IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress); - final IECCUtils cutils = IECCUtils(EthECCUtilsAddress); - - assert eccd => this; - assert cutils => this; - address[] keepers = eccd.getCurEpochConPubKeys(); - - uint curEpochStartHeight = eccd.getCurEpochStartHeight(); - - uint n = keepers.length(); - if (endorse(header.height, sender->this) >= curEpochStartHeight) { -// It's enough to verify rawHeader signature - assert(cutils.verifySig(header, headerSig, keepers, n - ( n - 1) / 3)); - } else { -// We need to verify the signature of curHeader - assert(cutils.verifySig(curHeader, headerSig, keepers, n - ( n - 1) / 3)); - - uint proveValue = cutils.merkleProveValue(headerProof, curHeader.blockRoot); - assert(cutils.getHeaderHash(header) == proveValue); - } - - // Parse the toMerkleValue struct and make sure the tx has not been processed, then mark this tx as processed - ToMerkleValue@IECCUtils toMerkleValue = cutils.merkleProve(proof, header.crossStatesRoot); - assert(!eccd.checkIfFromChainTxExist(toMerkleValue.fromChainID, toMerkleValue.txHash)); - eccd.markFromChainTxExist(toMerkleValue.fromChainID, toMerkleValue.txHash); - - // Ethereum ChainId is 2, we need to check the transaction is for Ethereum network - assert(toMerkleValue.toChainId == 2); - - // Obtain the targeting contract, so that Ethereum cross chain manager contract can trigger the executation of cross chain tx on Ethereum side - address toContract = toMerkleValue.toContract; - - IPolyCallee callee = IPolyCallee(toContract); - callee.polyCallback(sender, toMerkleValue.args, toMerkleValue.fromContract, toMerkleValue.fromChainID); - } -} \ No newline at end of file diff --git a/test/src/TestIfcTypechecking.java b/test/src/TestIfcTypechecking.java index 4ce90648..4b3303cb 100644 --- a/test/src/TestIfcTypechecking.java +++ b/test/src/TestIfcTypechecking.java @@ -59,6 +59,12 @@ public class TestIfcTypechecking { "ifcTypechecking/InterfaceLocalPrincipal", "ifcTypechecking/EndorseLabels", "ifcTypechecking/EmitIFC", + "ifcTypechecking/JoinLabelPrincipal", + "ifcTypechecking/CallTrustedReference", + "ifcTypechecking/TrustTestTwoBranches", + "ifcTypechecking/NewTail", + "ifcTypechecking/NewEndorseTail", + "ifcTypechecking/NewLocked", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -119,6 +125,15 @@ void testPositive(String contractName) throws Exception { "ifcTypechecking/EmitArgLow_W01", "ifcTypechecking/EmitPcLow_W01", "ifcTypechecking/EmitDependentLow_W01", + "ifcTypechecking/JoinLabelPrincipal_W01", + "ifcTypechecking/CastUnknownContract_W01", + "ifcTypechecking/CallLendsAuthority_W01", + "ifcTypechecking/ClosureCrossContractUntrustedSink_W01", + "ifcTypechecking/TrustTestAfterBranch_W01", + "ifcTypechecking/TrustTestBeforeBranch_W01", + "ifcTypechecking/TrustTestTwoBranches_W01", + "ifcTypechecking/NewUntrusted_W01", + "ifcTypechecking/NewNonTail_W01", // "examples/ERC20", }) void testNegative(String contractName) throws Exception { From 3ef7877271b95c8ededbf22d9ac10205f61fd2e2 Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Thu, 5 Nov 2026 18:55:59 -0500 Subject: [PATCH 4/8] Latest. --- sherrloc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sherrloc b/sherrloc index 82e16e83..5150f805 160000 --- a/sherrloc +++ b/sherrloc @@ -1 +1 @@ -Subproject commit 82e16e83887c1158fe5cf48cd8909cc42cc1640d +Subproject commit 5150f8054b3fc8ccfe6a050d76e32610b2087819 From 7cfa14df7db40b3b2b76fca8960a583ddb199292 Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Fri, 6 Nov 2026 06:43:34 -0500 Subject: [PATCH 5/8] assert false now has no normal path, which is needed for Dexible. exception types are now checked properly and support dependent types. --- sherrloc | 2 +- src/ast/Assert.java | 7 +- src/ast/Atomic.java | 20 ++-- src/ast/Attribute.java | 40 ++++++- src/ast/Call.java | 103 ++++++++++++------ src/ast/EndorseIfStatement.java | 7 +- src/ast/ExceptHandler.java | 39 ++++++- src/ast/ExceptionDef.java | 24 +++- src/ast/For.java | 29 ++--- src/ast/FunctionDef.java | 32 ++++-- src/ast/FunctionSig.java | 10 +- src/ast/GuardBlock.java | 7 +- src/ast/If.java | 33 +----- src/ast/Throw.java | 38 ++++++- src/ast/Try.java | 9 +- src/ast/While.java | 11 +- src/typecheck/FuncSym.java | 5 +- src/typecheck/InterfaceSym.java | 7 ++ src/typecheck/PathOutcome.java | 7 ++ src/typecheck/Utils.java | 53 ++++----- test/contracts/applications/Dexible.scif | 4 +- test/contracts/applications/ERC20_depmap.scif | 10 +- .../applications/ERC20_depmap_old.scif | 10 +- test/contracts/applications/HODLWallet.scif | 4 +- test/contracts/applications/IERC20.scif | 6 +- test/contracts/applications/IERC777.scif | 6 +- .../ifcTypechecking/AssertFalseHandler.scif | 20 ++++ .../ifcTypechecking/CallPcTrusted.scif | 13 +++ .../ifcTypechecking/CallPcUntrusted_W01.scif | 14 +++ .../ifcTypechecking/CallResultTrusted.scif | 9 ++ .../CallResultUntrusted_W01.scif | 10 ++ .../EndorseIfBodyPcLow_W01.scif | 16 +++ .../EndorseIfBodyPcTrusted.scif | 16 +++ .../EndorseIfThrowLow_W01.scif | 13 +++ .../EndorseIfThrowUntrusted.scif | 12 ++ .../ifcTypechecking/ExceptionData.scif | 18 +++ .../ifcTypechecking/ExceptionDataLow_W01.scif | 10 ++ .../ExceptionDependentLabel.scif | 22 ++++ .../ExceptionDependentLabelLow_W01.scif | 19 ++++ .../ExceptionDependentThrowLow_W01.scif | 10 ++ .../ExceptionExternalTrusted.scif | 17 +++ .../ExceptionExternalUntrusted_W01.scif | 18 +++ .../ExceptionHandlerLow_W01.scif | 19 ++++ .../ExceptionHandlerTrusted.scif | 18 +++ .../ExceptionThrowLow_W01.scif | 9 ++ .../ExceptionThrowUntrusted.scif | 9 ++ .../ifcTypechecking/HandlerThrowLow_W01.scif | 19 ++++ .../HandlerThrowUntrusted.scif | 19 ++++ .../ifcTypechecking/ICrossThrower.scif | 4 + .../ifcTypechecking/ISelfVouching.scif | 3 + .../IfEarlierThrowLow_W01.scif | 18 +++ .../IfEarlierThrowUntrusted.scif | 18 +++ .../ifcTypechecking/RescueAllLow_W01.scif | 14 +++ .../ifcTypechecking/RescueAllUntrusted.scif | 13 +++ .../Wallet_lock_exception.scif | 2 +- .../ifcTypechecking/WhileBodyPcLow_W01.scif | 16 +++ .../ifcTypechecking/WhileBodyPcTrusted.scif | 15 +++ .../ifcTypechecking/WhileThrowLow_W01.scif | 17 +++ .../ifcTypechecking/WhileThrowUntrusted.scif | 17 +++ test/contracts/multiContract/Dexible.scif | 4 +- .../multiContract/DexibleWithEvents.scif | 4 +- .../ExceptionParamLabelNotPrincipal_W01.scif | 6 + .../ExceptionParamLabels.scif | 6 + test/src/TestIfcTypechecking.java | 30 +++++ test/src/TestRegularTypechecking.java | 2 + 65 files changed, 851 insertions(+), 191 deletions(-) create mode 100644 test/contracts/ifcTypechecking/AssertFalseHandler.scif create mode 100644 test/contracts/ifcTypechecking/CallPcTrusted.scif create mode 100644 test/contracts/ifcTypechecking/CallPcUntrusted_W01.scif create mode 100644 test/contracts/ifcTypechecking/CallResultTrusted.scif create mode 100644 test/contracts/ifcTypechecking/CallResultUntrusted_W01.scif create mode 100644 test/contracts/ifcTypechecking/EndorseIfBodyPcLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/EndorseIfBodyPcTrusted.scif create mode 100644 test/contracts/ifcTypechecking/EndorseIfThrowLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/EndorseIfThrowUntrusted.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionData.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionDataLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionDependentLabel.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionDependentLabelLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionDependentThrowLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionExternalTrusted.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionExternalUntrusted_W01.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionHandlerLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionHandlerTrusted.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionThrowLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/ExceptionThrowUntrusted.scif create mode 100644 test/contracts/ifcTypechecking/HandlerThrowLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/HandlerThrowUntrusted.scif create mode 100644 test/contracts/ifcTypechecking/ICrossThrower.scif create mode 100644 test/contracts/ifcTypechecking/ISelfVouching.scif create mode 100644 test/contracts/ifcTypechecking/IfEarlierThrowLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/IfEarlierThrowUntrusted.scif create mode 100644 test/contracts/ifcTypechecking/RescueAllLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/RescueAllUntrusted.scif create mode 100644 test/contracts/ifcTypechecking/WhileBodyPcLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/WhileBodyPcTrusted.scif create mode 100644 test/contracts/ifcTypechecking/WhileThrowLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/WhileThrowUntrusted.scif create mode 100644 test/contracts/regularTypechecking/ExceptionParamLabelNotPrincipal_W01.scif create mode 100644 test/contracts/regularTypechecking/ExceptionParamLabels.scif diff --git a/sherrloc b/sherrloc index 5150f805..ab96ffe0 160000 --- a/sherrloc +++ b/sherrloc @@ -1 +1 @@ -Subproject commit 5150f8054b3fc8ccfe6a050d76e32610b2087819 +Subproject commit ab96ffe03bc7a3666b42f993c40a2a0a5d15ca71 diff --git a/src/ast/Assert.java b/src/ast/Assert.java index 5287302b..2c78a185 100644 --- a/src/ast/Assert.java +++ b/src/ast/Assert.java @@ -42,7 +42,12 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic } } - return test.genIFConstraints(env, tail_position).psi; + PathOutcome result = test.genIFConstraints(env, tail_position).psi; + if (test instanceof ConstantExpr c && c.value == Constant.FALSE) { + // An assertion of false always fails, so it never completes normally. + return result.withoutNormalPath(); + } + return result; } @Override diff --git a/src/ast/Atomic.java b/src/ast/Atomic.java index 7e0d92e0..13566da1 100644 --- a/src/ast/Atomic.java +++ b/src/ast/Atomic.java @@ -249,16 +249,15 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic PathOutcome input = new PathOutcome(); Map versionsBefore = env.saveVersions(); env.incScopeLayer(); - PathOutcome so = new PathOutcome(new PsiUnit(beginContext)); - Utils.genConsStmtsWithException(body, env, so, psi, tail_position); + psi.join(Utils.genConsStmtSeq(body, env, tail_position)); env.decScopeLayer(); for (ExceptHandler h : handlers) { - ExceptionTypeSym expSym = env.getExp(h.name()); + ExceptionTypeSym expSym = h.exceptionType(env); PsiUnit u = psi.psi.get(expSym); if (u != null) { env.checkFlow(u.c.lambda, beginContext.lambda, location, - "Try clause should maintain locks when throwing exception " + h.name()); + "Try clause should maintain locks when throwing exception " + h.type().name()); /*env.cons.add(new Constraint(new Inequality(u.c.pc, h.), env.hypothesis, location, env.curContractSym.name, "Try clause should maintain locks when throwing exception " + h.name));*/ @@ -279,9 +278,16 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.havoc(assignedInBody); env.incScopeLayer(); //env.inContext = new Context(h.getHandlerPcLabelName(), beginContext.lambda); - ExceptionTypeSym expSym = env.getExp(h.name()); - PsiUnit expUnit = input.psi.get(expSym); - if (expUnit != null) { + ExceptionTypeSym expSym = h.exceptionType(env); + if (expSym == null) { + // Nothing is known about where a failure came from, so a handler for + // failures runs with untrusted control flow. + env.inContext = new Context(env.curContractSym().any().toSHErrLocFmt(), + beginContext.lambda); + PathOutcome ho = h.genConsVisit(env, tail_position); + psi.join(ho); + exitVersions.add(env.saveVersions()); + } else if (input.psi.get(expSym) instanceof PsiUnit expUnit) { env.inContext = new Context(expUnit.c.pc, beginContext.lambda); PathOutcome ho = h.genConsVisit(env, tail_position); psi.join(ho); diff --git a/src/ast/Attribute.java b/src/ast/Attribute.java index 03ff0660..4e2f378e 100644 --- a/src/ast/Attribute.java +++ b/src/ast/Attribute.java @@ -97,7 +97,8 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S && attr.id.equals(Utils.CLOSURE_TARGET_MEMBER)) { return value.genIFConstraints(env, tail_position); } - if (!(varSym.typeSym instanceof StructTypeSym structType)) { + if (!(varSym.typeSym instanceof StructTypeSym + || varSym.typeSym instanceof ExceptionTypeSym)) { //TODO: throw errors: variable not struct // return null; throw new RuntimeException("Variable not a struct: " + varName); @@ -112,6 +113,22 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S // env.curContractSym().getName(), // "Integrity of the member")); // if (!ifAttLabel.equals(attrValueLabel)) { + if (varSym.typeSym instanceof ExceptionTypeSym expType) { + // An exception's data is trusted at its parameter's label, in which each + // principal parameter stands for the principal in that field. + java.util.Map mapping = new java.util.HashMap<>(); + for (VarSym param : expType.parameters()) { + if (param.isPrincipalTyped()) { + mapping.put(param.toSHErrLocFmt(), + exceptionFieldPrincipal(env, varSym, param.getName())); + } + } + VarSym member = expType.parameters().stream() + .filter(param -> param.getName().equals(attr.id)).findFirst() + .orElseThrow(); + attrValueLabel = Utils.joinLabels(attrValueLabel, + member.ifl.toSHErrLocFmt(mapping)); + } env.checkEqual(attrValueLabel, ifNameRnt, location, "Integrity of the index value must be trusted to indicate the attribute"); // } @@ -119,6 +136,27 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S return new ExpOutcome(ifNameRnt, vo.psi); } + @Override + public String principalName(VisitEnv env) { + if (value instanceof Name n && env.getCurSym(n.id) instanceof VarSym v + && v.typeSym instanceof ExceptionTypeSym expType) { + for (VarSym param : expType.parameters()) { + if (param.getName().equals(attr.id) && param.isPrincipalTyped()) { + return exceptionFieldPrincipal(env, v, attr.id); + } + } + } + return null; + } + + /** + * The principal held by the principal-typed field {@code field} of the caught exception + * {@code exception}, registered in {@code env}. Nothing is known about it. + */ + private static String exceptionFieldPrincipal(VisitEnv env, VarSym exception, String field) { + return env.freshPrincipal(exception.toSHErrLocFmt() + "." + field); + } + @Override public compile.ast.Expression solidityCodeGen(List result, CompileEnv code) { if (isClosureTarget) { diff --git a/src/ast/Call.java b/src/ast/Call.java index 04a21ef4..6bbf99e9 100644 --- a/src/ast/Call.java +++ b/src/ast/Call.java @@ -246,7 +246,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S String rtnTypeName = funcSym.returnType.toSHErrLocFmt(); env.addCons(now.genTypeConstraints(rtnTypeName, Relation.EQ, env, location)); - for (Map.Entry tl : funcSym.exceptions.entrySet()) { + for (Map.Entry tl : funcSym.exceptions.entrySet()) { if (!parent.isCheckedException(tl.getKey(), extern)) { throw new SemanticException("Unchecked exception: " + tl.getKey().getName(), location); @@ -644,6 +644,9 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) ? receiverPrincipal : env.thisSym().toSHErrLocFmt()); outputMapping.put(calleeThis, ifContRtn); } + // Whatever the callee's labels say, its results are trusted no more than the + // reference used to call it. + String calleeTrust = externalCall ? ifContRtn : null; // Generate constraints on arguments @@ -674,35 +677,25 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) } + String postCallLock = attenuate(ifFuncGammaLock.toSHErrLocFmt(outputMapping), calleeTrust); PathOutcome expPsi = new PathOutcome(new PsiUnit(new Context( - Utils.joinLabels(psi.getNormalPath().c.pc, funcSym.endPc().toSHErrLocFmt(outputMapping)), -// funcSym.getLabelNameCallGamma() - ifFuncGammaLock.toSHErrLocFmt(outputMapping) - ))); // post-call psi = (current normal path pc ⊔ function return label, gamma) - - for (Entry exp : funcSym.exceptions.entrySet()) { - ExceptionTypeSym curSym = exp.getKey(); - String expLabelName = exp.getValue(); - // pc_2 dropped - // expPsi.set(curSym, new PsiUnit( - // new Context( - // Utils.joinLabels(expLabelName, funcSym.externalPcSLC()), - // Utils.joinLabels(ifFuncGammaLock.toSHErrLocFmt(dependentLabelMapping), - // ifFuncCallPcAfter.toSHErrLocFmt(dependentLabelMapping))), - // true) - // ); // exception-path post-call psi = (function exception-path pc ⊔ pc_ex, - // // gamma ⊔ pc_in) - expPsi.set(curSym, new PsiUnit( + attenuate(Utils.joinLabels(psi.getNormalPath().c.pc, + funcSym.endPc().toSHErrLocFmt(outputMapping)), calleeTrust), + postCallLock + ))); // post-call psi = (current normal path pc ⊔ function end pc, gamma), attenuated + + if (externalCall) { + checkExceptionData(env, outputMapping); + } + for (Entry exp : funcSym.exceptions.entrySet()) { + // exception-path post-call psi = (current pc ⊔ declared exception label, gamma), + // attenuated like the normal path + expPsi.set(exp.getKey(), new PsiUnit( new Context( - Utils.joinLabels(expLabelName, funcSym.externalPcSLC()), - ifFuncGammaLock.toSHErrLocFmt(outputMapping)), - true) - ); // exception-path post-call psi = (function exception-path pc ⊔ pc_ex, gamma) - // TODO: should pc label get joined by pc_ex, or psi.normal.pc? - //TODO: dependent - //PsiUnit psiUnit = env.psi.get(curSym); - //env.cons.add(new Constraint(new Inequality(Utils.makeJoin(expLabelName, ifNameFuncCallPcAfter), psiUnit.pc), env.hypothesis, location, env.curContractSym.name, - //"Exception " + curSym.name + " is not trusted enough to throw")); + attenuate(Utils.joinLabels(psi.getNormalPath().c.pc, + exp.getValue().toSHErrLocFmt(outputMapping)), calleeTrust), + postCallLock), + true)); } //TODO @@ -717,10 +710,8 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // ); // pc_ex => pc_in ⊔ caller's lambda if (externalCall) { - // target address..lbl ⊔ gamma == post-call lambda - env.checkEqual( - Utils.joinLabels(ifContRtn, ifFuncGammaLock.toSHErrLocFmt(outputMapping)), - endContext.lambda, location, + // callee trust ⊔ gamma == post-call lambda + env.checkEqual(postCallLock, endContext.lambda, location, "Calling this function does not respect static reentrancy locks"); } @@ -740,7 +731,8 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // Finalize - String ifNameFuncRtnValue = funcSym.rtn.toSHErrLocFmt(outputMapping); + String ifNameFuncRtnValue = attenuate(funcSym.rtn.toSHErrLocFmt(outputMapping), + calleeTrust); // realLabels if (funcSym.returnType instanceof ClosureTypeSym retCs) { @@ -756,6 +748,49 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) return new ExpOutcome(ifNameFuncRtnValue, psi); } + /** + * Checks that the data of each exception the external callee may throw is trusted at the + * labels this contract's declaration of that exception gives it, since handlers here read + * it at those labels. The callee's labels are instantiated with {@code outputMapping}, + * with its exception's principal parameters standing for the corresponding parameters of + * this contract's declaration. + */ + private void checkExceptionData(VisitEnv env, Map outputMapping) { + // A handler reads each field at its label joined with the label of the exception's + // path, which is attenuated by the callee's trust, so the data is too. + for (ExceptionTypeSym calleeExp : funcSym.exceptions.keySet()) { + ExceptionTypeSym localExp = env.getExp(calleeExp.getName()); + if (localExp == null) { + throw new AssertionError("exception " + calleeExp.getName() + + " of an external call has no local declaration"); + } + Map mapping = new HashMap<>(outputMapping); + for (int i = 0; i < calleeExp.parameters().size(); ++i) { + VarSym calleeParam = calleeExp.parameters().get(i); + if (calleeParam.isPrincipalTyped()) { + mapping.put(calleeParam.toSHErrLocFmt(), env.freshPrincipal( + localExp.parameters().get(i).toSHErrLocFmt())); + } + } + for (int i = 0; i < calleeExp.parameters().size(); ++i) { + VarSym calleeParam = calleeExp.parameters().get(i); + VarSym localParam = localExp.parameters().get(i); + env.checkFlow(calleeParam.ifl.toSHErrLocFmt(mapping), + localParam.ifl.toSHErrLocFmt(), location, + "Data of exception " + calleeExp.getName() + + " from this call must be trusted at its declared label here"); + } + } + } + + /** + * The label {@code label} trusted no more than {@code trust}, or {@code label} itself if + * {@code trust} is null. {@code trust} may be null. + */ + private static String attenuate(String label, String trust) { + return trust == null ? label : Utils.joinLabels(label, trust); + } + /** * Reject the closure-invocation shapes whose code generation is not * implemented, rather than emitting partial code: results the @@ -906,7 +941,7 @@ public compile.ast.Expression solidityCodeGen(List result, CompileEnv // map exceptionID IfStatement mapExpIds = null; int i = 1; - for (Entry entry: funcSym.exceptions.entrySet()) { + for (Entry entry: funcSym.exceptions.entrySet()) { IfStatement ifexp = new IfStatement( new BinaryExpression(compile.Utils.SOL_BOOL_EQUAL, statVar, new Literal(String.valueOf(i))), List.of(new Assign(statVar, new Literal(String.valueOf(code.getExceptionId(entry.getKey()))))), diff --git a/src/ast/EndorseIfStatement.java b/src/ast/EndorseIfStatement.java index f2a58799..bceaeadd 100644 --- a/src/ast/EndorseIfStatement.java +++ b/src/ast/EndorseIfStatement.java @@ -130,9 +130,9 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // Context leftContext = new Context(curContext), rightContext = new Context(curContext); CodeLocation loc = null; - PathOutcome ifo = toOutCome.psi; + PathOutcome ifo = toOutCome.psi.withoutNormalPath(); env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - Utils.genConsStmts(ifStatement.body, env, ifo, tail_position); + ifo.join(Utils.genConsStmtSeq(ifStatement.body, env, tail_position)); // int index = 0; // for (Statement stmt : ifStatement.body) { @@ -162,9 +162,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.restoreVersions(versionsBefore); env.incScopeLayer(); // index = 0; - PathOutcome elseo = toOutCome.psi; env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - Utils.genConsStmts(ifStatement.orelse, env, elseo, tail_position); + PathOutcome elseo = Utils.genConsStmtSeq(ifStatement.orelse, env, tail_position); // for (Statement stmt : ifStatement.orelse) { // ++index; // elseo = stmt.genConsVisit(env, index == ifStatement.orelse.size() && tail_position); diff --git a/src/ast/ExceptHandler.java b/src/ast/ExceptHandler.java index 3a9262a1..2c5fc6b4 100644 --- a/src/ast/ExceptHandler.java +++ b/src/ast/ExceptHandler.java @@ -129,9 +129,11 @@ public PathOutcome genConsVisit(VisitEnv env, boolean tail_position) throws Sema Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); + if (!acceptall) { + declareCaughtVar(env, beginContext.pc); + } int index = 0; - PathOutcome so = new PathOutcome(new PsiUnit(beginContext)); - Utils.genConsStmts(body, env, so, tail_position); + PathOutcome so = Utils.genConsStmtSeq(body, env, tail_position); // for (Statement stmt : body) { // ++index; // so = stmt.genConsVisit(env, index == body.size() && tail_position); @@ -149,10 +151,43 @@ public Type type() { return labeledType.type(); } + /** + * Declares this handler's variable in the current scope of {@code env}, bound by control + * flow at {@code handlerPc}. If the variable's type has a label, that label is its label, + * and the control flow must be trusted at it; otherwise its label is {@code handlerPc}. + * An exception has a single label, so its fields are read at the variable's label. + */ + private void declareCaughtVar(VisitEnv env, String handlerPc) throws SemanticException { + Label declared = labeledType.label() == null ? null : env.toLabel(labeledType.label()); + VarSym var = new VarSym(name, exceptionType(env), declared, location, scopeContext, + false, true, false); + try { + env.addVar(name, var); + } catch (SymTab.AlreadyDefined e) { + throw new SemanticException("Already defined: " + name, location); + } + if (declared == null) { + env.checkEqual(var.labelNameSLC(), handlerPc, location, + "Label of " + name + " may be incorrect"); + } else { + env.checkEqual(var.labelNameSLC(), declared.toSHErrLocFmt(), location, + "Label of " + name + " may be incorrect"); + env.checkFlow(handlerPc, declared.toSHErrLocFmt(), location, + "The caught exception must be trusted at the label of " + name); + } + } + public String name() { return name; } + /** + * The exception type this handler catches, or null if it catches every failure. + */ + public ExceptionTypeSym exceptionType(VisitEnv env) { + return acceptall ? null : env.getExp(labeledType.type().name()); + } + protected Map readMap(CompileEnv code) { Map result = new HashMap<>(); for (Statement s: body) { diff --git a/src/ast/ExceptionDef.java b/src/ast/ExceptionDef.java index e53e392e..c50c6b6f 100644 --- a/src/ast/ExceptionDef.java +++ b/src/ast/ExceptionDef.java @@ -16,16 +16,14 @@ public class ExceptionDef extends TopLayerNode { String namespace; Arguments arguments; boolean isBuiltIn = false; - public ExceptionDef(String name, Arguments members) { - exceptionName = name; - this.arguments = members; - arguments.setToDefault(new PrimitiveIfLabel(new Name(Utils.LABEL_THIS))); + this(name, members, false); } public ExceptionDef(String name, Arguments members, Boolean isBuiltIn) { exceptionName = name; this.arguments = members; this.isBuiltIn = isBuiltIn; + // Like fields, unlabeled data is trusted by this contract. arguments.setToDefault(new PrimitiveIfLabel(new Name(Utils.LABEL_THIS))); } @@ -34,13 +32,27 @@ public void ntcGlobalInfo(NTCEnv env, ScopeContext parent) throws SemanticException { // exceptionType.setContractName(env.curContractSym().getName()); + ExceptionTypeSym expSym = env.newExceptionType(namespace, exceptionName, arguments, parent); try { - env.addSym(exceptionName, - env.newExceptionType(namespace, exceptionName, arguments, parent)); + env.addSym(exceptionName, expSym); } catch (SymTab.AlreadyDefined e) { throw new SemanticException("Exception already defined: " + exceptionName, location); } + // A parameter's label may name principal-typed parameters of the exception. + ScopeContext now = new ScopeContext(this, parent); + env.enterNewScope(); + try { + for (VarSym param : expSym.parameters()) { + env.addSym(param.getName(), param); + } + } catch (SymTab.AlreadyDefined e) { + throw new SemanticException("Parameter already defined: " + e.id, location); + } + for (Arg arg : arguments.args()) { + arg.annotation.label().genTypeConstraints(env, now); + } + env.exitNewScope(); } /** diff --git a/src/ast/For.java b/src/ast/For.java index 25ab212b..7a74ca5b 100644 --- a/src/ast/For.java +++ b/src/ast/For.java @@ -80,25 +80,18 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.inContext = new Context(ifNamePcAfter, beginContext.lambda); CodeLocation loc = null; - PathOutcome ifo = to.psi; - for (Statement stmt : body) { - ifo = stmt.IFCVisit(env, false); - - PsiUnit normalUnit = ifo.getNormalPath(); - if (normalUnit == null) break; - - env.inContext = normalUnit.c; - loc = stmt.location; - } - for (Statement s: iter) { - ifo = s.IFCVisit(env, false); - - PsiUnit normalUnit = ifo.getNormalPath(); - if (normalUnit == null) break; - - env.inContext = normalUnit.c; - loc = s.location; + List iteration = new ArrayList<>(body); + iteration.addAll(iter); + PathOutcome iterOutcome = Utils.genConsStmtSeq(iteration, env, false); + // The loop exits only from its head, which is also reached from the end of an + // iteration. + PathOutcome ifo = to.psi.withoutNormalPath(); + ifo.join(iterOutcome.withoutNormalPath()); + if (iterOutcome.getNormalPath() instanceof PsiUnit iterEnd) { + env.checkFlow(iterEnd.c.pc, ifNamePcAfter, location, + "Control flow at the end of the loop body contributes to the control flow of this for statement"); } + ifo.setNormalPath(new Context(ifNamePcAfter, to.psi.getNormalPath().c.lambda)); // The loop exits from its head or from a break in the body. env.havoc(assigned); diff --git a/src/ast/FunctionDef.java b/src/ast/FunctionDef.java index e76fb30f..7eab0b0d 100644 --- a/src/ast/FunctionDef.java +++ b/src/ast/FunctionDef.java @@ -59,7 +59,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S if (DEBUG) System.err.println("entering method: " + name + " " + body.size()); FuncSym funcSym = ((FuncSym) env.getCurSym(funcName)); Map exceptionTypeSyms = new HashMap<>(); - for (Map.Entry t : funcSym.exceptions.entrySet()) { + for (Map.Entry t : funcSym.exceptions.entrySet()) { exceptionTypeSyms.put(t.getKey(), true); } @@ -158,13 +158,6 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_info) throws SemanticExce funcLabels.gamma_label.location, "This function does not maintain reentrancy locks as specified in signature"); - HashMap psi = new HashMap<>(); - for (Map.Entry exp : funcSym.exceptions.entrySet()) { - psi.put(exp.getKey(), new PsiUnit( - //new Context(funcSym.getLabelNameException(exp.getKey()), ifNameGamma), true)); - new Context(exp.getKey().labelNameSLC(), ifNameGamma), true)); - } - Context funcBeginContext = curContext; PsiUnit funcEndContext = new PsiUnit(outPcName, outLockName); // env.inContext = funcBeginContext; @@ -177,6 +170,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_info) throws SemanticExce env.inContext = funcBeginContext; // Utils.genConsStatments(body, env, CO, true); int index = 0; + PathOutcome escaping = new PathOutcome(); // exception paths leaving the body for (Statement stmt : body) { // Context CO = new Context(Utils.getLabelNamePc(stmt.location), Utils.getLabelNameLock(stmt.location)); // env.outContext = CO; @@ -184,6 +178,10 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_info) throws SemanticExce String prevLambda = env.inContext.lambda; boolean isTail = index == body.size() && tail_info; CO = stmt.IFCVisit(env, isTail); + PathOutcome thrown = new PathOutcome(new java.util.HashMap<>(CO.psi)); + thrown.psi.remove(Utils.getNormalPathException()); + thrown.psi.remove(Utils.getReturnPathException()); + escaping.join(thrown); //Context CO = env.outContext; if (CO.existsNormalPath()) { env.inContext = Utils.genNewContextAndConstraints(env, isTail, CO.getNormalPath().c, prevLambda, stmt.nextPcSHL(), stmt.location()); @@ -199,6 +197,24 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_info) throws SemanticExce body.get(body.size() - 1).location); } + // Each exception that escapes must be trusted at its declared label, and must + // maintain the declared locks. + for (Map.Entry exp : escaping.psi.entrySet()) { + Label declared = funcSym.exceptions.get(exp.getKey()); + if (declared == null) { + throw new AssertionError("undeclared exception escapes " + funcLocalName + + ": " + exp.getKey().getName()); + } + Context c = exp.getValue().c; + env.checkFlow(c.pc, declared.toSHErrLocFmt(), declared.location(), + "Control flow that throws " + exp.getKey().getName() + + " must be trusted at its declared label"); + env.checkFlow(Utils.joinLabels(inLockName, c.lambda), ifNameGamma, + funcLabels.gamma_label.location, + "This function does not maintain reentrancy locks as specified in signature" + + " when it throws " + exp.getKey().getName()); + } + env.decScopeLayer(); env.exitMethod(); diff --git a/src/ast/FunctionSig.java b/src/ast/FunctionSig.java index 992339e5..ee148595 100644 --- a/src/ast/FunctionSig.java +++ b/src/ast/FunctionSig.java @@ -202,13 +202,13 @@ public void ntcGlobalInfo(NTCEnv env, ScopeContext parent) throws SemanticExcept VarSym sender = (VarSym) env.getCurSym(typecheck.Utils.LABEL_SENDER); List argsInfo = args.parseArgs(env, now); - Map exceptions = new HashMap<>(); + Map exceptions = new HashMap<>(); for (LabeledType t : exceptionList) { ExceptionTypeSym t1 = env.getExceptionTypeSym(t.type()); if (t1 == null) { throw new SemanticException("exception not found: " + t.type().name, t.location); } - exceptions.put(t1, null); + exceptions.put(t1, env.newLabel(t.label())); } funcSym = new FuncSym(name, isPublic, @@ -245,16 +245,14 @@ public void globalInfoVisit(InterfaceSym contractSym) throws SemanticException { if (rtn != null) { ifl = contractSym.newLabel(rtn.label()); } - Map exceptions = new HashMap<>(); + Map exceptions = new HashMap<>(); for (LabeledType t : exceptionList) { IfLabel label = null; /*if (t.type instanceof LabeledType) label = ((LabeledType) t.type).ifl;*/ ExceptionTypeSym exceptionTypeSym = contractSym.getExceptionSym(t.type().name); assert exceptionTypeSym != null; - exceptions.put(exceptionTypeSym, - typecheck.Utils.getLabelNameFuncExpLabel(scopeContext.getSHErrLocName(), - t.type().name())); + exceptions.put(exceptionTypeSym, contractSym.newLabel(t.label())); } // System.err.println("adding method: " + name); funcSym = diff --git a/src/ast/GuardBlock.java b/src/ast/GuardBlock.java index 81a5253a..eead09c5 100644 --- a/src/ast/GuardBlock.java +++ b/src/ast/GuardBlock.java @@ -61,13 +61,10 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic location, "Cannot grant a dynamic reentrancy lock"); // String newAfterLockLabel = Utils.getLabelNameLock(scopeContext.getSHErrLocName() + ".after"); - PathOutcome psi = new PathOutcome(new PsiUnit(curContext)); - //Context lastContext = new Context(curContext);//, prev2 = null; env.incScopeLayer(); - PathOutcome so = null; - env.inContext = psi.getNormalPath().c(); - Utils.genConsStmtsWithException(body, env, so, psi, tail_position); + env.inContext = curContext; + PathOutcome psi = Utils.genConsStmtSeq(body, env, tail_position); // int index = 0; // for (Statement stmt : body) { // ++index; diff --git a/src/ast/If.java b/src/ast/If.java index ad5d8cd9..f159b63a 100644 --- a/src/ast/If.java +++ b/src/ast/If.java @@ -112,24 +112,9 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // Context leftContext = new Context(curContext), rightContext = new Context(curContext); CodeLocation loc = null; - PathOutcome ifo = to.psi; + PathOutcome ifo = to.psi.withoutNormalPath(); env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - int index = 0; - for (Statement stmt : body) { - ++index; - String prevLambda = env.inContext.lambda; - ifo = stmt.IFCVisit(env, index == body.size() && tail_position); - // env.prevContext = tmp; - // prev2 = leftContext; - //env.context = context; - PsiUnit normalUnit = ifo.getNormalPath(); - if (normalUnit == null) { - break; - } - env.inContext = Utils.genNewContextAndConstraints(env, index == body.size() && tail_position, normalUnit.c, prevLambda, stmt.nextPcSHL(), stmt.location); - loc = stmt.location; - // leftContext = new Context(tmp); - } + ifo.join(Utils.genConsStmtSeq(body, env, tail_position)); env.decScopeLayer(); if (createdHypo) { @@ -142,20 +127,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic Map versionsThen = env.saveVersions(); env.restoreVersions(versionsBefore); env.incScopeLayer(); - index = 0; - PathOutcome elseo = to.psi; env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - for (Statement stmt : orelse) { - ++index; - String prevLambda = env.inContext.lambda; - elseo = stmt.IFCVisit(env, index == orelse.size() && tail_position); - PsiUnit normalUnit = elseo.getNormalPath(); - if (normalUnit == null) { - break; - } - env.inContext = Utils.genNewContextAndConstraints(env, index == body.size() && tail_position, normalUnit.c, prevLambda, stmt.nextPcSHL(), stmt.location); - // env.prevContext.lambda = rightContext.lambda; - } + PathOutcome elseo = Utils.genConsStmtSeq(orelse, env, tail_position); env.decScopeLayer(); env.mergeVersions(List.of(versionsThen, env.saveVersions())); diff --git a/src/ast/Throw.java b/src/ast/Throw.java index e6d9679d..4becf793 100644 --- a/src/ast/Throw.java +++ b/src/ast/Throw.java @@ -146,6 +146,10 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); PathOutcome psi = new PathOutcome(new PsiUnit(beginContext)); ExpOutcome ao = null; + String expName = ((Name) exception.value).id; + ExceptionTypeSym expSym = env.getExp(expName); + java.util.List argLabels = new java.util.ArrayList<>(); + java.util.List pcs = new java.util.ArrayList<>(); for (int i = 0; i < exception.args.size(); ++i) { Expression arg = exception.args.get(i); @@ -154,10 +158,23 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.inContext = new Context( Utils.joinLabels(ao.psi.getNormalPath().c.pc, beginContext.pc), beginContext.lambda); + argLabels.add(ao.valueLabelName); + pcs.add(env.inContext.pc); } - String expName = ((Name) exception.value).id; - ExceptionTypeSym expSym = env.getExp(expName); + // Each argument is checked like an argument of a call, with the exception's + // principal parameters standing for the principals passed for them. + java.util.Map mapping = paramMapping(env, expSym); + for (int i = 0; i < exception.args.size(); ++i) { + String paramLabel = expSym.parameters().get(i).ifl.toSHErrLocFmt(mapping); + CodeLocation argLoc = exception.args.get(i).location; + env.checkFlow(argLabels.get(i), paramLabel, argLoc, + "Input to the " + Utils.ordNumString(i + 1) + + " argument of the exception must be trusted enough"); + env.checkFlow(pcs.get(i), paramLabel, argLoc, + "Current control flow must be trusted to feed the " + + Utils.ordNumString(i + 1) + " argument of the exception"); + } PsiUnit expUnit = new PsiUnit( new Context(env.inContext.pc, @@ -172,6 +189,23 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic return psi; } + /** + * Maps each principal-typed parameter of {@code expSym} to the principal passed for it + * here, or to the top principal if the argument does not name one. + */ + private java.util.Map paramMapping(VisitEnv env, ExceptionTypeSym expSym) { + java.util.Map mapping = new java.util.HashMap<>(); + for (int i = 0; i < exception.args.size(); ++i) { + VarSym param = expSym.parameters().get(i); + if (param.isPrincipalTyped()) { + String principal = exception.args.get(i).principalName(env); + mapping.put(param.toSHErrLocFmt(), principal != null + ? principal : env.curContractSym().top().toSHErrLocFmt()); + } + } + return mapping; + } + @Override protected java.util.Map readMap(CompileEnv code) { return exception.readMap(code); diff --git a/src/ast/Try.java b/src/ast/Try.java index 38a02ace..008c1d79 100644 --- a/src/ast/Try.java +++ b/src/ast/Try.java @@ -161,8 +161,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic PathOutcome input = new PathOutcome(); Map versionsBefore = env.saveVersions(); env.incScopeLayer(); - PathOutcome so = new PathOutcome(new PsiUnit(beginContext)); - Utils.genConsStmtsWithException(body, env, so, psi, false); + psi.join(Utils.genConsStmtSeq(body, env, false)); // for (Statement s : body) { // so = s.genConsVisit(env, false); // psi.joinExe(so); @@ -172,12 +171,12 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // } env.decScopeLayer(); for (ExceptHandler h : handlers) { - ExceptionTypeSym expSym = env.getExp(h.name()); + ExceptionTypeSym expSym = h.exceptionType(env); PsiUnit u = psi.psi.get(expSym); if (u != null) { env.checkFlow(u.c.lambda, beginContext.lambda, location, - "Try clause should maintain locks when throwing exception " + h.name()); + "Try clause should maintain locks when throwing exception " + h.type().name()); /*env.cons.add(new Constraint(new Inequality(u.c.pc, h.), env.hypothesis, location, env.curContractSym.name, "Try clause should maintain locks when throwing exception " + h.name));*/ @@ -198,7 +197,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.havoc(assignedInBody); env.incScopeLayer(); //env.inContext = new Context(h.getHandlerPcLabelName(), beginContext.lambda); - ExceptionTypeSym expSym = env.getExp(h.name()); + ExceptionTypeSym expSym = h.exceptionType(env); PsiUnit expUnit = input.psi.get(expSym); if (expUnit != null) { env.inContext = Utils.genNewContextAndConstraints(env, false, expUnit.c, beginContext.lambda, h.nextPcSHL(), h.location); diff --git a/src/ast/While.java b/src/ast/While.java index b6f5a8c6..8102f8d9 100644 --- a/src/ast/While.java +++ b/src/ast/While.java @@ -65,8 +65,15 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.inContext = new Context(ifNamePcAfter, beginContext.lambda); CodeLocation loc = null; - PathOutcome ifo = to.psi; - Utils.genConsStmts(body, env, ifo, false); + PathOutcome bodyOutcome = Utils.genConsStmtSeq(body, env, false); + // The loop exits only from its head, which is also reached from the end of the body. + PathOutcome ifo = to.psi.withoutNormalPath(); + ifo.join(bodyOutcome.withoutNormalPath()); + if (bodyOutcome.getNormalPath() instanceof PsiUnit bodyEnd) { + env.checkFlow(bodyEnd.c.pc, ifNamePcAfter, location, + "Control flow at the end of the loop body contributes to the control flow of this while statement"); + } + ifo.setNormalPath(new Context(ifNamePcAfter, to.psi.getNormalPath().c.lambda)); // for (Statement stmt : body) { // ifo = stmt.genConsVisit(env, false); // diff --git a/src/typecheck/FuncSym.java b/src/typecheck/FuncSym.java index 50b46cdc..d637653f 100644 --- a/src/typecheck/FuncSym.java +++ b/src/typecheck/FuncSym.java @@ -19,7 +19,8 @@ public class FuncSym extends Sym { private VarSym sender; public final Label external_pc, internal_pc, gamma, rtn; - public Map exceptions; + /** The exceptions this method may throw, each with the declared label of its path. */ + public Map exceptions; public CodeLocation location; private boolean isPublic, isBuiltIn, isClosurable; private String plainSignature; @@ -35,7 +36,7 @@ public FuncSym(String funcName, List parameters, TypeSym returnType, Label returnLabel, - Map exceptions, + Map exceptions, ScopeContext defContext, VarSym sender, CodeLocation location) { super(funcName, defContext); diff --git a/src/typecheck/InterfaceSym.java b/src/typecheck/InterfaceSym.java index 1b1f5ac5..a0cf1d3d 100644 --- a/src/typecheck/InterfaceSym.java +++ b/src/typecheck/InterfaceSym.java @@ -367,6 +367,13 @@ public VarSym any() { return anySym; } + /** The top principal: the most trusted one. */ + public VarSym top() { + VarSym topSym = (VarSym) lookupSym(Utils.LABEL_TOP); + assert topSym != null; + return topSym; + } + public VarSym invoker() { VarSym invokerSym = (VarSym) lookupSym(Utils.LABEL_INVOKER); assert invokerSym != null; diff --git a/src/typecheck/PathOutcome.java b/src/typecheck/PathOutcome.java index 299fb1d8..e967b08c 100644 --- a/src/typecheck/PathOutcome.java +++ b/src/typecheck/PathOutcome.java @@ -73,6 +73,13 @@ public void setReturnPath(Context inContext) { set(Utils.getReturnPathException(), inContext); } + /** The paths of this outcome other than its normal path. */ + public PathOutcome withoutNormalPath() { + PathOutcome result = new PathOutcome(new HashMap<>(psi)); + result.psi.remove(Utils.getNormalPathException()); + return result; + } + public boolean existsNormalPath() { return getNormalPath() != null; } diff --git a/src/typecheck/Utils.java b/src/typecheck/Utils.java index 7e9cf4d1..b75de266 100644 --- a/src/typecheck/Utils.java +++ b/src/typecheck/Utils.java @@ -237,10 +237,6 @@ public static String getLabelNameArgLabel(String funcName, VarSym arg) { return funcName + "." + arg.getName() + ".lbl"; } - public static String getLabelNameFuncExpLabel(String funcName, String name) { - return funcName + "." + name + ".lbl"; - } - public static String joinLabels(String lhs, String rhs) { return "(" + lhs + " ⊔ " + rhs + ")"; } @@ -851,41 +847,34 @@ public static Context genNewContextAndConstraints(VisitEnv env, boolean tail_pos return new Context(newPc, c.lambda); } - public static void genConsStmtsWithException(List body, VisitEnv env, PathOutcome so, PathOutcome psi, boolean tail_positon) throws SemanticException { - int index = 0; - for (Statement s : body) { - ++index; - String prevLambda = env.inContext.lambda; - boolean isTail = index == body.size() && tail_positon; - so = s.IFCVisit(env, isTail); - psi.joinExe(so); - // env.inContext = new Context(so.getNormalPath().c.pc, beginContext.lambda); -// boolean nextIsTail = index + 1 == body.size() && tail_positon; - env.inContext = Utils.genNewContextAndConstraints(env, isTail, so.getNormalPath().c, prevLambda, s.nextPcSHL(), s.location()); -// new Context(so.getNormalPath().c); - - } - } - public static void genConsStmts(List body, VisitEnv env, PathOutcome so, boolean tail_posn) throws SemanticException { + /** + * Generates the information-flow constraints for the statement sequence {@code body}, + * which starts in the context {@code env.inContext}, and returns every way it can + * terminate: its exception and return paths, and its normal path if it can complete + * normally. The last statement is in tail position if {@code tailPosition} is. On return, + * {@code env.inContext} is the context after the last statement that completed normally. + */ + public static PathOutcome genConsStmtSeq(List body, VisitEnv env, + boolean tailPosition) throws SemanticException { + PathOutcome result = new PathOutcome(); int index = 0; - for (Statement s : body) { + for (ast.Statement s : body) { ++index; String prevLambda = env.inContext.lambda; - boolean isTail = index == body.size() && tail_posn; - so = s.IFCVisit(env, isTail); - PsiUnit normalUnit = so.getNormalPath(); - if (normalUnit == null) { - break; + boolean isTail = index == body.size() && tailPosition; + PathOutcome so = s.IFCVisit(env, isTail); + PsiUnit normal = so.getNormalPath(); + result.join(so.withoutNormalPath()); + if (normal == null) { + return result; } - // env.inContext = new Context(so.getNormalPath().c.pc, beginContext.lambda); -// boolean nextIsTail = (index + 1 == body.size() && tail_positon); - env.inContext = Utils.genNewContextAndConstraints(env, isTail, so.getNormalPath().c, prevLambda, s.nextPcSHL(), s.location()); -// new Context(so.getNormalPath().c); - + env.inContext = genNewContextAndConstraints(env, isTail, normal.c, prevLambda, + s.nextPcSHL(), s.location()); } + result.setNormalPath(env.inContext); + return result; } - static int inferredLabelCount = 0; public static IfLabel newInferredLabel() { return new PrimitiveIfLabel(new Name("__inferredLabel" + inferredLabelCount)); diff --git a/test/contracts/applications/Dexible.scif b/test/contracts/applications/Dexible.scif index 8638428f..7fbaea15 100644 --- a/test/contracts/applications/Dexible.scif +++ b/test/contracts/applications/Dexible.scif @@ -7,7 +7,7 @@ contract Dexible { constructor() { super(); } public void swap{sender}(IERC20 tokenIn, IERC20 tokenOut, IExchange router, uint amount) - throws (FailedSwap) { + throws (FailedSwap{any}) { atomic { tokenIn.approveFrom(sender, address(router), amount); router.exchange(sender, tokenIn, tokenOut, amount); @@ -17,7 +17,7 @@ contract Dexible { } public void multiSwap{sender}(IERC20[] tokens, IExchange[] routers, uint[] amounts) - throws (FailedSwap) { + throws (FailedSwap{any}) { atomic { uint{sender} i = 0; while (i < tokens.length() - 1) { diff --git a/test/contracts/applications/ERC20_depmap.scif b/test/contracts/applications/ERC20_depmap.scif index f37bbaf6..c66b1478 100644 --- a/test/contracts/applications/ERC20_depmap.scif +++ b/test/contracts/applications/ERC20_depmap.scif @@ -8,8 +8,8 @@ contract ERC20 implements IERC20 { bytes _name; bytes _symbol; - exception ERC20InsufficientBalance(address owner, uint cur, uint needed); - exception ERC20InsufficientAllowance(address owner, uint cur, uint needed); + exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); constructor(bytes name_, bytes symbol_) { _name = endorse(name_, sender -> this); @@ -65,7 +65,7 @@ contract ERC20 implements IERC20 { return _allowances[owner][spender]; } - public void transferFrom{sender -> from; sender}(final address from, address to, uint val) throws (ERC20InsufficientAllowance{this}, ERC20InsufficientBalance{this}) { + public void transferFrom{sender -> from; sender}(final address from, address to, uint val) throws (ERC20InsufficientAllowance{from}, ERC20InsufficientBalance{from}) { endorse([from, to, val], sender -> from) when (_allowances[from][sender] >= val) { transfer(from, to, val); @@ -73,7 +73,7 @@ contract ERC20 implements IERC20 { _allowances[from][sender] -= val; } } else { - throw ERC20InsufficientAllowance(to, _allowances[from][sender], val); + throw ERC20InsufficientAllowance(from, sender, _allowances[from][sender], val); } } @@ -82,7 +82,7 @@ contract ERC20 implements IERC20 { currentAllowance = allowance(owner, spender); if (currentAllowance != UINT_MAX) { if (currentAllowance < val) { - throw ERC20InsufficientAllowance(spender, currentAllowance, val); + throw ERC20InsufficientAllowance(owner, spender, currentAllowance, val); } unchecked { _allowances[owner][spender] = currentAllowance - val; diff --git a/test/contracts/applications/ERC20_depmap_old.scif b/test/contracts/applications/ERC20_depmap_old.scif index 219833d0..73489542 100644 --- a/test/contracts/applications/ERC20_depmap_old.scif +++ b/test/contracts/applications/ERC20_depmap_old.scif @@ -8,8 +8,8 @@ contract ERC20 implements IERC20 { bytes _name; bytes _symbol; - exception ERC20InsufficientBalance(address owner, uint cur, uint needed); - exception ERC20InsufficientAllowance(address owner, uint cur, uint needed); + exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); constructor(bytes name_, bytes symbol_) { _name = endorse(name_, sender -> this); @@ -64,14 +64,14 @@ contract ERC20 implements IERC20 { return _allowances[owner][spender]; } - public void transferFrom{sender -> from; sender}(final address from, address to, uint val) throws (ERC20InsufficientAllowance{this}, ERC20InsufficientBalance{this}) { + public void transferFrom{sender -> from; sender}(final address from, address to, uint val) throws (ERC20InsufficientAllowance{from}, ERC20InsufficientBalance{from}) { uint{from} currentAllowance = _allowances[from][sender]; endorse([from, to, val], sender -> from) when (currentAllowance >= val) { transfer(from, to, val); _allowances[from][sender] = currentAllowance - val; } else { - throw ERC20InsufficientAllowance(to, currentAllowance, val); + throw ERC20InsufficientAllowance(from, sender, currentAllowance, val); } } @@ -80,7 +80,7 @@ contract ERC20 implements IERC20 { currentAllowance = allowance(owner, spender); if (currentAllowance != UINT_MAX) { if (currentAllowance < val) { - throw ERC20InsufficientAllowance(spender, currentAllowance, val); + throw ERC20InsufficientAllowance(owner, spender, currentAllowance, val); } _allowances[owner][spender] = currentAllowance - val; } diff --git a/test/contracts/applications/HODLWallet.scif b/test/contracts/applications/HODLWallet.scif index 2ff0ce11..bf8bf619 100644 --- a/test/contracts/applications/HODLWallet.scif +++ b/test/contracts/applications/HODLWallet.scif @@ -11,7 +11,7 @@ contract HODLWallet { super(); } - public void doWithdraw{from -> this}(final address from, address to, uint amount) { + public void doWithdraw{from -> this; any}(final address from, address to, uint amount) { endorse([from, to, amount], from -> this) when (amount <= MAX_WITHDRAWAL && balances[from] >= amount && withdrawalCount[from] < 3) { unchecked { @@ -37,7 +37,7 @@ contract HODLWallet { } } - public void withdrawForTo{sender -> from; sender}(final address from, address to, uint amount) { + public void withdrawForTo{sender -> from; any}(final address from, address to, uint amount) { endorse([from, to, amount], sender -> from) when (approvals[from][sender]) { doWithdraw(from, to, amount); diff --git a/test/contracts/applications/IERC20.scif b/test/contracts/applications/IERC20.scif index 85db5c71..8e288420 100644 --- a/test/contracts/applications/IERC20.scif +++ b/test/contracts/applications/IERC20.scif @@ -1,9 +1,9 @@ interface IERC20 { - exception ERC20InsufficientBalance(address owner, uint cur, uint needed); - exception ERC20InsufficientAllowance(address owner, uint cur, uint needed); + exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); public uint balanceOf(address account); public void approve{sender}(address allowed, uint amount); public void approveFrom{from}(final address from, address spender, uint val); public void transfer{from -> this}(final address from, address to, uint amount) throws (ERC20InsufficientBalance{this}); - public void transferFrom{sender -> from; sender}(final address from, address to, uint amount) throws (ERC20InsufficientAllowance{this}, ERC20InsufficientBalance{this}); + public void transferFrom{sender -> from; sender}(final address from, address to, uint amount) throws (ERC20InsufficientAllowance{from}, ERC20InsufficientBalance{from}); } \ No newline at end of file diff --git a/test/contracts/applications/IERC777.scif b/test/contracts/applications/IERC777.scif index 7db9bc36..e70e3ceb 100644 --- a/test/contracts/applications/IERC777.scif +++ b/test/contracts/applications/IERC777.scif @@ -1,9 +1,9 @@ interface IERC777 { - exception ERC20InsufficientBalance(address owner, uint cur, uint needed); - exception ERC20InsufficientAllowance(address owner, uint cur, uint needed); + exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); public uint balanceOf(address account); public void approve{sender}(address allowed, uint amount); public void approveFrom{from}(final address from, address spender, uint val); public void transfer{from -> this; any}(final address from, address to, uint amount) throws (ERC20InsufficientBalance{this}); - public void transferFrom{sender -> from; any}(final address from, address to, uint amount) throws (ERC20InsufficientAllowance{this}, ERC20InsufficientBalance{this}); + public void transferFrom{sender -> from; any}(final address from, address to, uint amount) throws (ERC20InsufficientAllowance{from}, ERC20InsufficientBalance{this}); } \ No newline at end of file diff --git a/test/contracts/ifcTypechecking/AssertFalseHandler.scif b/test/contracts/ifcTypechecking/AssertFalseHandler.scif new file mode 100644 index 00000000..145a52c3 --- /dev/null +++ b/test/contracts/ifcTypechecking/AssertFalseHandler.scif @@ -0,0 +1,20 @@ +// An assertion of false never completes normally, so a handler that consists +// of one does not make control flow after the try untrusted. +contract AssertFalseHandler { + uint{this} count; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}() { + try { + g(); + } catch (E e) { + assert false; + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/CallPcTrusted.scif b/test/contracts/ifcTypechecking/CallPcTrusted.scif new file mode 100644 index 00000000..2863e002 --- /dev/null +++ b/test/contracts/ifcTypechecking/CallPcTrusted.scif @@ -0,0 +1,13 @@ +import "./ISelfVouching.scif"; +// Control flow after a call through a trusted reference stays trusted. +contract CallPcTrusted { + uint{this} count; + constructor() { super(); } + + public void f{this}(ISelfVouching{this} x) { + lock(this) { + x.get(); + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/CallPcUntrusted_W01.scif b/test/contracts/ifcTypechecking/CallPcUntrusted_W01.scif new file mode 100644 index 00000000..e4ff2ce1 --- /dev/null +++ b/test/contracts/ifcTypechecking/CallPcUntrusted_W01.scif @@ -0,0 +1,14 @@ +import "./ISelfVouching.scif"; +// Control flow after a call is trusted no more than the reference used to make +// it, whatever the callee's labels claim. +contract CallPcUntrusted_W01 { + uint{this} count; + constructor() { super(); } + + public void f{this}(ISelfVouching{any} x) { + lock(this) { + x.get(); + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/CallResultTrusted.scif b/test/contracts/ifcTypechecking/CallResultTrusted.scif new file mode 100644 index 00000000..93e3d047 --- /dev/null +++ b/test/contracts/ifcTypechecking/CallResultTrusted.scif @@ -0,0 +1,9 @@ +import "./ISelfVouching.scif"; +// A callee's return label is a claim trusted as much as the reference used to call it. +contract CallResultTrusted { + constructor() { super(); } + + public uint{this} f{this}(ISelfVouching{this} x) { + return x.get(); + } +} diff --git a/test/contracts/ifcTypechecking/CallResultUntrusted_W01.scif b/test/contracts/ifcTypechecking/CallResultUntrusted_W01.scif new file mode 100644 index 00000000..cba55e5c --- /dev/null +++ b/test/contracts/ifcTypechecking/CallResultUntrusted_W01.scif @@ -0,0 +1,10 @@ +import "./ISelfVouching.scif"; +// A callee's return label is a claim trusted no more than the reference used to +// call it: an untrusted contract cannot vouch for its result on its caller's behalf. +contract CallResultUntrusted_W01 { + constructor() { super(); } + + public uint{this} f{this}(ISelfVouching{any} x) { + return x.get(); + } +} diff --git a/test/contracts/ifcTypechecking/EndorseIfBodyPcLow_W01.scif b/test/contracts/ifcTypechecking/EndorseIfBodyPcLow_W01.scif new file mode 100644 index 00000000..7d8e6d22 --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorseIfBodyPcLow_W01.scif @@ -0,0 +1,16 @@ +import "./ISelfVouching.scif"; +// Control flow after an endorse-if is trusted no more than control flow at the +// end of its branches. +contract EndorseIfBodyPcLow_W01 { + uint{this} count; + constructor() { super(); } + + public void f{this}(ISelfVouching{any} x, uint{this} n) { + lock(this) { + endorse(n, this -> this) when (n > 0) { + x.get(); + } + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/EndorseIfBodyPcTrusted.scif b/test/contracts/ifcTypechecking/EndorseIfBodyPcTrusted.scif new file mode 100644 index 00000000..ef8dcb2a --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorseIfBodyPcTrusted.scif @@ -0,0 +1,16 @@ +import "./ISelfVouching.scif"; +// Control flow after an endorse-if whose branch calls a trusted contract stays +// trusted. +contract EndorseIfBodyPcTrusted { + uint{this} count; + constructor() { super(); } + + public void f{this}(ISelfVouching{this} x, uint{this} n) { + lock(this) { + endorse(n, this -> this) when (n > 0) { + x.get(); + } + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/EndorseIfThrowLow_W01.scif b/test/contracts/ifcTypechecking/EndorseIfThrowLow_W01.scif new file mode 100644 index 00000000..286f4eb9 --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorseIfThrowLow_W01.scif @@ -0,0 +1,13 @@ +// An exception thrown in a branch of an endorse-if must be trusted at its +// declared label. +contract EndorseIfThrowLow_W01 { + exception E(); + constructor() { super(); } + + public void f{any}(uint{any} n) throws (E{this}) { + endorse(n, any -> this) when (n > 0) { + } else { + throw E(); + } + } +} diff --git a/test/contracts/ifcTypechecking/EndorseIfThrowUntrusted.scif b/test/contracts/ifcTypechecking/EndorseIfThrowUntrusted.scif new file mode 100644 index 00000000..5be5fd05 --- /dev/null +++ b/test/contracts/ifcTypechecking/EndorseIfThrowUntrusted.scif @@ -0,0 +1,12 @@ +// An exception thrown in a branch of an endorse-if may be declared untrusted. +contract EndorseIfThrowUntrusted { + exception E(); + constructor() { super(); } + + public void f{any}(uint{any} n) throws (E{any}) { + endorse(n, any -> this) when (n > 0) { + } else { + throw E(); + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionData.scif b/test/contracts/ifcTypechecking/ExceptionData.scif new file mode 100644 index 00000000..3474e175 --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionData.scif @@ -0,0 +1,18 @@ +// Trusted data thrown in a trusted exception can be used as trusted in its handler. +contract ExceptionData { + uint{this} secret; + exception E(uint v); + constructor() { super(); } + + void g{this}(uint{this} x) throws (E{this}) { + throw E(x); + } + + public void f{this}(uint{this} x) { + try { + g(x); + } catch (E e) { + secret = e.v; + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionDataLow_W01.scif b/test/contracts/ifcTypechecking/ExceptionDataLow_W01.scif new file mode 100644 index 00000000..d19fb52c --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionDataLow_W01.scif @@ -0,0 +1,10 @@ +// An exception's label covers its data, so untrusted data cannot be thrown in +// an exception declared trusted. +contract ExceptionDataLow_W01 { + exception E(uint v); + constructor() { super(); } + + public void g{this}(uint{any} x) throws (E{this}) { + throw E(x); + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionDependentLabel.scif b/test/contracts/ifcTypechecking/ExceptionDependentLabel.scif new file mode 100644 index 00000000..1aba35c1 --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionDependentLabel.scif @@ -0,0 +1,22 @@ +// An exception's data may be labeled by a principal it carries. A handler can +// use the data as trusted once it knows that principal is trusted. +contract ExceptionDependentLabel { + uint{this} secret; + exception E(address owner, uint{owner} v); + constructor() { super(); } + + void g{this}(final address a, uint{a} x) throws (E{this}) { + throw E(a, x); + } + + public void f{this}(final address a, uint{a} x) { + try { + g(a, x); + } catch (E e) { + final address owner = e.owner; + if (owner => this) { + secret = e.v; + } + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionDependentLabelLow_W01.scif b/test/contracts/ifcTypechecking/ExceptionDependentLabelLow_W01.scif new file mode 100644 index 00000000..c37ea69b --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionDependentLabelLow_W01.scif @@ -0,0 +1,19 @@ +// Data labeled by a principal an exception carries is trusted only as much as +// that principal. +contract ExceptionDependentLabelLow_W01 { + uint{this} secret; + exception E(address owner, uint{owner} v); + constructor() { super(); } + + void g{this}(final address a, uint{a} x) throws (E{this}) { + throw E(a, x); + } + + public void f{this}(final address a, uint{a} x) { + try { + g(a, x); + } catch (E e) { + secret = e.v; + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionDependentThrowLow_W01.scif b/test/contracts/ifcTypechecking/ExceptionDependentThrowLow_W01.scif new file mode 100644 index 00000000..79260000 --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionDependentThrowLow_W01.scif @@ -0,0 +1,10 @@ +// Data thrown for a parameter labeled by a principal parameter must be trusted +// by the principal passed for it. +contract ExceptionDependentThrowLow_W01 { + exception E(address owner, uint{owner} v); + constructor() { super(); } + + public void g{this}(final address a, final address b, uint{a} x) throws (E{this}) { + throw E(b, x); + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionExternalTrusted.scif b/test/contracts/ifcTypechecking/ExceptionExternalTrusted.scif new file mode 100644 index 00000000..1dea844d --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionExternalTrusted.scif @@ -0,0 +1,17 @@ +import "./ICrossThrower.scif"; +// An exception from a trusted contract is trusted at its declared label. +contract ExceptionExternalTrusted { + uint{this} secret; + exception Oops(); + constructor() { super(); } + + public void f{this}(ICrossThrower{this} x) { + lock(this) { + try { + x.boom(); + } catch (Oops e) { + secret = 1; + } + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionExternalUntrusted_W01.scif b/test/contracts/ifcTypechecking/ExceptionExternalUntrusted_W01.scif new file mode 100644 index 00000000..e0fe5c36 --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionExternalUntrusted_W01.scif @@ -0,0 +1,18 @@ +import "./ICrossThrower.scif"; +// An exception from another contract is trusted no more than the reference used +// to call it, whatever label the callee declares. +contract ExceptionExternalUntrusted_W01 { + uint{this} secret; + exception Oops(); + constructor() { super(); } + + public void f{this}(ICrossThrower{any} x) { + lock(this) { + try { + x.boom(); + } catch (Oops e) { + secret = 1; + } + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionHandlerLow_W01.scif b/test/contracts/ifcTypechecking/ExceptionHandlerLow_W01.scif new file mode 100644 index 00000000..ec5bd92f --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionHandlerLow_W01.scif @@ -0,0 +1,19 @@ +// A handler runs on the exception's path, so it is trusted no more than the +// exception's declared label. +contract ExceptionHandlerLow_W01 { + uint{this} secret; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}() { + try { + g(); + } catch (E e) { + secret = 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionHandlerTrusted.scif b/test/contracts/ifcTypechecking/ExceptionHandlerTrusted.scif new file mode 100644 index 00000000..524c342c --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionHandlerTrusted.scif @@ -0,0 +1,18 @@ +// A handler for an exception declared trusted runs with trusted control flow. +contract ExceptionHandlerTrusted { + uint{this} secret; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{this}) { + throw E(); + } + + public void f{this}() { + try { + g(); + } catch (E e) { + secret = 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionThrowLow_W01.scif b/test/contracts/ifcTypechecking/ExceptionThrowLow_W01.scif new file mode 100644 index 00000000..4f18b2ec --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionThrowLow_W01.scif @@ -0,0 +1,9 @@ +// An exception must be thrown from control flow trusted at its declared label. +contract ExceptionThrowLow_W01 { + exception E(); + constructor() { super(); } + + public void g{any}() throws (E{this}) { + throw E(); + } +} diff --git a/test/contracts/ifcTypechecking/ExceptionThrowUntrusted.scif b/test/contracts/ifcTypechecking/ExceptionThrowUntrusted.scif new file mode 100644 index 00000000..9faa4ff5 --- /dev/null +++ b/test/contracts/ifcTypechecking/ExceptionThrowUntrusted.scif @@ -0,0 +1,9 @@ +// An untrusted control flow can throw an exception declared untrusted. +contract ExceptionThrowUntrusted { + exception E(); + constructor() { super(); } + + public void g{any}() throws (E{any}) { + throw E(); + } +} diff --git a/test/contracts/ifcTypechecking/HandlerThrowLow_W01.scif b/test/contracts/ifcTypechecking/HandlerThrowLow_W01.scif new file mode 100644 index 00000000..b5fcca51 --- /dev/null +++ b/test/contracts/ifcTypechecking/HandlerThrowLow_W01.scif @@ -0,0 +1,19 @@ +// An exception thrown in a handler leaves the method on its path, which is +// trusted no more than the exception the handler caught. +contract HandlerThrowLow_W01 { + exception E(); + exception F(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}() throws (F{this}) { + try { + g(); + } catch (E e) { + throw F(); + } + } +} diff --git a/test/contracts/ifcTypechecking/HandlerThrowUntrusted.scif b/test/contracts/ifcTypechecking/HandlerThrowUntrusted.scif new file mode 100644 index 00000000..35c6c97d --- /dev/null +++ b/test/contracts/ifcTypechecking/HandlerThrowUntrusted.scif @@ -0,0 +1,19 @@ +// An exception thrown in a handler may leave the method when declared as +// untrusted as the exception the handler caught. +contract HandlerThrowUntrusted { + exception E(); + exception F(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}() throws (F{any}) { + try { + g(); + } catch (E e) { + throw F(); + } + } +} diff --git a/test/contracts/ifcTypechecking/ICrossThrower.scif b/test/contracts/ifcTypechecking/ICrossThrower.scif new file mode 100644 index 00000000..9a10141d --- /dev/null +++ b/test/contracts/ifcTypechecking/ICrossThrower.scif @@ -0,0 +1,4 @@ +interface ICrossThrower { + exception Oops(); + public void boom{any}() throws (Oops{sender}); +} diff --git a/test/contracts/ifcTypechecking/ISelfVouching.scif b/test/contracts/ifcTypechecking/ISelfVouching.scif new file mode 100644 index 00000000..6015a795 --- /dev/null +++ b/test/contracts/ifcTypechecking/ISelfVouching.scif @@ -0,0 +1,3 @@ +interface ISelfVouching { + public uint{sender} get{any}(); +} diff --git a/test/contracts/ifcTypechecking/IfEarlierThrowLow_W01.scif b/test/contracts/ifcTypechecking/IfEarlierThrowLow_W01.scif new file mode 100644 index 00000000..8dddbe22 --- /dev/null +++ b/test/contracts/ifcTypechecking/IfEarlierThrowLow_W01.scif @@ -0,0 +1,18 @@ +// An exception from any statement of a branch, not only the last, leaves the +// method on its path. +contract IfEarlierThrowLow_W01 { + uint{this} count; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}(uint{this} n) throws (E{this}) { + if (n > 0) { + g(); + count = 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/IfEarlierThrowUntrusted.scif b/test/contracts/ifcTypechecking/IfEarlierThrowUntrusted.scif new file mode 100644 index 00000000..5b585637 --- /dev/null +++ b/test/contracts/ifcTypechecking/IfEarlierThrowUntrusted.scif @@ -0,0 +1,18 @@ +// An exception from an earlier statement of a branch may leave the method when +// it is declared untrusted. +contract IfEarlierThrowUntrusted { + uint{this} count; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}(uint{this} n) throws (E{any}) { + if (n > 0) { + g(); + count = 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/RescueAllLow_W01.scif b/test/contracts/ifcTypechecking/RescueAllLow_W01.scif new file mode 100644 index 00000000..4909f8a6 --- /dev/null +++ b/test/contracts/ifcTypechecking/RescueAllLow_W01.scif @@ -0,0 +1,14 @@ +// Nothing is known about where a failure came from, so a handler for failures +// runs with untrusted control flow. +contract RescueAllLow_W01 { + uint{this} count; + constructor() { super(); } + + public void f{this; any}(address{this} to) { + atomic { + send(to, 1); + } rescue * { + count = 0; + } + } +} diff --git a/test/contracts/ifcTypechecking/RescueAllUntrusted.scif b/test/contracts/ifcTypechecking/RescueAllUntrusted.scif new file mode 100644 index 00000000..28f4f2fd --- /dev/null +++ b/test/contracts/ifcTypechecking/RescueAllUntrusted.scif @@ -0,0 +1,13 @@ +// A handler for failures can update untrusted state. +contract RescueAllUntrusted { + uint{any} count; + constructor() { super(); } + + public void f{this; any}(address{this} to) { + atomic { + send(to, 1); + } rescue * { + count = 0; + } + } +} diff --git a/test/contracts/ifcTypechecking/Wallet_lock_exception.scif b/test/contracts/ifcTypechecking/Wallet_lock_exception.scif index 0ab7f080..d6df0b16 100644 --- a/test/contracts/ifcTypechecking/Wallet_lock_exception.scif +++ b/test/contracts/ifcTypechecking/Wallet_lock_exception.scif @@ -11,7 +11,7 @@ contract Wallet { // // ... // } constructor() { super(); } - public void withdraw(uint _amount) throws (balanceNotEnough, transferFailure) { + public void withdraw(uint _amount) throws (balanceNotEnough, transferFailure{any}) { uint amount = endorse(_amount, any -> this); if (balances[sender] >= amount) { lock(this) { diff --git a/test/contracts/ifcTypechecking/WhileBodyPcLow_W01.scif b/test/contracts/ifcTypechecking/WhileBodyPcLow_W01.scif new file mode 100644 index 00000000..88367ce4 --- /dev/null +++ b/test/contracts/ifcTypechecking/WhileBodyPcLow_W01.scif @@ -0,0 +1,16 @@ +import "./ISelfVouching.scif"; +// Control flow after a loop is trusted no more than control flow at the end of +// its body. +contract WhileBodyPcLow_W01 { + uint{this} count; + constructor() { super(); } + + public void f{this}(ISelfVouching{any} x, uint{this} n) { + lock(this) { + while (n > 0) { + x.get(); + } + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/WhileBodyPcTrusted.scif b/test/contracts/ifcTypechecking/WhileBodyPcTrusted.scif new file mode 100644 index 00000000..7ae2879e --- /dev/null +++ b/test/contracts/ifcTypechecking/WhileBodyPcTrusted.scif @@ -0,0 +1,15 @@ +import "./ISelfVouching.scif"; +// Control flow after a loop whose body calls a trusted contract stays trusted. +contract WhileBodyPcTrusted { + uint{this} count; + constructor() { super(); } + + public void f{this}(ISelfVouching{this} x, uint{this} n) { + lock(this) { + while (n > 0) { + x.get(); + } + } + count = count + 1; + } +} diff --git a/test/contracts/ifcTypechecking/WhileThrowLow_W01.scif b/test/contracts/ifcTypechecking/WhileThrowLow_W01.scif new file mode 100644 index 00000000..871623f9 --- /dev/null +++ b/test/contracts/ifcTypechecking/WhileThrowLow_W01.scif @@ -0,0 +1,17 @@ +// An exception thrown in a loop body leaves the method on its path. +contract WhileThrowLow_W01 { + uint{this} count; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}(uint{this} n) throws (E{this}) { + while (n > 0) { + g(); + count = 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/WhileThrowUntrusted.scif b/test/contracts/ifcTypechecking/WhileThrowUntrusted.scif new file mode 100644 index 00000000..b7ac80db --- /dev/null +++ b/test/contracts/ifcTypechecking/WhileThrowUntrusted.scif @@ -0,0 +1,17 @@ +// An exception thrown in a loop body may leave the method when declared untrusted. +contract WhileThrowUntrusted { + uint{this} count; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}(uint{this} n) throws (E{any}) { + while (n > 0) { + g(); + count = 1; + } + } +} diff --git a/test/contracts/multiContract/Dexible.scif b/test/contracts/multiContract/Dexible.scif index 18b1111e..a4d2e433 100644 --- a/test/contracts/multiContract/Dexible.scif +++ b/test/contracts/multiContract/Dexible.scif @@ -23,7 +23,7 @@ contract Dexible { constructor() { super(); } public void swap{sender}(IERC20 tokenIn, IERC20 tokenOut, IExchange router, uint amount) - throws (FailedSwap) { + throws (FailedSwap{any}) { emit Swap(tokenIn, tokenOut, router, amount); atomic { tokenIn.approveFrom(sender, address(router), amount); @@ -34,7 +34,7 @@ contract Dexible { } public void multiSwap{sender}(IERC20[] tokens, IExchange[] routers, uint[] amounts) - throws (FailedSwap) { + throws (FailedSwap{any}) { atomic { uint{sender} i = 0; while (i < tokens.length() - 1) { diff --git a/test/contracts/multiContract/DexibleWithEvents.scif b/test/contracts/multiContract/DexibleWithEvents.scif index 6ad05e3c..88e50103 100644 --- a/test/contracts/multiContract/DexibleWithEvents.scif +++ b/test/contracts/multiContract/DexibleWithEvents.scif @@ -23,7 +23,7 @@ contract Dexible { constructor() { super(); } public void swap{sender}(IERC20 tokenIn, IERC20 tokenOut, IExchange router, uint amount) - throws (FailedSwap) { + throws (FailedSwap{any}) { emit Swap(tokenIn, tokenOut, router, amount); atomic { tokenIn.approveFrom(sender, address(router), amount); @@ -34,7 +34,7 @@ contract Dexible { } public void multiSwap{sender}(IERC20[] tokens, IExchange[] routers, uint[] amounts) - throws (FailedSwap) { + throws (FailedSwap{any}) { atomic { uint{sender} i = 0; while (i < tokens.length() - 1) { diff --git a/test/contracts/regularTypechecking/ExceptionParamLabelNotPrincipal_W01.scif b/test/contracts/regularTypechecking/ExceptionParamLabelNotPrincipal_W01.scif new file mode 100644 index 00000000..29235f93 --- /dev/null +++ b/test/contracts/regularTypechecking/ExceptionParamLabelNotPrincipal_W01.scif @@ -0,0 +1,6 @@ +// The label of an exception parameter may name only principal-typed parameters +// of the exception. +contract ExceptionParamLabelNotPrincipal_W01 { + exception E(uint n, uint{n} v); + constructor() { super(); } +} diff --git a/test/contracts/regularTypechecking/ExceptionParamLabels.scif b/test/contracts/regularTypechecking/ExceptionParamLabels.scif new file mode 100644 index 00000000..142c47ab --- /dev/null +++ b/test/contracts/regularTypechecking/ExceptionParamLabels.scif @@ -0,0 +1,6 @@ +// Exception parameters may be unlabeled, labeled with contract-level principals, +// or labeled with principal-typed parameters of the exception. +contract ExceptionParamLabels { + exception E(address owner, uint{owner} balance, uint{any} needed, uint plain); + constructor() { super(); } +} diff --git a/test/src/TestIfcTypechecking.java b/test/src/TestIfcTypechecking.java index 4b3303cb..20b0b78b 100644 --- a/test/src/TestIfcTypechecking.java +++ b/test/src/TestIfcTypechecking.java @@ -65,6 +65,21 @@ public class TestIfcTypechecking { "ifcTypechecking/NewTail", "ifcTypechecking/NewEndorseTail", "ifcTypechecking/NewLocked", + "ifcTypechecking/CallResultTrusted", + "ifcTypechecking/CallPcTrusted", + "ifcTypechecking/ExceptionHandlerTrusted", + "ifcTypechecking/ExceptionThrowUntrusted", + "ifcTypechecking/ExceptionData", + "ifcTypechecking/ExceptionExternalTrusted", + "ifcTypechecking/ExceptionDependentLabel", + "ifcTypechecking/WhileBodyPcTrusted", + "ifcTypechecking/EndorseIfBodyPcTrusted", + "ifcTypechecking/EndorseIfThrowUntrusted", + "ifcTypechecking/IfEarlierThrowUntrusted", + "ifcTypechecking/WhileThrowUntrusted", + "ifcTypechecking/HandlerThrowUntrusted", + "ifcTypechecking/RescueAllUntrusted", + "ifcTypechecking/AssertFalseHandler", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -134,6 +149,21 @@ void testPositive(String contractName) throws Exception { "ifcTypechecking/TrustTestTwoBranches_W01", "ifcTypechecking/NewUntrusted_W01", "ifcTypechecking/NewNonTail_W01", + "ifcTypechecking/CallResultUntrusted_W01", + "ifcTypechecking/CallPcUntrusted_W01", + "ifcTypechecking/ExceptionHandlerLow_W01", + "ifcTypechecking/ExceptionThrowLow_W01", + "ifcTypechecking/ExceptionDataLow_W01", + "ifcTypechecking/ExceptionExternalUntrusted_W01", + "ifcTypechecking/ExceptionDependentLabelLow_W01", + "ifcTypechecking/ExceptionDependentThrowLow_W01", + "ifcTypechecking/WhileBodyPcLow_W01", + "ifcTypechecking/EndorseIfBodyPcLow_W01", + "ifcTypechecking/EndorseIfThrowLow_W01", + "ifcTypechecking/IfEarlierThrowLow_W01", + "ifcTypechecking/WhileThrowLow_W01", + "ifcTypechecking/HandlerThrowLow_W01", + "ifcTypechecking/RescueAllLow_W01", // "examples/ERC20", }) void testNegative(String contractName) throws Exception { diff --git a/test/src/TestRegularTypechecking.java b/test/src/TestRegularTypechecking.java index 00e76627..70ce96bb 100644 --- a/test/src/TestRegularTypechecking.java +++ b/test/src/TestRegularTypechecking.java @@ -50,6 +50,7 @@ public class TestRegularTypechecking { "regularTypechecking/ClosureJoinPcEx", "regularTypechecking/ClosureBinderRegularOK", "regularTypechecking/FinalFieldInitOK", + "regularTypechecking/ExceptionParamLabels", "regularTypechecking/TrustQueryNonFinal", }) void testPositive(String contractName) throws Exception { @@ -142,6 +143,7 @@ void testNegative(String contractName) { "regularTypechecking/FinalLocalReassign_W01", "regularTypechecking/FinalParamReassign_W01", "regularTypechecking/FieldLabelNonFinal_W01", + "regularTypechecking/ExceptionParamLabelNotPrincipal_W01", }) void testReportedError(String contractName) throws Exception { URL input = ClassLoader.getSystemResource(contractName + ".scif"); From 5f7232ba17e03d8249e68166e31d0801851777d2 Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Fri, 6 Nov 2026 07:30:13 -0500 Subject: [PATCH 6/8] Fine-grained checking of labels in struct fields. --- src/SCIF.cup | 4 +- src/ast/Attribute.java | 45 ++++++++++++++++++- src/ast/BinOp.java | 7 ++- src/ast/BoolOp.java | 7 ++- src/ast/Call.java | 16 ++++++- src/ast/ClosureCreation.java | 28 +++++++++++- src/ast/Compare.java | 7 ++- src/ast/ComplexIfLabel.java | 2 +- src/ast/ConstantExpr.java | 7 ++- src/ast/Contract.java | 3 ++ src/ast/Endorse.java | 7 ++- src/ast/Expression.java | 44 +++++++++++++++++- src/ast/FlowsToExp.java | 9 +++- src/ast/Hole.java | 8 +++- src/ast/IfLabel.java | 6 +++ src/ast/Interface.java | 3 ++ src/ast/Literal.java | 7 ++- src/ast/Name.java | 7 ++- src/ast/New.java | 31 ++++++++----- src/ast/PrimitiveIfLabel.java | 2 +- src/ast/StateVariableDeclaration.java | 5 +++ src/ast/StructDef.java | 37 +++++++++++++-- src/ast/Subscript.java | 18 +++++++- src/ast/UnaryOp.java | 8 +++- src/typecheck/StructTypeSym.java | 15 +++++++ src/typecheck/VisitEnv.java | 5 +++ test/contracts/applications/KoET.scif | 4 +- .../ifcTypechecking/IPointSource.scif | 7 +++ .../ifcTypechecking/StructForeignMember.scif | 14 ++++++ .../StructForeignMemberLow_W01.scif | 13 ++++++ .../ifcTypechecking/StructMemberInput.scif | 12 +++++ .../StructMemberInputLow_W01.scif | 12 +++++ .../ifcTypechecking/StructMemberRead.scif | 14 ++++++ .../StructMemberReadLow_W01.scif | 14 ++++++ .../ifcTypechecking/StructMemberWrite.scif | 13 ++++++ .../StructMemberWriteLow_W01.scif | 13 ++++++ .../StructMemberLabelNonFinal_W01.scif | 9 ++++ .../StructMemberLabelNotPrincipal_W01.scif | 8 ++++ .../StructMemberLabels.scif | 13 ++++++ test/src/TestIfcTypechecking.java | 8 ++++ test/src/TestRegularTypechecking.java | 3 ++ 41 files changed, 459 insertions(+), 36 deletions(-) create mode 100644 test/contracts/ifcTypechecking/IPointSource.scif create mode 100644 test/contracts/ifcTypechecking/StructForeignMember.scif create mode 100644 test/contracts/ifcTypechecking/StructForeignMemberLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/StructMemberInput.scif create mode 100644 test/contracts/ifcTypechecking/StructMemberInputLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/StructMemberRead.scif create mode 100644 test/contracts/ifcTypechecking/StructMemberReadLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/StructMemberWrite.scif create mode 100644 test/contracts/ifcTypechecking/StructMemberWriteLow_W01.scif create mode 100644 test/contracts/regularTypechecking/StructMemberLabelNonFinal_W01.scif create mode 100644 test/contracts/regularTypechecking/StructMemberLabelNotPrincipal_W01.scif create mode 100644 test/contracts/regularTypechecking/StructMemberLabels.scif diff --git a/src/SCIF.cup b/src/SCIF.cup index 55fc384b..528f1de0 100644 --- a/src/SCIF.cup +++ b/src/SCIF.cup @@ -666,10 +666,10 @@ plain_var_dec ::= // RESULT.setLoc(new CodeLocation(aleft, aright, filename)); // // :} - type:t NAME:a SEMI {: + maybeLabeledType:t NAME:a SEMI {: Name name = new Name((String)a); name.setLoc(new CodeLocation(aleft, aright, filename)); - StateVariableDeclaration rnt = new StateVariableDeclaration(name, new LabeledType(t, null), null, false, false, false); + StateVariableDeclaration rnt = new StateVariableDeclaration(name, t, null, false, false, false); RESULT = rnt; RESULT.setLoc(new CodeLocation(aleft, aright, filename)); :}; diff --git a/src/ast/Attribute.java b/src/ast/Attribute.java index 4e2f378e..42919ec2 100644 --- a/src/ast/Attribute.java +++ b/src/ast/Attribute.java @@ -86,7 +86,7 @@ public VarSym getVarInfo(NTCEnv env) throws SemanticException { } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { //TODO: assuming only one-level attribute access // to add support to multi-level access String varName = ((Name) value).id; @@ -113,7 +113,11 @@ public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws S // env.curContractSym().getName(), // "Integrity of the member")); // if (!ifAttLabel.equals(attrValueLabel)) { - if (varSym.typeSym instanceof ExceptionTypeSym expType) { + if (varSym.typeSym instanceof StructTypeSym structType) { + // A member is trusted at its label and no more than the struct value. + attrValueLabel = Utils.joinLabels(attrValueLabel, memberLabel(env, + structType.member(attr.id).ifl, attrValueLabel)); + } else if (varSym.typeSym instanceof ExceptionTypeSym expType) { // An exception's data is trusted at its parameter's label, in which each // principal parameter stands for the principal in that field. java.util.Map mapping = new java.util.HashMap<>(); @@ -149,6 +153,30 @@ public String principalName(VisitEnv env) { return null; } + /** + * The label {@code label} of a struct member, as this contract reads it in a struct value + * labeled {@code structLabel}. A struct declared by another contract labels its members in + * that contract's terms: its {@code this} is trusted only as much as the value, its top and + * bottom principals are this contract's, and any other principal it names is unknown here. + */ + private static String memberLabel(VisitEnv env, Label label, String structLabel) { + String own = env.thisSym().toSHErrLocFmt(); + String namespace = own.substring(0, own.length() - Utils.LABEL_THIS.length()); + java.util.Map mapping = new java.util.HashMap<>(); + for (VarSym p : label.principalSyms()) { + String name = p.toSHErrLocFmt(); + if (name.startsWith(namespace)) { + continue; + } + mapping.put(name, switch (p.getName()) { + case Utils.LABEL_THIS -> structLabel; + case Utils.LABEL_TOP -> env.curContractSym().top().toSHErrLocFmt(); + default -> env.curContractSym().any().toSHErrLocFmt(); + }); + } + return label.toSHErrLocFmt(mapping); + } + /** * The principal held by the principal-typed field {@code field} of the caught exception * {@code exception}, registered in {@code env}. Nothing is known about it. @@ -210,4 +238,17 @@ public java.util.Map readMap(CompileEnv code) { public Map writeMap(CompileEnv code) { return value.writeMap(code); } + + @Override + protected TypeSym staticType(VisitEnv env) { + TypeSym base = value.type(); + if (base instanceof ClosureTypeSym && attr.id.equals(Utils.CLOSURE_TARGET_MEMBER)) { + return env.builtinType(BuiltInT.ADDRESS); + } else if (base instanceof StructTypeSym structType) { + return structType.member(attr.id).typeSym; + } else if (base instanceof ExceptionTypeSym expType) { + return expType.getMemberVarInfo("", attr.id).typeSym; + } + throw new AssertionError("no member " + attr.id + " in type " + base.getName()); + } } diff --git a/src/ast/BinOp.java b/src/ast/BinOp.java index 182a3b44..fd65f60a 100644 --- a/src/ast/BinOp.java +++ b/src/ast/BinOp.java @@ -43,7 +43,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); @@ -110,4 +110,9 @@ public java.util.Map readMap(CompileEnv code) { result.putAll(right.readMap(code)); return result; } + + @Override + protected TypeSym staticType(VisitEnv env) { + return env.builtinType(BuiltInT.UINT); + } } diff --git a/src/ast/BoolOp.java b/src/ast/BoolOp.java index c257648b..ec514ba7 100644 --- a/src/ast/BoolOp.java +++ b/src/ast/BoolOp.java @@ -36,7 +36,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); @@ -103,4 +103,9 @@ public java.util.Map readMap(CompileEnv code) { result.putAll(right.readMap(code)); return result; } + + @Override + protected TypeSym staticType(VisitEnv env) { + return env.builtinType(BuiltInT.BOOL); + } } diff --git a/src/ast/Call.java b/src/ast/Call.java index 6bbf99e9..021813ca 100644 --- a/src/ast/Call.java +++ b/src/ast/Call.java @@ -50,6 +50,8 @@ ClosureTypeSym.RealLabels realLabels() { private List closurePcExLeaves = null; /** The invoked closure's declared type, kept for codegen. */ private ClosureTypeSym closureSym = null; + /** The type of this call's result, once it has been IFC-checked. */ + private TypeSym resultType; public Call() { this.args = new ArrayList<>(); @@ -276,7 +278,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S * operator other than {@code invoke} */ @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { ExpOutcome result = callIFConstraints(env, tail_position); // A call may assign to any field, so fields hold new principals afterwards. @@ -370,6 +372,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) env.checkFlow(psi.getNormalPath().c.lambda, beginContext.lambda, location, Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION); } + resultType = env.builtinType(BuiltInT.VOID); return new ExpOutcome(ifNamePc, psi); } else if (funcName.equals("push")) { // require pc => integrity of the array var @@ -389,6 +392,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) env.checkFlow(psi.getNormalPath().c.lambda, beginContext.lambda, location, Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION); } + resultType = env.builtinType(BuiltInT.VOID); return new ExpOutcome(ifNamePc, psi); } else if (funcName.equals("length")) { // return uint the same as @@ -398,6 +402,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) env.checkFlow(psi.getNormalPath().c.lambda, beginContext.lambda, location, Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION); } + resultType = env.builtinType(BuiltInT.UINT); return new ExpOutcome(var.ifl.toSHErrLocFmt(), psi); } else { throw new SemanticException("Unrecognized operator", location); @@ -529,6 +534,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) "Post-invocation control flow must carry the closure's value label"); psi.setNormalPath(endContext); env.checkFlow(psi.getNormalPath().c.pc, ifNameFuncRtnValue, location, "ln_to_t"); + resultType = cs.returnType; return new ExpOutcome(ifNameFuncRtnValue, psi); } @@ -586,6 +592,8 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) env.checkFlow(psi.getNormalPath().c.lambda, beginContext.lambda, location, Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION); } + resultType = env.containsContract(funcName) ? env.getContract(funcName) + : (TypeSym) env.getCurSym(funcName); return new ExpOutcome(ifNameArgValue, psi); } else { throw new SemanticException("method not found: " + funcName, location); @@ -745,6 +753,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // post-call pc (normal) => function return label (l_n) env.checkFlow(psi.getNormalPath().c.pc, ifNameFuncRtnValue, location, "Control flow after this call must be trusted by the function's return value label"); + resultType = funcSym.returnType; return new ExpOutcome(ifNameFuncRtnValue, psi); } @@ -1084,4 +1093,9 @@ public void checkAndRenameSuper(String extendsContractName) { } } } + + @Override + protected TypeSym staticType(VisitEnv env) { + return resultType; + } } diff --git a/src/ast/ClosureCreation.java b/src/ast/ClosureCreation.java index f5b9b98e..c0dda8f5 100644 --- a/src/ast/ClosureCreation.java +++ b/src/ast/ClosureCreation.java @@ -286,7 +286,7 @@ private ExpOutcome bindIFConstraints(VisitEnv env, PathOutcome psi, } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { Context beginContext = env.inContext; Context endContext = new Context(Utils.getLabelNamePc(toSHErrLocFmt()), @@ -561,4 +561,30 @@ public List children() { rtn.addAll(args); return rtn; } + + @Override + protected TypeSym staticType(VisitEnv env) { + List declaredParams = new ArrayList<>(); + TypeSym retSym; + if (callee instanceof Attribute att && att.value instanceof Name recvName) { + FuncSym target = ((InterfaceSym) env.getVar(recvName.id).typeSym) + .getFunc(att.attr.id); + for (VarSym p : target.parameters) { + declaredParams.add(p.typeSym); + } + retSym = target.returnType; + } else { + ClosureTypeSym source = + (ClosureTypeSym) env.getVar(((Name) callee).id).typeSym; + declaredParams.addAll(source.unboundParams); + retSym = source.returnType; + } + List stillUnbound = new ArrayList<>(); + for (int i = 0; i < args.size(); ++i) { + if (args.get(i) instanceof Hole) { + stillUnbound.add(declaredParams.get(i)); + } + } + return new ClosureTypeSym(stillUnbound, retSym, new ArrayList<>(), scopeContext); + } } diff --git a/src/ast/Compare.java b/src/ast/Compare.java index e2a3c344..60c0b467 100644 --- a/src/ast/Compare.java +++ b/src/ast/Compare.java @@ -44,7 +44,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); @@ -110,4 +110,9 @@ public java.util.Map readMap(CompileEnv code) { result.putAll(right.readMap(code)); return result; } + + @Override + protected TypeSym staticType(VisitEnv env) { + return env.builtinType(BuiltInT.BOOL); + } } diff --git a/src/ast/ComplexIfLabel.java b/src/ast/ComplexIfLabel.java index ac9ae0f7..d29045cb 100644 --- a/src/ast/ComplexIfLabel.java +++ b/src/ast/ComplexIfLabel.java @@ -100,7 +100,7 @@ public String toSHErrLocFmt(ScopeContext defContext) { // return rnt; // } - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) { // TODO: deal with dynamic labels return null; diff --git a/src/ast/ConstantExpr.java b/src/ast/ConstantExpr.java index ccec8dc1..3985d26f 100644 --- a/src/ast/ConstantExpr.java +++ b/src/ast/ConstantExpr.java @@ -25,7 +25,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) { } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) { Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), @@ -60,4 +60,9 @@ public List children() { List rtn = new ArrayList<>(); return rtn; } + + @Override + protected TypeSym staticType(VisitEnv env) { + return env.builtinType(BuiltInT.BOOL); + } } diff --git a/src/ast/Contract.java b/src/ast/Contract.java index 0924f756..6f42dde5 100644 --- a/src/ast/Contract.java +++ b/src/ast/Contract.java @@ -244,6 +244,9 @@ public void globalInfoVisit(InterfaceSym contractSym) throws SemanticException { for (StateVariableDeclaration dec : varDeclarations) { dec.globalInfoVisit(contractSym); } + for (StructDef def: structDefs) { + def.resolveMemberLabels(contractSym); + } trustSetting.globalInfoVisit(contractSym); diff --git a/src/ast/Endorse.java b/src/ast/Endorse.java index 7ede5270..82a9a799 100644 --- a/src/ast/Endorse.java +++ b/src/ast/Endorse.java @@ -36,7 +36,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { ExpOutcome vo = value.genIFConstraints(env, tail_position); String ifNameValue = vo.valueLabelName; String ifNameRtn = scopeContext.getSHErrLocName() + "." + "endorse" + location.toString(); @@ -100,4 +100,9 @@ public ArrayList children() { public java.util.Map readMap(CompileEnv code) { return value.readMap(code); } + + @Override + protected TypeSym staticType(VisitEnv env) { + return value.type(); + } } diff --git a/src/ast/Expression.java b/src/ast/Expression.java index b634c190..991b0c78 100644 --- a/src/ast/Expression.java +++ b/src/ast/Expression.java @@ -31,9 +31,51 @@ public VarSym getVarInfo(NTCEnv env) throws SemanticException { // return "unknown"; // } - public abstract ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) + /** The static type of this expression, once it has been checked for information flow. */ + private TypeSym type; + + /** + * Generates the information-flow constraints for this expression, evaluated in the context + * {@code env.inContext}, and returns the label of its value and the paths by which its + * evaluation can end. Afterwards {@link #type()} is defined. + */ + public final ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) + throws SemanticException { + ExpOutcome outcome = genFlowConstraints(env, tail_position); + type = staticType(env); + if (type == null) { + throw new AssertionError("no static type for " + getClass().getSimpleName() + + " at " + location); + } + return outcome; + } + + /** + * Generates the information-flow constraints for this expression, as specified for + * {@link #genIFConstraints}. + */ + protected abstract ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException; + /** + * The static type of this expression in {@code env}, called once its information-flow + * constraints have been generated in {@code env}. + */ + protected abstract TypeSym staticType(VisitEnv env) throws SemanticException; + + /** + * The static type of this expression. + * + * @throws AssertionError if this expression has not been checked for information flow + */ + public TypeSym type() { + if (type == null) { + throw new AssertionError("no static type for " + getClass().getSimpleName() + + " at " + location); + } + return type; + } + /** * If this expression is a principal-typed variable, or a cast of one to a contract or * address type, the SHErrLoc name of the principal it holds at the current program point. diff --git a/src/ast/FlowsToExp.java b/src/ast/FlowsToExp.java index f9d39f37..b52bcbbc 100644 --- a/src/ast/FlowsToExp.java +++ b/src/ast/FlowsToExp.java @@ -14,6 +14,7 @@ import typecheck.ExpOutcome; import typecheck.NTCEnv; import typecheck.ScopeContext; +import typecheck.TypeSym; import typecheck.VarSym; import typecheck.VisitEnv; import typecheck.exceptions.SemanticException; @@ -28,7 +29,8 @@ public FlowsToExp(Name a, Name b) { } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) + throws SemanticException { Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); @@ -105,4 +107,9 @@ public java.util.Map readMap(CompileEnv code) { result.putAll(rhs.readMap(code)); return result; } + + @Override + protected TypeSym staticType(VisitEnv env) { + return env.builtinType(BuiltInT.BOOL); + } } diff --git a/src/ast/Hole.java b/src/ast/Hole.java index 5ed8efdb..3e56634a 100644 --- a/src/ast/Hole.java +++ b/src/ast/Hole.java @@ -7,6 +7,7 @@ import typecheck.ExpOutcome; import typecheck.NTCEnv; import typecheck.ScopeContext; +import typecheck.TypeSym; import typecheck.VisitEnv; import typecheck.exceptions.SemanticException; @@ -25,7 +26,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { throw new UnsupportedOperationException( "hole `_` has no IFC constraints outside closure creation"); @@ -46,4 +47,9 @@ public boolean typeMatch(Expression expression) { public List children() { return new ArrayList<>(); } + + @Override + protected TypeSym staticType(VisitEnv env) { + throw new UnsupportedOperationException("hole `_` has no type outside closure creation"); + } } diff --git a/src/ast/IfLabel.java b/src/ast/IfLabel.java index 55b6cb45..909ffbef 100644 --- a/src/ast/IfLabel.java +++ b/src/ast/IfLabel.java @@ -1,7 +1,9 @@ package ast; import java.util.Map; +import typecheck.BuiltInT; import typecheck.Label; +import typecheck.TypeSym; import typecheck.ScopeContext; import typecheck.VarSym; import typecheck.VisitEnv; @@ -33,4 +35,8 @@ public VarSym getVarInfo(VisitEnv env, boolean tail_position, Map children() { } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) { VarSym var = env.getVar(id); String ifNameRtn = var.labelNameSLC(); return new ExpOutcome(ifNameRtn, new PathOutcome(new PsiUnit(env.inContext))); @@ -114,4 +114,9 @@ public java.util.Map readMap(CompileEnv code) { public Map writeMap(CompileEnv code) { return readMap(code); } + + @Override + protected TypeSym staticType(VisitEnv env) { + return env.getVar(id).typeSym; + } } diff --git a/src/ast/New.java b/src/ast/New.java index 143ad589..49c251c1 100644 --- a/src/ast/New.java +++ b/src/ast/New.java @@ -101,7 +101,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { ExpOutcome result = newIFConstraints(env, tail_position); // A contract's constructor may call back and assign to any field, so fields hold // new principals afterwards. @@ -118,29 +118,34 @@ public String principalName(VisitEnv env) { private ExpOutcome newIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { if (!isConstructor) { - // creating a struct var returns a join of its arg values + // Each member is checked like an argument of a call, against the member's label. + // The struct value itself is trusted as much as the control flow creating it. Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); - - List argValueLabelNames = new ArrayList<>(); - String ifNameRtn = scopeContext.getSHErrLocName() + "." + "newStruct" + location.toString(); + StructTypeSym structSym = + (StructTypeSym) env.getCurSym(((Name) constructor_call.value).id); PathOutcome psi = new PathOutcome(new PsiUnit(endContext)); ExpOutcome ao = null; - for (Expression arg : constructor_call.args) { + for (int i = 0; i < constructor_call.args.size(); ++i) { + Expression arg = constructor_call.args.get(i); ao = arg.genIFConstraints(env, false); psi.joinExe(ao.psi); - argValueLabelNames.add(ao.valueLabelName); env.inContext = new Context( Utils.joinLabels(ao.psi.getNormalPath().c.pc, beginContext.pc), beginContext.lambda); - env.checkTrustedBy(ao.valueLabelName, ifNameRtn, location, - "Integrity of left hand expression doesn't flow to value of this binary operation"); + String memberLabel = structSym.members().get(i).ifl.toSHErrLocFmt(); + env.checkFlow(ao.valueLabelName, memberLabel, arg.location, + "Input to member " + structSym.members().get(i).getName() + + " must be trusted at its label"); + env.checkFlow(env.inContext.pc, memberLabel, arg.location, + "Current control flow must be trusted to set member " + + structSym.members().get(i).getName()); } - return new ExpOutcome(ifNameRtn, psi); + return new ExpOutcome(env.inContext.pc, psi); } else { // creating contract requires pc and arguments to be of integirty level `this` Context beginContext = env.inContext; @@ -283,4 +288,10 @@ public boolean typeMatch(Expression expression) { public List children() { return List.of(constructor_call); } + + @Override + protected TypeSym staticType(VisitEnv env) { + String name = ((Name) constructor_call.value).id; + return isConstructor ? env.getContract(name) : (StructTypeSym) env.getCurSym(name); + } } diff --git a/src/ast/PrimitiveIfLabel.java b/src/ast/PrimitiveIfLabel.java index a4f4a2fe..a8f232bd 100644 --- a/src/ast/PrimitiveIfLabel.java +++ b/src/ast/PrimitiveIfLabel.java @@ -107,7 +107,7 @@ public String toSherrlocFmtApply(HashSet strSet, int no) { } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) { return null; } diff --git a/src/ast/StateVariableDeclaration.java b/src/ast/StateVariableDeclaration.java index 83c46094..c36ec43e 100644 --- a/src/ast/StateVariableDeclaration.java +++ b/src/ast/StateVariableDeclaration.java @@ -229,6 +229,11 @@ public VarDec solidityCodeGenStruct(CompileEnv code) { return new VarDec(varType, varName); } + /** The declared label of this variable. */ + IfLabel label() { + return type.label(); + } + public VarSym toVarInfo(InterfaceSym interfaceSym) throws SemanticException { IfLabel ifl = null; if (type != null) { diff --git a/src/ast/StructDef.java b/src/ast/StructDef.java index 0d27d170..24c88285 100644 --- a/src/ast/StructDef.java +++ b/src/ast/StructDef.java @@ -9,6 +9,7 @@ import typecheck.ScopeContext; import typecheck.StructTypeSym; import typecheck.Sym; +import typecheck.VarSym; import typecheck.exceptions.SemanticException; import java.util.ArrayList; @@ -34,6 +35,17 @@ public void globalInfoVisit(InterfaceSym contractSym) throws SemanticException { location); } + /** + * Resolves the labels of the members of this struct's type in {@code contractSym}, whose + * state variables, including the built-in principals, must already be declared. + */ + public void resolveMemberLabels(InterfaceSym contractSym) throws SemanticException { + StructTypeSym structSym = (StructTypeSym) contractSym.lookupSym(structName); + for (int i = 0; i < members.size(); ++i) { + structSym.members().get(i).setLabel(contractSym.newLabel(members.get(i).label())); + } + } + @Override public void ntcGlobalInfo(NTCEnv env, ScopeContext parent) throws SemanticException { if (members.isEmpty()) { @@ -46,9 +58,28 @@ public void ntcGlobalInfo(NTCEnv env, ScopeContext parent) throws SemanticExcept env.addType(structName, env.toStructType(structName, members)); } - public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) { -// assert false; - return null; + @Override + public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) + throws SemanticException { + ScopeContext now = new ScopeContext(this, parent); + for (StateVariableDeclaration member : members) { + member.label().genTypeConstraints(env, now); + // Like the label of a field, a member's label must mean the same thing wherever + // the struct is used, so it may not name a principal that can change. + checkNamesFinal(member.label(), env); + } + return now; + } + + private void checkNamesFinal(IfLabel label, NTCEnv env) throws SemanticException { + if (label instanceof ComplexIfLabel c) { + checkNamesFinal(c.getLeft(), env); + checkNamesFinal(c.getRight(), env); + } else if (label instanceof PrimitiveIfLabel p + && env.getCurSym(p.value().id) instanceof VarSym v && v.isMutablePrincipal()) { + throw new SemanticException("the label of a member of struct " + structName + + " names " + v.getName() + ", which is not final", label.location()); + } } public compile.ast.StructDef solidityCodeGen(CompileEnv code) { diff --git a/src/ast/Subscript.java b/src/ast/Subscript.java index 3ef6b4be..67662359 100644 --- a/src/ast/Subscript.java +++ b/src/ast/Subscript.java @@ -18,6 +18,8 @@ * value is of type map or array, and index's type should be right accordingly. */ public class Subscript extends TrailerExpr { + /** The type of the collection subscripted, once this expression has been IFC-checked. */ + private TypeSym collectionType; Expression index; @@ -99,13 +101,14 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { Context beginContext = env.inContext; Context endContext = new Context(typecheck.Utils.getLabelNamePc(toSHErrLocFmt()), typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); Map dependentMapping = new HashMap<>(); VarSym valueVarSym = value.getVarInfo(env, false, dependentMapping); + collectionType = valueVarSym.typeSym; String ifNameValue = valueVarSym.labelNameSLC(); // String ifNameRtnValue = ifNameValue + "." + "Subscript" + location.toString(); @@ -203,7 +206,8 @@ public VarSym getVarInfo(VisitEnv env, boolean tail_position, Map readMap(CompileEnv code) { public Map writeMap(CompileEnv code) { return value.writeMap(code); } + + @Override + protected TypeSym staticType(VisitEnv env) { + if (collectionType instanceof ArrayTypeSym arrayType) { + return arrayType.valueType; + } else if (collectionType instanceof MapTypeSym mapType) { + return mapType.valueType; + } + throw new AssertionError("subscript of " + collectionType.getName()); + } } diff --git a/src/ast/UnaryOp.java b/src/ast/UnaryOp.java index 9355b31a..218b8e0a 100644 --- a/src/ast/UnaryOp.java +++ b/src/ast/UnaryOp.java @@ -36,7 +36,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } @Override - public ExpOutcome genIFConstraints(VisitEnv env, boolean tail_position) throws SemanticException { + protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { return operand.genIFConstraints(env, tail_position); } @@ -62,4 +62,10 @@ public ArrayList children() { public java.util.Map readMap(CompileEnv code) { return operand.readMap(code); } + + @Override + protected TypeSym staticType(VisitEnv env) { + return env.builtinType(op == UnaryOperator.USub || op == UnaryOperator.UAdd + ? BuiltInT.UINT : BuiltInT.BOOL); + } } diff --git a/src/typecheck/StructTypeSym.java b/src/typecheck/StructTypeSym.java index de6a24c6..775c151f 100644 --- a/src/typecheck/StructTypeSym.java +++ b/src/typecheck/StructTypeSym.java @@ -44,6 +44,21 @@ public VarSym getMemberVarInfo(String prefix, int index) { return rtn; } + /** The member named {@code name}, or null if there is none. */ + public VarSym member(String name) { + for (VarSym mb : members) { + if (mb.getName().equals(name)) { + return mb; + } + } + return null; + } + + /** The members of this struct type, in order. */ + public List members() { + return members; + } + public int merberSize() { return members.size(); } diff --git a/src/typecheck/VisitEnv.java b/src/typecheck/VisitEnv.java index b8f3084d..14b75904 100644 --- a/src/typecheck/VisitEnv.java +++ b/src/typecheck/VisitEnv.java @@ -125,6 +125,11 @@ public boolean containsFunc(String id) { return getFunc(id) != null; } + /** The built-in type {@code t}. */ + public TypeSym builtinType(BuiltInT t) { + return (TypeSym) curSymTab.lookup(Utils.BuiltinType2ID(t)); + } + public boolean containsContract(String funcName) { return getContract(funcName) != null; } diff --git a/test/contracts/applications/KoET.scif b/test/contracts/applications/KoET.scif index 5e347594..4f6e7702 100644 --- a/test/contracts/applications/KoET.scif +++ b/test/contracts/applications/KoET.scif @@ -63,7 +63,7 @@ contract KoET { currentClaimPrice = startingClaimPrice; currentMonarch = endorse(new Monarch( wizardAddress, - name, + endorse(name, sender->this), 0, now ), sender->this); @@ -114,7 +114,7 @@ contract KoET { pastMonarchs.push(currentMonarch); currentMonarch = endorse(new Monarch( sender, - name, + endorse(name, sender->this), valuePaid, now ), sender->this); diff --git a/test/contracts/ifcTypechecking/IPointSource.scif b/test/contracts/ifcTypechecking/IPointSource.scif new file mode 100644 index 00000000..a24e3c8f --- /dev/null +++ b/test/contracts/ifcTypechecking/IPointSource.scif @@ -0,0 +1,7 @@ +interface IPointSource { + struct P { + uint v; + uint{any} u; + } + public P{sender} get{any}(); +} diff --git a/test/contracts/ifcTypechecking/StructForeignMember.scif b/test/contracts/ifcTypechecking/StructForeignMember.scif new file mode 100644 index 00000000..f96482ee --- /dev/null +++ b/test/contracts/ifcTypechecking/StructForeignMember.scif @@ -0,0 +1,14 @@ +import "./IPointSource.scif"; +// A struct declared by another contract labels its members in that contract's +// terms. A member it trusts is trusted here as much as the struct value. +contract StructForeignMember { + uint{this} secret; + constructor() { super(); } + + public void f{this}(IPointSource{this} x) { + lock(this) { + P@IPointSource p = x.get(); + secret = p.v; + } + } +} diff --git a/test/contracts/ifcTypechecking/StructForeignMemberLow_W01.scif b/test/contracts/ifcTypechecking/StructForeignMemberLow_W01.scif new file mode 100644 index 00000000..20452f66 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructForeignMemberLow_W01.scif @@ -0,0 +1,13 @@ +import "./IPointSource.scif"; +// A member that the declaring contract labels untrusted is untrusted here too. +contract StructForeignMemberLow_W01 { + uint{this} secret; + constructor() { super(); } + + public void f{this}(IPointSource{this} x) { + lock(this) { + P@IPointSource p = x.get(); + secret = p.u; + } + } +} diff --git a/test/contracts/ifcTypechecking/StructMemberInput.scif b/test/contracts/ifcTypechecking/StructMemberInput.scif new file mode 100644 index 00000000..615b34d0 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructMemberInput.scif @@ -0,0 +1,12 @@ +// Untrusted data can be stored in a member declared untrusted. +contract StructMemberInput { + struct S { + uint{any} v; + } + S s; + constructor() { super(); } + + public void f{this}(uint{any} x) { + s = new S(x); + } +} diff --git a/test/contracts/ifcTypechecking/StructMemberInputLow_W01.scif b/test/contracts/ifcTypechecking/StructMemberInputLow_W01.scif new file mode 100644 index 00000000..41c12cea --- /dev/null +++ b/test/contracts/ifcTypechecking/StructMemberInputLow_W01.scif @@ -0,0 +1,12 @@ +// Each member of a new struct is checked against its declared label. +contract StructMemberInputLow_W01 { + struct S { + uint{this} v; + } + S s; + constructor() { super(); } + + public void f{this}(uint{any} x) { + s = new S(x); + } +} diff --git a/test/contracts/ifcTypechecking/StructMemberRead.scif b/test/contracts/ifcTypechecking/StructMemberRead.scif new file mode 100644 index 00000000..d89aa843 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructMemberRead.scif @@ -0,0 +1,14 @@ +// A trusted member of a trusted struct can be used as trusted. +contract StructMemberRead { + struct S { + uint{any} v; + uint w; + } + S s; + uint{this} secret; + constructor() { super(); } + + public void f{this}() { + secret = s.w; + } +} diff --git a/test/contracts/ifcTypechecking/StructMemberReadLow_W01.scif b/test/contracts/ifcTypechecking/StructMemberReadLow_W01.scif new file mode 100644 index 00000000..d6611a2a --- /dev/null +++ b/test/contracts/ifcTypechecking/StructMemberReadLow_W01.scif @@ -0,0 +1,14 @@ +// A member is read at its declared label, even in a trusted struct. +contract StructMemberReadLow_W01 { + struct S { + uint{any} v; + uint w; + } + S s; + uint{this} secret; + constructor() { super(); } + + public void f{this}() { + secret = s.v; + } +} diff --git a/test/contracts/ifcTypechecking/StructMemberWrite.scif b/test/contracts/ifcTypechecking/StructMemberWrite.scif new file mode 100644 index 00000000..be081ead --- /dev/null +++ b/test/contracts/ifcTypechecking/StructMemberWrite.scif @@ -0,0 +1,13 @@ +// Untrusted data can be written to an untrusted member of a trusted struct. +contract StructMemberWrite { + struct S { + uint{any} v; + uint w; + } + S s; + constructor() { super(); } + + public void f{this}(uint{any} x) { + s.v = x; + } +} diff --git a/test/contracts/ifcTypechecking/StructMemberWriteLow_W01.scif b/test/contracts/ifcTypechecking/StructMemberWriteLow_W01.scif new file mode 100644 index 00000000..0a7014fb --- /dev/null +++ b/test/contracts/ifcTypechecking/StructMemberWriteLow_W01.scif @@ -0,0 +1,13 @@ +// Untrusted data cannot be written to a trusted member. +contract StructMemberWriteLow_W01 { + struct S { + uint{any} v; + uint w; + } + S s; + constructor() { super(); } + + public void f{this}(uint{any} x) { + s.w = x; + } +} diff --git a/test/contracts/regularTypechecking/StructMemberLabelNonFinal_W01.scif b/test/contracts/regularTypechecking/StructMemberLabelNonFinal_W01.scif new file mode 100644 index 00000000..a13e3cd7 --- /dev/null +++ b/test/contracts/regularTypechecking/StructMemberLabelNonFinal_W01.scif @@ -0,0 +1,9 @@ +// A struct member's label must mean the same thing wherever the struct is +// used, so it may not name a principal that can change. +contract StructMemberLabelNonFinal_W01 { + struct S { + uint{owner} v; + } + address owner; + constructor() { super(); } +} diff --git a/test/contracts/regularTypechecking/StructMemberLabelNotPrincipal_W01.scif b/test/contracts/regularTypechecking/StructMemberLabelNotPrincipal_W01.scif new file mode 100644 index 00000000..3d26fa4b --- /dev/null +++ b/test/contracts/regularTypechecking/StructMemberLabelNotPrincipal_W01.scif @@ -0,0 +1,8 @@ +// A struct member's label may name only principals. +contract StructMemberLabelNotPrincipal_W01 { + struct S { + uint{n} v; + } + uint n; + constructor() { super(); } +} diff --git a/test/contracts/regularTypechecking/StructMemberLabels.scif b/test/contracts/regularTypechecking/StructMemberLabels.scif new file mode 100644 index 00000000..68d307bd --- /dev/null +++ b/test/contracts/regularTypechecking/StructMemberLabels.scif @@ -0,0 +1,13 @@ +// Struct members may be unlabeled, or labeled with principals that cannot change. +contract StructMemberLabels { + struct S { + uint{owner} v; + uint{any} u; + uint w; + } + final address{any} owner; + constructor(address o) { + owner = o; + super(); + } +} diff --git a/test/src/TestIfcTypechecking.java b/test/src/TestIfcTypechecking.java index 20b0b78b..077b281a 100644 --- a/test/src/TestIfcTypechecking.java +++ b/test/src/TestIfcTypechecking.java @@ -80,6 +80,10 @@ public class TestIfcTypechecking { "ifcTypechecking/HandlerThrowUntrusted", "ifcTypechecking/RescueAllUntrusted", "ifcTypechecking/AssertFalseHandler", + "ifcTypechecking/StructMemberInput", + "ifcTypechecking/StructMemberRead", + "ifcTypechecking/StructMemberWrite", + "ifcTypechecking/StructForeignMember", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -164,6 +168,10 @@ void testPositive(String contractName) throws Exception { "ifcTypechecking/WhileThrowLow_W01", "ifcTypechecking/HandlerThrowLow_W01", "ifcTypechecking/RescueAllLow_W01", + "ifcTypechecking/StructMemberInputLow_W01", + "ifcTypechecking/StructMemberReadLow_W01", + "ifcTypechecking/StructMemberWriteLow_W01", + "ifcTypechecking/StructForeignMemberLow_W01", // "examples/ERC20", }) void testNegative(String contractName) throws Exception { diff --git a/test/src/TestRegularTypechecking.java b/test/src/TestRegularTypechecking.java index 70ce96bb..1c5fb91a 100644 --- a/test/src/TestRegularTypechecking.java +++ b/test/src/TestRegularTypechecking.java @@ -51,6 +51,7 @@ public class TestRegularTypechecking { "regularTypechecking/ClosureBinderRegularOK", "regularTypechecking/FinalFieldInitOK", "regularTypechecking/ExceptionParamLabels", + "regularTypechecking/StructMemberLabels", "regularTypechecking/TrustQueryNonFinal", }) void testPositive(String contractName) throws Exception { @@ -144,6 +145,8 @@ void testNegative(String contractName) { "regularTypechecking/FinalParamReassign_W01", "regularTypechecking/FieldLabelNonFinal_W01", "regularTypechecking/ExceptionParamLabelNotPrincipal_W01", + "regularTypechecking/StructMemberLabelNonFinal_W01", + "regularTypechecking/StructMemberLabelNotPrincipal_W01", }) void testReportedError(String contractName) throws Exception { URL input = ClassLoader.getSystemResource(contractName + ".scif"); From 27bffce130feace7be4675197881508fd233f174 Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Fri, 6 Nov 2026 10:19:53 -0500 Subject: [PATCH 7/8] Expressions now record their types, member and element access is compositional for both reads and writes, and receivers can be any variable, member or element. --- src/ast/Assign.java | 2 +- src/ast/Attribute.java | 95 +++++------ src/ast/Call.java | 143 ++++++++-------- src/ast/Delete.java | 2 +- src/ast/Expression.java | 28 ++++ src/ast/Name.java | 6 + src/ast/Subscript.java | 154 +++++++++--------- test/contracts/applications/ERC20_depmap.scif | 4 +- .../applications/ERC20_depmap_noe.scif | 2 +- .../applications/ERC20_depmap_old.scif | 4 +- .../applications/ERC20_nodepmap.scif | 3 +- test/contracts/applications/HODLWallet.scif | 2 +- test/contracts/applications/IERC20.scif | 2 +- test/contracts/applications/IERC777.scif | 2 +- test/contracts/applications/SysEscrow.scif | 91 ++++++----- .../contracts/applications/Uniswap_ERC20.scif | 16 +- .../applications/Uniswap_ERC20_noe.scif | 16 +- .../applications/Uniswap_ERC777_noe.scif | 16 +- .../ifcTypechecking/CallElementReceiver.scif | 11 ++ .../CallElementReceiverLow_W01.scif | 12 ++ .../ifcTypechecking/CallMemberReceiver.scif | 14 ++ .../CallMemberReceiverLow_W01.scif | 15 ++ .../ifcTypechecking/DepMapKeyRead.scif | 10 ++ .../ifcTypechecking/DepMapKeyReadLow_W01.scif | 10 ++ .../ifcTypechecking/DepMapKeyWrite.scif | 10 ++ .../ifcTypechecking/DepMapUnnamedKeyRead.scif | 13 ++ .../DepMapUnnamedKeyReadLow_W01.scif | 13 ++ .../DepMapUnnamedKeyWriteLow_W01.scif | 10 ++ .../ifcTypechecking/MapIndexRead.scif | 10 ++ .../ifcTypechecking/MapIndexReadLow_W01.scif | 11 ++ .../ifcTypechecking/MapIndexWrite.scif | 9 + .../ifcTypechecking/MapIndexWriteLow_W01.scif | 10 ++ .../ifcTypechecking/MemberArrayPush.scif | 13 ++ .../MemberArrayPushLow_W01.scif | 13 ++ .../ifcTypechecking/NestedMapWrite.scif | 9 + .../NestedMapWriteLow_W01.scif | 9 + .../ifcTypechecking/StructElementRead.scif | 14 ++ .../StructElementReadLow_W01.scif | 14 ++ .../ifcTypechecking/StructElementWrite.scif | 13 ++ .../StructElementWriteLow_W01.scif | 13 ++ .../ifcTypechecking/StructNestedRead.scif | 17 ++ .../StructNestedReadLow_W01.scif | 17 ++ .../ifcTypechecking/StructNestedWrite.scif | 16 ++ .../StructNestedWriteLow_W01.scif | 16 ++ .../CallReceiverNotVariable_W01.scif | 12 +- .../DepMapMutableIndex.scif | 10 ++ .../DepMapNonFinalIndex_W01.scif | 9 - test/src/TestIfcTypechecking.java | 27 +++ test/src/TestRegularTypechecking.java | 2 +- 49 files changed, 692 insertions(+), 278 deletions(-) create mode 100644 test/contracts/ifcTypechecking/CallElementReceiver.scif create mode 100644 test/contracts/ifcTypechecking/CallElementReceiverLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/CallMemberReceiver.scif create mode 100644 test/contracts/ifcTypechecking/CallMemberReceiverLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/DepMapKeyRead.scif create mode 100644 test/contracts/ifcTypechecking/DepMapKeyReadLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/DepMapKeyWrite.scif create mode 100644 test/contracts/ifcTypechecking/DepMapUnnamedKeyRead.scif create mode 100644 test/contracts/ifcTypechecking/DepMapUnnamedKeyReadLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/DepMapUnnamedKeyWriteLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/MapIndexRead.scif create mode 100644 test/contracts/ifcTypechecking/MapIndexReadLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/MapIndexWrite.scif create mode 100644 test/contracts/ifcTypechecking/MapIndexWriteLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/MemberArrayPush.scif create mode 100644 test/contracts/ifcTypechecking/MemberArrayPushLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/NestedMapWrite.scif create mode 100644 test/contracts/ifcTypechecking/NestedMapWriteLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/StructElementRead.scif create mode 100644 test/contracts/ifcTypechecking/StructElementReadLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/StructElementWrite.scif create mode 100644 test/contracts/ifcTypechecking/StructElementWriteLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/StructNestedRead.scif create mode 100644 test/contracts/ifcTypechecking/StructNestedReadLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/StructNestedWrite.scif create mode 100644 test/contracts/ifcTypechecking/StructNestedWriteLow_W01.scif create mode 100644 test/contracts/regularTypechecking/DepMapMutableIndex.scif delete mode 100644 test/contracts/regularTypechecking/DepMapNonFinalIndex_W01.scif diff --git a/src/ast/Assign.java b/src/ast/Assign.java index c8458f42..83f0797e 100644 --- a/src/ast/Assign.java +++ b/src/ast/Assign.java @@ -89,7 +89,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // env.prevContext = valueContext; /*env.cons.add(new Constraint(new Inequality(prevLockName, CompareOperator.Eq, valueContext.lambda), env.hypothesis, value.location, env.curContractSym.name, "Lock should be maintained before execution of this operation"));*/ - to = target.genIFConstraints(env, false); + to = target.genLocationConstraints(env); ifNameTgt = to.valueLabelName; // env.inContext = new Context(to.psi.getNormalPath().c.lambda, beginContext.lambda); env.inContext = Utils.genNewContextAndConstraints(env, false, to.psi.getNormalPath().c, beginContext.lambda, target.nextPcSHL(), location); diff --git a/src/ast/Attribute.java b/src/ast/Attribute.java index 42919ec2..e15f15d9 100644 --- a/src/ast/Attribute.java +++ b/src/ast/Attribute.java @@ -87,56 +87,34 @@ public VarSym getVarInfo(NTCEnv env) throws SemanticException { @Override protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) throws SemanticException { - //TODO: assuming only one-level attribute access - // to add support to multi-level access - String varName = ((Name) value).id; - // TODO: steph: needs to check instanceof - - VarSym varSym = env.getVar(varName); - if (varSym != null && varSym.typeSym instanceof ClosureTypeSym - && attr.id.equals(Utils.CLOSURE_TARGET_MEMBER)) { - return value.genIFConstraints(env, tail_position); - } - if (!(varSym.typeSym instanceof StructTypeSym - || varSym.typeSym instanceof ExceptionTypeSym)) { - //TODO: throw errors: variable not struct -// return null; - throw new RuntimeException("Variable not a struct: " + varName); - } - - //String prevLockName = env.prevContext.lambda; ExpOutcome vo = value.genIFConstraints(env, tail_position); + TypeSym baseType = value.type(); + if (baseType instanceof ClosureTypeSym && attr.id.equals(Utils.CLOSURE_TARGET_MEMBER)) { + return vo; + } + // A member is trusted at its label and no more than the value it belongs to. String attrValueLabel = vo.valueLabelName; -// String ifAttLabel = structType.getMemberLabel(attr.id); - String ifNameRnt = scopeContext.getSHErrLocName() + ".struct" + location.toString(); -// env.cons.add(new Constraint(new Inequality(ifNameRnt, ifAttLabel), env.hypothesis(), location, -// env.curContractSym().getName(), -// "Integrity of the member")); -// if (!ifAttLabel.equals(attrValueLabel)) { - if (varSym.typeSym instanceof StructTypeSym structType) { - // A member is trusted at its label and no more than the struct value. - attrValueLabel = Utils.joinLabels(attrValueLabel, memberLabel(env, - structType.member(attr.id).ifl, attrValueLabel)); - } else if (varSym.typeSym instanceof ExceptionTypeSym expType) { - // An exception's data is trusted at its parameter's label, in which each - // principal parameter stands for the principal in that field. - java.util.Map mapping = new java.util.HashMap<>(); - for (VarSym param : expType.parameters()) { - if (param.isPrincipalTyped()) { - mapping.put(param.toSHErrLocFmt(), - exceptionFieldPrincipal(env, varSym, param.getName())); - } + if (baseType instanceof StructTypeSym structType) { + attrValueLabel = Utils.joinLabels(attrValueLabel, + memberLabel(env, structType.member(attr.id).ifl, attrValueLabel)); + } else if (baseType instanceof ExceptionTypeSym expType) { + // Each principal parameter in the label stands for the principal in that field. + java.util.Map mapping = new java.util.HashMap<>(); + for (VarSym param : expType.parameters()) { + if (param.isPrincipalTyped()) { + mapping.put(param.toSHErrLocFmt(), value instanceof Name n + ? exceptionFieldPrincipal(env, env.getVar(n.id), param.getName()) + : env.curContractSym().any().toSHErrLocFmt()); } - VarSym member = expType.parameters().stream() - .filter(param -> param.getName().equals(attr.id)).findFirst() - .orElseThrow(); - attrValueLabel = Utils.joinLabels(attrValueLabel, - member.ifl.toSHErrLocFmt(mapping)); } - env.checkEqual(attrValueLabel, ifNameRnt, location, - "Integrity of the index value must be trusted to indicate the attribute"); -// } - + VarSym member = expType.getMemberVarInfo("", attr.id); + attrValueLabel = Utils.joinLabels(attrValueLabel, member.ifl.toSHErrLocFmt(mapping)); + } else { + throw new AssertionError("no member " + attr.id + " in type " + baseType.getName()); + } + String ifNameRnt = scopeContext.getSHErrLocName() + ".struct" + location.toString(); + env.checkEqual(attrValueLabel, ifNameRnt, location, + "Integrity of the index value must be trusted to indicate the attribute"); return new ExpOutcome(ifNameRnt, vo.psi); } @@ -251,4 +229,29 @@ protected TypeSym staticType(VisitEnv env) { } throw new AssertionError("no member " + attr.id + " in type " + base.getName()); } + + @Override + protected ExpOutcome genLocationFlow(VisitEnv env) throws SemanticException { + ExpOutcome bo = value.genLocationConstraints(env); + TypeSym baseType = value.type(); + String location = bo.valueLabelName; + if (baseType instanceof StructTypeSym structType) { + location = Utils.joinLabels(location, + memberLabel(env, structType.member(attr.id).ifl, bo.valueLabelName)); + } else if (baseType instanceof ExceptionTypeSym expType) { + java.util.Map mapping = new java.util.HashMap<>(); + for (VarSym param : expType.parameters()) { + if (param.isPrincipalTyped()) { + mapping.put(param.toSHErrLocFmt(), value instanceof Name n + ? exceptionFieldPrincipal(env, env.getVar(n.id), param.getName()) + : env.curContractSym().any().toSHErrLocFmt()); + } + } + location = Utils.joinLabels(location, + expType.getMemberVarInfo("", attr.id).ifl.toSHErrLocFmt(mapping)); + } else { + throw new SemanticException("member " + attr.id + " cannot be assigned", this.location); + } + return new ExpOutcome(location, bo.psi); + } } diff --git a/src/ast/Call.java b/src/ast/Call.java index 021813ca..99f8c56d 100644 --- a/src/ast/Call.java +++ b/src/ast/Call.java @@ -32,7 +32,10 @@ public class Call extends TrailerExpr { List args; - // store the called func symbol after regular typechecking + /** + * The method this call invokes, as resolved by the regular type check, or null if it + * invokes none. Code generation uses it; the IFC check resolves the callee separately. + */ FuncSym funcSym = null; private boolean isCast; boolean builtIn = false, ntced = false; @@ -45,10 +48,17 @@ ClosureTypeSym.RealLabels realLabels() { return realLabels; } + /** Whether this call invokes a closure, as determined by the regular type check. */ private boolean isClosureInvoke = false; - // Closure's external begin label (lextbef) + /** + * For a closure invocation, the principals in the closure's external begin label (lextbef), + * set by the IFC check. Code generation requires it. + */ private List closurePcExLeaves = null; - /** The invoked closure's declared type, kept for codegen. */ + /** + * For a closure invocation, the invoked closure's declared type with its labels, set by the + * IFC check. Code generation requires it. + */ private ClosureTypeSym closureSym = null; /** The type of this call's result, once it has been IFC-checked. */ private TypeSym resultType; @@ -104,22 +114,22 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S // a.b(c), a must be a contract or an array extern = true; - if (!(att.value instanceof Name)) { - throw new SemanticException( - "the receiver of a method call must be a variable", att.value.location); - } - String varName = ((Name) att.value).id; // a - // System.out.println(varName); funcName = att.attr.id; // b - Sym s = env.getCurSym(varName); - if (s == null) { - throw new SemanticException("variable not found: " + varName, att.value.location); + if (att.value instanceof Name n && env.getCurSym(n.id) == null) { + throw new SemanticException("variable not found: " + n.id, att.value.location); } - // logger.debug("var " + varName + ": " + s.getName()); - - if (!(s instanceof VarSym varSym)) { - throw new SemanticException("type error in call (callee must be a variable): ", this.location); + // The receiver is a variable, or a member or element of one. + VarSym varSym = att.value.getVarInfo(env); + if (varSym == null) { + throw new SemanticException( + "the receiver of a method call must be a variable, a member, or an element", + att.value.location); } + if (!(att.value instanceof Name)) { + att.value.genTypeConstraints(env, now); + } + String varName = varSym.getName(); + Sym s; if (varSym.typeSym instanceof InterfaceSym contractSym) { s = contractSym.getFunc(funcName); @@ -165,6 +175,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } else if (varSym.typeSym instanceof ClosureTypeSym closureTypeSym) { // c.invoke(args): args must match the still-unbound parameters; the call's type is the closure's return. this.builtIn = true; + this.isClosureInvoke = true; if (!funcName.equals("invoke")) { throw new SemanticException("type error: unknown closure operator " + funcName, this.location); } @@ -321,7 +332,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) String funcName; String ifNamePc; // currentMethod.PC - FuncSym funcSym; + FuncSym calleeSym; String namespace = ""; Label ifFuncCallPcBefore, ifFuncCallPcAfter, ifFuncGammaLock; @@ -329,7 +340,6 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) boolean externalCall = false; InterfaceSym externalContractSym = null; - VarSym externalTargetSym = null; String ifContRtn = null; String receiverPrincipal = null; // the principal the receiver holds, if it is a variable @@ -349,12 +359,10 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) ifContRtn = vo.valueLabelName; // a..lbl receiverPrincipal = att.value.principalName(env); - //TODO: assuming a's depth is 1 - String varName = ((Name) att.value).id; // a funcName = (att.attr).id; // b - VarSym var = env.getVar(varName); + TypeSym receiverType = att.value.type(); - if (var.typeSym instanceof ArrayTypeSym arrayTypeSym) { + if (receiverType instanceof ArrayTypeSym arrayTypeSym) { // case 1a: a is an array (terminating branch) TypeSym arrayTSym = arrayTypeSym.valueType; @@ -362,10 +370,11 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) //TODO: change the hard-code style if (funcName.equals("pop")) { - // requires pc => integrity of the array var + // pop modifies the array, so the control flow must be trusted at its location. + String arrayLocation = att.value.genLocationConstraints(env).valueLabelName; Utils.contextFlow(env, psi.getNormalPath().c, endContext, location); ifNamePc = Utils.getLabelNamePc(scopeContext.getSHErrLocName()); - env.checkFlow(ifNamePc, ifContRtn, location, + env.checkFlow(ifNamePc, arrayLocation, location, "Current control flow must be trusted to call this method"); // pc => l if (!tail_position) { @@ -375,18 +384,19 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) resultType = env.builtinType(BuiltInT.VOID); return new ExpOutcome(ifNamePc, psi); } else if (funcName.equals("push")) { - // require pc => integrity of the array var - // require the element => integrity of the array var + // push modifies the array, so the control flow and the element must be + // trusted at its location. + String arrayLocation = att.value.genLocationConstraints(env).valueLabelName; Expression arg = args.get(0); ExpOutcome argOutcome = arg.genIFConstraints(env, false); psi.join(argOutcome.psi); String argLabel = argOutcome.valueLabelName; Utils.contextFlow(env, psi.getNormalPath().c, endContext, location); ifNamePc = Utils.getLabelNamePc(scopeContext.getSHErrLocName()); - env.checkFlow(ifNamePc, var.ifl.toSHErrLocFmt(), location, + env.checkFlow(ifNamePc, arrayLocation, location, "Current control flow must be trusted to call this method"); // pc => ? - env.checkFlow(argLabel, var.ifl.toSHErrLocFmt(), location, + env.checkFlow(argLabel, arrayLocation, location, "Current control flow must be trusted to call this method"); if (!tail_position) { env.checkFlow(psi.getNormalPath().c.lambda, beginContext.lambda, location, @@ -403,13 +413,13 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION); } resultType = env.builtinType(BuiltInT.UINT); - return new ExpOutcome(var.ifl.toSHErrLocFmt(), psi); + return new ExpOutcome(ifContRtn, psi); } else { throw new SemanticException("Unrecognized operator", location); } } - if (var.typeSym instanceof ClosureTypeSym cs) { + if (receiverType instanceof ClosureTypeSym cs) { // case 1b: a is a closure value; a.invoke(args) (terminating branch) if (!funcName.equals("invoke")) { @@ -417,8 +427,6 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) "type error: unknown closure operator " + funcName, location); } - this.builtIn = true; - this.isClosureInvoke = true; this.closureSym = cs; cs.registerAtoms(env); @@ -539,27 +547,26 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) } // case 1c: external call - // look up funcSym, update dependentLabelMapping, and read labels from funcSym + // look up calleeSym, update dependentLabelMapping, and read labels from calleeSym - externalTargetSym = var; - namespace = var.toSHErrLocFmt(); - TypeSym conType = var.typeSym; + TypeSym conType = receiverType; + namespace = conType.getName(); externalContractSym = env.getContract(conType.getName()); env.addSigReq(namespace, conType.getName()); ifNamePc = Utils.getLabelNamePc(scopeContext.getSHErrLocName()); InterfaceSym contractSym = env.getContract(conType.getName()); - funcSym = contractSym.getFunc(funcName); - if (funcSym == null) + calleeSym = contractSym.getFunc(funcName); + if (calleeSym == null) throw new SemanticException("not found: " + conType.getName() + "." + funcName, location); - dependentLabelMapping.put(funcSym.sender().toSHErrLocFmt(), env.thisSym().toSHErrLocFmt()); + dependentLabelMapping.put(calleeSym.sender().toSHErrLocFmt(), env.thisSym().toSHErrLocFmt()); dependentLabelMapping.put(contractSym.any().toSHErrLocFmt(), env.curContractSym().any().toSHErrLocFmt()); dependentLabelMapping.put(contractSym.invoker().toSHErrLocFmt(), env.curContractSym().invoker().toSHErrLocFmt()); - ifFuncCallPcBefore = funcSym.externalPc(); - ifFuncCallPcAfter = funcSym.internalPc(); - ifFuncGammaLock = funcSym.callGamma(); + ifFuncCallPcBefore = calleeSym.externalPc(); + ifFuncCallPcAfter = calleeSym.internalPc(); + ifFuncGammaLock = calleeSym.callGamma(); } else { // case 2: a(b) - local contract call or type cast funcName = ((Name) value).id; @@ -602,20 +609,20 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // case 2b: local call - funcSym = env.getFunc(funcName); + calleeSym = env.getFunc(funcName); -// dependentLabelMapping.put(funcSym.sender().toSHErrLocFmt(), env.sender().toSHErrLocFmt()); - dependentLabelMapping.put(funcSym.sender().toSHErrLocFmt(), env.inContext.pc); +// dependentLabelMapping.put(calleeSym.sender().toSHErrLocFmt(), env.sender().toSHErrLocFmt()); + dependentLabelMapping.put(calleeSym.sender().toSHErrLocFmt(), env.inContext.pc); - ifFuncCallPcBefore = funcSym.externalPc(); - ifFuncCallPcAfter = funcSym.internalPc(); - ifFuncGammaLock = funcSym.callGamma(); + ifFuncCallPcBefore = calleeSym.externalPc(); + ifFuncCallPcAfter = calleeSym.internalPc(); + ifFuncGammaLock = calleeSym.callGamma(); } // build hypothesis for sender and this // make sender equal to this // Inequality senderHypo = new Inequality( -// funcSym.sender().toSHErrLocFmt(), +// calleeSym.sender().toSHErrLocFmt(), // CompareOperator.Eq, // env.curContractSym.toSHErrLocFmt() // ); @@ -631,7 +638,7 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // A principal parameter of the callee names the principal passed for it. for (int i = 0; i < args.size(); ++i) { - VarSym argSym = funcSym.parameters.get(i); + VarSym argSym = calleeSym.parameters.get(i); if (argSym.isPrincipalTyped() && argPrincipalNames.get(i) != null) { dependentLabelMapping.put(argSym.toSHErrLocFmt(), argPrincipalNames.get(i)); } @@ -660,9 +667,9 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) for (int i = 0; i < args.size(); ++i) { Expression arg = args.get(i); - VarSym argSym = funcSym.parameters.get(i); + VarSym argSym = calleeSym.parameters.get(i); String ifNameArgValue = argValueLabelNames.get(i); // argument's label name - Label ifArgLabel = funcSym.getLabelArg(i); // parameter's label + Label ifArgLabel = calleeSym.getLabelArg(i); // parameter's label assert ifArgLabel != null : argSym.getName(); env.checkFlow(ifNameArgValue, ifArgLabel.toSHErrLocFmt(inputMapping), arg.location, "Input to the " + Utils.ordNumString(i + 1) @@ -688,14 +695,14 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) String postCallLock = attenuate(ifFuncGammaLock.toSHErrLocFmt(outputMapping), calleeTrust); PathOutcome expPsi = new PathOutcome(new PsiUnit(new Context( attenuate(Utils.joinLabels(psi.getNormalPath().c.pc, - funcSym.endPc().toSHErrLocFmt(outputMapping)), calleeTrust), + calleeSym.endPc().toSHErrLocFmt(outputMapping)), calleeTrust), postCallLock ))); // post-call psi = (current normal path pc ⊔ function end pc, gamma), attenuated if (externalCall) { - checkExceptionData(env, outputMapping); + checkExceptionData(env, calleeSym, outputMapping); } - for (Entry exp : funcSym.exceptions.entrySet()) { + for (Entry exp : calleeSym.exceptions.entrySet()) { // exception-path post-call psi = (current pc ⊔ declared exception label, gamma), // attenuated like the normal path expPsi.set(exp.getKey(), new PsiUnit( @@ -739,11 +746,11 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // Finalize - String ifNameFuncRtnValue = attenuate(funcSym.rtn.toSHErrLocFmt(outputMapping), + String ifNameFuncRtnValue = attenuate(calleeSym.rtn.toSHErrLocFmt(outputMapping), calleeTrust); // realLabels - if (funcSym.returnType instanceof ClosureTypeSym retCs) { + if (calleeSym.returnType instanceof ClosureTypeSym retCs) { this.realLabels = retCs.renderLabels(outputMapping); } @@ -753,21 +760,22 @@ private ExpOutcome callIFConstraints(VisitEnv env, boolean tail_position) // post-call pc (normal) => function return label (l_n) env.checkFlow(psi.getNormalPath().c.pc, ifNameFuncRtnValue, location, "Control flow after this call must be trusted by the function's return value label"); - resultType = funcSym.returnType; + resultType = calleeSym.returnType; return new ExpOutcome(ifNameFuncRtnValue, psi); } /** - * Checks that the data of each exception the external callee may throw is trusted at the - * labels this contract's declaration of that exception gives it, since handlers here read - * it at those labels. The callee's labels are instantiated with {@code outputMapping}, - * with its exception's principal parameters standing for the corresponding parameters of - * this contract's declaration. + * Checks that the data of each exception the external callee {@code calleeSym} may throw is + * trusted at the labels this contract's declaration of that exception gives it, since + * handlers here read it at those labels. The callee's labels are instantiated with + * {@code outputMapping}, with its exception's principal parameters standing for the + * corresponding parameters of this contract's declaration. */ - private void checkExceptionData(VisitEnv env, Map outputMapping) { + private void checkExceptionData(VisitEnv env, FuncSym calleeSym, + Map outputMapping) { // A handler reads each field at its label joined with the label of the exception's // path, which is attenuated by the callee's trust, so the data is too. - for (ExceptionTypeSym calleeExp : funcSym.exceptions.keySet()) { + for (ExceptionTypeSym calleeExp : calleeSym.exceptions.keySet()) { ExceptionTypeSym localExp = env.getExp(calleeExp.getName()); if (localExp == null) { throw new AssertionError("exception " + calleeExp.getName() @@ -823,6 +831,11 @@ private void checkInvokeSupported() { @Override public compile.ast.Expression solidityCodeGen(List result, CompileEnv code) { if (isClosureInvoke) { + if (closureSym == null || closurePcExLeaves == null) { + throw new AssertionError( + "code generation for a closure invocation requires the IFC check: " + + location); + } assert value instanceof Attribute; compile.ast.Expression closureExp = ((Attribute) value).value.solidityCodeGen(result, code); diff --git a/src/ast/Delete.java b/src/ast/Delete.java index beb65fb1..fc92ff94 100644 --- a/src/ast/Delete.java +++ b/src/ast/Delete.java @@ -42,7 +42,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic //Assuming target is Name ifNameTgt = env.getVar(((Name) target).id).labelNameSLC(); } else if (target instanceof Subscript || target instanceof Attribute) { - to = target.genIFConstraints(env, false); + to = target.genLocationConstraints(env); ifNameTgt = to.valueLabelName; env.inContext = Utils.genNewContextAndConstraints(env, false, to.psi.getNormalPath().c, beginContext.lambda, target.nextPcSHL(), location); } else { diff --git a/src/ast/Expression.java b/src/ast/Expression.java index 991b0c78..e612d774 100644 --- a/src/ast/Expression.java +++ b/src/ast/Expression.java @@ -63,6 +63,34 @@ protected abstract ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_posi */ protected abstract TypeSym staticType(VisitEnv env) throws SemanticException; + /** + * Generates the information-flow constraints for evaluating this expression as the target + * of an assignment, in the context {@code env.inContext}. Returns the label of the location + * it denotes, which a value must flow to in order to be stored there, with the paths by + * which the evaluation can end. Afterwards {@link #type()} is defined. + * + * @throws SemanticException if this expression does not denote a location + */ + public final ExpOutcome genLocationConstraints(VisitEnv env) throws SemanticException { + ExpOutcome outcome = genLocationFlow(env); + type = staticType(env); + if (type == null) { + throw new AssertionError("no static type for " + getClass().getSimpleName() + + " at " + location); + } + return outcome; + } + + /** + * Generates the information-flow constraints for this expression as the target of an + * assignment, as specified for {@link #genLocationConstraints}. + * + * @throws SemanticException if this expression does not denote a location + */ + protected ExpOutcome genLocationFlow(VisitEnv env) throws SemanticException { + throw new SemanticException("not an assignable location", location); + } + /** * The static type of this expression. * diff --git a/src/ast/Name.java b/src/ast/Name.java index 856b4f51..09353aed 100644 --- a/src/ast/Name.java +++ b/src/ast/Name.java @@ -119,4 +119,10 @@ public Map writeMap(CompileEnv code) { protected TypeSym staticType(VisitEnv env) { return env.getVar(id).typeSym; } + + @Override + protected ExpOutcome genLocationFlow(VisitEnv env) { + return new ExpOutcome(env.getVar(id).labelNameSLC(), + new PathOutcome(new PsiUnit(env.inContext))); + } } diff --git a/src/ast/Subscript.java b/src/ast/Subscript.java index 67662359..34163be9 100644 --- a/src/ast/Subscript.java +++ b/src/ast/Subscript.java @@ -18,8 +18,6 @@ * value is of type map or array, and index's type should be right accordingly. */ public class Subscript extends TrailerExpr { - /** The type of the collection subscripted, once this expression has been IFC-checked. */ - private TypeSym collectionType; Expression index; @@ -67,19 +65,12 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S value.genTypeConstraints(env, now); if (valueVarSym.typeSym instanceof DepMapTypeSym) { - // index must match, and must be a final address/contract or a principal - boolean validIndex = true; - if (index instanceof Name) { - VarSym indexSym = index.getVarInfo(env); - if (!indexSym.isPrincipalVar()) { - validIndex = false; - } - } else { - validIndex = false; - } - if (!validIndex) { + // The index must be a variable holding a principal, which the element's label + // may name. + if (!(index instanceof Name && index.getVarInfo(env) instanceof VarSym indexSym + && indexSym.isPrincipalTyped())) { throw new SemanticException( - "the index of a dependent map must be a final address or contract variable", + "the index of a dependent map must be an address or contract variable", index.location); } return now; @@ -107,53 +98,29 @@ protected ExpOutcome genFlowConstraints(VisitEnv env, boolean tail_position) thr typecheck.Utils.getLabelNameLock(toSHErrLocFmt())); Map dependentMapping = new HashMap<>(); - VarSym valueVarSym = value.getVarInfo(env, false, dependentMapping); - collectionType = valueVarSym.typeSym; + ExpOutcome vo = value.genIFConstraints(env, false); + TypeSym collectionType = value.type(); - String ifNameValue = valueVarSym.labelNameSLC(); + String ifNameValue = vo.valueLabelName; // String ifNameRtnValue = ifNameValue + "." + "Subscript" + location.toString(); String ifNameRtnValue = toSHErrLocFmt(); ExpOutcome io = index.genIFConstraints(env, tail_position); // String ifNameRtnLock = ""; - if (valueVarSym.typeSym instanceof DepMapTypeSym depMapTypeSym) { - // value[index] where value is of dependent map type - // precondition: the type of index and value match; index is a final address/contract or a principal - // the result value of this expression has the label dependent to index - - // DepMapTypeSym typeSym = (DepMapTypeSym) valueVarSym.typeSym; - Map indexMapping = new HashMap<>(); - VarSym indexVarSym = index.getVarInfo(env, tail_position, indexMapping); - // logger.debug("subscript/DepMap:"); - // logger.debug("lookup at: " + index.toString()); - // logger.debug(indexVarSym.toString()); - String ifNameIndex = indexVarSym.toSHErrLocFmt(); - - if (indexVarSym.isPrincipalVar()) { -// // logger.debug("typename {} to {}", valueVarSym.typeSym.getName(), ifNameIndex); - System.err.println("typename " + depMapTypeSym.label() + " to " + ifNameIndex); - - dependentMapping.put(depMapTypeSym.key().toSHErrLocFmt(), ifNameIndex); - String ifDepMapValue = depMapTypeSym.label().toSHErrLocFmt(dependentMapping); -// String ifDepMapValue = depMapTypeSym.label().toSHErrLocFmt(depMapTypeSym.key().toSHErrLocFmt(), -// ifNameIndex); -// -// env.addTrustConstraint( -// new Constraint(new Inequality(ifDepMapValue, Relation.EQ, ifNameRtnValue), -// env.hypothesis(), location, env.curContractSym().getName(), -// "Integrity level of the subscripted value is not expected")); - return new ExpOutcome(ifDepMapValue, io.psi); - - } else { - throw new AssertionError(); - } + if (collectionType instanceof DepMapTypeSym depMapTypeSym) { + String elementLabel = elementLabel(depMapTypeSym, indexPrincipal(env)); + // Unless the element's label names its key, the index chooses which element is + // read, so the result is trusted no more than the index. + return new ExpOutcome(namesKey(depMapTypeSym) ? elementLabel + : Utils.joinLabels(elementLabel, io.valueLabelName), io.psi); } else { Context indexContext = io.psi.getNormalPath().c; String ifNameIndex = io.valueLabelName; //ifNameRtnValue = // scopeContext.getSHErrLocName() + "." + "Subscript" + location.toString(); - env.checkEqual(ifNameValue, ifNameRtnValue, location, + // The index chooses which element is read, so the result is trusted no more than it. + env.checkEqual(Utils.joinLabels(ifNameValue, ifNameIndex), ifNameRtnValue, location, "Integrity level of the subscript value is not trustworthy enough"); // env.cons.add(new Constraint(new Inequality(ifNameIndex, ifNameRtnValue), env.hypothesis, location)); @@ -176,32 +143,15 @@ public VarSym getVarInfo(VisitEnv env, boolean tail_position, Map indexMapping = new HashMap<>(); - VarSym indexVarSym = index.getVarInfo(env, tail_position, indexMapping); - String ifNameIndex = indexVarSym.toSHErrLocFmt(); - if (indexVarSym.isPrincipalVar()) { - - TypeSym rtnTypeSym = depMapTypeSym.valueType; - dependentMapping.put(depMapTypeSym.key().toSHErrLocFmt(), ifNameIndex); - rtnVarSym = new VarSym(ifNameRtn, rtnTypeSym, valueVarSym.ifl, location, - rtnTypeSym.defContext(), false, false, false); - env.checkFlow(depMapTypeSym.label().toSHErrLocFmt(dependentMapping), - rtnVarSym.labelNameSLC(), location, "Label of the subscript value"); - -// String ifDepMapValue = (valueVarSym).ifl.toSHErrLocFmt(valueVarSym.typeSym.name(), -// ifNameIndex); - -// env.cons.add( -// new Constraint(new Inequality(ifDepMapValue, ifNameRtn), env.hypothesis, -// location, env.curContractSym.name(), -// "Label of the subscript variable")); - - } else { -// logger.error(, -// locToString()); - throw new SemanticException( - "the index of a dependent map must be a final address variable", index.location); + TypeSym rtnTypeSym = depMapTypeSym.valueType; + dependentMapping.put(depMapTypeSym.key().toSHErrLocFmt(), indexPrincipal(env)); + rtnVarSym = new VarSym(ifNameRtn, rtnTypeSym, valueVarSym.ifl, location, + rtnTypeSym.defContext(), false, false, false); + env.checkFlow(elementLabel(depMapTypeSym, indexPrincipal(env)), + rtnVarSym.labelNameSLC(), location, "Label of the subscript value"); + if (!namesKey(depMapTypeSym)) { + env.checkFlow(index.genIFConstraints(env, tail_position).valueLabelName, + rtnVarSym.labelNameSLC(), location, "Label of the subscript index value"); } } else { String ifNameIndex = index.genIFConstraints(env, tail_position).valueLabelName; @@ -256,6 +206,7 @@ public Map writeMap(CompileEnv code) { @Override protected TypeSym staticType(VisitEnv env) { + TypeSym collectionType = value.type(); if (collectionType instanceof ArrayTypeSym arrayType) { return arrayType.valueType; } else if (collectionType instanceof MapTypeSym mapType) { @@ -263,4 +214,57 @@ protected TypeSym staticType(VisitEnv env) { } throw new AssertionError("subscript of " + collectionType.getName()); } + + @Override + protected ExpOutcome genLocationFlow(VisitEnv env) throws SemanticException { + ExpOutcome bo = value.genLocationConstraints(env); + TypeSym collectionType = value.type(); + ExpOutcome io = index.genIFConstraints(env, false); + PathOutcome psi = io.psi; + psi.join(bo.psi.withoutNormalPath()); + if (collectionType instanceof DepMapTypeSym depMapTypeSym) { + String elementLabel = elementLabel(depMapTypeSym, indexPrincipal(env)); + // If the element's label names its key, it is trusted by the principal the index + // holds, whoever chose it. Otherwise the index chooses which element is + // overwritten, so it must be trusted at the element's label. + if (!namesKey(depMapTypeSym)) { + env.checkFlow(io.valueLabelName, elementLabel, index.location, + "The index of an assignment target must be trusted at the label of the element"); + } + return new ExpOutcome(elementLabel, psi); + } + // The index chooses which element is overwritten, so it must be trusted as much as + // the collection. + env.checkFlow(io.valueLabelName, bo.valueLabelName, index.location, + "The index of an assignment target must be trusted at the label of the collection"); + return new ExpOutcome(bo.valueLabelName, psi); + } + + /** + * The principal the index of this subscript holds, which must be a principal-typed + * variable. + */ + private String indexPrincipal(VisitEnv env) throws SemanticException { + String principal = index.principalName(env); + if (principal == null) { + throw new SemanticException( + "the index of a dependent map must be an address or contract variable", + index.location); + } + return principal; + } + + /** The label of the element of a map of type {@code type} whose key holds {@code key}. */ + private static String elementLabel(DepMapTypeSym type, String key) { + Map mapping = new HashMap<>(); + mapping.put(type.key().toSHErrLocFmt(), key); + return type.label().toSHErrLocFmt(mapping); + } + + /** Whether the label of the elements of a map of type {@code type} names its key. */ + private static boolean namesKey(DepMapTypeSym type) { + String key = type.key().toSHErrLocFmt(); + return type.label().principalSyms().stream() + .anyMatch(p -> p.toSHErrLocFmt().equals(key)); + } } diff --git a/test/contracts/applications/ERC20_depmap.scif b/test/contracts/applications/ERC20_depmap.scif index c66b1478..238b36bf 100644 --- a/test/contracts/applications/ERC20_depmap.scif +++ b/test/contracts/applications/ERC20_depmap.scif @@ -8,7 +8,7 @@ contract ERC20 implements IERC20 { bytes _name; bytes _symbol; - exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientBalance(address{any} owner, uint{owner} balance, uint{owner} needed); exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); constructor(bytes name_, bytes symbol_) { @@ -61,7 +61,7 @@ contract ERC20 implements IERC20 { } } - public uint{owner} allowance(final address owner, final address spender) { + public uint{owner ⨆ sender} allowance(final address owner, final address spender) { return _allowances[owner][spender]; } diff --git a/test/contracts/applications/ERC20_depmap_noe.scif b/test/contracts/applications/ERC20_depmap_noe.scif index 9b417e98..5b21a58b 100644 --- a/test/contracts/applications/ERC20_depmap_noe.scif +++ b/test/contracts/applications/ERC20_depmap_noe.scif @@ -58,7 +58,7 @@ contract ERC20 implements IERC20 { } } - public uint{owner} allowance(final address owner, final address spender) { + public uint{owner ⨆ sender} allowance(final address owner, final address spender) { return _allowances[owner][spender]; } diff --git a/test/contracts/applications/ERC20_depmap_old.scif b/test/contracts/applications/ERC20_depmap_old.scif index 73489542..235eef4d 100644 --- a/test/contracts/applications/ERC20_depmap_old.scif +++ b/test/contracts/applications/ERC20_depmap_old.scif @@ -8,7 +8,7 @@ contract ERC20 implements IERC20 { bytes _name; bytes _symbol; - exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientBalance(address{any} owner, uint{owner} balance, uint{owner} needed); exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); constructor(bytes name_, bytes symbol_) { @@ -60,7 +60,7 @@ contract ERC20 implements IERC20 { } } - public uint{owner} allowance(final address owner, final address spender) { + public uint{owner ⨆ sender} allowance(final address owner, final address spender) { return _allowances[owner][spender]; } diff --git a/test/contracts/applications/ERC20_nodepmap.scif b/test/contracts/applications/ERC20_nodepmap.scif index 7986f447..4a393634 100644 --- a/test/contracts/applications/ERC20_nodepmap.scif +++ b/test/contracts/applications/ERC20_nodepmap.scif @@ -51,9 +51,10 @@ contract ERC20 { public void approveFrom{from->this}(final address from, address spender, uint val) { address _from = endorse(from, from->this); + address owner = endorse(from, from->this); address _to = endorse(spender, from->this); uint _val = endorse(val, from->this); - _allowances[from][spender] = val; + _allowances[owner][_to] = _val; } public void transfer{from->this}(final address from, address to, uint val) throws (ERC20InsufficientBalance) { diff --git a/test/contracts/applications/HODLWallet.scif b/test/contracts/applications/HODLWallet.scif index bf8bf619..e5d5e6a0 100644 --- a/test/contracts/applications/HODLWallet.scif +++ b/test/contracts/applications/HODLWallet.scif @@ -2,7 +2,7 @@ contract HODLWallet { map(address, uint) balances; map(address, uint) withdrawalCount; - map(address, map(address, bool{owner}){owner}) approvals; + map(address owner, map(address, bool{owner}){owner}) approvals; uint MAX_WITHDRAWAL; diff --git a/test/contracts/applications/IERC20.scif b/test/contracts/applications/IERC20.scif index 8e288420..a97e1713 100644 --- a/test/contracts/applications/IERC20.scif +++ b/test/contracts/applications/IERC20.scif @@ -1,5 +1,5 @@ interface IERC20 { - exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientBalance(address{any} owner, uint{owner} balance, uint{owner} needed); exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); public uint balanceOf(address account); public void approve{sender}(address allowed, uint amount); diff --git a/test/contracts/applications/IERC777.scif b/test/contracts/applications/IERC777.scif index e70e3ceb..7d347d95 100644 --- a/test/contracts/applications/IERC777.scif +++ b/test/contracts/applications/IERC777.scif @@ -1,5 +1,5 @@ interface IERC777 { - exception ERC20InsufficientBalance(address{any} owner, uint balance, uint{owner} needed); + exception ERC20InsufficientBalance(address{any} owner, uint{owner} balance, uint{owner} needed); exception ERC20InsufficientAllowance(address{any} owner, address{owner} spender, uint{owner} allowance, uint{owner ⨆ spender} needed); public uint balanceOf(address account); public void approve{sender}(address allowed, uint amount); diff --git a/test/contracts/applications/SysEscrow.scif b/test/contracts/applications/SysEscrow.scif index 46c095a1..3a78260d 100644 --- a/test/contracts/applications/SysEscrow.scif +++ b/test/contracts/applications/SysEscrow.scif @@ -38,9 +38,8 @@ contract SysEscrow { address _buyer, // The buying party of the trade uint _paymentWindowInSeconds // The time in seconds from Escrow creation that the buyer can return money ) { - Escrow currentEscrow = escrows[_tradeHash]; - endorse([_seller, _buyer, _paymentWindowInSeconds], any -> this) - when (value >= MinDeposit && !(currentEscrow.exists)) { + endorse([_tradeHash, _seller, _buyer, _paymentWindowInSeconds], any -> this) + when (value >= MinDeposit && !(escrows[_tradeHash].exists)) { uint _buyerCanCancelAfter = now + _paymentWindowInSeconds; escrows[_tradeHash] = new Escrow(true, _seller, _buyer, value, _buyerCanCancelAfter, false, false); } @@ -63,24 +62,27 @@ contract SysEscrow { address _buyer, // The buying party of the trade uint _value // ) { - - Escrow currentEscrow = escrows[_tradeHash]; - assert(currentEscrow.exists); - assert(currentEscrow.buyerCanCancelAfter this) + when (escrows[_tradeHash].exists && escrows[_tradeHash].buyerCanCancelAfter < now) { + Escrow currentEscrow = escrows[_tradeHash]; + + uint arbitratorValue; + uint buyerValue; + unchecked { + arbitratorValue = currentEscrow.summ*ARBITRATOR_PERCENT/100; + buyerValue = currentEscrow.summ - arbitratorValue; + } + + lock(this) { + send(currentEscrow.buyer, buyerValue); + send(arbitrator, arbitratorValue); + } + + delete escrows[_tradeHash]; + } else { + assert false; } - - lock(this) { - send(currentEscrow.buyer, buyerValue); - send(arbitrator, arbitratorValue); - } - - delete escrows[_tradeHash]; } public void approveEscrow( @@ -92,10 +94,13 @@ contract SysEscrow { address _buyer, // The buying party of the trade uint _value // Trade value ) { - Escrow currentEscrow = escrows[_tradeHash]; - assert(currentEscrow.exists); - assert(currentEscrow.buyer==sender); - currentEscrow.buyerApprovedTheTransaction = true; + // Only the buyer of an escrow may approve it. + endorse(_tradeHash, sender -> this) + when (escrows[_tradeHash].exists && escrows[_tradeHash].buyer == sender) { + escrows[_tradeHash].buyerApprovedTheTransaction = true; + } else { + assert false; + } } public void releaseEscrow( @@ -107,25 +112,29 @@ contract SysEscrow { address _buyer, // The buying party of the trade uint _value // Trade value ) { - Escrow currentEscrow = escrows[_tradeHash]; - assert(currentEscrow.exists); - assert(currentEscrow.buyerApprovedTheTransaction); - - - uint arbitratorValue; - uint buyerValue; - unchecked { - arbitratorValue = currentEscrow.summ*ARBITRATOR_PERCENT/100; - buyerValue = currentEscrow.summ - arbitratorValue; + // Anyone may release an escrow that exists and that its buyer approved. + endorse(_tradeHash, sender -> this) + when (escrows[_tradeHash].exists && escrows[_tradeHash].buyerApprovedTheTransaction) { + Escrow currentEscrow = escrows[_tradeHash]; + + + uint arbitratorValue; + uint buyerValue; + unchecked { + arbitratorValue = currentEscrow.summ*ARBITRATOR_PERCENT/100; + buyerValue = currentEscrow.summ - arbitratorValue; + } + + lock(this) { + send(currentEscrow.buyer, buyerValue); + send(arbitrator, arbitratorValue); + } + + delete escrows[_tradeHash]; + } else { + assert false; } - lock(this) { - send(currentEscrow.buyer, buyerValue); - send(arbitrator, arbitratorValue); - } - - delete escrows[_tradeHash]; - } private bool isExistsEscrow( diff --git a/test/contracts/applications/Uniswap_ERC20.scif b/test/contracts/applications/Uniswap_ERC20.scif index 75e4a6e3..ae8ad5dc 100644 --- a/test/contracts/applications/Uniswap_ERC20.scif +++ b/test/contracts/applications/Uniswap_ERC20.scif @@ -227,13 +227,13 @@ contract Uniswap extends ERC20 { } public uint tokenToTokenSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToTokenTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { @@ -257,13 +257,13 @@ contract Uniswap extends ERC20 { } public uint tokenToTokenSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToTokenTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToExchangeSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address exchange_addr) { diff --git a/test/contracts/applications/Uniswap_ERC20_noe.scif b/test/contracts/applications/Uniswap_ERC20_noe.scif index ddc07804..9df07424 100644 --- a/test/contracts/applications/Uniswap_ERC20_noe.scif +++ b/test/contracts/applications/Uniswap_ERC20_noe.scif @@ -185,13 +185,13 @@ contract Uniswap extends ERC20 { } public uint tokenToTokenSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToTokenTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { @@ -209,13 +209,13 @@ contract Uniswap extends ERC20 { } public uint tokenToTokenSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToTokenTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToExchangeSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address exchange_addr) { diff --git a/test/contracts/applications/Uniswap_ERC777_noe.scif b/test/contracts/applications/Uniswap_ERC777_noe.scif index 05fe05fc..ae401669 100644 --- a/test/contracts/applications/Uniswap_ERC777_noe.scif +++ b/test/contracts/applications/Uniswap_ERC777_noe.scif @@ -193,13 +193,13 @@ contract Uniswap extends ERC20 { } public uint tokenToTokenSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, sender, endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToTokenTransferInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address recipient, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenInput(endorse(tokens_sold, sender->this), min_tokens_bought, min_eth_bought, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } uint tokenToTokenOutput(uint tokens_bought, uint{any} max_tokens_sold, uint{any} max_eth_sold, uint{any} deadline, address buyer, address recipient, Uniswap exchange) { @@ -219,13 +219,13 @@ contract Uniswap extends ERC20 { } public uint tokenToTokenSwapOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, sender, endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToTokenTransferOutput(uint tokens_bought, uint max_tokens_sold, uint max_eth_sold, uint deadline, address recipient, address token_addr) { - address exchange_addr = token_to_exchange[token_addr]; - return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), exchange_addr -> this)); + address{sender} exchange_addr = token_to_exchange[token_addr]; + return tokenToTokenOutput(endorse(tokens_bought, sender->this), max_tokens_sold, max_eth_sold, deadline, sender, endorse(recipient, sender->this), endorse(Uniswap(exchange_addr), (sender ⨆ exchange_addr) -> this)); } public uint tokenToExchangeSwapInput(uint tokens_sold, uint min_tokens_bought, uint min_eth_bought, uint deadline, address exchange_addr) { diff --git a/test/contracts/ifcTypechecking/CallElementReceiver.scif b/test/contracts/ifcTypechecking/CallElementReceiver.scif new file mode 100644 index 00000000..e0cb142f --- /dev/null +++ b/test/contracts/ifcTypechecking/CallElementReceiver.scif @@ -0,0 +1,11 @@ +import "./IOracle.scif"; +// A method can be called on an element of an array of contract references. +contract CallElementReceiver { + IOracle[] oracles; + uint{this} secret; + constructor() { super(); } + + public void f{this}(uint{this} k) { + secret = oracles[k].price(); + } +} diff --git a/test/contracts/ifcTypechecking/CallElementReceiverLow_W01.scif b/test/contracts/ifcTypechecking/CallElementReceiverLow_W01.scif new file mode 100644 index 00000000..8f3643fe --- /dev/null +++ b/test/contracts/ifcTypechecking/CallElementReceiverLow_W01.scif @@ -0,0 +1,12 @@ +import "./IOracle.scif"; +// A reference chosen by an untrusted index is trusted no more than the index, +// so the results of calls through it are not trusted. +contract CallElementReceiverLow_W01 { + IOracle[] oracles; + uint{this} secret; + constructor() { super(); } + + public void f{this}(uint{any} k) { + secret = oracles[k].price(); + } +} diff --git a/test/contracts/ifcTypechecking/CallMemberReceiver.scif b/test/contracts/ifcTypechecking/CallMemberReceiver.scif new file mode 100644 index 00000000..a21c9d14 --- /dev/null +++ b/test/contracts/ifcTypechecking/CallMemberReceiver.scif @@ -0,0 +1,14 @@ +import "./IOracle.scif"; +// A method can be called on a struct member that holds a contract reference. +contract CallMemberReceiver { + struct Feed { + IOracle oracle; + } + Feed feed; + uint{this} secret; + constructor() { super(); } + + public void f{this}() { + secret = feed.oracle.price(); + } +} diff --git a/test/contracts/ifcTypechecking/CallMemberReceiverLow_W01.scif b/test/contracts/ifcTypechecking/CallMemberReceiverLow_W01.scif new file mode 100644 index 00000000..1d3ee5f5 --- /dev/null +++ b/test/contracts/ifcTypechecking/CallMemberReceiverLow_W01.scif @@ -0,0 +1,15 @@ +import "./IOracle.scif"; +// A reference held in an untrusted member is untrusted, so the results of calls +// through it are not trusted. +contract CallMemberReceiverLow_W01 { + struct Feed { + IOracle{any} oracle; + } + Feed feed; + uint{this} secret; + constructor() { super(); } + + public void f{this}() { + secret = feed.oracle.price(); + } +} diff --git a/test/contracts/ifcTypechecking/DepMapKeyRead.scif b/test/contracts/ifcTypechecking/DepMapKeyRead.scif new file mode 100644 index 00000000..c709c6f4 --- /dev/null +++ b/test/contracts/ifcTypechecking/DepMapKeyRead.scif @@ -0,0 +1,10 @@ +// When an element's label names its key, the element is trusted by the +// principal the index holds, whoever chose the index. +contract DepMapKeyRead { + map(address a, uint{a}) m; + constructor() { super(); } + + public uint{k} f{this}(final address{any} k) { + return m[k]; + } +} diff --git a/test/contracts/ifcTypechecking/DepMapKeyReadLow_W01.scif b/test/contracts/ifcTypechecking/DepMapKeyReadLow_W01.scif new file mode 100644 index 00000000..06b48cd0 --- /dev/null +++ b/test/contracts/ifcTypechecking/DepMapKeyReadLow_W01.scif @@ -0,0 +1,10 @@ +// An element whose label names its key is trusted only as much as the +// principal the index holds. +contract DepMapKeyReadLow_W01 { + map(address a, uint{a}) m; + constructor() { super(); } + + public uint{this} f{this}(final address{any} k) { + return m[k]; + } +} diff --git a/test/contracts/ifcTypechecking/DepMapKeyWrite.scif b/test/contracts/ifcTypechecking/DepMapKeyWrite.scif new file mode 100644 index 00000000..bb151963 --- /dev/null +++ b/test/contracts/ifcTypechecking/DepMapKeyWrite.scif @@ -0,0 +1,10 @@ +// When an element's label names its key, any index may be written, since the +// element is labeled by the principal the index holds. +contract DepMapKeyWrite { + map(address a, uint{a}) m; + constructor() { super(); } + + public void f{this}(address{any} k) { + m[k] = 1; + } +} diff --git a/test/contracts/ifcTypechecking/DepMapUnnamedKeyRead.scif b/test/contracts/ifcTypechecking/DepMapUnnamedKeyRead.scif new file mode 100644 index 00000000..4f6b7e6e --- /dev/null +++ b/test/contracts/ifcTypechecking/DepMapUnnamedKeyRead.scif @@ -0,0 +1,13 @@ +// An element whose label does not name its key, read at a trusted index, is +// trusted at its label. +contract DepMapUnnamedKeyRead { + map(address a, uint{this}) m; + uint{this} secret; + constructor() { super(); } + + public void f{this}(address{this} k) { + if (k => this) { + secret = m[k]; + } + } +} diff --git a/test/contracts/ifcTypechecking/DepMapUnnamedKeyReadLow_W01.scif b/test/contracts/ifcTypechecking/DepMapUnnamedKeyReadLow_W01.scif new file mode 100644 index 00000000..4603d483 --- /dev/null +++ b/test/contracts/ifcTypechecking/DepMapUnnamedKeyReadLow_W01.scif @@ -0,0 +1,13 @@ +// When an element's label does not name its key, the index chooses which +// element is read, so the result is trusted no more than the index. +contract DepMapUnnamedKeyReadLow_W01 { + map(address a, uint{this}) m; + uint{this} secret; + constructor() { super(); } + + public void f{this}(address{any} k) { + if (k => this) { + secret = m[k]; + } + } +} diff --git a/test/contracts/ifcTypechecking/DepMapUnnamedKeyWriteLow_W01.scif b/test/contracts/ifcTypechecking/DepMapUnnamedKeyWriteLow_W01.scif new file mode 100644 index 00000000..bea95398 --- /dev/null +++ b/test/contracts/ifcTypechecking/DepMapUnnamedKeyWriteLow_W01.scif @@ -0,0 +1,10 @@ +// When an element's label does not name its key, the index of a write must be +// trusted at the element's label. +contract DepMapUnnamedKeyWriteLow_W01 { + map(address a, uint{this}) m; + constructor() { super(); } + + public void f{this}(address{any} k) { + m[k] = 1; + } +} diff --git a/test/contracts/ifcTypechecking/MapIndexRead.scif b/test/contracts/ifcTypechecking/MapIndexRead.scif new file mode 100644 index 00000000..6e856ea1 --- /dev/null +++ b/test/contracts/ifcTypechecking/MapIndexRead.scif @@ -0,0 +1,10 @@ +// An element of a trusted map read at a trusted index is trusted. +contract MapIndexRead { + map(address, uint) m; + uint{this} secret; + constructor() { super(); } + + public void f{this}(address{this} k) { + secret = m[k]; + } +} diff --git a/test/contracts/ifcTypechecking/MapIndexReadLow_W01.scif b/test/contracts/ifcTypechecking/MapIndexReadLow_W01.scif new file mode 100644 index 00000000..46c5b75c --- /dev/null +++ b/test/contracts/ifcTypechecking/MapIndexReadLow_W01.scif @@ -0,0 +1,11 @@ +// The index of a map read chooses which element is read, so the result is +// trusted no more than the index. +contract MapIndexReadLow_W01 { + map(address, uint) m; + uint{this} secret; + constructor() { super(); } + + public void f{this}(address{any} k) { + secret = m[k]; + } +} diff --git a/test/contracts/ifcTypechecking/MapIndexWrite.scif b/test/contracts/ifcTypechecking/MapIndexWrite.scif new file mode 100644 index 00000000..c757b615 --- /dev/null +++ b/test/contracts/ifcTypechecking/MapIndexWrite.scif @@ -0,0 +1,9 @@ +// A trusted map can be written at a trusted index. +contract MapIndexWrite { + map(address, uint) m; + constructor() { super(); } + + public void f{this}(address{this} k) { + m[k] = 1; + } +} diff --git a/test/contracts/ifcTypechecking/MapIndexWriteLow_W01.scif b/test/contracts/ifcTypechecking/MapIndexWriteLow_W01.scif new file mode 100644 index 00000000..7e18556f --- /dev/null +++ b/test/contracts/ifcTypechecking/MapIndexWriteLow_W01.scif @@ -0,0 +1,10 @@ +// The index of a map write chooses which element is overwritten, so it must be +// trusted at the map's label. +contract MapIndexWriteLow_W01 { + map(address, uint) m; + constructor() { super(); } + + public void f{this}(address{any} k) { + m[k] = 1; + } +} diff --git a/test/contracts/ifcTypechecking/MemberArrayPush.scif b/test/contracts/ifcTypechecking/MemberArrayPush.scif new file mode 100644 index 00000000..0279f5f5 --- /dev/null +++ b/test/contracts/ifcTypechecking/MemberArrayPush.scif @@ -0,0 +1,13 @@ +// Pushing onto an array held in a struct member requires trust at the +// member's location. +contract MemberArrayPush { + struct Log { + uint[] entries; + } + Log log; + constructor() { super(); } + + public void f{this}(uint{this} x) { + log.entries.push(x); + } +} diff --git a/test/contracts/ifcTypechecking/MemberArrayPushLow_W01.scif b/test/contracts/ifcTypechecking/MemberArrayPushLow_W01.scif new file mode 100644 index 00000000..a52f60d9 --- /dev/null +++ b/test/contracts/ifcTypechecking/MemberArrayPushLow_W01.scif @@ -0,0 +1,13 @@ +// Untrusted data cannot be pushed onto a trusted array held in a struct +// member. +contract MemberArrayPushLow_W01 { + struct Log { + uint[] entries; + } + Log log; + constructor() { super(); } + + public void f{this}(uint{any} x) { + log.entries.push(x); + } +} diff --git a/test/contracts/ifcTypechecking/NestedMapWrite.scif b/test/contracts/ifcTypechecking/NestedMapWrite.scif new file mode 100644 index 00000000..2f0d1b0b --- /dev/null +++ b/test/contracts/ifcTypechecking/NestedMapWrite.scif @@ -0,0 +1,9 @@ +// Trusted data can be written through nested subscripts of a trusted map. +contract NestedMapWrite { + map(address, map(address, uint)) m; + constructor() { super(); } + + public void f{this}(address{this} a, address{this} b, uint{this} x) { + m[a][b] = x; + } +} diff --git a/test/contracts/ifcTypechecking/NestedMapWriteLow_W01.scif b/test/contracts/ifcTypechecking/NestedMapWriteLow_W01.scif new file mode 100644 index 00000000..6ff5b811 --- /dev/null +++ b/test/contracts/ifcTypechecking/NestedMapWriteLow_W01.scif @@ -0,0 +1,9 @@ +// Writes through nested subscripts are checked against the map's label. +contract NestedMapWriteLow_W01 { + map(address, map(address, uint)) m; + constructor() { super(); } + + public void f{this}(address{this} a, address{this} b, uint{any} x) { + m[a][b] = x; + } +} diff --git a/test/contracts/ifcTypechecking/StructElementRead.scif b/test/contracts/ifcTypechecking/StructElementRead.scif new file mode 100644 index 00000000..0d4d0431 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructElementRead.scif @@ -0,0 +1,14 @@ +// A trusted member of a trusted array element can be used as trusted. +contract StructElementRead { + struct S { + uint{any} v; + uint w; + } + S[] ss; + uint{this} secret; + constructor() { super(); } + + public void f{this}(uint{this} k) { + secret = ss[k].w; + } +} diff --git a/test/contracts/ifcTypechecking/StructElementReadLow_W01.scif b/test/contracts/ifcTypechecking/StructElementReadLow_W01.scif new file mode 100644 index 00000000..7120f953 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructElementReadLow_W01.scif @@ -0,0 +1,14 @@ +// A member of an array element is read at its own label. +contract StructElementReadLow_W01 { + struct S { + uint{any} v; + uint w; + } + S[] ss; + uint{this} secret; + constructor() { super(); } + + public void f{this}(uint{this} k) { + secret = ss[k].v; + } +} diff --git a/test/contracts/ifcTypechecking/StructElementWrite.scif b/test/contracts/ifcTypechecking/StructElementWrite.scif new file mode 100644 index 00000000..91af4aa1 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructElementWrite.scif @@ -0,0 +1,13 @@ +// Untrusted data can be written to an untrusted member of an array element. +contract StructElementWrite { + struct S { + uint{any} v; + uint w; + } + S[] ss; + constructor() { super(); } + + public void f{this}(uint{this} k, uint{any} x) { + ss[k].v = x; + } +} diff --git a/test/contracts/ifcTypechecking/StructElementWriteLow_W01.scif b/test/contracts/ifcTypechecking/StructElementWriteLow_W01.scif new file mode 100644 index 00000000..90925666 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructElementWriteLow_W01.scif @@ -0,0 +1,13 @@ +// Untrusted data cannot be written to a trusted member of an array element. +contract StructElementWriteLow_W01 { + struct S { + uint{any} v; + uint w; + } + S[] ss; + constructor() { super(); } + + public void f{this}(uint{this} k, uint{any} x) { + ss[k].w = x; + } +} diff --git a/test/contracts/ifcTypechecking/StructNestedRead.scif b/test/contracts/ifcTypechecking/StructNestedRead.scif new file mode 100644 index 00000000..84dd39d7 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructNestedRead.scif @@ -0,0 +1,17 @@ +// A trusted member of a nested struct can be used as trusted. +contract StructNestedRead { + struct In { + uint{any} v; + uint w; + } + struct Out { + In i; + } + Out o; + uint{this} secret; + constructor() { super(); } + + public void f{this}() { + secret = o.i.w; + } +} diff --git a/test/contracts/ifcTypechecking/StructNestedReadLow_W01.scif b/test/contracts/ifcTypechecking/StructNestedReadLow_W01.scif new file mode 100644 index 00000000..2da971a9 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructNestedReadLow_W01.scif @@ -0,0 +1,17 @@ +// A member of a nested struct is read at its own label. +contract StructNestedReadLow_W01 { + struct In { + uint{any} v; + uint w; + } + struct Out { + In i; + } + Out o; + uint{this} secret; + constructor() { super(); } + + public void f{this}() { + secret = o.i.v; + } +} diff --git a/test/contracts/ifcTypechecking/StructNestedWrite.scif b/test/contracts/ifcTypechecking/StructNestedWrite.scif new file mode 100644 index 00000000..2a655810 --- /dev/null +++ b/test/contracts/ifcTypechecking/StructNestedWrite.scif @@ -0,0 +1,16 @@ +// Untrusted data can be written to an untrusted member of a nested struct. +contract StructNestedWrite { + struct In { + uint{any} v; + uint w; + } + struct Out { + In i; + } + Out o; + constructor() { super(); } + + public void f{this}(uint{any} x) { + o.i.v = x; + } +} diff --git a/test/contracts/ifcTypechecking/StructNestedWriteLow_W01.scif b/test/contracts/ifcTypechecking/StructNestedWriteLow_W01.scif new file mode 100644 index 00000000..80fe83aa --- /dev/null +++ b/test/contracts/ifcTypechecking/StructNestedWriteLow_W01.scif @@ -0,0 +1,16 @@ +// Untrusted data cannot be written to a trusted member of a nested struct. +contract StructNestedWriteLow_W01 { + struct In { + uint{any} v; + uint w; + } + struct Out { + In i; + } + Out o; + constructor() { super(); } + + public void f{this}(uint{any} x) { + o.i.w = x; + } +} diff --git a/test/contracts/regularTypechecking/CallReceiverNotVariable_W01.scif b/test/contracts/regularTypechecking/CallReceiverNotVariable_W01.scif index 1e7cf62c..6cf5dfe1 100644 --- a/test/contracts/regularTypechecking/CallReceiverNotVariable_W01.scif +++ b/test/contracts/regularTypechecking/CallReceiverNotVariable_W01.scif @@ -1,8 +1,8 @@ -// The receiver of a method call is not a variable. +// The receiver of a method call must be a variable, a member, or an element, +// not the result of another call, even in parentheses. contract CallReceiverNotVariable_W01 { - struct S { CallReceiverNotVariable_W01 c; } - S s; - uint x; - constructor() { super(); x = s.c.g(); } - public uint g() { return 1; } + CallReceiverNotVariable_W01 c; + constructor() { super(); } + public CallReceiverNotVariable_W01 self() { return c; } + public uint g() { return (self()).g(); } } diff --git a/test/contracts/regularTypechecking/DepMapMutableIndex.scif b/test/contracts/regularTypechecking/DepMapMutableIndex.scif new file mode 100644 index 00000000..d228363c --- /dev/null +++ b/test/contracts/regularTypechecking/DepMapMutableIndex.scif @@ -0,0 +1,10 @@ +// A dependent map may be indexed by any address variable, final or not. The +// element's label names the principal the index holds when the map is read. +contract DepMapMutableIndex { + map(address x, uint{x}) m; + uint{this} y; + constructor() { super(); } + public void f{this}(address a) { + y = endorse(m[a], any -> this); + } +} diff --git a/test/contracts/regularTypechecking/DepMapNonFinalIndex_W01.scif b/test/contracts/regularTypechecking/DepMapNonFinalIndex_W01.scif deleted file mode 100644 index 59067f11..00000000 --- a/test/contracts/regularTypechecking/DepMapNonFinalIndex_W01.scif +++ /dev/null @@ -1,9 +0,0 @@ -// A dependent map is indexed by a non-final address. -contract DepMapNonFinalIndex_W01 { - map(address x, uint{x}) m; - uint{this} y; - constructor() { super(); } - public void f{this}(address a) { - y = endorse(m[a], any -> this); - } -} diff --git a/test/src/TestIfcTypechecking.java b/test/src/TestIfcTypechecking.java index 077b281a..49419803 100644 --- a/test/src/TestIfcTypechecking.java +++ b/test/src/TestIfcTypechecking.java @@ -84,6 +84,20 @@ public class TestIfcTypechecking { "ifcTypechecking/StructMemberRead", "ifcTypechecking/StructMemberWrite", "ifcTypechecking/StructForeignMember", + "ifcTypechecking/StructNestedRead", + "ifcTypechecking/StructNestedWrite", + "ifcTypechecking/StructElementRead", + "ifcTypechecking/StructElementWrite", + "ifcTypechecking/NestedMapWrite", + "basic/StructEx04", + "ifcTypechecking/MapIndexRead", + "ifcTypechecking/MapIndexWrite", + "ifcTypechecking/DepMapKeyRead", + "ifcTypechecking/DepMapUnnamedKeyRead", + "ifcTypechecking/DepMapKeyWrite", + "ifcTypechecking/CallElementReceiver", + "ifcTypechecking/CallMemberReceiver", + "ifcTypechecking/MemberArrayPush", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -172,6 +186,19 @@ void testPositive(String contractName) throws Exception { "ifcTypechecking/StructMemberReadLow_W01", "ifcTypechecking/StructMemberWriteLow_W01", "ifcTypechecking/StructForeignMemberLow_W01", + "ifcTypechecking/StructNestedReadLow_W01", + "ifcTypechecking/StructNestedWriteLow_W01", + "ifcTypechecking/StructElementReadLow_W01", + "ifcTypechecking/StructElementWriteLow_W01", + "ifcTypechecking/NestedMapWriteLow_W01", + "ifcTypechecking/MapIndexReadLow_W01", + "ifcTypechecking/MapIndexWriteLow_W01", + "ifcTypechecking/DepMapKeyReadLow_W01", + "ifcTypechecking/DepMapUnnamedKeyReadLow_W01", + "ifcTypechecking/DepMapUnnamedKeyWriteLow_W01", + "ifcTypechecking/CallElementReceiverLow_W01", + "ifcTypechecking/CallMemberReceiverLow_W01", + "ifcTypechecking/MemberArrayPushLow_W01", // "examples/ERC20", }) void testNegative(String contractName) throws Exception { diff --git a/test/src/TestRegularTypechecking.java b/test/src/TestRegularTypechecking.java index 1c5fb91a..bfa07504 100644 --- a/test/src/TestRegularTypechecking.java +++ b/test/src/TestRegularTypechecking.java @@ -52,6 +52,7 @@ public class TestRegularTypechecking { "regularTypechecking/FinalFieldInitOK", "regularTypechecking/ExceptionParamLabels", "regularTypechecking/StructMemberLabels", + "regularTypechecking/DepMapMutableIndex", "regularTypechecking/TrustQueryNonFinal", }) void testPositive(String contractName) throws Exception { @@ -133,7 +134,6 @@ void testNegative(String contractName) { "regularTypechecking/CallOptionUnsupported_W01", "regularTypechecking/ThrowsUnknownException_W01", "regularTypechecking/EmptyFile_W01", - "regularTypechecking/DepMapNonFinalIndex_W01", "regularTypechecking/ThrowUndeclared_W01", "regularTypechecking/SubscriptNonCollection_W01", "regularTypechecking/LabelNonPrincipalVar_W01", From 5ee1b5e29eeb87593cc3f94ef64e864af6e070ed Mon Sep 17 00:00:00 2001 From: Andrew Myers Date: Fri, 6 Nov 2026 11:36:18 -0500 Subject: [PATCH 8/8] Simplified and unified checking of various statement-level constructs by adding a Block node to the AST to replace the previous fairly ad-hoc 'list of statements' approach. Also got rid of the endorse statement that was not really suppored anyway. --- src/SCIF.cup | 12 -- src/ast/Atomic.java | 41 +++--- src/ast/Block.java | 124 ++++++++++++++++++ src/ast/Contract.java | 4 +- src/ast/EndorseIfStatement.java | 14 +- src/ast/EndorseStatement.java | 89 ------------- src/ast/ExceptHandler.java | 37 ++---- src/ast/For.java | 36 ++--- src/ast/FunctionDef.java | 67 +++------- src/ast/GuardBlock.java | 36 ++--- src/ast/If.java | 71 ++++------ src/ast/Try.java | 33 ++--- src/ast/UncheckedBlock.java | 60 ++------- src/ast/While.java | 35 ++--- src/test.cup | 12 -- src/typecheck/Utils.java | 28 ---- .../UncheckedEarlierThrowLow_W01.scif | 18 +++ .../UncheckedEarlierThrowUntrusted.scif | 18 +++ .../ifcTypechecking/UncheckedEmpty.scif | 11 ++ test/src/TestIfcTypechecking.java | 3 + 20 files changed, 301 insertions(+), 448 deletions(-) create mode 100644 src/ast/Block.java delete mode 100644 src/ast/EndorseStatement.java create mode 100644 test/contracts/ifcTypechecking/UncheckedEarlierThrowLow_W01.scif create mode 100644 test/contracts/ifcTypechecking/UncheckedEarlierThrowUntrusted.scif create mode 100644 test/contracts/ifcTypechecking/UncheckedEmpty.scif diff --git a/src/SCIF.cup b/src/SCIF.cup index 528f1de0..ccc07fcd 100644 --- a/src/SCIF.cup +++ b/src/SCIF.cup @@ -269,7 +269,6 @@ nonterminal List except_types; nonterminal Throw throw_stmt; nonterminal List constructors; nonterminal FunctionDef constructor; -//nonterminal EndorseStatement endorse_stmt; nonterminal EndorseIfStatement endorseIf_stmt; nonterminal Call call_expr; nonterminal Revert revert_stmt; @@ -1158,9 +1157,6 @@ stmt ::= assign_stmt:a {: RESULT = a; :} | throw_stmt:a {: RESULT = a;:} | revert_stmt:a {: RESULT = a;:} | emit_stmt:a {: RESULT = a; :} -// | endorse_stmt:a {: -// RESULT = a; -// :} | endorseIf_stmt:a {: RESULT = a; :} @@ -1173,14 +1169,6 @@ stmt ::= assign_stmt:a {: RESULT = a; :} :} ; -//endorse_stmt ::= -// ENDORSEIF:x LPAR name_list:exprList COMMA ifLabel:from RIGHT_ARROW ifLabel:to RPAR IN suite:s {: -// EndorseStatement statement = new EndorseStatement(exprList, from, to, s); -// RESULT = statement; -// RESULT.setLoc(new CodeLocation(xleft, xright, filename)); -// :} -// ; - emit_stmt ::= EMIT:x call_expr:e SEMI {: if (!(e instanceof Call)) { diff --git a/src/ast/Atomic.java b/src/ast/Atomic.java index 13566da1..e624a1b8 100644 --- a/src/ast/Atomic.java +++ b/src/ast/Atomic.java @@ -20,14 +20,14 @@ public class Atomic extends Statement { - List body; + Block body; List handlers; // ArrayList orelse; // ArrayList finalbody; public Atomic(List body, List handlers) {//, ArrayList orelse, ArrayList finalbody) { - this.body = body; + this.body = new Block(body); this.handlers = handlers; // this.orelse = orelse; // this.finalbody = finalbody; @@ -53,10 +53,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S // now.addException(t, false); // } - for (Statement s : body) { -// assert !now.getSHErrLocName().startsWith("null"); - tmp = s.genTypeConstraints(env, atomicScope); - } + body.genTypeConstraints(env, atomicScope); env.exitNewScope(); env.exitAtomic(); @@ -107,10 +104,11 @@ public List solidityCodeGen(CompileEnv code) { readMap = new HashMap<>(); writeMap = new HashMap<>(); - if (body.size() == 1 && body.get(0) instanceof CallStatement && ((CallStatement) body.get(0)).call.value instanceof Attribute) { + List stmts = body.statements(); + if (stmts.size() == 1 && stmts.get(0) instanceof CallStatement callStmt && callStmt.call.value instanceof Attribute) { // if it is a single call - Call call = ((CallStatement) body.get(0)).call; - List result = body.get(0).solidityCodeGen(code); + Call call = callStmt.call; + List result = callStmt.solidityCodeGen(code); if (result.size() == 1 && result.get(0) instanceof ExternalCall) { ExternalCall externalCall = (ExternalCall) result.get(0); // LowLevelCall lowLevelCall = new LowLevelCall(); @@ -137,14 +135,11 @@ public List solidityCodeGen(CompileEnv code) { if (lowLevelCall == null) { // find out read and written local variables - for (Statement s : body) { - readMap.putAll(s.readMap(code)); - writeMap.putAll(s.writeMap(code)); - // solBody.addAll(s.solidityCodeGen(code)); - } + readMap.putAll(body.readMap(code)); + writeMap.putAll(body.writeMap(code)); readMap.putAll(writeMap); code.enterNewVarScope(); - Function newTempFunction = code.makeMethod(body, readMap, writeMap, ScopeType.ATOMIC); + Function newTempFunction = code.makeMethod(body.statements(), readMap, writeMap, ScopeType.ATOMIC); lowLevelCall = new LowLevelCall(newTempFunction, compile.Utils.THIS_ADDRESS, newTempFunction.funcName(), newTempFunction.argNames().stream().map(name -> new SingleVar(name)).collect( Collectors.toList())); @@ -249,7 +244,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic PathOutcome input = new PathOutcome(); Map versionsBefore = env.saveVersions(); env.incScopeLayer(); - psi.join(Utils.genConsStmtSeq(body, env, tail_position)); + psi.join(body.IFCVisit(env, tail_position)); env.decScopeLayer(); for (ExceptHandler h : handlers) { ExceptionTypeSym expSym = h.exceptionType(env); @@ -272,7 +267,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // A handler may be entered from any point in the body. List> exitVersions = new ArrayList<>(); exitVersions.add(env.saveVersions()); - java.util.Set assignedInBody = ast.Assign.assignedNames(body); + java.util.Set assignedInBody = ast.Assign.assignedNames(body.statements()); for (ExceptHandler h : handlers) { env.restoreVersions(versionsBefore); env.havoc(assignedInBody); @@ -321,7 +316,7 @@ public void passScopeContext(ScopeContext parent) { @Override public List children() { List rtn = new ArrayList<>(); - rtn.addAll(body); + rtn.add(body); rtn.addAll(handlers); return rtn; } @@ -333,10 +328,7 @@ public boolean exceptionHandlingFree() { @Override protected Map readMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } + Map result = new HashMap<>(body.readMap(code)); for (ExceptHandler s: handlers) { result.putAll(s.readMap(code)); } @@ -345,10 +337,7 @@ protected Map readMap(CompileEnv code) { @Override protected Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } + Map result = new HashMap<>(body.writeMap(code)); for (ExceptHandler s: handlers) { result.putAll(s.writeMap(code)); } diff --git a/src/ast/Block.java b/src/ast/Block.java new file mode 100644 index 00000000..b2286474 --- /dev/null +++ b/src/ast/Block.java @@ -0,0 +1,124 @@ +package ast; + +import compile.CompileEnv; +import compile.ast.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import typecheck.NTCEnv; +import typecheck.PathOutcome; +import typecheck.PsiUnit; +import typecheck.ScopeContext; +import typecheck.Utils; +import typecheck.VisitEnv; +import typecheck.exceptions.SemanticException; + +/** + * A sequence of statements executed in order, such as the body of a method or a branch of a + * conditional. A block does not introduce a scope; the construct that contains it decides + * which scopes its statements are in. + */ +public class Block extends Statement { + + private final List statements; + + public Block(List statements) { + this.statements = new ArrayList<>(statements); + } + + /** The statements of this block, in order. */ + public List statements() { + return Collections.unmodifiableList(statements); + } + + /** Whether this block has no statements. */ + public boolean isEmpty() { + return statements.isEmpty(); + } + + /** + * Generates the type constraints of each statement in the scope context {@code parent}, + * and returns the scope context of the last statement, or {@code parent} if there is none. + */ + @Override + public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) + throws SemanticException { + ScopeContext last = parent; + for (Statement s : statements) { + last = s.genTypeConstraints(env, parent); + } + return last; + } + + /** + * Generates the information-flow constraints for this block, which starts in the context + * {@code env.inContext}, and returns every way it can terminate: its exception and return + * paths, and its normal path if it can complete normally. The last statement is in tail + * position if {@code tail_position} is. On return, {@code env.inContext} is the context + * after the last statement that completed normally. + */ + @Override + public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws SemanticException { + PathOutcome result = new PathOutcome(); + int index = 0; + for (Statement s : statements) { + ++index; + String prevLambda = env.inContext.lambda; + boolean isTail = index == statements.size() && tail_position; + PathOutcome so = s.IFCVisit(env, isTail); + PsiUnit normal = so.getNormalPath(); + result.join(so.withoutNormalPath()); + if (normal == null) { + return result; + } + env.inContext = Utils.genNewContextAndConstraints(env, isTail, normal.c, prevLambda, + s.nextPcSHL(), s.location()); + } + result.setNormalPath(env.inContext); + return result; + } + + @Override + public List solidityCodeGen(CompileEnv code) { + List result = new ArrayList<>(); + for (Statement s : statements) { + result.addAll(s.solidityCodeGen(code)); + } + return result; + } + + @Override + public List children() { + return new ArrayList<>(statements); + } + + @Override + public boolean exceptionHandlingFree() { + for (Statement s : statements) { + if (!s.exceptionHandlingFree()) { + return false; + } + } + return true; + } + + @Override + protected Map readMap(CompileEnv code) { + Map result = new HashMap<>(); + for (Statement s : statements) { + result.putAll(s.readMap(code)); + } + return result; + } + + @Override + protected Map writeMap(CompileEnv code) { + Map result = new HashMap<>(); + for (Statement s : statements) { + result.putAll(s.writeMap(code)); + } + return result; + } +} diff --git a/src/ast/Contract.java b/src/ast/Contract.java index 6f42dde5..f162720e 100644 --- a/src/ast/Contract.java +++ b/src/ast/Contract.java @@ -202,7 +202,7 @@ private void markFinalFieldInits() throws SemanticException { Set initialized = new HashSet<>(); for (FunctionDef fDef : methodDeclarations) { if (!fDef.isConstructor() && !Utils.isSuperConstructor(fDef.getName())) continue; - for (Statement stmt : fDef.body) { + for (Statement stmt : fDef.body.statements()) { if (stmt instanceof Assign a && a.target instanceof Name n && finalFields.containsKey(n.id)) { if (!initialized.add(n.id)) { @@ -668,7 +668,7 @@ public void codePasteContract(Map contractMap, Map 0 || ifStatement.orelse.size() > 0) { + if (!ifStatement.body.isEmpty() || !ifStatement.orelse.isEmpty()) { Utils.genSequenceConstraints(env, beginContext.lambda, IfTestNormalPc, IfTestNormalLambda, IfNamePcAfter, ifStatement.test.location); } Map versionsBefore = env.saveVersions(); @@ -132,7 +132,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic CodeLocation loc = null; PathOutcome ifo = toOutCome.psi.withoutNormalPath(); env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - ifo.join(Utils.genConsStmtSeq(ifStatement.body, env, tail_position)); + ifo.join(ifStatement.body.IFCVisit(env, tail_position)); // int index = 0; // for (Statement stmt : ifStatement.body) { @@ -163,7 +163,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.incScopeLayer(); // index = 0; env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - PathOutcome elseo = Utils.genConsStmtSeq(ifStatement.orelse, env, tail_position); + PathOutcome elseo = ifStatement.orelse.IFCVisit(env, tail_position); // for (Statement stmt : ifStatement.orelse) { // ++index; // elseo = stmt.genConsVisit(env, index == ifStatement.orelse.size() && tail_position); @@ -178,7 +178,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.mergeVersions(List.of(versionsThen, env.saveVersions())); // Assignments in the then branch go to the endorsed copies, which share storage // with the original variables. - env.havoc(Assign.assignedNames(ifStatement.body)); + env.havoc(Assign.assignedNames(ifStatement.body.statements())); ifo.join(elseo); // env.cons.add(new Constraint(new Inequality(IfNameLock, Relation.REQ, Utils.joinLabels(leftContext.lambda, rightContext.lambda)), env.hypothesis, location)); @@ -197,8 +197,8 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic @Override public List children() { ArrayList rtn = new ArrayList<>(); - rtn.addAll(ifStatement.body); - rtn.addAll(ifStatement.orelse); + rtn.add(ifStatement.body); + rtn.add(ifStatement.orelse); return rtn; } diff --git a/src/ast/EndorseStatement.java b/src/ast/EndorseStatement.java deleted file mode 100644 index bf3b175c..00000000 --- a/src/ast/EndorseStatement.java +++ /dev/null @@ -1,89 +0,0 @@ -package ast; - -import compile.CompileEnv; -import compile.Utils; -import compile.ast.IfStatement; -import compile.ast.Literal; -import compile.ast.Type; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.swing.plaf.nimbus.State; -import typecheck.NTCEnv; -import typecheck.PathOutcome; -import typecheck.ScopeContext; -import typecheck.VisitEnv; - -public class EndorseStatement extends Statement { - - List expressionList; - IfLabel from, to; - List body; - - public EndorseStatement(List expressionList, IfLabel from, IfLabel to, - List body) { - this.expressionList = expressionList; - this.from = from; - this.to = to; - this.body = body; - } - - public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) { - // TODO - return parent; - } - - @Override - public List solidityCodeGen(CompileEnv code) { - code.countEndorse(expressionList.size()); - List result = new ArrayList<>(); - for (Statement s: body) { - result.addAll(s.solidityCodeGen(code)); - } - return List.of(new IfStatement(new Literal(Utils.SOL_TRUE), result)); - } - - @Override - public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) { - //TODO - - return null; - } - - @Override - public ArrayList children() { - ArrayList rtn = new ArrayList<>(); - rtn.addAll(expressionList); - rtn.add(from); - rtn.add(to); - return rtn; - } - - @Override - public boolean exceptionHandlingFree() { - for (Statement s: body) { - if (!s.exceptionHandlingFree()) { - return false; - } - } - return true; - } - - @Override - protected java.util.Map readMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } - return result; - } - - protected Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } - return result; - } -} diff --git a/src/ast/ExceptHandler.java b/src/ast/ExceptHandler.java index 2c5fc6b4..2de88799 100644 --- a/src/ast/ExceptHandler.java +++ b/src/ast/ExceptHandler.java @@ -19,13 +19,13 @@ public class ExceptHandler extends Node { private LabeledType labeledType; private String name; - private List body; + private Block body; final private boolean acceptall; public ExceptHandler(LabeledType type, String name, List body) { this.labeledType = type; this.name = name; - this.body = body; + this.body = new Block(body); acceptall = false; } @@ -33,13 +33,10 @@ public ExceptHandler(LabeledType type, String name, List body) { * Create a handler that accept all other types of exceptions(try/catch) or failures(atomic/rescue) */ public ExceptHandler(List body) { - this.body = body; + this.body = new Block(body); acceptall = true; } - public void setBody(List body) { - this.body = body; - } @Override public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { @@ -59,9 +56,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } } - for (Statement s : body) { - ScopeContext tmp = s.genTypeConstraints(env, now); - } + body.genTypeConstraints(env, now); env.exitNewScope(); return now; } @@ -69,9 +64,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S public IfStatement solidityCodeGen(CompileEnv code, Map writeMap, SingleVar dataVar) { List solBody = new ArrayList<>(); if (acceptall) { - for (Statement s: body) { - solBody.addAll(s.solidityCodeGen(code)); - } + solBody.addAll(body.solidityCodeGen(code)); return code.testNonNormalPath(solBody); } @@ -101,9 +94,7 @@ public IfStatement solidityCodeGen(CompileEnv code, Map children() { List rtn = new ArrayList<>(); rtn.add(labeledType); - rtn.addAll(body); + rtn.add(body); return rtn; } @@ -133,7 +124,7 @@ public PathOutcome genConsVisit(VisitEnv env, boolean tail_position) throws Sema declareCaughtVar(env, beginContext.pc); } int index = 0; - PathOutcome so = Utils.genConsStmtSeq(body, env, tail_position); + PathOutcome so = body.IFCVisit(env, tail_position); // for (Statement stmt : body) { // ++index; // so = stmt.genConsVisit(env, index == body.size() && tail_position); @@ -189,18 +180,10 @@ public ExceptionTypeSym exceptionType(VisitEnv env) { } protected Map readMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } - return result; + return body.readMap(code); } protected Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } - return result; + return body.writeMap(code); } } diff --git a/src/ast/For.java b/src/ast/For.java index 7a74ca5b..74e5484e 100644 --- a/src/ast/For.java +++ b/src/ast/For.java @@ -19,14 +19,14 @@ public class For extends Statement { AnnAssign newVars; Expression test; - List iter; - List body; + Block iter; + Block body; public For(AnnAssign newVars, Expression test, List iter, List body) { this.newVars = newVars; this.test = test; - this.iter = iter; - this.body = body; + this.iter = new Block(iter); + this.body = new Block(body); } public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { @@ -40,12 +40,8 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S // the test condition must be boolean env.addCons(rtn.genTypeConstraints(Utils.BuiltinType2ID(BuiltInT.BOOL), Relation.EQ, env, location)); - for (Statement s: body) { - s.genTypeConstraints(env, now); - } - for (Statement s: iter) { - s.genTypeConstraints(env, now); - } + body.genTypeConstraints(env, now); + iter.genTypeConstraints(env, now); env.exitNewScope(); @@ -62,8 +58,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic PathOutcome newVarOut = newVars.IFCVisit(env, false); beginContext = newVarOut.getNormalPath().c; // The loop head is reached from before the loop and from the end of an iteration. - java.util.Set assigned = Assign.assignedNames(body); - assigned.addAll(Assign.assignedNames(iter)); + java.util.Set assigned = Assign.assignedNames(List.of(body, iter)); env.havoc(assigned); ExpOutcome to = test.genIFConstraints(env, false); @@ -80,9 +75,9 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.inContext = new Context(ifNamePcAfter, beginContext.lambda); CodeLocation loc = null; - List iteration = new ArrayList<>(body); - iteration.addAll(iter); - PathOutcome iterOutcome = Utils.genConsStmtSeq(iteration, env, false); + List iteration = new ArrayList<>(body.statements()); + iteration.addAll(iter.statements()); + PathOutcome iterOutcome = new Block(iteration).IFCVisit(env, false); // The loop exits only from its head, which is also reached from the end of an // iteration. PathOutcome ifo = to.psi.withoutNormalPath(); @@ -119,18 +114,13 @@ public ArrayList children() { ArrayList rtn = new ArrayList<>(); rtn.add(newVars); rtn.add(test); - rtn.addAll(iter); - rtn.addAll(body); + rtn.add(iter); + rtn.add(body); return rtn; } @Override public boolean exceptionHandlingFree() { - for (Statement s: body) { - if (!s.exceptionHandlingFree()) { - return false; - } - } - return true; + return body.exceptionHandlingFree(); } } diff --git a/src/ast/FunctionDef.java b/src/ast/FunctionDef.java index 7eab0b0d..c3451e95 100644 --- a/src/ast/FunctionDef.java +++ b/src/ast/FunctionDef.java @@ -28,24 +28,24 @@ public class FunctionDef extends FunctionSig { private static boolean DEBUG = false; - List body; + Block body; public FunctionDef(String name, FuncLabels funcLabels, Arguments args, List body, List decoratorList, LabeledType rtn, boolean isConstructor, CodeLocation location) { super(name, funcLabels, args, decoratorList, rtn, isConstructor, location); - this.body = body; + this.body = new Block(body); } public FunctionDef(String name, FuncLabels funcLabels, Arguments args, List body, List decoratorList, LabeledType rtn, boolean isConstructor, boolean isBuiltIn, CodeLocation location) { super(name, funcLabels, args, decoratorList, rtn, isConstructor, isBuiltIn, location); - this.body = body; + this.body = new Block(body); } public FunctionDef(FunctionSig funcSig, List body) { super(funcSig); - this.body = body; + this.body = new Block(body); } @@ -56,7 +56,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S env.enterNewScope(); // add args to local sym; String funcName = this.name; - if (DEBUG) System.err.println("entering method: " + name + " " + body.size()); + if (DEBUG) System.err.println("entering method: " + name + " " + body.statements().size()); FuncSym funcSym = ((FuncSym) env.getCurSym(funcName)); Map exceptionTypeSyms = new HashMap<>(); for (Map.Entry t : funcSym.exceptions.entrySet()) { @@ -89,10 +89,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S } //if (!isBuiltIn()) { // TODO: add support for signatures - for (Statement stmt : body) { - // // logger.debug("stmt: " + stmt); - stmt.genTypeConstraints(env, now); - } + body.genTypeConstraints(env, now); if (isConstructor()) { // TODO(steph): fix assert if (!env.superCalled()) throw new SemanticException("constructor of super contract is not called in the constructor of " + env.currentSourceFileFullName(), location); @@ -165,36 +162,15 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_info) throws SemanticExce args.genConsVisit(env, false); // Context prev = new Context(env.prevContext);//, prev2 = null; - CodeLocation loc = null; - PathOutcome CO = null; env.inContext = funcBeginContext; -// Utils.genConsStatments(body, env, CO, true); - int index = 0; - PathOutcome escaping = new PathOutcome(); // exception paths leaving the body - for (Statement stmt : body) { - // Context CO = new Context(Utils.getLabelNamePc(stmt.location), Utils.getLabelNameLock(stmt.location)); - // env.outContext = CO; - ++index; - String prevLambda = env.inContext.lambda; - boolean isTail = index == body.size() && tail_info; - CO = stmt.IFCVisit(env, isTail); - PathOutcome thrown = new PathOutcome(new java.util.HashMap<>(CO.psi)); - thrown.psi.remove(Utils.getNormalPathException()); - thrown.psi.remove(Utils.getReturnPathException()); - escaping.join(thrown); - //Context CO = env.outContext; - if (CO.existsNormalPath()) { - env.inContext = Utils.genNewContextAndConstraints(env, isTail, CO.getNormalPath().c, prevLambda, stmt.nextPcSHL(), stmt.location()); - -// env.inContext = new Context(CO.getNormalPath().c()); - } else { - break; - } - } - if (body.size() > 0 && CO.existsNormalPath()) { + PathOutcome CO = body.IFCVisit(env, tail_info); + PathOutcome escaping = CO.withoutNormalPath(); // exception paths leaving the body + escaping.psi.remove(Utils.getReturnPathException()); + if (!body.isEmpty() && CO.existsNormalPath()) { if (DEBUG) System.err.println("method: " + funcLocalName); + List stmts = body.statements(); Utils.contextFlow(env, CO.getNormalPath().c(), funcEndContext.c(), - body.get(body.size() - 1).location); + stmts.get(stmts.size() - 1).location); } // Each exception that escapes must be trusted at its declared label, and must @@ -256,9 +232,7 @@ public Function solidityCodeGen(CompileEnv code) { List arguments = args.solidityCodeGen(code); List statements = new ArrayList<>(); compile.Utils.addBuiltInVars(true, statements, code); - for (Statement s: body) { - statements.addAll(s.solidityCodeGen(code)); - } + statements.addAll(body.solidityCodeGen(code)); code.exitVarScope(); code.popScope(); return new Constructor(arguments, statements); @@ -295,10 +269,7 @@ public Function solidityCodeGen(CompileEnv code) { wapperStatements.addAll(code.enterFuncCheck(funcLabels, args)); // statements.addAll(code.enterFuncCheck(funcLabels, args)); } - for (Statement s: body) { - List tmp = s.solidityCodeGen(code); - statements.addAll(tmp); - } + statements.addAll(body.solidityCodeGen(code)); if (!exceptionFree()) { statements.addAll(code.compileReturn(rtn.type().isVoid() ? null : new SingleVar( @@ -343,23 +314,19 @@ public Function solidityCodeGen(CompileEnv code) { } private boolean exceptionHandlingFree() { - if (!exceptionFree()) return false; - for (Statement s: body) { - if (!s.exceptionHandlingFree()) return false; - } - return true; + return exceptionFree() && body.exceptionHandlingFree(); } @Override public List children() { List rtn = super.children(); - rtn.addAll(body); + rtn.add(body); return rtn; } public void renameSuperCall(String extendsContractName) { List newBody = new ArrayList<>(); - for (Statement s: body) { + for (Statement s: body.statements()) { if (s instanceof ast.CallStatement callStatement) { CallStatement newS = new CallStatement(callStatement); newS.call.checkAndRenameSuper(extendsContractName); diff --git a/src/ast/GuardBlock.java b/src/ast/GuardBlock.java index eead09c5..e21d4e26 100644 --- a/src/ast/GuardBlock.java +++ b/src/ast/GuardBlock.java @@ -15,12 +15,12 @@ public class GuardBlock extends Statement { PrimitiveIfLabel l; - List body; + Block body; Expression target; public GuardBlock(PrimitiveIfLabel l, List body, Expression target) { this.l = l; - this.body = body; + this.body = new Block(body); this.target = target; } @@ -30,10 +30,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S ScopeContext now = new ScopeContext(this, parent); env.enterNewScope(); - ScopeContext rtn = null; - for (Statement s : body) { - rtn = s.genTypeConstraints(env, now); - } + body.genTypeConstraints(env, now); env.exitNewScope(); return now; @@ -64,7 +61,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic //Context lastContext = new Context(curContext);//, prev2 = null; env.incScopeLayer(); env.inContext = curContext; - PathOutcome psi = Utils.genConsStmtSeq(body, env, tail_position); + PathOutcome psi = body.IFCVisit(env, tail_position); // int index = 0; // for (Statement stmt : body) { // ++index; @@ -144,9 +141,7 @@ public List solidityCodeGen(CompileEnv code) { code.enterNewVarScope(); String lockP = l.value().id; result.add(new Assert(code.lock(lockP))); - for (Statement stmt : body) { - result.addAll(stmt.solidityCodeGen(code)); - } + result.addAll(body.solidityCodeGen(code)); result.add(new Assert(code.unlock(lockP))); code.exitVarScope(); return result; @@ -164,7 +159,7 @@ public void passScopeContext(ScopeContext parent) { public ArrayList children() { ArrayList rtn = new ArrayList<>(); rtn.add(l); - rtn.addAll(body); + rtn.add(body); if (target != null) { rtn.add(target); } @@ -172,29 +167,16 @@ public ArrayList children() { } @Override public boolean exceptionHandlingFree() { - for (Statement s: body) { - if (!s.exceptionHandlingFree()) { - return false; - } - } - return true; + return body.exceptionHandlingFree(); } @Override protected java.util.Map readMap(CompileEnv code) { - java.util.Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } - return result; + return body.readMap(code); } @Override protected java.util.Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } - return result; + return body.writeMap(code); } } diff --git a/src/ast/If.java b/src/ast/If.java index f159b63a..a77f1473 100644 --- a/src/ast/If.java +++ b/src/ast/If.java @@ -18,19 +18,19 @@ public class If extends Statement { Expression test; - List body; - List orelse; + Block body; + Block orelse; public If(Expression test, List body, List orelse) { this.test = test; - this.body = body; - this.orelse = orelse; + this.body = new Block(body); + this.orelse = new Block(orelse); } public If(Expression test, List body) { this.test = test; - this.body = body; - this.orelse = new ArrayList<>(); + this.body = new Block(body); + this.orelse = new Block(new ArrayList<>()); } public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { @@ -42,14 +42,13 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S env.addCons(testCon); env.enterNewScope(); - for (Statement s : body) { - assert !now.getSHErrLocName().startsWith("null"): now.getSHErrLocName(); - rtn = s.genTypeConstraints(env, now); + if (!body.isEmpty()) { + rtn = body.genTypeConstraints(env, now); } env.exitNewScope(); env.enterNewScope(); - for (Statement s : orelse) { - rtn = s.genTypeConstraints(env, now); + if (!orelse.isEmpty()) { + rtn = orelse.genTypeConstraints(env, now); } env.exitNewScope(); env.addCons(now.genTypeConstraints(rtn, Relation.EQ, env, location)); @@ -67,7 +66,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic String rtnValueLabel; ExpOutcome to = test.genIFConstraints(env, - body.size() == 0 && orelse.size() == 0 && tail_position); + body.isEmpty() && orelse.isEmpty() && tail_position); //Context condContext = to.psi rtnValueLabel = to.valueLabelName; String IfNameTest = to.valueLabelName; @@ -102,7 +101,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.checkFlow(IfNameTest, IfNamePcAfter, location, "Integrity of this condition doesn't flow to the control flow in this if branch"); - if (body.size() > 0 || orelse.size() > 0) { + if (!body.isEmpty() || !orelse.isEmpty()) { Utils.genSequenceConstraints(env, beginContext.lambda, IfTestNormalPc, IfTestNormalLambda, IfNamePcAfter, test.location); // env.cons.add(new Constraint(new Inequality(IfTestNormalPc, IfNamePcAfter), env.hypothesis(), test.location, env.curContractSym().getName(), // Utils.ERROR_MESSAGE_LOCK_IN_NONLAST_OPERATION)); @@ -114,7 +113,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic CodeLocation loc = null; PathOutcome ifo = to.psi.withoutNormalPath(); env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - ifo.join(Utils.genConsStmtSeq(body, env, tail_position)); + ifo.join(body.IFCVisit(env, tail_position)); env.decScopeLayer(); if (createdHypo) { @@ -128,7 +127,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.restoreVersions(versionsBefore); env.incScopeLayer(); env.inContext = new Context(IfNamePcAfter, beginContext.lambda); - PathOutcome elseo = Utils.genConsStmtSeq(orelse, env, tail_position); + PathOutcome elseo = orelse.IFCVisit(env, tail_position); env.decScopeLayer(); env.mergeVersions(List.of(versionsThen, env.saveVersions())); @@ -151,16 +150,12 @@ public List solidityCodeGen(CompileEnv code) { List ifBranch = new ArrayList<>(); compile.ast.Expression cond = test.solidityCodeGen(result, code); code.enterNewVarScope(); - for (Statement stmt : body) { - ifBranch.addAll(stmt.solidityCodeGen(code)); - } + ifBranch.addAll(body.solidityCodeGen(code)); code.exitVarScope(); if (!orelse.isEmpty()) { List elseBranch = new ArrayList<>(); code.enterNewVarScope(); - for (Statement stmt : orelse) { - elseBranch.addAll(stmt.solidityCodeGen(code)); - } + elseBranch.addAll(orelse.solidityCodeGen(code)); code.exitVarScope(); result.add(new IfStatement(cond, ifBranch, elseBranch)); } else { @@ -181,46 +176,26 @@ public void passScopeContext(ScopeContext parent) { @Override public List children() { ArrayList rtn = new ArrayList<>(); - rtn.addAll(body); - rtn.addAll(orelse); + rtn.add(body); + rtn.add(orelse); return rtn; } @Override public boolean exceptionHandlingFree() { - for (Statement s: body) { - if (!s.exceptionHandlingFree()) { - return false; - } - } - for (Statement s: orelse) { - if (!s.exceptionHandlingFree()) { - return false; - } - } - return true; + return body.exceptionHandlingFree() && orelse.exceptionHandlingFree(); } @Override protected java.util.Map readMap(CompileEnv code) { - java.util.Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } - for (Statement s: orelse) { - result.putAll(s.readMap(code)); - } + java.util.Map result = new HashMap<>(body.readMap(code)); + result.putAll(orelse.readMap(code)); return result; } @Override protected java.util.Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } - for (Statement s: orelse) { - result.putAll(s.writeMap(code)); - } + Map result = new HashMap<>(body.writeMap(code)); + result.putAll(orelse.writeMap(code)); return result; } } diff --git a/src/ast/Try.java b/src/ast/Try.java index 008c1d79..1988c1f9 100644 --- a/src/ast/Try.java +++ b/src/ast/Try.java @@ -25,14 +25,14 @@ public class Try extends Statement { - List body; + Block body; List handlers; // ArrayList orelse; // ArrayList finalbody; public Try(List body, List handlers) {//, ArrayList orelse, ArrayList finalbody) { - this.body = body; + this.body = new Block(body); this.handlers = handlers; // this.orelse = orelse; // this.finalbody = finalbody; @@ -58,9 +58,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S now.addException(t, true); } - for (Statement s : body) { - tmp = s.genTypeConstraints(env, now); - } + body.genTypeConstraints(env, now); env.exitNewScope(); for (ExceptHandler h : handlers) { @@ -79,14 +77,11 @@ public List solidityCodeGen(CompileEnv code) { writeMap = new HashMap<>(); // find out read and written local variables - for (Statement s : body) { - readMap.putAll(s.readMap(code)); - writeMap.putAll(s.writeMap(code)); - // solBody.addAll(s.solidityCodeGen(code)); - } + readMap.putAll(body.readMap(code)); + writeMap.putAll(body.writeMap(code)); readMap.putAll(writeMap); code.enterNewVarScope(); - Function newTempFunction = code.makeMethod(body, readMap, writeMap, ScopeType.TRY); + Function newTempFunction = code.makeMethod(body.statements(), readMap, writeMap, ScopeType.TRY); code.exitVarScope(); code.addTemporaryFunction(newTempFunction); @@ -161,7 +156,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic PathOutcome input = new PathOutcome(); Map versionsBefore = env.saveVersions(); env.incScopeLayer(); - psi.join(Utils.genConsStmtSeq(body, env, false)); + psi.join(body.IFCVisit(env, false)); // for (Statement s : body) { // so = s.genConsVisit(env, false); // psi.joinExe(so); @@ -191,7 +186,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic // A handler may be entered from any point in the body. List> exitVersions = new ArrayList<>(); exitVersions.add(env.saveVersions()); - java.util.Set assignedInBody = ast.Assign.assignedNames(body); + java.util.Set assignedInBody = ast.Assign.assignedNames(body.statements()); for (ExceptHandler h : handlers) { env.restoreVersions(versionsBefore); env.havoc(assignedInBody); @@ -234,7 +229,7 @@ public void passScopeContext(ScopeContext parent) { @Override public List children() { ArrayList rtn = new ArrayList<>(); - rtn.addAll(body); + rtn.add(body); rtn.addAll(handlers); return rtn; } @@ -246,10 +241,7 @@ public boolean exceptionHandlingFree() { @Override protected Map readMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } + Map result = new HashMap<>(body.readMap(code)); for (ExceptHandler s: handlers) { result.putAll(s.readMap(code)); } @@ -258,10 +250,7 @@ protected Map readMap(CompileEnv code) { @Override protected Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } + Map result = new HashMap<>(body.writeMap(code)); for (ExceptHandler s: handlers) { result.putAll(s.writeMap(code)); } diff --git a/src/ast/UncheckedBlock.java b/src/ast/UncheckedBlock.java index 7fd6f83f..eb463325 100644 --- a/src/ast/UncheckedBlock.java +++ b/src/ast/UncheckedBlock.java @@ -18,87 +18,47 @@ public class UncheckedBlock extends Statement { - List body; + Block body; public UncheckedBlock(List body) { - this.body = body; + this.body = new Block(body); } public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { - for (Statement s : body) { - s.genTypeConstraints(env, parent); - } - + body.genTypeConstraints(env, parent); return parent; } public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws SemanticException { - - int index = 0; - PathOutcome ifo = null; - for (Statement stmt: body) { - ++index; - String prevLambda = env.inContext.lambda; - ifo = stmt.IFCVisit(env, index == body.size() && tail_position); - PsiUnit normalUnit = ifo.getNormalPath(); - if (normalUnit == null) { - break; - } - env.inContext = Utils.genNewContextAndConstraints(env, index == body.size() && tail_position, normalUnit.c, prevLambda, stmt.nextPcSHL(), stmt.location); - } - return ifo; + return body.IFCVisit(env, tail_position); } @Override public List solidityCodeGen(CompileEnv code) { List result = new ArrayList<>(); - List block_body = new ArrayList<>(); - for (Statement stmt : body) { - block_body.addAll(stmt.solidityCodeGen(code)); - } - result.add(new UncheckedStatement(block_body)); + result.add(new UncheckedStatement(body.solidityCodeGen(code))); return result; } - @Override - public void passScopeContext(ScopeContext parent) { - scopeContext = parent; - for (Node node : children()) { - node.passScopeContext(scopeContext); - } - } - @Override public ArrayList children() { ArrayList rtn = new ArrayList<>(); - rtn.addAll(body); + rtn.add(body); return rtn; } + @Override public boolean exceptionHandlingFree() { - for (Statement s: body) { - if (!s.exceptionHandlingFree()) { - return false; - } - } - return true; + return body.exceptionHandlingFree(); } @Override protected Map readMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } - return result; + return body.readMap(code); } @Override protected Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } - return result; + return body.writeMap(code); } } diff --git a/src/ast/While.java b/src/ast/While.java index 8102f8d9..50846b4e 100644 --- a/src/ast/While.java +++ b/src/ast/While.java @@ -17,11 +17,11 @@ public class While extends Statement { Expression test; - List body; + Block body; public While(Expression test, List body) { this.test = test; - this.body = body; + this.body = new Block(body); } public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws SemanticException { @@ -32,9 +32,7 @@ public ScopeContext genTypeConstraints(NTCEnv env, ScopeContext parent) throws S ScopeContext rtn = test.genTypeConstraints(env, now); env.addCons(rtn.genTypeConstraints(Utils.BuiltinType2ID(BuiltInT.BOOL), Relation.EQ, env, location)); - for (Statement s : body) { - ScopeContext tmp = s.genTypeConstraints(env, now); - } + body.genTypeConstraints(env, now); env.exitNewScope(); env.addCons(now.genTypeConstraints(rtn, Relation.EQ, env, location)); return now; @@ -47,7 +45,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic Utils.getLabelNameLock(toSHErrLocFmt())); // The loop head is reached from before the loop and from the end of the body. - java.util.Set assigned = Assign.assignedNames(body); + java.util.Set assigned = Assign.assignedNames(body.statements()); env.havoc(assigned); ExpOutcome to = test.genIFConstraints(env, false); @@ -65,7 +63,7 @@ public PathOutcome IFCVisit(VisitEnv env, boolean tail_position) throws Semantic env.inContext = new Context(ifNamePcAfter, beginContext.lambda); CodeLocation loc = null; - PathOutcome bodyOutcome = Utils.genConsStmtSeq(body, env, false); + PathOutcome bodyOutcome = body.IFCVisit(env, false); // The loop exits only from its head, which is also reached from the end of the body. PathOutcome ifo = to.psi.withoutNormalPath(); ifo.join(bodyOutcome.withoutNormalPath()); @@ -100,9 +98,7 @@ public List solidityCodeGen(CompileEnv code) { compile.ast.Expression cond = test.solidityCodeGen(condPrep, code); result.addAll(condPrep); code.enterNewVarScope(); - for (Statement stmt : body) { - whileBody.addAll(stmt.solidityCodeGen(code)); - } + whileBody.addAll(body.solidityCodeGen(code)); // add prep the end of whileBody excluding declarations for (compile.ast.Statement s: condPrep) { @@ -127,34 +123,23 @@ public void passScopeContext(ScopeContext parent) { public ArrayList children() { ArrayList rtn = new ArrayList<>(); rtn.add(test); - rtn.addAll(body); + rtn.add(body); return rtn; } @Override public boolean exceptionHandlingFree() { - for (Statement s: body) { - if (!s.exceptionHandlingFree()) { - return false; - } - } - return true; + return body.exceptionHandlingFree(); } @Override protected java.util.Map readMap(CompileEnv code) { java.util.Map result = test.readMap(code); - for (Statement s: body) { - result.putAll(s.readMap(code)); - } + result.putAll(body.readMap(code)); return result; } @Override protected java.util.Map writeMap(CompileEnv code) { - Map result = new HashMap<>(); - for (Statement s: body) { - result.putAll(s.writeMap(code)); - } - return result; + return body.writeMap(code); } } diff --git a/src/test.cup b/src/test.cup index e8c68f9f..07b572e3 100644 --- a/src/test.cup +++ b/src/test.cup @@ -238,7 +238,6 @@ nonterminal List except_types; nonterminal Throw throw_stmt; nonterminal List constructors; nonterminal FunctionDef constructor; -//nonterminal EndorseStatement endorse_stmt; nonterminal EndorseIfStatement endorseIf_stmt; nonterminal Call call_expr; nonterminal Revert revert_stmt; @@ -1086,9 +1085,6 @@ stmt ::= assign_stmt:a {: RESULT = a; :} | throw_stmt:a {: RESULT = a;:} | revert_stmt:a {: RESULT = a;:} | emit_stmt:a {: RESULT = a; :} -// | endorse_stmt:a {: -// RESULT = a; -// :} | endorseIf_stmt:a {: RESULT = a; :} @@ -1101,14 +1097,6 @@ stmt ::= assign_stmt:a {: RESULT = a; :} :} ; -//endorse_stmt ::= -// ENDORSEIF:x LPAR name_list:exprList COMMA ifLabel:from RIGHT_ARROW ifLabel:to RPAR IN suite:s {: -// EndorseStatement statement = new EndorseStatement(exprList, from, to, s); -// RESULT = statement; -// RESULT.setLoc(new CodeLocation(xleft, xright, filename)); -// :} -// ; - emit_stmt ::= EMIT:x call_expr:e SEMI {: if (!(e instanceof Call)) { diff --git a/src/typecheck/Utils.java b/src/typecheck/Utils.java index b75de266..de18807c 100644 --- a/src/typecheck/Utils.java +++ b/src/typecheck/Utils.java @@ -847,34 +847,6 @@ public static Context genNewContextAndConstraints(VisitEnv env, boolean tail_pos return new Context(newPc, c.lambda); } - /** - * Generates the information-flow constraints for the statement sequence {@code body}, - * which starts in the context {@code env.inContext}, and returns every way it can - * terminate: its exception and return paths, and its normal path if it can complete - * normally. The last statement is in tail position if {@code tailPosition} is. On return, - * {@code env.inContext} is the context after the last statement that completed normally. - */ - public static PathOutcome genConsStmtSeq(List body, VisitEnv env, - boolean tailPosition) throws SemanticException { - PathOutcome result = new PathOutcome(); - int index = 0; - for (ast.Statement s : body) { - ++index; - String prevLambda = env.inContext.lambda; - boolean isTail = index == body.size() && tailPosition; - PathOutcome so = s.IFCVisit(env, isTail); - PsiUnit normal = so.getNormalPath(); - result.join(so.withoutNormalPath()); - if (normal == null) { - return result; - } - env.inContext = genNewContextAndConstraints(env, isTail, normal.c, prevLambda, - s.nextPcSHL(), s.location()); - } - result.setNormalPath(env.inContext); - return result; - } - static int inferredLabelCount = 0; public static IfLabel newInferredLabel() { return new PrimitiveIfLabel(new Name("__inferredLabel" + inferredLabelCount)); diff --git a/test/contracts/ifcTypechecking/UncheckedEarlierThrowLow_W01.scif b/test/contracts/ifcTypechecking/UncheckedEarlierThrowLow_W01.scif new file mode 100644 index 00000000..d65ee37e --- /dev/null +++ b/test/contracts/ifcTypechecking/UncheckedEarlierThrowLow_W01.scif @@ -0,0 +1,18 @@ +// An exception from any statement of an unchecked block, not only the last, +// leaves the method on its path. +contract UncheckedEarlierThrowLow_W01 { + uint{this} count; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}() throws (E{this}) { + unchecked { + g(); + count = count + 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/UncheckedEarlierThrowUntrusted.scif b/test/contracts/ifcTypechecking/UncheckedEarlierThrowUntrusted.scif new file mode 100644 index 00000000..eb012d35 --- /dev/null +++ b/test/contracts/ifcTypechecking/UncheckedEarlierThrowUntrusted.scif @@ -0,0 +1,18 @@ +// An exception from an earlier statement of an unchecked block may leave the +// method when it is declared untrusted. +contract UncheckedEarlierThrowUntrusted { + uint{this} count; + exception E(); + constructor() { super(); } + + void g{this}() throws (E{any}) { + throw E(); + } + + public void f{this}() throws (E{any}) { + unchecked { + g(); + count = count + 1; + } + } +} diff --git a/test/contracts/ifcTypechecking/UncheckedEmpty.scif b/test/contracts/ifcTypechecking/UncheckedEmpty.scif new file mode 100644 index 00000000..2829eeab --- /dev/null +++ b/test/contracts/ifcTypechecking/UncheckedEmpty.scif @@ -0,0 +1,11 @@ +// An empty unchecked block completes normally. +contract UncheckedEmpty { + uint{this} count; + constructor() { super(); } + + public void f{this}() { + unchecked { + } + count = count + 1; + } +} diff --git a/test/src/TestIfcTypechecking.java b/test/src/TestIfcTypechecking.java index 49419803..bd8d6cc7 100644 --- a/test/src/TestIfcTypechecking.java +++ b/test/src/TestIfcTypechecking.java @@ -98,6 +98,8 @@ public class TestIfcTypechecking { "ifcTypechecking/CallElementReceiver", "ifcTypechecking/CallMemberReceiver", "ifcTypechecking/MemberArrayPush", + "ifcTypechecking/UncheckedEarlierThrowUntrusted", + "ifcTypechecking/UncheckedEmpty", }) void testPositive(String contractName) throws Exception { File logDir = new File("./.scif"); @@ -199,6 +201,7 @@ void testPositive(String contractName) throws Exception { "ifcTypechecking/CallElementReceiverLow_W01", "ifcTypechecking/CallMemberReceiverLow_W01", "ifcTypechecking/MemberArrayPushLow_W01", + "ifcTypechecking/UncheckedEarlierThrowLow_W01", // "examples/ERC20", }) void testNegative(String contractName) throws Exception {