Skip to content

Commit

Permalink
Implementation of SetApprovalForAll system contract (#8723)
Browse files Browse the repository at this point in the history
Signed-off-by: Valentin Valkanov <[email protected]>
Co-authored-by: Mustafa Uzun <[email protected]>
  • Loading branch information
MrValioBg and mustafauzunn authored Sep 29, 2023
1 parent 895bec9 commit e1ba045
Show file tree
Hide file tree
Showing 9 changed files with 655 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
* Copyright (C) 2023 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package contract;

import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE;
import static contract.HtsErc721TransferXTestConstants.APPROVED_ID;
import static contract.HtsErc721TransferXTestConstants.UNAUTHORIZED_SPENDER_ID;
import static contract.MiscClassicTransfersXTestConstants.INITIAL_RECEIVER_AUTO_ASSOCIATIONS;
import static contract.MiscClassicTransfersXTestConstants.NEXT_ENTITY_NUM;
import static contract.XTestConstants.AN_ED25519_KEY;
import static contract.XTestConstants.ERC721_TOKEN_ADDRESS;
import static contract.XTestConstants.ERC721_TOKEN_ID;
import static contract.XTestConstants.OWNER_ADDRESS;
import static contract.XTestConstants.OWNER_BESU_ADDRESS;
import static contract.XTestConstants.OWNER_HEADLONG_ADDRESS;
import static contract.XTestConstants.OWNER_ID;
import static contract.XTestConstants.RECEIVER_HEADLONG_ADDRESS;
import static contract.XTestConstants.RECEIVER_ID;
import static contract.XTestConstants.SENDER_ADDRESS;
import static contract.XTestConstants.SENDER_BESU_ADDRESS;
import static contract.XTestConstants.SENDER_HEADLONG_ADDRESS;
import static contract.XTestConstants.SENDER_ID;
import static contract.XTestConstants.SN_1234;
import static contract.XTestConstants.SN_1234_METADATA;
import static contract.XTestConstants.SN_2345;
import static contract.XTestConstants.SN_2345_METADATA;
import static contract.XTestConstants.addErc721Relation;
import static contract.XTestConstants.assertSuccess;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.NftID;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.base.TokenType;
import com.hedera.hapi.node.state.common.EntityIDPair;
import com.hedera.hapi.node.state.primitives.ProtoBytes;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.state.token.Nft;
import com.hedera.hapi.node.state.token.Token;
import com.hedera.hapi.node.state.token.TokenRelation;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.ClassicTransfersTranslator;
import com.hedera.node.app.spi.state.ReadableKVState;
import java.util.HashMap;
import java.util.Map;
import org.apache.tuweni.bytes.Bytes;
import org.jetbrains.annotations.NotNull;

/**
* Exercises setApprovalForAll via the following steps relative to an {@code OWNER} account:
* <ol>
* <li>Transfer {@code ERC721_TOKEN} serialN 1234 from Owner's account. Should fail with SPENDER_DOES_NOT_HAVE_ALLOWANCE</li>
* <li>SetApprovalForAll to true and verify successful operation</li>
* <li>Transfer {@code ERC721_TOKEN} serialN 1234 from Owner's account and verify successful operation</li>
* <li>SetApprovalForAll to false and verify successful operation</li>
* <li>Transfer {@code ERC721_TOKEN} serialN 2345 from Owner's account. Should fail with SPENDER_DOES_NOT_HAVE_ALLOWANCE</li>
* <li>SetApprovalForAll with ERC call to true and verify successful operation</li>
* <li>Transfer {@code ERC721_TOKEN} serialN 2345 from Owner's account and verify successful operation</li>
* <li>SetApprovalForAll with ERC call to false and verify successful operation</li>
* <li>Via {@code assertExpectedAccounts} verify that 2 NFTs have been transferred from the OWNER to the RECEIVER</li>
* </ol>
*/
public class SetApprovalForAllXTest extends AbstractContractXTest {

private static final long NUMBER_OWNED_NFT = 3L;

@Override
protected void doScenarioOperations() {
// Transfer series 1234 of ERC721_TOKEN to RECEIVER
runHtsCallAndExpectOnSuccess(
SENDER_BESU_ADDRESS,
Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT
.encodeCallWithArgs(
ERC721_TOKEN_ADDRESS,
OWNER_HEADLONG_ADDRESS,
RECEIVER_HEADLONG_ADDRESS,
SN_1234.serialNumber())
.array()),
output -> assertEquals(
Bytes.wrap(ReturnTypes.encodedRc(SPENDER_DOES_NOT_HAVE_ALLOWANCE)
.array()),
output));

// Set approval for all to true
runHtsCallAndExpectOnSuccess(
OWNER_BESU_ADDRESS,
Bytes.wrap(SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL
.encodeCallWithArgs(ERC721_TOKEN_ADDRESS, SENDER_HEADLONG_ADDRESS, true)
.array()),
assertSuccess());

// Transfer series 1234 of ERC721_TOKEN to RECEIVER
runHtsCallAndExpectOnSuccess(
SENDER_BESU_ADDRESS,
Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT
.encodeCallWithArgs(
ERC721_TOKEN_ADDRESS,
OWNER_HEADLONG_ADDRESS,
RECEIVER_HEADLONG_ADDRESS,
SN_1234.serialNumber())
.array()),
assertSuccess());

// Set approval for all to false
runHtsCallAndExpectOnSuccess(
OWNER_BESU_ADDRESS,
Bytes.wrap(SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL
.encodeCallWithArgs(ERC721_TOKEN_ADDRESS, SENDER_HEADLONG_ADDRESS, false)
.array()),
assertSuccess());

// Attempt to transfer series 2345 of ERC721_TOKEN to RECEIVER
runHtsCallAndExpectOnSuccess(
SENDER_BESU_ADDRESS,
Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT
.encodeCallWithArgs(
ERC721_TOKEN_ADDRESS,
OWNER_HEADLONG_ADDRESS,
RECEIVER_HEADLONG_ADDRESS,
SN_2345.serialNumber())
.array()),
output -> assertEquals(
Bytes.wrap(ReturnTypes.encodedRc(SPENDER_DOES_NOT_HAVE_ALLOWANCE)
.array()),
output));

