def _check_ghost_functions(program: VyperProgram): if not isinstance(program, VyperInterface): node = first(program.node.stmts) or program.node for implemented_ghost in program.ghost_function_implementations.values( ): if program.ghost_functions.get(implemented_ghost.name) is None: raise InvalidProgramException( implemented_ghost.node, 'missing.ghost', f'This contract is implementing an unknown ghost function. ' f'None of the interfaces, this contract implements, declares a ghost ' f'function "{implemented_ghost.name}".') for interface in program.interfaces.values(): for ghost_function_list in interface.ghost_functions.values(): for ghost_function in ghost_function_list: imported_ghost_functions = [ ghost_func for ghost_func in program.ghost_functions.get( ghost_function.name, []) if ghost_func.file == ghost_function.file ] if not imported_ghost_functions: prefix_length = len( os.path.commonprefix( [ghost_function.file, program.file])) raise InvalidProgramException( node, 'missing.ghost', f'The interface "{interface.name}" ' f'needs a ghost function "{ghost_function.name}" from ' f'".{os.path.sep}{ghost_function.file[prefix_length:]}" but it ' f'was not imported for this contract.') imported_ghost_functions = [ ghost_func for ghost_func in program.ghost_functions.get(ghost_function.name) if ghost_func.interface == ghost_function.interface ] for imported_ghost_function in imported_ghost_functions: if ghost_function.file != imported_ghost_function.file: prefix_length = len( os.path.commonprefix([ ghost_function.file, imported_ghost_function.file ])) ghost_function_file = ghost_function.file[ prefix_length:] imported_ghost_function_file = imported_ghost_function.file[ prefix_length:] raise InvalidProgramException( node, 'duplicate.ghost', f'There are two versions of the ghost function ' f'"{ghost_function.name}" defined in an interface ' f'"{ghost_function.interface}" one from ' f'[...]"{imported_ghost_function_file}" the other from ' f'[...]"{ghost_function_file}".')
def _check_ghost_implements(program: VyperProgram): def check(cond, node, ghost_name, interface_name): if not cond: raise InvalidProgramException( node, 'ghost.not.implemented', f'The ghost function "{ghost_name}" from the interface "{interface_name}" ' f'has not been implemented correctly.') ghost_function_implementations = dict( program.ghost_function_implementations) for itype in program.implements: interface = program.interfaces[itype.name] for ghost in interface.own_ghost_functions.values(): implementation = ghost_function_implementations.pop( ghost.name, None) check(implementation is not None, program.node, ghost.name, itype.name) check(implementation.name == ghost.name, implementation.node, ghost.name, itype.name) check( len(implementation.args) == len(ghost.args), implementation.node, ghost.name, itype.name) check(implementation.type == ghost.type, implementation.node, ghost.name, itype.name) if len(ghost_function_implementations) > 0: raise InvalidProgramException( first(ghost_function_implementations.values()).node, 'invalid.ghost.implemented', f'This contract implements some ghost functions that have no declaration in ' f'any of the implemented interfaces.\n' f'(Ghost functions without declaration: {list(ghost_function_implementations)})' )
def visit_AnnAssign(self, node: ast.AnnAssign): if node.is_ghost_code: raise InvalidProgramException(node, 'invalid.ghost.code') # No local specs are allowed before contract state variables self._check_no_local_spec() variable_name = node.target.id if variable_name == names.IMPLEMENTS: interface_name = node.annotation.id if interface_name not in [interfaces.ERC20, interfaces.ERC721 ] or interface_name in self.interfaces: interface_type = InterfaceType(interface_name) self.implements.append(interface_type) elif interface_name in [interfaces.ERC20, interfaces.ERC721]: interface_type = InterfaceType(interface_name) self.additional_implements.append(interface_type) # We ignore the units declarations elif variable_name != names.UNITS: variable_type = self.type_builder.build(node.annotation) if isinstance(variable_type, EventType): event = VyperEvent(variable_name, variable_type) self.events[variable_name] = event else: self.field_types[variable_name] = variable_type
def funcdef(self, children, meta): decorators = children[0] name = str(children[1]) args = children[2] if len(children) == 3: # This is a function stub without a return value if decorators: raise InvalidProgramException(decorators[0], 'invalid.function.stub') return ast.FunctionStub(name, args, None) elif len(children) == 4: if isinstance(children[3], list): # This is a function definition without a return value body = children[3] return ast.FunctionDef(name, args, body, decorators, None) else: # This is a function stub with a return value ret = children[3].children ret = ret[0] if len(ret) == 1 else self._tuple(ret, meta) return ast.FunctionStub(name, args, ret) elif len(children) == 5: # This is a function definition with a return value ret = children[3].children ret = ret[0] if len(ret) == 1 else self._tuple(ret, meta) body = children[4] return ast.FunctionDef(name, args, body, decorators, ret)
def check_success_args(arg: ast.Node): if isinstance(arg, ast.Name): _assert(arg.id in names.SUCCESS_CONDITIONS, arg, 'spec.success') elif isinstance(arg, ast.BoolOp) and arg.op == ast.BoolOperator.OR: check_success_args(arg.left) check_success_args(arg.right) else: raise InvalidProgramException(arg, 'spec.success')
def visit_ContractDef(self, node: ast.ContractDef): if node.is_ghost_code: raise InvalidProgramException(node, 'invalid.ghost.code') vyper_type = self.type_builder.build(node) if isinstance(vyper_type, ContractType): contract = VyperContract(node.name, vyper_type, node) self.contracts[contract.name] = contract
def float(self, children, meta): assert 'e' not in children[0] numd = 10 first, second = children[0].split('.') if len(second) > numd: node = self.number(children, meta) raise InvalidProgramException(node, 'invalid.decimal.literal') int_value = int(first) * 10**numd + int(second.ljust(numd, '0')) return ast.Num(Decimal[numd](scaled_value=int_value))
def visit_If(self, node: ast.If): # This is a preserves clause, since we replace all preserves clauses with if statements # when preprocessing if self.is_preserves: raise InvalidProgramException(node, 'preserves.in.preserves') self.is_preserves = True for stmt in node.body: self.visit(stmt) self.is_preserves = False
def _check_no_ghost_function(self): if self.ghost_functions: cond = "Ghost function declaration" node = first(self.ghost_functions.values()).node elif self.ghost_function_implementations: cond = "Ghost function definition" node = first(self.ghost_function_implementations.values()).node else: return raise InvalidProgramException( node, 'invalid.ghost', f'{cond} only allowed after "#@ interface"')
def decorator(self, children, meta): name = str(children[0]) if len(children) == 1: args = [] else: args, kwargs = children[1] if kwargs: raise InvalidProgramException(kwargs[0], 'invalid.decorator') return ast.Decorator(name, args)
def arguments(self, children, meta): args = [] kwargs = [] for c in children: if isinstance(c, ast.Keyword): kwargs.append(c) else: # kwargs cannot come before normal args if kwargs: raise InvalidProgramException(kwargs[0], 'invalid.kwargs') args.append(c) return args, kwargs
def visit_Assign(self, node: ast.Assign): if node.is_ghost_code: if isinstance(node.target, ast.Name) and node.target.id == names.INVARIANT: if self._last_loop and node in self._possible_loop_invariant_nodes: self.loop_invariants.setdefault(self._last_loop, []).append(node.value) else: raise InvalidProgramException( node, 'invalid.loop.invariant', 'You may only write loop invariants at beginning in loops' ) return None return node
def visit_FunctionStub(self, node: ast.FunctionStub): # A function stub on the top-level is a resource declaration self._check_no_local_spec() name, is_derived = Resource.get_name_and_derived_flag(node) if name in self.resources or name in names.SPECIAL_RESOURCES: raise InvalidProgramException(node, 'duplicate.resource') vyper_type = self.type_builder.build(node) if isinstance(vyper_type, ResourceType): if is_derived: resource = Resource(vyper_type, node, self.path, node.returns) else: resource = Resource(vyper_type, node, self.path) self.resources[name] = resource
def funccall(self, children, meta): func = children[0] args, kwargs = children[1] if isinstance(func, ast.Name): return ast.FunctionCall(func.id, args, kwargs) elif isinstance(func, ast.Subscript) and isinstance( func.value, ast.Name): return ast.FunctionCall(func.value.id, args, kwargs, func.index) elif isinstance(func, ast.Attribute): return ast.ReceiverCall(func.attr, func.value, args, kwargs) elif isinstance(func, ast.Subscript) and isinstance( func.value, ast.Attribute): return ast.ReceiverCall(func.value.attr, func, args, kwargs) else: raise InvalidProgramException(func, 'invalid.receiver')
def visit_ImportFrom(self, node: ast.ImportFrom): if node.is_ghost_code: raise InvalidProgramException(node, 'invalid.ghost.code') module = node.module or '' components = module.split('.') if not self.parse_further_interfaces: return if components == interfaces.VYPER_INTERFACES: for alias in node.names: name = alias.name if name == interfaces.ERC20: self.contracts[name] = VyperContract( name, interfaces.ERC20_TYPE, None) elif name == interfaces.ERC721: self.contracts[name] = VyperContract( name, interfaces.ERC721_TYPE, None) else: assert False return if node.level == 0: path = os.path.join(self.root or '', *components) else: path = self.path for _ in range(node.level): path = os.path.dirname(path) path = os.path.join(path, *components) files = {} for alias in node.names: name = alias.name interface_path = os.path.join(path, f'{name}.vy') files[interface_path] = name for file, name in files.items(): if name in [ names.SELF, names.LOG, names.LEMMA, *names.ENV_VARIABLES ]: raise UnsupportedException(node, 'Invalid file name.') interface = parse(file, self.root, True, name, self.parse_level + 1) self.interfaces[name] = interface
def _check_no_local_spec(self): """ Checks that there are no specifications for functions pending, i.e. there are no local specifications followed by either global specifications or eof. """ if self.postconditions: cond = "Postcondition" node = self.postconditions[0] elif self.checks: cond = "Check" node = self.checks[0] elif self.performs: cond = "Performs" node = self.performs[0] else: return raise InvalidProgramException(node, 'local.spec', f"{cond} only allowed before function")
def visit_FunctionDef(self, node: ast.FunctionDef): args = {arg.name: self._arg(arg) for arg in node.args} defaults = {arg.name: arg.default for arg in node.args} arg_types = [arg.type for arg in args.values()] return_type = None if node.returns is None else self.type_builder.build( node.returns) vyper_type = FunctionType(arg_types, return_type) decs = node.decorators loop_invariant_transformer = LoopInvariantTransformer() loop_invariant_transformer.visit(node) function = VyperFunction(node.name, self.function_counter, args, defaults, vyper_type, self.postconditions, self.preconditions, self.checks, loop_invariant_transformer.loop_invariants, self.performs, decs, node) if node.is_lemma: if node.decorators: decorator = node.decorators[0] if len(node.decorators ) > 1 or decorator.name != names.INTERPRETED_DECORATOR: raise InvalidProgramException( decorator, 'invalid.lemma', f'A lemma can have only one decorator: {names.INTERPRETED_DECORATOR}' ) if not decorator.is_ghost_code: raise InvalidProgramException( decorator, 'invalid.lemma', 'The decorator of a lemma must be ghost code') if vyper_type.return_type is not None: raise InvalidProgramException( node, 'invalid.lemma', 'A lemma cannot have a return type') if node.name in self.lemmas: raise InvalidProgramException(node, 'duplicate.lemma') if self.is_interface: raise InvalidProgramException( node, 'invalid.lemma', 'Lemmas are not allowed in interfaces') self.lemmas[node.name] = function else: for decorator in node.decorators: if decorator.is_ghost_code and decorator.name != names.PURE: raise InvalidProgramException(decorator, 'invalid.ghost.code') self.functions[node.name] = function self.function_counter += 1 # Reset local specs self.postconditions = [] self.preconditions = [] self.checks = [] self.performs = []
def visit_Ghost(self, node: ast.Ghost): for func in node.body: def check_ghost(cond): if not cond: raise InvalidProgramException(func, 'invalid.ghost') check_ghost(isinstance(func, ast.FunctionDef)) assert isinstance(func, ast.FunctionDef) check_ghost(len(func.body) == 1) func_body = func.body[0] check_ghost(isinstance(func_body, ast.ExprStmt)) assert isinstance(func_body, ast.ExprStmt) check_ghost(func.returns) decorators = [dec.name for dec in func.decorators] check_ghost(len(decorators) == len(func.decorators)) name = func.name args = {arg.name: self._arg(arg) for arg in func.args} arg_types = [arg.type for arg in args.values()] return_type = None if func.returns is None else self.type_builder.build( func.returns) vyper_type = FunctionType(arg_types, return_type) if names.IMPLEMENTS in decorators: check_ghost(not self.is_interface) check_ghost(len(decorators) == 1) ghost_functions = self.ghost_function_implementations else: check_ghost(self.is_interface) check_ghost(not decorators) check_ghost(isinstance(func_body.value, ast.Ellipsis)) ghost_functions = self.ghost_functions if name in ghost_functions: raise InvalidProgramException(func, 'duplicate.ghost') ghost_functions[name] = GhostFunction(name, args, vyper_type, func, self.path)
def _visit_FunctionCall(self, node: ast.FunctionCall) -> VyperType: # We allow # - public, indexed: not important for verification # - map: map type # - event: event type # Not allowed is # - constant: should already be replaced # Anything else is treated as a unit if node.name == names.PUBLICFIELD or node.name == names.INDEXED: return self.visit(node.args[0]) elif node.name == names.MAP: key_type = self.visit(node.args[0]) value_type = self.visit(node.args[1]) return MapType(key_type, value_type) elif node.name == names.EVENT: dict_literal = node.args[0] arg_types = [self.visit(arg) for arg in dict_literal.values] return EventType(arg_types) else: type = self.type_map.get(node.name) or TYPES.get(node.name) if type is None: raise InvalidProgramException(node, 'invalid.type') return type
def visit_Import(self, node: ast.Import): if node.is_ghost_code: raise InvalidProgramException(node, 'invalid.ghost.code') if not self.parse_further_interfaces: return files = {} for alias in node.names: components = alias.name.split('.') components[-1] = f'{components[-1]}.vy' path = os.path.join(self.root or '', *components) files[path] = alias.asname.value if hasattr( alias.asname, 'value') else alias.asname for file, name in files.items(): if name in [ names.SELF, names.LOG, names.LEMMA, *names.ENV_VARIABLES ]: raise UnsupportedException(node, 'Invalid file name.') interface = parse(file, self.root, True, name, self.parse_level + 1) self.interfaces[name] = interface
def check_ghost(cond): if not cond: raise InvalidProgramException(func, 'invalid.ghost')
def _assert(cond: bool, node: ast.Node, error_code: str, msg: Optional[str] = None): if not cond: raise InvalidProgramException(node, error_code, msg)
def check(self, program: VyperProgram): if program.resources and not program.config.has_option(names.CONFIG_ALLOCATION): msg = "Resources require allocation config option." raise InvalidProgramException(first(program.node.stmts) or program.node, 'alloc.not.alloc', msg) seen_functions = set() for implements in program.real_implements: interface = program.interfaces.get(implements.name) if interface is not None: function_names = set(function.name for function in interface.functions.values()) else: contract = program.contracts[implements.name] function_names = set(contract.type.function_types) _assert(not seen_functions & function_names, program.node, 'invalid.implemented.interfaces', f'Implemented interfaces should not have a function that shares the name with another function of ' f'another implemented interface.\n' f'(Conflicting functions: {seen_functions & function_names})') seen_functions.update(function_names) for function in program.functions.values(): self.visit(function.node, _Context.CODE, program, function) if function.is_pure(): self._function_pure_checker.check_function(function, program) for postcondition in function.postconditions: self.visit(postcondition, _Context.POSTCONDITION, program, function) if function.preconditions: _assert(not function.is_public(), function.preconditions[0], 'invalid.preconditions', 'Public functions are not allowed to have preconditions.') for precondition in function.preconditions: self.visit(precondition, _Context.PRECONDITION, program, function) if function.checks: _assert(not program.is_interface(), function.checks[0], 'invalid.checks', 'No checks are allowed in interfaces.') for check in function.checks: self.visit(check, _Context.CHECK, program, function) if function.performs: _assert(function.name != names.INIT, function.performs[0], 'invalid.performs', '__init__ does not require and must not have performs clauses.') _assert(function.is_public(), function.performs[0], 'invalid.performs', 'Private functions are not allowed to have performs clauses.') for performs in function.performs: self._visit_performs(performs, program, function) for lemma in program.lemmas.values(): for default_val in lemma.defaults.values(): _assert(default_val is None, lemma.node, 'invalid.lemma') _assert(not lemma.postconditions, first(lemma.postconditions), 'invalid.lemma', 'No postconditions are allowed for lemmas.') _assert(not lemma.checks, first(lemma.checks), 'invalid.lemma', 'No checks are allowed for lemmas.') _assert(not lemma.performs, first(lemma.performs), 'invalid.lemma') self.visit(lemma.node, _Context.LEMMA, program, lemma) for stmt in lemma.node.body: _assert(isinstance(stmt, ast.ExprStmt), stmt, 'invalid.lemma', 'All steps of the lemma should be expressions') for precondition in lemma.preconditions: self.visit(precondition, _Context.LEMMA, program, lemma) for invariant in program.invariants: self.visit(invariant, _Context.INVARIANT, program, None) if program.general_checks: _assert(not program.is_interface(), program.general_checks[0], 'invalid.checks', 'No checks are allowed in interfaces.') for check in program.general_checks: self.visit(check, _Context.CHECK, program, None) for postcondition in program.general_postconditions: self.visit(postcondition, _Context.POSTCONDITION, program, None) if program.transitive_postconditions: _assert(not program.is_interface(), program.transitive_postconditions[0], 'invalid.transitive.postconditions', 'No transitive postconditions are allowed in interfaces') for postcondition in program.transitive_postconditions: self.visit(postcondition, _Context.TRANSITIVE_POSTCONDITION, program, None) for ghost_function in program.ghost_function_implementations.values(): self.visit(ghost_function.node, _Context.GHOST_FUNCTION, program, None) if isinstance(program, VyperInterface): for caller_private in program.caller_private: self._visited_caller_spec = False self._num_visited_conditional = 0 self.visit(caller_private, _Context.CALLER_PRIVATE, program, None) _assert(self._visited_caller_spec, caller_private, 'invalid.caller.private', 'A caller private expression must contain "caller()"') _assert(self._num_visited_conditional <= 1, caller_private, 'invalid.caller.private', 'A caller private expression can only contain at most one "conditional(...)"')
def _visit_Name(self, node: ast.Name) -> VyperType: type = self.type_map.get(node.id) or TYPES.get(node.id) if type is None: raise InvalidProgramException(node, 'invalid.type') return type
def generic_visit(self, node): raise InvalidProgramException(node, 'invalid.type')
def _check_no_lemmas(self): if self.lemmas: raise InvalidProgramException( first(self.lemmas.values()).node, 'invalid.lemma', 'Lemmas are not allowed in interfaces')
def _check_resources(program: VyperProgram): if not isinstance(program, VyperInterface): node = first(program.node.stmts) or program.node for interface in program.interfaces.values(): for resource_name, resources_list in interface.resources.items(): for resource in resources_list: if resource.file is None: if program.resources.get(resource_name) is None: if not program.config.has_option( names.CONFIG_ALLOCATION): raise InvalidProgramException( node, 'alloc.not.alloc', f'The interface "{interface.name}" uses the ' f'allocation config option. Therefore, this contract ' f'also has to enable this config option.') raise InvalidProgramException( node, 'missing.resource', f'The interface "{interface.name}" ' f'needs a default resource "{resource_name}" ' f'that is not present in this contract.') continue imported_resources = [ r for r in program.resources.get(resource_name, []) if r.file == resource.file ] if not imported_resources: prefix_length = len( os.path.commonprefix([resource.file, program.file])) raise InvalidProgramException( node, 'missing.resource', f'The interface "{interface.name}" ' f'needs a resource "{resource_name}" from ' f'".{os.path.sep}{resource.file[prefix_length:]}" but it ' f'was not imported for this contract.') imported_resources = [ r for r in program.resources.get(resource_name) if r.interface == resource.interface ] for imported_resource in imported_resources: if resource.file != imported_resource.file: prefix_length = len( os.path.commonprefix( [resource.file, imported_resource.file])) resource_file = resource.file[prefix_length:] imported_resource_file = imported_resource.file[ prefix_length:] raise InvalidProgramException( node, 'duplicate.resource', f'There are two versions of the resource ' f'"{resource_name}" defined in an interface ' f'"{imported_resource.interface}" one from ' f'[...]"{imported_resource_file}" the other from ' f'[...]"{resource_file}".') for interface_type in program.implements: interface = program.interfaces[interface_type.name] for resource_name, resource in program.declared_resources.items(): if resource_name in interface.own_resources: raise InvalidProgramException( resource.node, 'duplicate.resource', f'A contract cannot redeclare a resource it already imports. ' f'The resource "{resource_name}" got already declared in the ' f'interface {interface.name}.')
def check(cond, node, ghost_name, interface_name): if not cond: raise InvalidProgramException( node, 'ghost.not.implemented', f'The ghost function "{ghost_name}" from the interface "{interface_name}" ' f'has not been implemented correctly.')
def generic_visit(self, node: ast.Node, *args): raise InvalidProgramException(node, 'invalid.spec')
def visit_Assign(self, node: ast.Assign): # This is for invariants and postconditions which get translated to # assignments during preprocessing. name = node.target.id if name != names.GENERAL_POSTCONDITION and self.is_preserves: msg = f"Only general postconditions are allowed in preserves. ({name} is not allowed)" raise InvalidProgramException(node, 'invalid.preserves', msg) with switch(name) as case: if case(names.CONFIG): if isinstance(node.value, ast.Name): options = [node.value.id] elif isinstance(node.value, ast.Tuple): options = [n.id for n in node.value.elements] for option in options: if option not in names.CONFIG_OPTIONS: msg = f"Option {option} is invalid." raise InvalidProgramException(node, 'invalid.config.option', msg) if self.config is not None: raise InvalidProgramException( node, 'invalid.config', 'The "config" is specified multiple times.') self.config = Config(options) elif case(names.INTERFACE): self._check_no_local_spec() self._check_no_ghost_function() self._check_no_lemmas() self.is_interface = True elif case(names.INVARIANT): # No local specifications allowed before invariants self._check_no_local_spec() self.local_state_invariants.append(node.value) elif case(names.INTER_CONTRACT_INVARIANTS): # No local specifications allowed before inter contract invariants self._check_no_local_spec() self.inter_contract_invariants.append(node.value) elif case(names.GENERAL_POSTCONDITION): # No local specifications allowed before general postconditions self._check_no_local_spec() if self.is_preserves: self.transitive_postconditions.append(node.value) else: self.general_postconditions.append(node.value) elif case(names.GENERAL_CHECK): # No local specifications allowed before general check self._check_no_local_spec() self.general_checks.append(node.value) elif case(names.POSTCONDITION): self.postconditions.append(node.value) elif case(names.PRECONDITION): self.preconditions.append(node.value) elif case(names.CHECK): self.checks.append(node.value) elif case(names.CALLER_PRIVATE): # No local specifications allowed before caller private self._check_no_local_spec() self.caller_private.append(node.value) elif case(names.PERFORMS): self.performs.append(node.value) else: assert False