All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.alephium.ralph.StaticAnalysis.scala Maven / Gradle / Ivy

// Copyright 2018 The Alephium Authors
// This file is part of the alephium project.
//
// The library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the library. If not, see .

package org.alephium.ralph

import scala.collection.mutable

import org.alephium.protocol.vm
import org.alephium.ralph.Ast._
import org.alephium.util.AVector

object StaticAnalysis {
  @SuppressWarnings(Array("org.wartremover.warts.IsInstanceOf"))
  def checkMethodsStateless[Ctx <: vm.StatelessContext](
      ast: Ast.ContractT[Ctx],
      methods: AVector[vm.Method[Ctx]],
      state: Compiler.State[Ctx]
  ): Unit = {
    assume(ast.funcs.length == methods.length)
    checkIfPrivateMethodsUsed(ast, state)
    ast.funcs.zip(methods.toIterable).foreach { case (func, method) =>
      // skip check update fields for main function
      if (!(ast.isInstanceOf[Ast.TxScript] && (func.name == "main"))) {
        checkUpdateFields(state, func, method)
      }
    }
  }

  def checkMethodsStateful(
      ast: Ast.ContractWithState,
      methods: AVector[vm.Method[vm.StatefulContext]],
      state: Compiler.State[vm.StatefulContext]
  ): Unit = {
    checkMethodsStateless(ast, methods, state)
    ast.funcs.zip(methods.toIterable).foreach { case (func, method) =>
      checkCodeUsingContractAssets(ast.ident, func, method)
      checkCodeUsingPayToContract(ast.ident, func, method)
      checkCodeUsingAssets(ast.ident, func, method)
    }
  }

  def checkMethods(
      ast: Ast.Contract,
      code: vm.StatefulContract,
      state: Compiler.State[vm.StatefulContext]
  ): Unit = {
    checkMethodsStateful(ast, code.methods, state)
  }

  def checkIfPrivateMethodsUsed[Ctx <: vm.StatelessContext](
      ast: Ast.ContractT[Ctx],
      state: Compiler.State[Ctx]
  ): Unit = {
    val unusedFuncs = ast.funcs.filter { func =>
      func.isPrivate &&
      func.definedIn(ast.ident) &&
      !state.internalCallsReversed.get(func.id).exists(_.nonEmpty)
    }
    if (unusedFuncs.nonEmpty) {
      state.warnUnusedPrivateFunction(ast.ident, unusedFuncs.map(_.name))
    }
  }

  private[ralph] lazy val contractAssetsInstrs: Set[vm.Instr[_]] =
    Set(
      vm.TransferAlphFromSelf,
      vm.TransferTokenFromSelf,
      vm.TransferAlphToSelf,
      vm.TransferTokenToSelf,
      vm.DestroySelf,
      vm.SelfAddress
    )
  private lazy val payToContractInstrs: Set[vm.Instr[_]] =
    Set(vm.TransferAlphToSelf, vm.TransferTokenToSelf, vm.SelfAddress)
  private lazy val spendContractAssetsInstrs: Set[vm.Instr[_]] =
    Set(vm.TransferAlphFromSelf, vm.TransferTokenFromSelf, vm.DestroySelf)
  private lazy val payToContractInstrsExceptSelfAddress: Set[vm.Instr[_]] =
    Set(vm.TransferAlphToSelf, vm.TransferTokenToSelf)

  def checkCodeUsingContractAssets(
      contractId: Ast.TypeId,
      func: Ast.FuncDef[vm.StatefulContext],
      method: vm.Method[vm.StatefulContext]
  ): Unit = {
    if (
      func.useAssetsInContract == Ast.UseContractAssets &&
      !method.instrs.exists(contractAssetsInstrs.contains(_))
    ) {
      throw Compiler.Error(
        s"Function ${Ast.funcName(contractId, func.id)} does not use contract assets, but the annotation `assetsInContract` is enabled. " +
          "Please remove the `assetsInContract` annotation or set it to `enforced`",
        func.sourceIndex
      )
    }

    if (
      func.useAssetsInContract == Ast.NotUseContractAssets &&
      method.instrs.exists(spendContractAssetsInstrs.contains)
    ) {
      throw Compiler.Error(
        s"Function ${Ast.funcName(contractId, func.id)} uses contract assets, please use annotation `assetsInContract = true`.",
        func.sourceIndex
      )
    }
  }

