neo icon indicating copy to clipboard operation
neo copied to clipboard

Make the contract deployer accessible to smart contracts

Open lock9 opened this issue 2 years ago • 11 comments

Summary or problem description We created a dApp that has allow some actions to be performed only by the deployer of another contract. The deployer is not stored on the manifest, but it is possible to calculate ownership using the GetContractHash method. This method is not public, we had to implement it inside our smart contract.

https://github.com/neo-project/neo/blob/8e68c3fabf8b7cad3bd27e0c556cbeda17c2b123/src/Neo/SmartContract/Helper.cs#L94

Do you have any solution you want to propose? The real problem is to ensure that we are talking to the deployer of a certain contract. We could make this method available for contracts, or we could add this information somewhere 'reachable'. Edit: We had to calculate the NEF checksum too. Maybe this is not the best solution.

Neo Version

  • Neo 3

Where in the software does this update applies to?

  • Compiler
  • SDK

lock9 avatar Jul 22 '22 02:07 lock9

@lock9 the class and the method are public, you mean accesible inside the smart contract?

shargon avatar Jul 22 '22 08:07 shargon

Yes. We need to update both Neo library and the Compiler. I think that only adding this method won't resolve the problem (it will improve but not fix it totally). How can we retrieve, at smart contract level, the deployer of a certain contract?

lock9 avatar Jul 22 '22 11:07 lock9

Would it make sense to have a method in the ContractManagement or LedgerContract native contract which takes a contract hash as the argument and returns the transaction hash in which it was deployed? Then you can get the tx sender or signers data from LedgerContract as needed.

EdgeDLT avatar Jul 22 '22 18:07 EdgeDLT

We don't store the transaction hash inside the ContractState object. This information exists only on the transaction that deployed the contract. Adding this information anywhere else would be duplicating data. In our specific use case, we just need to ensure that they are the owner, and not get the owner itself. I would be happy to have the deployer easily accessible, but I don't know if this is easily doable (or efficient).

A method like "isDeployer(contractHash, deployerHash)" solves the problem. A "getDeployer(contractHash)" does help, but it is more than I need. The first solution won't need changes in the current ledger design.

Since the deployer is a just 20 bytes, adding a pointer to the transaction hash will likely consume more storage

lock9 avatar Jul 23 '22 01:07 lock9

The sender of a deployed contract is not necessarily the owner of the contract. A contract may have logic to modify its owner. Maybe CheckWitness(contract.Hash) directly?

erikzhang avatar Jul 23 '22 01:07 erikzhang

The sender of a deployed contract is not necessarily the owner of the contract. A contract may have logic to modify its owner. Maybe CheckWitness(contract.Hash) directly?

Yes, it may not be the "owner". Contracts always have a deployer, but not always an "owner". We can guarantee that the deployer is an "existing user". This may not be true for "custom owners".

How this CheckWitness would work? Would we send 2 parameters? The hash and the possible owner? (this is how we implemented it)

The idea behind this dApp is to "extend" existing contracts. Only the admin or the contract deployer can update this data. We discussed the idea of needing the extended contract to call our contract, but that would need existing contracts to be redeployed. This solution doesn't require that

lock9 avatar Jul 23 '22 02:07 lock9

The hash and the possible owner?

Just need hash. If hash is equal to AppalicationEngine.CallingScriptHash, CheckWitness returns true; otherwise, the contract's verify method will be called.

erikzhang avatar Jul 23 '22 02:07 erikzhang

The hash and the possible owner?

Just need hash. If hash is equal to AppalicationEngine.CallingScriptHash, CheckWitness returns true; otherwise, the contract's verify method will be called.

Hmm, there is something here that I'm not getting. It is not the contract who is going to call us, is the contract admin. The script hashes are different (deployer vs contract hash) Also, the verify is used for transfers. We don't want to mix with that logic, this dApp doesn't even have a token. I'll share the source once I leave shower 😂

lock9 avatar Jul 23 '22 02:07 lock9

Also, the verify is used for transfers.

It's not true. verify is used to obtain the authorization of the contract. Contracts can call CheckWitness(owner) in verify if needed.

erikzhang avatar Jul 23 '22 02:07 erikzhang

Maybe that will work then. Maybe I'm confused with the onNep17Payment. In my case, I want to be able to integrate even if they are not prepared for it. I'm not sure if depending on the verify method is a good option. Could a contract lie and say that someone is not the owner? Or the opposite? I'm not against custom logic, but idk, it would feel weird if a contract could lie about it's deployer 🤔

lock9 avatar Jul 23 '22 02:07 lock9