// Set approval for all to true via ERC call
runHtsCallAndExpectOnSuccess(
OWNER_BESU_ADDRESS,
bytesForRedirect(
SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL.encodeCallWithArgs(
SENDER_HEADLONG_ADDRESS, true),
ERC721_TOKEN_ID),
assertSuccess());

// Transfer series 2345 of ERC721_TOKEN to RECEIVER
runHtsCallAndExpectOnSuccess(
SENDER_BESU_ADDRESS,
Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT
.encodeCallWithArgs(
ERC721_TOKEN_ADDRESS,
OWNER_HEADLONG_ADDRESS,
RECEIVER_HEADLONG_ADDRESS,
SN_2345.serialNumber())
.array()),
assertSuccess());

// Set approval for all to false via ERC call
runHtsCallAndExpectOnSuccess(
OWNER_BESU_ADDRESS,
bytesForRedirect(
SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL.encodeCallWithArgs(
SENDER_HEADLONG_ADDRESS, false),
ERC721_TOKEN_ID),
assertSuccess());
}

@Override
protected long initialEntityNum() {
return NEXT_ENTITY_NUM - 1;
}

@Override
protected Map<ProtoBytes, AccountID> initialAliases() {
final var aliases = new HashMap<ProtoBytes, AccountID>();
aliases.put(ProtoBytes.newBuilder().value(SENDER_ADDRESS).build(), SENDER_ID);
aliases.put(ProtoBytes.newBuilder().value(OWNER_ADDRESS).build(), OWNER_ID);
return aliases;
}

@Override
protected Map<TokenID, Token> initialTokens() {
final var tokens = new HashMap<TokenID, Token>();
tokens.put(
ERC721_TOKEN_ID,
Token.newBuilder()
.tokenId(ERC721_TOKEN_ID)
.treasuryAccountId(UNAUTHORIZED_SPENDER_ID)
.tokenType(TokenType.NON_FUNGIBLE_UNIQUE)
.build());
return tokens;
}

@Override
protected Map<EntityIDPair, TokenRelation> initialTokenRelationships() {
final var tokenRelationships = new HashMap<EntityIDPair, TokenRelation>();
addErc721Relation(tokenRelationships, OWNER_ID, NUMBER_OWNED_NFT);
return tokenRelationships;
}

@Override
protected Map<NftID, Nft> initialNfts() {
final var nfts = new HashMap<NftID, Nft>();
nfts.put(
SN_1234,
Nft.newBuilder()
.nftId(SN_1234)
.ownerId(OWNER_ID)
.spenderId(APPROVED_ID)
.metadata(SN_1234_METADATA)
.build());
nfts.put(
SN_2345,
Nft.newBuilder()
.nftId(SN_2345)
.ownerId(OWNER_ID)
.metadata(SN_2345_METADATA)
.build());
return nfts;
}

@Override
protected Map<AccountID, Account> initialAccounts() {
final var accounts = new HashMap<AccountID, Account>();
accounts.put(
SENDER_ID,
Account.newBuilder()
.accountId(SENDER_ID)
.alias(SENDER_ADDRESS)
.smartContract(true)
.build());
accounts.put(
OWNER_ID,
Account.newBuilder()
.accountId(OWNER_ID)
.alias(OWNER_ADDRESS)
.key(AN_ED25519_KEY)
.numberOwnedNfts(NUMBER_OWNED_NFT)
.build());
accounts.put(
RECEIVER_ID,
Account.newBuilder()
.accountId(RECEIVER_ID)
.maxAutoAssociations(INITIAL_RECEIVER_AUTO_ASSOCIATIONS)
.build());
accounts.put(
UNAUTHORIZED_SPENDER_ID,
Account.newBuilder().accountId(UNAUTHORIZED_SPENDER_ID).build());
return accounts;
}

