neo
neo copied to clipboard
Make the contract deployer accessible to smart contracts
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 the class and the method are public, you mean accesible inside the smart contract?
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?
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.
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
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?
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
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.
The hash and the possible owner?
Just need
hash
. Ifhash
is equal toAppalicationEngine.CallingScriptHash
,CheckWitness
returnstrue
; otherwise, the contract'sverify
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 😂
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.
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 🤔
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!"