  private def checkCodeUsingPayToContract(
      contractId: Ast.TypeId,
      func: Ast.FuncDef[vm.StatefulContext],
      method: vm.Method[vm.StatefulContext]
  ): Unit = {
    if (func.usePayToContractOnly && !method.instrs.exists(payToContractInstrs.contains)) {
      throw Compiler.Error(
        s"Function ${Ast.funcName(contractId, func.id)} does not pay to the contract, but the annotation `payToContractOnly` is enabled.",
        func.sourceIndex
      )
    }

    val hasPayToContractInstr = method.instrs.exists(payToContractInstrsExceptSelfAddress.contains)
    val isUseContractAssets =
      func.usePayToContractOnly || func.useAssetsInContract != Ast.NotUseContractAssets
    if (!isUseContractAssets && hasPayToContractInstr) {
      throw Compiler.Error(
        s"Function ${Ast.funcName(contractId, func.id)} transfers assets to the contract, please set either `assetsInContract` or `payToContractOnly` to true.",
        func.sourceIndex
      )
    }

    if (isUseContractAssets && hasPayToContractInstr && !func.usePreapprovedAssets) {
      throw Compiler.Error(
        s"Function ${Ast.funcName(contractId, func.id)} transfers assets to the contract, please use annotation `preapprovedAssets = true`.",
        func.sourceIndex
      )
    }
  }

  private lazy val useAssetsInstrs: Set[vm.Instr[_]] =
    Set(vm.ApproveAlph, vm.ApproveToken, vm.TransferAlph, vm.TransferToken, vm.BurnToken)

  private def checkCodeUsingAssets(
      contractId: Ast.TypeId,
      func: Ast.FuncDef[vm.StatefulContext],
      method: vm.Method[vm.StatefulContext]
  ): Unit = {
    val isNotUseAssets =
      !func.usePreapprovedAssets &&
        func.useAssetsInContract == Ast.NotUseContractAssets &&
        !func.usePayToContractOnly
    if (isNotUseAssets && method.instrs.exists(useAssetsInstrs.contains)) {
      throw Compiler.Error(
        s"Function ${Ast.funcName(contractId, func.id)} uses assets, please use annotation `preapprovedAssets = true` or `assetsInContract = true`",
        func.sourceIndex
      )
    }
  }

  def checkUpdateFields[Ctx <: vm.StatelessContext](
      state: Compiler.State[Ctx],
      func: FuncDef[Ctx],
      method: vm.Method[Ctx]
  ): Unit = {
    val updateFields = method.instrs.exists {
      case _: vm.StoreMutField | _: vm.StoreMutFieldByIndex.type | _: vm.MigrateWithFields.type =>
        true
      case _ => false
    }

    if (updateFields && !func.useUpdateFields) {
      state.warnNoUpdateFieldsCheck(state.typeId, func.id)
    }

    if (!updateFields && func.useUpdateFields) {
      state.warnUnnecessaryUpdateFieldsCheck(state.typeId, func.id)
    }
  }

