From d0a0bc7b07214c5b9dae0e261175c1fbc6cd6808 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 30 Apr 2021 15:21:34 +0000 Subject: [PATCH] feat: ethereum method handling --- --- a/pallets/nft/src/eth/mod.rs +++ b/pallets/nft/src/eth/mod.rs @@ -4,3 +4,304 @@ use abi::{AbiReader, AbiWriter}; pub mod log; +use sp_std::borrow::ToOwned; +use sp_std::vec::Vec; +use sp_std::convert::TryInto; + +use codec::{Decode, Encode}; +use pallet_evm::{AddressMapping, PrecompileLog, PrecompileOutput, ExitReason, ExitRevert, ExitSucceed}; +use sp_core::{H160, H256}; +use frame_support::storage::{StorageMap, StorageDoubleMap}; + +use crate::{Allowances, NftItemList, Module, Balance, Config, CollectionById, CollectionHandle, CollectionId, CollectionMode}; + +pub struct NftErcSupport(core::marker::PhantomData); + +// 0x17c4e6453Cc49AAAaEACA894e6D9683e00000001 - collection +// TODO: Unhardcode prefix +const ETH_ACCOUNT_PREFIX: [u8; 16] = [0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e]; + +fn map_eth_to_id(eth: &H160) -> Option { + if ð[0..16] != ETH_ACCOUNT_PREFIX { + return None; + } + let mut id_bytes = [0; 4]; + id_bytes.copy_from_slice(ð[16..20]); + Some(u32::from_be_bytes(id_bytes)) +} +pub fn collection_id_to_address(id: u32) -> H160 { + let mut out = [0; 20]; + out[0..16].copy_from_slice(Ð_ACCOUNT_PREFIX); + out[16..20].copy_from_slice(&u32::to_be_bytes(id)); + H160(out) +} + +fn result_to_output(result: Result>, logs: Vec) -> PrecompileOutput { + sp_io::storage::start_transaction(); + match result { + Ok(result) => { + sp_io::storage::commit_transaction(); + // TODO: weight + PrecompileOutput(ExitReason::Succeed(ExitSucceed::Returned), result.finish(), 0, logs) + } + Err(Some(s)) => { + sp_io::storage::rollback_transaction(); + // Error(string) + let mut out = AbiWriter::new_call(0x08c379a0); + out.string(&s); + PrecompileOutput(ExitReason::Revert(ExitRevert::Reverted), out.finish(), 0, Vec::new()) + } + Err(None) => { + sp_io::storage::rollback_transaction(); + PrecompileOutput(ExitReason::Revert(ExitRevert::Reverted), Vec::new(), 0, Vec::new()) + } + } +} + +fn call_internal(sender: H160, collection: &CollectionHandle, method_id: u32, mut input: AbiReader) -> Result> { + let erc20 = matches!(collection.mode, CollectionMode::Fungible(_)); + let erc721 = matches!(collection.mode, CollectionMode::ReFungible); + + Ok(match method_id { + // function name() external view returns (string memory) + 0x06fdde03 => { + let name = collection.name.iter() + .map(|&e| e.try_into().ok() as Option) + .collect::>>() + .ok_or(Some("non-ascii name"))?; + + crate::abi_encode!(memory(&name)) + } + // function symbol() external view returns (string memory) + 0x95d89b41 => { + let name = collection.token_prefix.iter() + .map(|&e| e.is_ascii_uppercase().then(|| e)) + .collect::>>() + .ok_or(Some("non-uppercase prefix"))?; + + crate::abi_encode!(memory(&name)) + } + // function decimals() external view returns (uint8 decimals) + 0x313ce567 if erc20 => { + if let CollectionMode::Fungible(decimals) = &collection.mode { + crate::abi_encode!(uint8(*decimals)) + } else { + unreachable!() + } + } + // function totalSupply() external view returns (uint256) + 0x18160ddd if erc20 => { + // TODO: can't be implemented, as we don't track total amount of fungibles + crate::abi_encode!(uint256(0)) + } + // function balanceOf(address account) external view returns (uint256) + 0x70a08231 if erc20 || erc721 => { + crate::abi_decode!(input, account: address); + let account = T::EvmAddressMapping::into_account_id(account); + let balance = >::get(collection.id, account); + crate::abi_encode!(uint256(balance)) + } + // function ownerOf(uint256 tokenId) external view returns (address) + 0x6352211e if erc721 => { + crate::abi_decode!(input, token_id: uint256); + let token_id: u32 = token_id.try_into().map_err(|_| "bad token id")?; + + let token = >::get(collection.id, token_id).ok_or("unknown token")?; + + crate::abi_encode!(address(token.owner.as_eth().clone())) + } + // function transfer(address recipient, uint256 amount) external returns (bool) { + 0xa9059cbb if erc20 => { + crate::abi_decode!(input, recipient: address, amount: uint256); + let sender = T::CrossAccountId::from_eth(sender); + let recipient = T::CrossAccountId::from_eth(recipient); + + >::transfer_internal( + sender, + recipient, + &collection, + 1, + amount, + ).map_err(|_| "transfer error")?; + + crate::abi_encode!(bool(true)) + } + // function allowance(address owner, address spender) external view returns (uint256) + 0xdd62ed3e if erc20 => { + crate::abi_decode!(input, owner: address, spender: address); + let owner = T::EvmAddressMapping::into_account_id(owner); + let spender = T::EvmAddressMapping::into_account_id(spender); + let allowance = >::get(collection.id, (1, &owner, &spender)); + crate::abi_encode!(uint256(allowance)) + } + // function approve(address spender, uint256 amount) external returns (bool) + // FIXME: All current implementations resets amount to specified value, ours - adds it + // FIXME: Our implementation doesn't handle resets (approve with zero amount) + 0x095ea7b3 if erc20 => { + crate::abi_decode!(input, spender: address, amount: uint256); + let sender = T::CrossAccountId::from_eth(sender); + let spender = T::CrossAccountId::from_eth(spender); + + >::approve_internal( + sender, + spender, + &collection, + 1, + amount, + ).map_err(|_| "approve error")?; + + crate::abi_encode!(bool(true)) + } + // function approve(address approved, uint256 tokenId) external payable + 0x095ea7b3 if erc721 => { + crate::abi_decode!(input, approved: address, token_id: uint256); + let sender = T::CrossAccountId::from_eth(sender); + let approved = T::CrossAccountId::from_eth(approved); + let token_id = token_id.try_into().map_err(|_| "bad token id")?; + + >::approve_internal( + sender, + approved, + &collection, + token_id, + 1, + ).map_err(|_| "approve error")?; + crate::abi_encode!() + } + // function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) + 0x23b872dd if erc20 => { + crate::abi_decode!(input, from: address, recipient: address, amount: uint256); + let sender = T::CrossAccountId::from_eth(sender); + let from = T::CrossAccountId::from_eth(from); + let recipient = T::CrossAccountId::from_eth(recipient); + + >::transfer_from_internal( + sender, + from, + recipient, + &collection, + 1, + amount, + ).map_err(|_| "transfer_from error")?; + + crate::abi_encode!(bool(true)) + } + // function transferFrom(address from, address to, uint256 tokenId) external payable + 0x23b872dd if erc721 => { + crate::abi_decode!(input, from: address, recipient: address, token_id: uint256); + let sender = T::CrossAccountId::from_eth(sender); + let from = T::CrossAccountId::from_eth(from); + let recipient = T::CrossAccountId::from_eth(recipient); + let token_id = token_id.try_into().map_err(|_| "bad token id")?; + + >::transfer_from_internal( + sender, + from, + recipient, + &collection, + token_id, + 1, + ).map_err(|_| "transfer_from error")?; + + crate::abi_encode!() + } + // function supportsInterface(bytes4 interfaceID) public pure returns (bool) + 0x01ffc9a7 => { + crate::abi_decode!(input, interface_id: uint32); + let supports = match interface_id { + // ERC165 + 0x01ffc9a7 => true, + // ERC20 + 0x36372b07 if erc20 => true, + // ERC721 + 0x80ac58cd if erc721 => true, + _ => false, + }; + crate::abi_encode!(bool(supports)) + } + _ => return Err(None) + }) +} + +impl pallet_evm::OnMethodCall for NftErcSupport { + fn is_reserved(target: &H160) -> bool { + map_eth_to_id(target).is_some() + } + fn is_used(target: &H160) -> bool { + map_eth_to_id(target) + .map(>::contains_key) + .unwrap_or(false) + } + fn get_code(target: &H160) -> Option> { + map_eth_to_id(&target) + .and_then(>::get) + .map(|collection| { + match collection.mode { + CollectionMode::NFT => include_bytes!("stubs/ERC721.bin") as &[u8], + CollectionMode::Fungible(_) => include_bytes!("stubs/ERC20.bin") as &[u8], + CollectionMode::ReFungible => include_bytes!("stubs/ERC1633.bin") as &[u8], + CollectionMode::Invalid => include_bytes!("stubs/Invalid.bin") as &[u8], + }.to_owned() + }) + } + fn call( + source: &H160, + target: &H160, + input: &[u8], + ) -> Option { + let collection = map_eth_to_id(&target) + .and_then(>::get)?; + let (method_id, input) = AbiReader::new_call(input).unwrap(); + let result = call_internal(*source, &collection, method_id, input); + Some(result_to_output(result, collection.logs.retrieve_logs_for_contract(*target))) + } +} + +/// event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); +pub const TRANSFER_NFT_TOPIC: H256 = H256(hex_literal::hex!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")); +/// event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); +pub const APPROVAL_NFT_TOPIC: H256 = H256(hex_literal::hex!("8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925")); +// TODO: event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + +/// event Transfer(address indexed from, address indexed to, uint256 indexed amount); +pub const TRANSFER_FUNGIBLE_TOPIC: H256 = H256(hex_literal::hex!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")); +/// event Approval(address indexed owner, address indexed approved, uint256 indexed amount); +pub const APPROVAL_FUNGIBLE_TOPIC: H256 = H256(hex_literal::hex!("8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925")); + +pub fn address_to_topic(address: &H160) -> H256 { + let mut output = [0; 32]; + output[12..32].copy_from_slice(&address.0); + H256(output) +} + + +// TODO: This function is slow, and output can be memoized +pub fn generate_transaction(collection_id: u32, chain_id: u64) -> ethereum::Transaction { + let contract = collection_id_to_address(collection_id); + + // TODO: Make it work without native runtime by forking ethereum_tx_sign, and + // switching to pure-rust implementation of secp256k1 + #[cfg(feature = "std")] + { + let signed = ethereum_tx_sign::RawTransaction { + nonce: 0.into(), + to: Some(contract.0.into()), + value: 0.into(), + gas_price: 0.into(), + gas: 0.into(), + // zero selector, this transaction always have same sender, so all data should be acquired from logs + data: Vec::from([0, 0, 0, 0]), + }.sign( + // TODO: move to pallet config + // 0xF70631E55faff9f3FD3681545aa6c724226a3853 + // 9dbaef9b3ebc00e53f67c6a77bcfbf2c4f2aebe4d70d94af4f2df01744b7a91a + &hex_literal::hex!("9dbaef9b3ebc00e53f67c6a77bcfbf2c4f2aebe4d70d94af4f2df01744b7a91a").into(), + &chain_id + ); + rlp::decode::(&signed).expect("transaction is just created, it can't be broken") + } + #[cfg(not(feature = "std"))] + { + panic!("transaction generation not yet supported by wasm runtime") + } +} \ No newline at end of file --- /dev/null +++ b/pallets/nft/src/eth/stubs/ERC1633.bin @@ -0,0 +1 @@ +TODO \ No newline at end of file --- /dev/null +++ b/pallets/nft/src/eth/stubs/ERC20.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: OTHER + +pragma solidity >=0.8.0 <0.9.0; + +contract ERC20 { + uint8 _dummy = 0; + string stub_error = "this contract does not exists, code for collections is implemented at pallet side"; + + // 0x18160ddd + function totalSupply() external view returns (uint256) { + require(false, stub_error); + _dummy; + return 0; + } + + // 0x70a08231 + function balanceOf(address account) external view returns (uint256) { + require(false, stub_error); + account; + _dummy; + return 0; + } + + // 0xa9059cbb + function transfer(address recipient, uint256 amount) external returns (bool) { + require(false, stub_error); + recipient; + amount; + _dummy = 0; + return false; + } + + // 0xdd62ed3e + function allowance(address owner, address spender) external view returns (uint256) { + require(false, stub_error); + owner; + spender; + return _dummy; + } + + // 0x095ea7b3 + function approve(address spender, uint256 amount) external returns (bool) { + require(false, stub_error); + spender; + amount; + _dummy = 0; + return false; + } + + // 0x23b872dd + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + require(false, stub_error); + sender; + recipient; + amount; + _dummy = 0; + return false; + } + + // While ERC165 is not required by spec of ERC20, better implement it + // 0x01ffc9a7 + function supportsInterface(bytes4 interfaceID) public pure returns (bool) { + return + // ERC20 + interfaceID == 0x36372b07 || + // ERC165 + interfaceID == 0x01ffc9a7; + } +} \ No newline at end of file --- /dev/null +++ b/pallets/nft/src/eth/stubs/ERC721.bin @@ -0,0 +1 @@ +0x608060405260008060006101000a81548160ff021916908360ff16021790555060008060016101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060405180608001604052806051815260200162000cd6605191396001908051906020019061008f9291906100a2565b5034801561009c57600080fd5b506101a6565b8280546100ae90610145565b90600052602060002090601f0160209004810192826100d05760008555610117565b82601f106100e957805160ff1916838001178555610117565b82800160010185558215610117579182015b828111156101165782518255916020019190600101906100fb565b5b5090506101249190610128565b5090565b5b80821115610141576000816000905550600101610129565b5090565b6000600282049050600182168061015d57607f821691505b6020821081141561017157610170610177565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b610b2080620001b66000396000f3fe6080604052600436106100915760003560e01c80636352211e116100595780636352211e1461016457806370a08231146101a1578063a22cb465146101de578063b88d4fde14610207578063e985e9c51461022357610091565b806301ffc9a714610096578063081812fc146100d3578063095ea7b31461011057806323b872dd1461012c57806342842e0e14610148575b600080fd5b3480156100a257600080fd5b506100bd60048036038101906100b89190610821565b610260565b6040516100ca919061093b565b60405180910390f35b3480156100df57600080fd5b506100fa60048036038101906100f5919061084a565b6102c2565b6040516101079190610920565b60405180910390f35b61012a600480360381019061012591906107e5565b610333565b005b610146600480360381019061014191906106da565b61037d565b005b610162600480360381019061015d91906106da565b6103c8565b005b34801561017057600080fd5b5061018b6004803603810190610186919061084a565b610413565b6040516101989190610920565b60405180910390f35b3480156101ad57600080fd5b506101c860048036038101906101c39190610675565b610484565b6040516101d59190610978565b60405180910390f35b3480156101ea57600080fd5b50610205600480360381019061020091906107a9565b6104d4565b005b610221600480360381019061021c9190610729565b610539565b005b34801561022f57600080fd5b5061024a6004803603810190610245919061069e565b610586565b604051610257919061093b565b60405180910390f35b60006380ac58cd60e01b827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff191614806102bb57506301ffc9a760e01b827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916145b9050919050565b600080600190610308576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102ff9190610956565b60405180910390fd5b50600060019054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050919050565b6000600190610378576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161036f9190610956565b60405180910390fd5b505050565b60006001906103c2576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103b99190610956565b60405180910390fd5b50505050565b600060019061040d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104049190610956565b60405180910390fd5b50505050565b600080600190610459576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104509190610956565b60405180910390fd5b50600060019054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050919050565b6000806001906104ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104c19190610956565b60405180910390fd5b5060009050919050565b6000600190610519576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105109190610956565b60405180910390fd5b5060008060006101000a81548160ff021916908360ff1602179055505050565b600060019061057e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105759190610956565b60405180910390fd5b505050505050565b6000806001906105cc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105c39190610956565b60405180910390fd5b506000905092915050565b6000813590506105e681610a8e565b92915050565b6000813590506105fb81610aa5565b92915050565b60008135905061061081610abc565b92915050565b60008083601f84011261062857600080fd5b8235905067ffffffffffffffff81111561064157600080fd5b60208301915083600182028301111561065957600080fd5b9250929050565b60008135905061066f81610ad3565b92915050565b60006020828403121561068757600080fd5b6000610695848285016105d7565b91505092915050565b600080604083850312156106b157600080fd5b60006106bf858286016105d7565b92505060206106d0858286016105d7565b9150509250929050565b6000806000606084860312156106ef57600080fd5b60006106fd868287016105d7565b935050602061070e868287016105d7565b925050604061071f86828701610660565b9150509250925092565b60008060008060006080868803121561074157600080fd5b600061074f888289016105d7565b9550506020610760888289016105d7565b945050604061077188828901610660565b935050606086013567ffffffffffffffff81111561078e57600080fd5b61079a88828901610616565b92509250509295509295909350565b600080604083850312156107bc57600080fd5b60006107ca858286016105d7565b92505060206107db858286016105ec565b9150509250929050565b600080604083850312156107f857600080fd5b6000610806858286016105d7565b925050602061081785828601610660565b9150509250929050565b60006020828403121561083357600080fd5b600061084184828501610601565b91505092915050565b60006020828403121561085c57600080fd5b600061086a84828501610660565b91505092915050565b61087c816109b9565b82525050565b61088b816109cb565b82525050565b6000815461089e81610a2d565b6108a881866109a8565b945060018216600081146108c357600181146108d557610908565b60ff1983168652602086019350610908565b6108de85610993565b60005b83811015610900578154818901526001820191506020810190506108e1565b808801955050505b50505092915050565b61091a81610a23565b82525050565b60006020820190506109356000830184610873565b92915050565b60006020820190506109506000830184610882565b92915050565b600060208201905081810360008301526109708184610891565b905092915050565b600060208201905061098d6000830184610911565b92915050565b60008190508160005260206000209050919050565b600082825260208201905092915050565b60006109c482610a03565b9050919050565b60008115159050919050565b60007fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b60006002820490506001821680610a4557607f821691505b60208210811415610a5957610a58610a5f565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b610a97816109b9565b8114610aa257600080fd5b50565b610aae816109cb565b8114610ab957600080fd5b50565b610ac5816109d7565b8114610ad057600080fd5b50565b610adc81610a23565b8114610ae757600080fd5b5056fea26469706673582212206e53fcc41e6b23f5bd49a262462ecf5ff647f480649658ee8b67c91a8f733ba864736f6c634300080100337468697320636f6e747261637420646f6573206e6f74206578697374732c20636f646520666f7220636f6c6c656374696f6e7320697320696d706c656d656e7465642061742070616c6c65742073696465 \ No newline at end of file --- /dev/null +++ b/pallets/nft/src/eth/stubs/ERC721.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: OTHER + +pragma solidity >=0.8.0 <0.9.0; + +contract ERC721 { + uint8 _dummy = 0; + address _dummy_addr = 0x0000000000000000000000000000000000000000; + string _dummy_string = ""; + string stub_error = + "this contract does not exists, code for collections is implemented at pallet side"; + + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + + // 0x18160ddd + function totalSupply() external view returns (uint256) { + require(false, stub_error); + return 0; + } + + function name() external view returns (string memory res_name) { + require(false, stub_error); + res_name = _dummy_string; + } + + function symbol() external view returns (string memory res_symbol) { + require(false, stub_error); + res_symbol = _dummy_string; + } + + function tokenURI(uint256 tokenId) external view returns (string memory) { + require(false, stub_error); + tokenId; + return _dummy_string; + } + + function tokenByIndex(uint256 index) external view returns (uint256) { + require(false, stub_error); + index; + return 0; + } + + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) { + require(false, stub_error); + owner; + index; + return 0; + } + + // 0x70a08231 + function balanceOf(address owner) external view returns (uint256) { + require(false, stub_error); + owner; + return 0; + } + + // 0x6352211e + function ownerOf(uint256 tokenId) external view returns (address) { + require(false, stub_error); + tokenId; + return _dummy_addr; + } + + // 0xb88d4fde + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external payable { + require(false, stub_error); + from; + to; + tokenId; + data; + } + + // 0x42842e0e + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external payable { + require(false, stub_error); + from; + to; + tokenId; + } + + // 0x23b872dd + function transferFrom( + address from, + address to, + uint256 tokenId + ) external payable { + require(false, stub_error); + from; + to; + tokenId; + } + + // 0x095ea7b3 + function approve(address approved, uint256 tokenId) external payable { + require(false, stub_error); + approved; + tokenId; + } + + // 0xa22cb465 + function setApprovalForAll(address operator, bool approved) external { + require(false, stub_error); + operator; + approved; + _dummy = 0; + } + + // 0x081812fc + function getApproved(uint256 tokenId) external view returns (address) { + require(false, stub_error); + tokenId; + return _dummy_addr; + } + + // 0xe985e9c5 + function isApprovedForAll(address owner, address operator) + external + view + returns (bool) + { + require(false, stub_error); + owner; + operator; + return false; + } + + // 0x01ffc9a7 + function supportsInterface(bytes4 interfaceID) public pure returns (bool) { + return + // ERC721 + interfaceID == 0x80ac58cd || + // ERC721Metadata + interfaceID == 0x5b5e139f || + // ERC721Enumerable + interfaceID == 0x780e9d63 || + // ERC165 + interfaceID == 0x01ffc9a7; + } +} --- /dev/null +++ b/pallets/nft/src/eth/stubs/Invalid.bin @@ -0,0 +1 @@ +TODO \ No newline at end of file --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -55,6 +55,7 @@ mod default_weights; mod eth; +pub use eth::NftErcSupport; pub use eth::account::*; pub const MAX_DECIMAL_POINTS: DecimalPoints = 30; -- gitstuff