scalus icon indicating copy to clipboard operation
scalus copied to clipboard

Support of object private methods called from overrided inline functions.

Open rssh opened this issue 2 months ago • 1 comments

When private method is called from overrided inline function (like spend), then compiler gednerate calls to bridge method MyObject$inline$method inside inline$method. Need to handle this during code generation..

rssh avatar Oct 15 '25 19:10 rssh

Just a sample repro:

object Vault extends Validator {

    inline override def spend(
        d: prelude.Option[Data],
        redeemer: Data,
        tx: TxInfo,
        ownRef: TxOutRef
    ): Unit = {
        val datum = d.get.to[Datum]
        redeemer.to[Redeemer] match {
            case Deposit            => deposit(tx, ownRef, datum)
            case InitiateWithdrawal => initiateWithdrawal(tx, ownRef, datum)
            case FinalizeWithdrawal => finalize(tx, ownRef, datum)
            case Cancel             => cancel(tx, ownRef, datum)
        }
    }

    private def deposit(tx: TxInfo, ownRef: TxOutRef, datum: Datum) = {
        val ownInput = tx.findOwnInput(ownRef).getOrFail(OwnInputNotFound)

        val out = getVaultOutput(tx, ownRef)
        requireSameOwner(out, datum)
        requireOutputToOwnAddress(ownInput, out, WrongDepositDestination)

        val value = out.value
        require(value.withoutLovelace.isZero, CannotAddTokens)

        require(value.getLovelace > ownInput.resolved.value.getLovelace, AdaNotConserved)
        requireEntireVaultIsSpent(datum, ownInput.resolved)
        val newDatum = getVaultDatum(out)
        require(newDatum.amount == value.getLovelace, VaultAmountChanged)
        require(newDatum.waitTime == datum.waitTime, WaitTimeChanged)
        require(
          newDatum.finalizationDeadline == datum.finalizationDeadline,
          FinalizationDeadlineChanged
        )
    }

    private def initiateWithdrawal(tx: TxInfo, ownRef: TxOutRef, datum: Datum) = {
        require(
          datum.state.isIdle,
          WithdrawalAlreadyPending
        )
        val ownInput = tx.findOwnInput(ownRef).getOrFail(OwnInputNotFound)
        val out = getVaultOutput(tx, ownRef)
        requireSameOwner(out, datum)
        requireOutputToOwnAddress(
          ownInput,
          out,
          NotExactlyOneVaultOutput
        )

        val requestTime = tx.validRange.getEarliestTime
        val finalizationDeadline = requestTime + datum.waitTime
        val newDatum = getVaultDatum(out)
        require(newDatum.state.isPending, MustBePending)
        require(
          newDatum.finalizationDeadline == finalizationDeadline,
          IncorrectDatumFinalization
        )
    }

    private def finalize(tx: TxInfo, ownRef: TxOutRef, datum: Datum) = {
        require(datum.state.isPending, ContractMustBePending)
        require(tx.validRange.isAfter(datum.finalizationDeadline), DeadlineNotPassed)
        val ownInput = tx.findOwnInput(ownRef).getOrFail(OwnInputNotFound)
        requireEntireVaultIsSpent(datum, ownInput.resolved)

        val scriptOutputs = tx.outputs.filter(out =>
            out.address.credential === ownInput.resolved.address.credential
        )
        require(scriptOutputs.size == BigInt(0), WithdrawalsMustNotSendBackToVault)
        val ownerOutputs =
            tx.outputs.filter(out => addressEquals(out.address.credential, datum.owner))
        require(ownerOutputs.size > BigInt(0), WrongAddressWithdrawal)
        val totalToOwner =
            ownerOutputs.foldLeft(BigInt(0))((acc, out) => acc + out.value.getLovelace)
        require(totalToOwner == datum.amount, VaultAmountChanged)
    }

    private def cancel(tx: TxInfo, ownRef: TxOutRef, datum: Datum) = {
        val out = getVaultOutput(tx, ownRef)
        requireSameOwner(out, datum)
        val vaultDatum = getVaultDatum(out)
        require(vaultDatum.amount == datum.amount, VaultAmountChanged)
        require(
          out.value.getLovelace == datum.amount,
          WrongOutputAmount
        )
        require(vaultDatum.state.isIdle, StateNotIdle)
        require(vaultDatum.waitTime == datum.waitTime, WaitTimeChanged)
    }

    private def requireEntireVaultIsSpent(datum: Datum, output: TxOut) = {
        val amountToSpend = datum.amount
        val adaSpent = output.value.getLovelace
        require(amountToSpend == adaSpent, AdaLeftover)
    }

    private def requireOutputToOwnAddress(ownInput: TxInInfo, out: TxOut, message: String) =
        require(out.address.credential === ownInput.resolved.address.credential, message)

    private def getVaultOutput(tx: TxInfo, ownRef: TxOutRef): TxOut = {
        val ownInput = tx.findOwnInput(ownRef).getOrFail("Cannot find own input")
        val scriptOutputs = tx.outputs.filter(out =>
            out.address.credential === ownInput.resolved.address.credential
        )
        require(scriptOutputs.size == BigInt(1), NotExactlyOneVaultOutput)
        scriptOutputs.head
    }

    private def getVaultDatum(vaultOutput: TxOut) = vaultOutput.datum match {
        case ledger.api.v2.OutputDatum.OutputDatum(d) => d.to[Datum]
        case _                                        => fail(NoDatumProvided)
    }

    private def requireSameOwner(out: TxOut, datum: Datum) =
        out.datum match {
            case scalus.ledger.api.v2.OutputDatum.OutputDatum(newDatum) =>
                require(
                  newDatum.to[Datum].owner == datum.owner,
                  VaultOwnerChanged
                )
            case _ => fail(NoInlineDatum)
        }

    inline val NoDatumExists = "Contract has no datum"

    inline val NoDatumProvided = "Vault transactions must have an inline datum"

    inline val FinalizationDeadlineChanged =
        "Deposit transactions must not change the finalization deadline"

    inline val VaultAmountChanged = "Datum amount must match output lovelace amount"

    inline val CannotAddTokens = "Deposits must only contain ADA"

    inline val AdaNotConserved = "Deposits must add ADA to the vault"

    inline val WrongDepositDestination =
        "Deposit transactions can only be made to the vault"

    inline val NotExactlyOneVaultOutput =
        "Vault transaction must have exactly 1 output to the vault script"

    inline val OwnInputNotFound = "Own input not found"

    inline val IncorrectDatumFinalization =
        "Finalization deadline must be request time plus wait time"

    inline val MustBePending = "Output must have datum with State = Pending"

    inline val WithdrawalAlreadyPending =
        "Cannot withdraw, another withdrawal request is pending"

    inline val WrongAddressWithdrawal =
        "Withdrawal finalization must send funds to the vault owner"
    inline val WithdrawalsMustNotSendBackToVault =
        "Withdrawal finalization must not send funds back to the vault"
    inline val DeadlineNotPassed =
        "Finalization can only happen after the finalization deadline"
    private inline val ContractMustBePending = "Contract must be Pending"
    private inline val WrongOutputAmount = "Cancel transactions must not change the vault amount"
    private inline val WaitTimeChanged = "Wait time must remain the same"
    private inline val StateNotIdle = "Idle transactions must change the vault state to Idle"
    private inline val NoInlineDatum = "Vault transactions must have an inline datum"
    private inline val VaultOwnerChanged = "Vault transactions cannot change the vault owner"
    private inline val AdaLeftover = "Must spend entire vault"

    private def addressEquals(left: Credential, right: ByteString) = {
        left match {
            case v1.Credential.PubKeyCredential(hash) => hash.hash === right
            case v1.Credential.ScriptCredential(hash) => hash === right
        }
    }

    extension (s: State) {
        def isPending: Boolean = s match {
            case State.Idle    => false
            case State.Pending => true
        }

        def isIdle: Boolean = s match {
            case State.Idle    => true
            case State.Pending => false
        }
    }

    extension (i: Interval) {
        def getEarliestTime: PosixTime = i.from.boundType match {
            case IntervalBoundType.Finite(t) => t
            case _                           => BigInt(0)
        }

        def isAfter(timePoint: PosixTime): Boolean = i.from.boundType match {
            case IntervalBoundType.Finite(time) => timePoint < time
            case _                              => false
        }
    }
}

sae3023 avatar Oct 15 '25 19:10 sae3023