Here is the code. Some fix + filter options are the next things to be fixed. But in general, this code would allow someone to say that they own a contract, without having to update their contracts. This is important to me because we want to solve a problem with existing contracts. The dapp is basically a list to store URLs for contract icons. Wallets and explorers often don't show the dApp icon because it takes too much effort (devs have to after it). The idea is that NDapp will upload and maintain this list, while also allowing the contract deployer to update this data if they want. Not sure if they code will help to understand what we are trying to achieve.

from typing import Any, List, Optional, cast

from boa3.builtin import CreateNewEvent, NeoMetadata, metadata, public, to_script_hash
from boa3.builtin.interop import runtime, storage
from boa3.builtin.interop.blockchain import Transaction
from boa3.builtin.interop.contract import Contract
from boa3.builtin.interop.contract.contractmanifest import ContractManifest
from boa3.builtin.nativecontract.contractmanagement import ContractManagement
from boa3.builtin.nativecontract.cryptolib import CryptoLib
from boa3.builtin.nativecontract.stdlib import StdLib
from boa3.builtin.type import ByteString, UInt160


# -------------------------------------------
# METADATA
# -------------------------------------------

@metadata
def manifest_metadata() -> NeoMetadata:
    """
    Defines this smart contract's metadata information
    """
    meta = NeoMetadata()
    meta.description = "Smart Contract used to store visual meta-data for contracts, like icons and other images."
    meta.name = "Meta DApp"
    meta.author = "NDapp.org"
    meta.email = "[email protected]"
    meta.supported_standards = []
    meta.add_permission(methods=[])
    return meta


#
# Events
#

onNewProperty = CreateNewEvent(
    [
        ('name', str),
        ('description', str)
    ],
    'NewMetaDataProperty'
)

onOwnershipAcquired = CreateNewEvent(
    [
        ('contract', UInt160),
        ('owner', UInt160)
    ],
    'OwnershipAcquired'
)

onSetMetaData = CreateNewEvent(
    [
        ('contract', UInt160),
        ('property', str),
        ('value', str)
    ],
    'MetaDatadaUpdated'
)

@public
def _deploy(data: Any, update: bool):
    if not update:
        # setup instructions that will be executed when the smart contract is deployed
        container: Transaction = runtime.script_container
        storage.put(get_owner_key(), container.sender)
    else:
        # code for updating the contract after it was deployed
        return

@public(safe=True)
def update(nef_file: bytes, manifest: bytes, data: Any = None):
    if runtime.check_witness(get_owner()):
        ContractManagement.update(nef_file, manifest, data)


@public
def name() -> str:
    return "Meta DApp"


@public
def get_owner() -> UInt160:
    return UInt160(storage.get(get_owner_key()))  # can't test with TestEngine
    # return UInt160(to_script_hash("NYWYuH8xn1SkguWFCPsArCfrpsd7cFQt55"))


@public(name="addProperty")
def add_property(property_name: str, description: str) -> bool:
    # admin only
    if not runtime.check_witness(get_owner()):
        return False

    assert (len(property_name) > 0 and len(property_name) < 31)
    assert (len(description) > 0 and len(description) < 255)

    properties_key = get_properties_key()
    property_bytes = storage.get(properties_key)
    if (property_bytes is not None and len(property_bytes) > 0):
        properties_json = StdLib.deserialize(property_bytes)
        properties: dict = properties_json
        if (properties[property_name] is not None):
            properties[property_name] = description
            storage.put(properties_key, StdLib.serialize(properties))
    else:
        properties = {property_name: description}
        serialized_properties = StdLib.serialize(properties)
        storage.put(properties_key, serialized_properties)

    onNewProperty(property_name, description)
    return True


@public(name="getProperties", safe=True)
def get_properties() -> dict:
    properties_bytes = storage.get(get_properties_key())
    if (properties_bytes is not None and len(properties_bytes) > 0):
        properties_json = StdLib.deserialize(properties_bytes)
        properties: dict = properties_json
        return properties
    else:
        return {}


@public(name="setMetaData")
def set_meta_data(script_hash: UInt160, property_name: str, value: str) -> bool:
    # admin or deployer
    if not runtime.check_witness(get_owner()):
        contract_owner: UInt160 = get_contract_owner(script_hash)
        if not isinstance(contract_owner, UInt160) or not runtime.check_witness(contract_owner):
            return False

    property_names = get_properties()
    if property_name not in property_names:
        return False

    if property_names[property_name] is not None:
        contract_properties = get_meta_data(script_hash)
        contract_properties[property_name] = value
        contract_key = get_contract_property_key(script_hash)
        storage.put(contract_key, StdLib.serialize(contract_properties))
        onSetMetaData(script_hash, property_name, value)
        return True
    return False


@public(name="getMetaData", safe=True)
def get_meta_data(script_hash: UInt160) -> dict:
    parent_contract = get_contract_parent(script_hash)
    if isinstance(parent_contract, UInt160):
        contract_key = get_contract_property_key(parent_contract)
    else:
        contract_key = get_contract_property_key(script_hash)

    contract_properties_bytes = storage.get(contract_key)
    contract_properties = {}

    if (contract_properties_bytes is not None and len(contract_properties_bytes) > 0):
        properties_json = StdLib.deserialize(contract_properties_bytes)
        contract_properties: dict = properties_json

    if isinstance(parent_contract, UInt160):
        contract_properties["parent"] = parent_contract

    return contract_properties


@public(name="getMultipleMetaData", safe=True)
def get_multiple_meta_data(contract_hashes: List[UInt160]) -> dict:
    metadata = {}
    for hash in contract_hashes:
        contract_metadata = get_meta_data(hash)
        metadata[hash] = contract_metadata
    return metadata


# Reuses other contract meta-data
@public(name="setContractParent")
def set_contract_parent(child_hash: UInt160, parent_hash: UInt160) -> bool:
    # admin or deployer
    if not runtime.check_witness(get_owner()):
        contract_owner: UInt160 = get_contract_owner(parent_hash)
        if not isinstance(contract_owner, UInt160) or not runtime.check_witness(contract_owner):
            return False

    # assert len(child_hash) == 20
    child_key = get_child_key(child_hash)
    storage.put(child_key, parent_hash)

    return True


@public(name="getContractParent", safe=True)
def get_contract_parent(child_hash: UInt160) -> Optional[UInt160]:
    # assert len(child_hash) == 20
    storage_result = storage.get(get_child_key(child_hash))
    if len(storage_result) > 0:
        return UInt160(storage_result)
    return None


def get_contract_owner(script_hash: UInt160) -> Optional[UInt160]:
    contract_owner_key = get_contract_owner_key(script_hash)
    storage_result = storage.get(contract_owner_key)
    if len(storage_result) > 0:
        return UInt160(storage_result)

    return None


@public(name='setOwnership')
def set_ownership(script_hash: UInt160, sender: UInt160) -> bool:
    if not runtime.check_witness(sender):
        return False

    contract_deployer = get_deployer(script_hash, sender)
    if isinstance(contract_deployer, UInt160):
        contract_owner_key = get_contract_owner_key(script_hash)
        storage.put(contract_owner_key, contract_deployer)
        onOwnershipAcquired(script_hash, sender)
        return True
    return False


def get_deployer(script_hash: UInt160, sender: UInt160) -> Optional[UInt160]:
    contract: Contract = ContractManagement.get_contract(script_hash)
    if not isinstance(contract, Contract):
        return None

    computed_script_hash: bytes = _compute_contract_hash(sender, contract)
    if computed_script_hash != script_hash:
        return None

    return sender


def _compute_contract_hash(sender: UInt160, contract: Contract) -> bytes:
    nef_check_sum = contract.nef  # there's a bug with calling contract.nef[:4] directly
    nef_check_sum = nef_check_sum[-4:]

    manifest: ContractManifest = contract.manifest  # there's a bug with calling contract.manifest.name directly
    contract_name = manifest.name.to_bytes()

    contract_name_size = len(contract_name)
    if contract_name_size < 0x100:
        serialized_name = b'\x0c' + contract_name_size.to_bytes()   # PUSHDATA1
    elif contract_name_size < 0x10000:
        serialized_name = b'\x0d' + contract_name_size.to_bytes()   # PUSHDATA2
    else:
        serialized_name = b'\x0e' + contract_name_size.to_bytes()[:4]  # PUSHDATA4

    validation_script = (b'\x38' +                      # ABORT
                         b'\x0c\x14' + sender +         # PUSHDATA1 + 20 bytes
                         b'\x02' + nef_check_sum +      # PUSHINT32
                         serialized_name + contract_name)

    return CryptoLib.ripemd160(CryptoLib.sha256(validation_script))


# Keys
def get_properties_key() -> ByteString:
    return b"!properties!"


def get_contract_property_key(script_hash: UInt160) -> ByteString:
    return b"!metadata_" + script_hash


def get_child_key(child_hash: UInt160) -> ByteString:
    return b"!child_" + child_hash


def get_contract_owner_key(script_hash: UInt160) -> ByteString:
    return b"!contract_owner_" + script_hash


def get_owner_key() -> ByteString:
    return b"!owner!"

lock9 avatar Jul 23 '22 02:07 lock9