@Override
protected void assertExpectedAccounts(@NotNull final ReadableKVState<AccountID, Account> accounts) {
final var ownerAccount = accounts.get(OWNER_ID);
final var receiverAccount = accounts.get(RECEIVER_ID);
assertNotNull(ownerAccount);
assertNotNull(receiverAccount);

// Number of owned NFTs should be decreased by 2
// Number of receiver NFTs should be 2
assertEquals(NUMBER_OWNED_NFT - 2, ownerAccount.numberOwnedNfts());
assertEquals(2, receiverAccount.numberOwnedNfts());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ class XTestConstants {
.build();
static final Bytes SENDER_ADDRESS =
com.hedera.pbj.runtime.io.buffer.Bytes.fromHex("f91e624b8b8ea7244e8159ba7c0deeea2b6be990");
static final com.esaulpaugh.headlong.abi.Address SENDER_HEADLONG_ADDRESS =
asHeadlongAddress(SENDER_ADDRESS.toByteArray());
static final Address SENDER_BESU_ADDRESS = pbjToBesuAddress(SENDER_ADDRESS);
static final AccountID RECEIVER_ID =
AccountID.newBuilder().accountNum(987654321L).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.name.NameTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ownerof.OwnerOfTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.pauses.PausesTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.symbol.SymbolTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.tokenuri.TokenUriTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.totalsupply.TotalSupplyTranslator;
Expand Down Expand Up @@ -140,6 +141,13 @@ static HtsCallTranslator provideOwnerOfTranslator(@NonNull final OwnerOfTranslat
return translator;
}

@Provides
@Singleton
@IntoSet
static HtsCallTranslator provideSetApprovalForAllTranslator(@NonNull final SetApprovalForAllTranslator translator) {
return translator;
}

@Provides
@Singleton
@IntoSet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2023 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval;

import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.asTokenId;

import com.esaulpaugh.headlong.abi.Address;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.token.CryptoApproveAllowanceTransactionBody;
import com.hedera.hapi.node.token.NftAllowance;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Singleton;

@Singleton
public class SetApprovalForAllDecoder {

@Inject
public SetApprovalForAllDecoder() {
// Dagger 2 constructor
}

/**
* Decodes the given {@code attempt} into a {@link TransactionBody} for a setApprovalForAll function call.
*
* @param attempt the attempt to decode
* @return a {@link TransactionBody}
*/
public TransactionBody decodeSetApprovalForAll(@NonNull final HtsCallAttempt attempt) {
final var call = SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL.decodeCall(attempt.inputBytes());
return bodyOf(approveAllAllowanceNFTBody(
attempt.addressIdConverter(), attempt.senderId(), asTokenId(call.get(0)), call.get(1), call.get(2)));
}

/**
* Decodes the given {@code attempt} into a {@link TransactionBody} for a setApprovalForAll ERC function call.
*
* @param attempt the attempt to decode
* @return a {@link TransactionBody}
*/
public TransactionBody decodeSetApprovalForAllERC(@NonNull final HtsCallAttempt attempt) {
final var call = SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL.decodeCall(attempt.inputBytes());
final var tokenId = attempt.redirectTokenId();
Objects.requireNonNull(tokenId, "Redirect Token ID is null.");

return bodyOf(approveAllAllowanceNFTBody(
attempt.addressIdConverter(), attempt.senderId(), tokenId, call.get(0), call.get(1)));
}

private CryptoApproveAllowanceTransactionBody approveAllAllowanceNFTBody(
@NonNull final AddressIdConverter addressIdConverter,
@NonNull final AccountID senderId,
@NonNull final TokenID tokenID,
@NonNull final Address operatorAddress,
final boolean approved) {
return CryptoApproveAllowanceTransactionBody.newBuilder()
.nftAllowances(NftAllowance.newBuilder()
.tokenId(tokenID)
.owner(senderId)
.spender(addressIdConverter.convert(operatorAddress))
.approvedForAll(approved)
.build())
.build();
}

private TransactionBody bodyOf(
@NonNull final CryptoApproveAllowanceTransactionBody approveAllowanceTransactionBody) {
return TransactionBody.newBuilder()
.cryptoApproveAllowance(approveAllowanceTransactionBody)
.build();
}
}
Loading

0 comments on commit e1ba045

Please sign in to comment.