Skip to content

Commit 6834bf5

Browse files
chore: Enable Create and Create2 operations in hook executions and add more tests (#21906)
Signed-off-by: Neeharika-Sompalli <[email protected]>
1 parent a9240a4 commit 6834bf5

File tree

13 files changed

+632
-65
lines changed

13 files changed

+632
-65
lines changed

hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/operations/AbstractCustomCreateOperation.java

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
package com.hedera.node.app.service.contract.impl.exec.operations;
33

4+
import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.HtsSystemContract.HTS_HOOKS_CONTRACT_ADDRESS;
45
import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.ILLEGAL_STATE_CHANGE;
56
import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.INSUFFICIENT_GAS;
67
import static org.hyperledger.besu.evm.internal.Words.clampedToLong;
@@ -14,6 +15,7 @@
1415
import org.hyperledger.besu.datatypes.Address;
1516
import org.hyperledger.besu.datatypes.Wei;
1617
import org.hyperledger.besu.evm.EVM;
18+
import org.hyperledger.besu.evm.account.MutableAccount;
1719
import org.hyperledger.besu.evm.code.CodeFactory;
1820
import org.hyperledger.besu.evm.frame.ExceptionalHaltReason;
1921
import org.hyperledger.besu.evm.frame.MessageFrame;
@@ -91,9 +93,6 @@ protected AbstractCustomCreateOperation(
9193

9294
@Override
9395
public OperationResult execute(@NonNull final MessageFrame frame, @NonNull final EVM evm) {
94-
if (FrameUtils.isHookExecution(frame)) {
95-
return new OperationResult(0, ExceptionalHaltReason.INVALID_OPERATION);
96-
}
9796
if (!isEnabled(frame)) {
9897
return INVALID_RESPONSE;
9998
}
@@ -108,23 +107,26 @@ public OperationResult execute(@NonNull final MessageFrame frame, @NonNull final
108107
return new Operation.OperationResult(cost, INSUFFICIENT_GAS);
109108
}
110109
final var value = Wei.wrap(frame.getStackItem(0));
111-
final var account = frame.getWorldUpdater().getAccount(frame.getRecipientAddress());
110+
111+
final var senderAddress = getSenderAddress(frame);
112+
final var senderAccount = frame.getWorldUpdater().getAccount(senderAddress);
112113
frame.clearReturnData();
113-
if (value.compareTo(account.getBalance()) > 0 || frame.getDepth() >= MAX_STACK_DEPTH) {
114+
if (value.compareTo(senderAccount.getBalance()) > 0 || frame.getDepth() >= MAX_STACK_DEPTH) {
114115
fail(frame);
115116
} else {
116-
spawnChildMessage(frame);
117+
// since the sender address should be the hook owner for HTS hook executions,
118+
// we need to explicitly pass in the senderAddress and not use sender.getAddress()
119+
spawnChildMessage(frame, senderAccount, senderAddress);
117120
}
118121
return new Operation.OperationResult(cost, null);
119122
}
120123

121-
private void spawnChildMessage(@NonNull final MessageFrame frame) {
124+
private void spawnChildMessage(
125+
@NonNull final MessageFrame frame, final MutableAccount sender, @NonNull final Address senderAddress) {
122126
// Calculate memory cost prior to expansion
123127
final var cost = cost(frame);
124128
frame.decrementRemainingGas(cost);
125-
126-
final var account = frame.getWorldUpdater().getAccount(frame.getRecipientAddress());
127-
account.incrementNonce();
129+
sender.incrementNonce();
128130

129131
final var value = Wei.wrap(frame.getStackItem(0));
130132
final var inputOffset = clampedToLong(frame.getStackItem(1));
@@ -139,6 +141,7 @@ private void spawnChildMessage(@NonNull final MessageFrame frame) {
139141

140142
final var childGasStipend = gasCalculator().gasAvailableForChildCreate(frame.getRemainingGas());
141143
frame.decrementRemainingGas(childGasStipend);
144+
142145
// child frame is added to frame stack via build method
143146
MessageFrame.builder()
144147
.parentMessageFrame(frame)
@@ -147,7 +150,7 @@ private void spawnChildMessage(@NonNull final MessageFrame frame) {
147150
.address(contractAddress)
148151
.contract(contractAddress)
149152
.inputData(Bytes.EMPTY)
150-
.sender(frame.getRecipientAddress())
153+
.sender(senderAddress)
151154
.value(value)
152155
.apparentValue(value)
153156
.code(codeFactory.createCode(inputData, false))
@@ -184,4 +187,17 @@ private void complete(@NonNull final MessageFrame frame, @NonNull final MessageF
184187
final var currentPC = frame.getPC();
185188
frame.setPC(currentPC + 1);
186189
}
190+
191+
/**
192+
* Returns the sender address for the next frame.
193+
* This is the recipient address for normal contract creation, but the hook owner address for HTS hook executions.
194+
*
195+
* @param frame the frame
196+
* @return the sender address
197+
*/
198+
protected Address getSenderAddress(final @NonNull MessageFrame frame) {
199+
return frame.getRecipientAddress().equals(HTS_HOOKS_CONTRACT_ADDRESS)
200+
? FrameUtils.hookOwnerAddress(frame)
201+
: frame.getRecipientAddress();
202+
}
187203
}

hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/operations/CustomCreate2Operation.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ protected long cost(@NonNull final MessageFrame frame) {
7070
if (updater.isHollowAccount(alias) && !featureFlags.isImplicitCreationEnabled()) {
7171
return null;
7272
}
73-
updater.setupInternalAliasedCreate(frame.getRecipientAddress(), alias);
73+
final var sender = getSenderAddress(frame);
74+
updater.setupInternalAliasedCreate(sender, alias);
7475
frame.warmUpAddress(alias);
7576
return alias;
7677
}

hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/operations/CustomCreateOperation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ protected long cost(@NonNull final MessageFrame frame) {
5555
protected @NonNull Address setupPendingCreation(@NonNull final MessageFrame frame) {
5656
final var updater = (ProxyWorldUpdater) frame.getWorldUpdater();
5757

58-
final var origin = frame.getRecipientAddress();
58+
final var origin = getSenderAddress(frame);
5959
final var originNonce = requireNonNull(updater.getAccount(origin)).getNonce();
6060
// Decrement nonce by 1 to normalize the effect of transaction execution
6161
final var address = Address.contractAddress(origin, originNonce - 1);

hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip1195/Hip1195StreamParityTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
@SuppressWarnings({"rawtypes", "unchecked"})
6262
@Tag(ADHOC)
6363
public class Hip1195StreamParityTest {
64-
public static final String HOOK_CONTRACT = "0.0.365";
64+
public static final String HOOK_CONTRACT_NUM = "365";
6565

6666
private static final TupleType SET_AND_PASS_ARGS = TupleType.parse("(uint32,address)");
6767

@@ -260,16 +260,16 @@ final Stream<DynamicTest> hookExecutionsWithAutoCreations() {
260260
recordWith().status(SUCCESS).memo(AUTO_MEMO),
261261
recordWith()
262262
.status(SUCCESS)
263-
.contractCallResult(resultWith().contract(HOOK_CONTRACT)),
263+
.contractCallResult(resultWith().contract(HOOK_CONTRACT_NUM)),
264264
recordWith()
265265
.status(SUCCESS)
266-
.contractCallResult(resultWith().contract(HOOK_CONTRACT)),
266+
.contractCallResult(resultWith().contract(HOOK_CONTRACT_NUM)),
267267
recordWith()
268268
.status(SUCCESS)
269-
.contractCallResult(resultWith().contract(HOOK_CONTRACT)),
269+
.contractCallResult(resultWith().contract(HOOK_CONTRACT_NUM)),
270270
recordWith()
271271
.status(SUCCESS)
272-
.contractCallResult(resultWith().contract(HOOK_CONTRACT)))
272+
.contractCallResult(resultWith().contract(HOOK_CONTRACT_NUM)))
273273
.logged(),
274274
getAliasedAccountInfo("alias").has(accountWith().balance(10L)).hasToken(relationshipWith("tokenA")));
275275
}

hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/hip1195/Hip1195BasicTests.java

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import static com.hedera.services.bdd.suites.HapiSuite.ONE_MILLION_HBARS;
3636
import static com.hedera.services.bdd.suites.integration.hip1195.Hip1195EnabledTest.OWNER;
3737
import static com.hedera.services.bdd.suites.integration.hip1195.Hip1195EnabledTest.PAYER;
38-
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_EXECUTION_EXCEPTION;
3938
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED;
4039
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.HOOKS_EXECUTIONS_REQUIRE_TOP_LEVEL_CRYPTO_TRANSFER;
4140
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.HOOK_ID_IN_USE;
@@ -1046,36 +1045,6 @@ final Stream<DynamicTest> hooksExecutionsInScheduleNotAllowed() {
10461045
.payingWith(PAYER));
10471046
}
10481047

1049-
@HapiTest
1050-
final Stream<DynamicTest> createOpIsDisabledInHookExecution() {
1051-
return hapiTest(
1052-
cryptoCreate(OWNER).withHooks(accountAllowanceHook(123L, CREATE_HOOK.name())),
1053-
cryptoCreate(PAYER).receiverSigRequired(true),
1054-
cryptoTransfer(TokenMovement.movingHbar(10).between(OWNER, PAYER))
1055-
.withPreHookFor(OWNER, 123L, 25_000L, "")
1056-
.payingWith(PAYER)
1057-
.hasKnownStatus(REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK)
1058-
.via("failedTxn"),
1059-
getTxnRecord("failedTxn")
1060-
.andAllChildRecords()
1061-
.hasChildRecords(TransactionRecordAsserts.recordWith().status(CONTRACT_EXECUTION_EXCEPTION)));
1062-
}
1063-
1064-
@HapiTest
1065-
final Stream<DynamicTest> create2OpIsDisabledInHookExecution() {
1066-
return hapiTest(
1067-
cryptoCreate(OWNER).withHooks(accountAllowanceHook(123L, CREATE2_HOOK.name())),
1068-
cryptoCreate(PAYER).receiverSigRequired(true),
1069-
cryptoTransfer(TokenMovement.movingHbar(10).between(OWNER, PAYER))
1070-
.withPreHookFor(OWNER, 123L, 25_000L, "")
1071-
.payingWith(PAYER)
1072-
.hasKnownStatus(REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK)
1073-
.via("failedTxn"),
1074-
getTxnRecord("failedTxn")
1075-
.andAllChildRecords()
1076-
.hasChildRecords(TransactionRecordAsserts.recordWith().status(CONTRACT_EXECUTION_EXCEPTION)));
1077-
}
1078-
10791048
@HapiTest
10801049
final Stream<DynamicTest> selfDestructOpIsDisabledInHookExecution() {
10811050
return hapiTest(

hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/hip1195/Hip1195EnabledTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord;
1414
import static com.hedera.services.bdd.spec.transactions.TxnUtils.accountAllowanceHook;
1515
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.accountLambdaSStore;
16+
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCallWithFunctionAbi;
1617
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate;
1718
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer;
1819
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate;
@@ -28,6 +29,7 @@
2829
import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving;
2930
import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.movingHbar;
3031
import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.movingUnique;
32+
import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor;
3133
import static com.hedera.services.bdd.spec.utilops.EmbeddedVerbs.viewAccount;
3234
import static com.hedera.services.bdd.spec.utilops.SidecarVerbs.GLOBAL_WATCHER;
3335
import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed;
@@ -115,6 +117,12 @@ public class Hip1195EnabledTest {
115117
@Contract(contract = "TokenRedirectHook", creationGas = 5_000_000)
116118
static SpecContract TOKEN_REDIRECT_HOOK;
117119

120+
@Contract(contract = "CreateOpHook", creationGas = 5_000_000)
121+
static SpecContract CREATE_OP_HOOK;
122+
123+
@Contract(contract = "Create2OpHook", creationGas = 5_000_000)
124+
static SpecContract CREATE2_OP_HOOK;
125+
118126
static final String OWNER = "owner";
119127
static final String PAYER = "payer";
120128
public static final String HOOK_CONTRACT_NUM = "365";
@@ -124,6 +132,9 @@ static void beforeAll(@NonNull final TestLifecycle testLifecycle) {
124132
testLifecycle.overrideInClass(Map.of("hooks.hooksEnabled", "true"));
125133
testLifecycle.doAdhoc(FALSE_ALLOWANCE_HOOK.getInfo());
126134
testLifecycle.doAdhoc(TRUE_ALLOWANCE_HOOK.getInfo());
135+
testLifecycle.doAdhoc(CREATE_OP_HOOK.getInfo());
136+
testLifecycle.doAdhoc(CREATE2_OP_HOOK.getInfo());
137+
127138
testLifecycle.doAdhoc(TRUE_PRE_POST_ALLOWANCE_HOOK.getInfo());
128139
testLifecycle.doAdhoc(FALSE_PRE_POST_ALLOWANCE_HOOK.getInfo());
129140
testLifecycle.doAdhoc(TRANSFER_HOOK.getInfo());
@@ -1154,4 +1165,77 @@ final Stream<DynamicTest> cannotDeleteHookWithStorage() {
11541165
assertEquals(2, a.numberLambdaStorageSlots());
11551166
}));
11561167
}
1168+
1169+
@HapiTest
1170+
final Stream<DynamicTest> createOpHook_createsChildOwnedByHookOwner_and_onlyOwnerChecks() {
1171+
final String ONLY_OWNER_ABI =
1172+
"{\"inputs\":[],\"name\":\"onlyOwner\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"}";
1173+
return hapiTest(
1174+
cryptoCreate(PAYER),
1175+
cryptoCreate(OWNER).withHooks(accountAllowanceHook(210L, CREATE_OP_HOOK.name())),
1176+
cryptoTransfer(movingHbar(1).between(OWNER, GENESIS))
1177+
.withPreHookFor(OWNER, 210L, 5_000_000L, "")
1178+
.payingWith(PAYER)
1179+
.via("createHookTxn"),
1180+
withOpContext((spec, opLog) -> {
1181+
final var successTxn = getTxnRecord("createHookTxn")
1182+
.andAllChildRecords()
1183+
.hasNonStakingChildRecordCount(2)
1184+
.logged();
1185+
allRunFor(spec, successTxn);
1186+
1187+
spec.registry()
1188+
.saveContractId(
1189+
"HOOK_CHILD",
1190+
successTxn.getChildRecord(1).getReceipt().getContractID());
1191+
1192+
final var op1 = contractCallWithFunctionAbi("HOOK_CHILD", ONLY_OWNER_ABI)
1193+
.payingWith(OWNER)
1194+
.gas(100_000)
1195+
.via("onlyOwnerByOwner");
1196+
final var op2 = contractCallWithFunctionAbi("HOOK_CHILD", ONLY_OWNER_ABI)
1197+
.payingWith(PAYER)
1198+
.gas(100_000)
1199+
.hasKnownStatus(CONTRACT_REVERT_EXECUTED)
1200+
.via("onlyOwnerByNonOwner");
1201+
allRunFor(spec, op1, op2);
1202+
}));
1203+
}
1204+
1205+
@HapiTest
1206+
final Stream<DynamicTest> create2OpHook_createsChildOwnedByHookOwner_and_onlyOwnerChecks() {
1207+
final String ONLY_OWNER_ABI =
1208+
"{\"inputs\":[],\"name\":\"onlyOwner\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"}";
1209+
final AtomicReference<String> creation = new AtomicReference<>();
1210+
return hapiTest(
1211+
cryptoCreate(PAYER),
1212+
cryptoCreate(OWNER).withHooks(accountAllowanceHook(211L, CREATE2_OP_HOOK.name())),
1213+
cryptoTransfer(movingHbar(1).between(OWNER, GENESIS))
1214+
.withPreHookFor(OWNER, 211L, 5_000_000L, "")
1215+
.payingWith(OWNER)
1216+
.via("create2HookTxn"),
1217+
withOpContext((spec, opLog) -> {
1218+
final var successTxn = getTxnRecord("create2HookTxn")
1219+
.andAllChildRecords()
1220+
.hasNonStakingChildRecordCount(2)
1221+
.logged();
1222+
allRunFor(spec, successTxn);
1223+
1224+
spec.registry()
1225+
.saveContractId(
1226+
"HOOK_CHILD2",
1227+
successTxn.getChildRecord(1).getReceipt().getContractID());
1228+
1229+
final var op1 = contractCallWithFunctionAbi("HOOK_CHILD2", ONLY_OWNER_ABI)
1230+
.payingWith(OWNER)
1231+
.gas(100_000)
1232+
.via("onlyOwner2ByOwner");
1233+
final var op2 = contractCallWithFunctionAbi("HOOK_CHILD2", ONLY_OWNER_ABI)
1234+
.payingWith(PAYER)
1235+
.gas(100_000)
1236+
.hasKnownStatus(CONTRACT_REVERT_EXECUTED)
1237+
.via("onlyOwner2ByNonOwner");
1238+
allRunFor(spec, op1, op2);
1239+
}));
1240+
}
11571241
}

0 commit comments

Comments
 (0)