  private[ralph] def checkExternalCallPermissions(
      nonSimpleViewFuncSet: mutable.Set[(TypeId, FuncId)],
      contractState: Compiler.State[vm.StatefulContext],
      contract: Contract,
      checkExternalCallerTables: mutable.Map[TypeId, mutable.Map[FuncId, Boolean]]
  ): Unit = {
    val allNoCheckExternalCallers: mutable.Set[(TypeId, FuncId)] = mutable.Set.empty
    val table = checkExternalCallerTables(contract.ident)
    contract.funcs.foreach { func =>
      if (
        func.isPublic &&
        !table(func.id) &&
        nonSimpleViewFuncSet.contains(contract.ident -> func.id)
      ) {
        allNoCheckExternalCallers.addOne(contract.ident -> func.id)
      }
    }
    allNoCheckExternalCallers.foreach { case (typeId, funcId) =>
      contractState.warnCheckExternalCaller(typeId, funcId)
    }
  }

  def buildNonSimpleViewFuncSet(
      multiContract: MultiContract,
      states: AVector[Compiler.State[vm.StatefulContext]]
  ): mutable.Set[(TypeId, FuncId)] = {
    val nonSimpleViewFuncSet = mutable.Set.empty[(TypeId, FuncId)]
    multiContract.contracts.zipWithIndex.foreach {
      case (contract: Contract, index) =>
        val state = states(index)
        contract.funcs.foreach { func =>
          val key = contract.ident -> func.id
          if (!func.isSimpleViewFunc(state)) {
            nonSimpleViewFuncSet.addOne(key)
          }
        }
      case _ => ()
    }
    updateNonSimpleViewFuncSet(nonSimpleViewFuncSet, multiContract, states)
    nonSimpleViewFuncSet
  }

  // Optimize: we will need to introduce `MultiState` later
  @SuppressWarnings(Array("org.wartremover.warts.Recursion"))
  def updateNonSimpleViewFuncSet(
      nonSimpleViewFuncSet: mutable.Set[(TypeId, FuncId)],
      multiContract: MultiContract,
      states: AVector[Compiler.State[vm.StatefulContext]]
  ): Unit = {
    val startSize = nonSimpleViewFuncSet.size
    multiContract.contracts.zipWithIndex.foreach {
      case (contract: Contract, index) =>
        val state = states(index)
        state.internalCalls.foreach { case (caller, callees) =>
          val key = contract.ident -> caller
          if (
            !nonSimpleViewFuncSet.contains(key) &&
            callees.exists(callee => nonSimpleViewFuncSet.contains(contract.ident -> callee))
          ) {
            nonSimpleViewFuncSet.addOne(key)
          }
        }
        state.externalCalls.foreach { case (caller, callees) =>
          val key = contract.ident -> caller
          if (
            !nonSimpleViewFuncSet.contains(key) &&
            callees.exists(callee => nonSimpleViewFuncSet.contains(callee))
          ) {
            nonSimpleViewFuncSet.addOne(key)
          }
        }
      case _ => ()
    }
    val endSize = nonSimpleViewFuncSet.size
    if (endSize > startSize) {
      updateNonSimpleViewFuncSet(nonSimpleViewFuncSet, multiContract, states)
    }
  }

  def checkExternalCalls(
      multiContract: MultiContract,
      states: AVector[Compiler.State[vm.StatefulContext]]
  ): Unit = {
    val checkExternalCallerTables = mutable.Map.empty[TypeId, mutable.Map[FuncId, Boolean]]
    multiContract.contracts.zipWithIndex.foreach {
      case (contract: Contract, index) if !contract.isAbstract =>
        val state = states(index)
        val table = contract.buildCheckExternalCallerTable(state)
        checkExternalCallerTables.update(contract.ident, table)
      case (interface: ContractInterface, _) =>
        val table = mutable.Map.from(interface.funcs.map(_.id -> true))
        checkExternalCallerTables.update(interface.ident, table)
      case _ => ()
    }
    val nonSimpleViewFuncSet = buildNonSimpleViewFuncSet(multiContract, states)
    multiContract.contracts.zipWithIndex.foreach {
      case (contract: Contract, index) if !contract.isAbstract =>
        val state = states(index)
        checkExternalCallPermissions(
          nonSimpleViewFuncSet,
          state,
          contract,
          checkExternalCallerTables
        )
      case _ =>
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy