diff --git a/contracts/src/main/kotlin/contracts/Cash.kt b/contracts/src/main/kotlin/contracts/Cash.kt index f6d036d5a4..067cf7308a 100644 --- a/contracts/src/main/kotlin/contracts/Cash.kt +++ b/contracts/src/main/kotlin/contracts/Cash.kt @@ -93,32 +93,35 @@ class Cash : Contract { "there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L } } - if (verifyIssueCommands(inputs, outputs, tx, currency, issuer)) continue + val issueCommand = tx.commands.select().firstOrNull() + if (issueCommand != null) { + verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer) + } else { + val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group") + val outputAmount = outputs.sumCashOrZero(currency) - val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group") - val outputAmount = outputs.sumCashOrZero(currency) + // If we want to remove cash from the ledger, that must be signed for by the issuer. + // A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero. + val exitCommand = tx.commands.select(party = issuer).singleOrNull() + val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency) - // If we want to remove cash from the ledger, that must be signed for by the issuer. - // A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero. - val exitCommand = tx.commands.select(party = issuer).singleOrNull() - val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency) + requireThat { + "there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L } + "for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by + (inputAmount == outputAmount + amountExitingLedger) + } - requireThat { - "there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L } - "for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by - (inputAmount == outputAmount + amountExitingLedger) + verifyMoveCommands(inputs, tx) } - - verifyMoveCommands(inputs, tx) } } - private fun verifyIssueCommands(inputs: List, outputs: List, tx: TransactionForVerification, currency: Currency, issuer: Party): Boolean { - val issueCommand = tx.commands.select().singleOrNull() - if (issueCommand == null || outputs.isEmpty()) { - return false - } - + private fun verifyIssueCommand(inputs: List, + outputs: List, + tx: TransactionForVerification, + issueCommand: AuthenticatedObject, + currency: Currency, + issuer: Party) { // If we have an issue command, perform special processing: the group is allowed to have no inputs, // and the output states must have a deposit reference owned by the signer. // @@ -132,12 +135,13 @@ class Cash : Contract { // The grouping ensures that all outputs have the same deposit reference and currency. val inputAmount = inputs.sumCashOrZero(currency) val outputAmount = outputs.sumCash() + val cashCommands = tx.commands.select() requireThat { "the issue command has a nonce" by (issueCommand.value.nonce != 0L) "output deposits are owned by a command signer" by (issuer in issueCommand.signingParties) "output values sum to more than the inputs" by (outputAmount > inputAmount) + "there is only a single issue command" by (cashCommands.count() == 1) } - return true } /** diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index e1345dc9cd..29cac0a74e 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -143,6 +143,26 @@ class CashTests { arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } this `fails requirement` "output values sum to more than the inputs" } + + // Can't have any other commands if we have an issue command (because the issue command overrules them) + transaction { + input { inState } + output { inState.copy(amount = inState.amount * 2) } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } + tweak { + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } + this `fails requirement` "there is only a single issue command" + } + tweak { + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this `fails requirement` "there is only a single issue command" + } + tweak { + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) } + this `fails requirement` "there is only a single issue command" + } + this.accepts() + } } @Test