diff --git a/scripts/upgrade_factory_from_dao.sh b/scripts/upgrade_factory_from_dao.sh new file mode 100644 index 000000000..b3de976e7 --- /dev/null +++ b/scripts/upgrade_factory_from_dao.sh @@ -0,0 +1,140 @@ +#!/bin/bash +#### -------------------------------------------- +#### NOTE: The following flows are supported in this file, for testing! +# - Create an UpgradeDAO via sputnikv2.testnet, funded with enough for 10 upgrades +# - Create an Upgradeable DAO via sputnikv2.testnet, for testing v2-v3 upgrade +# - UpgradeDAO proposal to store_blob on Upgradeable DAO +# - Upgradeable DAO proposal UpgradeSelf with hash from UpgradeDAO store_blob +# - Check code_hash on Upgradeable DAO +#### -------------------------------------------- +set -e + +# # TODO: Change to the official approved commit: +# COMMIT_V3=596f27a649c5df3310e945a37a41a957492c0322 +# # git checkout $COMMIT_V3 + +# build the things +./build.sh + +export NEAR_ENV=testnet +export FACTORY=testnet + +if [ -z ${NEAR_ACCT+x} ]; then + # export NEAR_ACCT=sputnikv2.$FACTORY + export NEAR_ACCT=sputnikpm.$FACTORY +else + export NEAR_ACCT=$NEAR_ACCT +fi + +# export FACTORY_ACCOUNT_ID=sputnikv2.$FACTORY +export FACTORY_ACCOUNT_ID=factory_1.$NEAR_ACCT +# export DAO_ACCOUNT_ID=croncat.sputnikv2.$FACTORY +export MAX_GAS=300000000000000 +export GAS_100_TGAS=100000000000000 +export GAS_150_TGAS=150000000000000 +export GAS_220_TGAS=220000000000000 +export BOND_AMOUNT=1 +export BYTE_STORAGE_COST=10000000000000000000000000 + + +# #### -------------------------------------------- +# #### New Factory for entire test +# #### -------------------------------------------- +near create-account $FACTORY_ACCOUNT_ID --masterAccount $NEAR_ACCT --initialBalance 80 +# #### -------------------------------------------- + +# #### -------------------------------------------- +# #### Build and deploy factory +# - Build and Deploy factory contract by running the following command from your current directory _(`sputnik-dao-contract/sputnikdao-factory2`)_: +# - Initialize factory +# #### -------------------------------------------- +near deploy $FACTORY_ACCOUNT_ID --wasmFile=res/sputnikdao_factory2.wasm --accountId $FACTORY_ACCOUNT_ID +near call $FACTORY_ACCOUNT_ID new --accountId $FACTORY_ACCOUNT_ID --gas 100000000000000 +# #### -------------------------------------------- + + +#### -------------------------------------------- +#### Deploy DAO that can upgrade factory +#### -------------------------------------------- +export COUNCIL='["'$NEAR_ACCT'"]' +export TIMESTAMP=$(date +"%s") +export DAO_NAME=upgrademe-1-$TIMESTAMP +export DAO_ARGS=`echo '{"config": {"name": "'$DAO_NAME'", "purpose": "A dao that can upgrade a factory", "metadata":""}, "policy": '$COUNCIL'}' | base64` +near call $FACTORY_ACCOUNT_ID create "{\"name\": \"$DAO_NAME\", \"args\": \"$DAO_ARGS\"}" --accountId $FACTORY_ACCOUNT_ID --gas $GAS_150_TGAS --amount 12 +export GENDAO=$DAO_NAME.$FACTORY_ACCOUNT_ID +#### -------------------------------------------- + + +#### -------------------------------------------- +#### Quick sanity check on getters +#### -------------------------------------------- +near view $FACTORY_ACCOUNT_ID get_dao_list +#### -------------------------------------------- + +#### -------------------------------------------- +#### Set owner of factory as DAO +#### -------------------------------------------- +near call $FACTORY_ACCOUNT_ID set_owner '{"owner_id":"'$GENDAO'"}' --accountId $FACTORY_ACCOUNT_ID +#### -------------------------------------------- + +#### -------------------------------------------- +#### Modify factory, rebuild, and create proposal to store +#### -------------------------------------------- +# Store the code data +export FACTORYCODE='cat sputnikdao_factory2.wasm | base64' +# - most likely need to use near-cli-rs due to wasm string size limit +# Store blob in DAO +echo '{ "proposal": { "description": "Store upgrade", "kind": { "FunctionCall": { "receiver_id": "'$GENDAO'", "actions": [ { "method_name": "store_blob", "args": "'$(eval $FACTORYCODE)'", "deposit": "'$BYTE_STORAGE_COST'", "gas": "'$GAS_150_TGAS'" } ]}}}}' | base64 | pbcopy +# near call $GENDAO store $(eval "$FACTORYCODE") --base64 --accountId $FACTORY_ACCOUNT_ID --gas $GAS_100_TGAS --amount 10 > new_factory_hash.txt +# Once proposal created on Genesis DAO that is owner of factory account now, vote on it so that it can act_proposal storing the new factory code and returning a hash +# Vote on proposal +near call $GENDAO act_proposal '{"id": 0, "action" :"VoteApprove"}' --accountId $CONTRACT_ID --gas $MAX_GAS +# Act proposal might fail due to exceeded pre paid gas limit but factory is stored +# Set factory hash +export FACTORY_HASH="" + +#### -------------------------------------------- +#### Create proposal to upgrade factory to new factory hash +#### -------------------------------------------- +near call $GENDAO add_proposal '{ + "proposal": { + "description": "Upgrade to new factory hash using local stored code", + "kind": { + "UpgradeRemote": { + "receiver_id": "spudnike.testnet", + "method_name": "upgrade_factory", + "hash": "'$FACTORY_HASH'" + } + } + } +}' --accountId $CONTRACT_ID --amount $BOND_AMOUNT --gas $MAX_GAS +# Vote on proposal +near call $GENDAO act_proposal '{"id": 2, "action" :"VoteApprove"}' --accountId $CONTRACT_ID --gas $MAX_GAS + +# Factory should be pointing to new hash + +# Optionally store factory metadata +export PROPOSALARGS=`echo '{"factory_hash": "'$FACTORY_HASH'", "metadata": {"version": [1,0], "commit_id": ""}, "set_default": true}' | base64` +near call $GENDAO add_proposal '{ + "proposal": { + "description": "Store factory metadata", + "kind": { + "FunctionCall": { + "receiver_id": "'$CONTRACT_ID'", + "actions": [ + { + "method_name": "store_factory_metadata", + "args": "'$PROPOSALARGS'", + "deposit": "'$BYTE_STORAGE_COST'", + "gas": "'$GAS_220_TGAS'" + } + ] + } + } + } +}' --accountId $CONTRACT_ID --amount $BOND_AMOUNT --gas $MAX_GAS + +# Vote on storing factory metadata + +near call $GENDAO act_proposal '{"id": 1, "action" :"VoteApprove"}' --accountId $CONTRACT_ID --gas $MAX_GAS +#### -------------------------------------------- diff --git a/sputnikdao-factory2/res/sputnikdao_factory2.wasm b/sputnikdao-factory2/res/sputnikdao_factory2.wasm index e11c55200..d8897cfcf 100755 Binary files a/sputnikdao-factory2/res/sputnikdao_factory2.wasm and b/sputnikdao-factory2/res/sputnikdao_factory2.wasm differ diff --git a/sputnikdao-factory2/src/lib.rs b/sputnikdao-factory2/src/lib.rs index b1f028e1f..28840487f 100644 --- a/sputnikdao-factory2/src/lib.rs +++ b/sputnikdao-factory2/src/lib.rs @@ -14,6 +14,8 @@ type Version = [u8; 2]; // The keys used for writing data to storage via `env::storage_write`. const DEFAULT_CODE_HASH_KEY: &[u8; 4] = b"CODE"; const FACTORY_OWNER_KEY: &[u8; 5] = b"OWNER"; +const DEFAULT_FACTORY_HASH_KEY: &[u8; 7] = b"FACTORY"; +const FACTORY_METADATA_KEY: &[u8; 7] = b"FACMETA"; const CODE_METADATA_KEY: &[u8; 8] = b"METADATA"; // The values used when writing initial data to the storage. @@ -39,6 +41,19 @@ pub struct DaoContractMetadata { pub changelog_url: Option, } +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Clone, Debug))] +#[serde(crate = "near_sdk::serde")] +pub struct DaoFactoryMetadata { + // version of the DAO factory code (e.g. [2, 0] -> 2.0, [3, 1] -> 3.1, [4, 0] -> 4.0) + pub version: Version, + // commit id of https://github.com/near-daos/sputnik-dao-contract + // representing a snapshot of the code that generated the wasm + pub commit_id: String, + // if available, url to the changelog to see the changes introduced in this version + pub changelog_url: Option, +} + #[near_bindgen] #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] pub struct SputnikDAOFactory { @@ -291,6 +306,10 @@ impl SputnikDAOFactory { slice_to_hash(&env::storage_read(DEFAULT_CODE_HASH_KEY).expect("Must have code hash")) } + pub fn get_default_factory_hash(&self) -> Base58CryptoHash { + slice_to_hash(&env::storage_read(DEFAULT_FACTORY_HASH_KEY).expect("Must have code hash")) + } + pub fn get_default_version(&self) -> Version { let storage_metadata = env::storage_read(CODE_METADATA_KEY).expect("INTERNAL_FAIL"); let deserialized_metadata: UnorderedMap = @@ -301,6 +320,16 @@ impl SputnikDAOFactory { default_metadata.version } + pub fn get_default_factory_version(&self) -> Version { + let storage_metadata = env::storage_read(CODE_METADATA_KEY).expect("INTERNAL_FAIL"); + let deserialized_metadata: UnorderedMap = + BorshDeserialize::try_from_slice(&storage_metadata).expect("INTERNAL_FAIL"); + let default_metadata = deserialized_metadata + .get(&self.get_default_code_hash()) + .expect("INTERNAL_FAIL"); + default_metadata.version + } + /// Returns non serialized code by given code hash. pub fn get_code(&self, code_hash: Base58CryptoHash) { self.factory_manager.get_code(code_hash); @@ -342,6 +371,42 @@ impl SputnikDAOFactory { } } + pub fn store_factory_metadata( + &self, + factory_hash: Base58CryptoHash, + metadata: DaoFactoryMetadata, + set_default: bool, + ) { + self.assert_owner(); + let hash: CryptoHash = factory_hash.into(); + assert!( + env::storage_has_key(&hash), + "Factory not found for the given factory hash. Please store the factory code first." + ); + + let storage_metadata = env::storage_read(FACTORY_METADATA_KEY); + if storage_metadata.is_none() { + let mut storage_metadata: UnorderedMap = + UnorderedMap::new(b"m".to_vec()); + storage_metadata.insert(&factory_hash, &metadata); + let serialized_metadata = + BorshSerialize::try_to_vec(&storage_metadata).expect("INTERNAL_FAIL"); + env::storage_write(FACTORY_METADATA_KEY, &serialized_metadata); + } else { + let storage_metadata = storage_metadata.expect("INTERNAL_FAIL"); + let mut deserialized_metadata: UnorderedMap = + BorshDeserialize::try_from_slice(&storage_metadata).expect("INTERNAL_FAIL"); + deserialized_metadata.insert(&factory_hash, &metadata); + let serialized_metadata = + BorshSerialize::try_to_vec(&deserialized_metadata).expect("INTERNAL_FAIL"); + env::storage_write(FACTORY_METADATA_KEY, &serialized_metadata); + } + + if set_default { + env::storage_write(DEFAULT_FACTORY_HASH_KEY, &hash); + } + } + pub fn delete_contract_metadata(&self, code_hash: Base58CryptoHash) { self.assert_owner(); let storage_metadata = env::storage_read(CODE_METADATA_KEY).expect("INTERNAL_FAIL"); @@ -353,6 +418,17 @@ impl SputnikDAOFactory { env::storage_write(CODE_METADATA_KEY, &serialized_metadata); } + pub fn delete_factory_metadata(&self, factory_hash: Base58CryptoHash) { + self.assert_owner(); + let storage_metadata = env::storage_read(FACTORY_METADATA_KEY).expect("INTERNAL_FAIL"); + let mut deserialized_metadata: UnorderedMap = + BorshDeserialize::try_from_slice(&storage_metadata).expect("INTERNAL_FAIL"); + deserialized_metadata.remove(&factory_hash); + let serialized_metadata = + BorshSerialize::try_to_vec(&deserialized_metadata).expect("INTERNAL_FAIL"); + env::storage_write(FACTORY_METADATA_KEY, &serialized_metadata); + } + pub fn get_contracts_metadata(&self) -> Vec<(Base58CryptoHash, DaoContractMetadata)> { let storage_metadata = env::storage_read(CODE_METADATA_KEY).expect("INTERNAL_FAIL"); let deserialized_metadata: UnorderedMap = @@ -360,6 +436,13 @@ impl SputnikDAOFactory { return deserialized_metadata.to_vec(); } + pub fn get_factories_metadata(&self) -> Vec<(Base58CryptoHash, DaoFactoryMetadata)> { + let storage_metadata = env::storage_read(FACTORY_METADATA_KEY).expect("INTERNAL_FAIL"); + let deserialized_metadata: UnorderedMap = + BorshDeserialize::try_from_slice(&storage_metadata).expect("INTERNAL_FAIL"); + return deserialized_metadata.to_vec(); + } + fn assert_owner(&self) { assert_eq!( self.get_owner(), @@ -392,6 +475,27 @@ pub extern "C" fn store() { ); } +/// Store new contract. Non serialized argument is the contract. +/// Returns base58 of the hash of the contract. +#[no_mangle] +pub extern "C" fn upgrade_factory() { + env::setup_panic_hook(); + let contract: SputnikDAOFactory = env::state_read().expect("Contract is not initialized"); + contract.assert_owner(); + + let current_id = env::current_account_id(); + + let input = env::input().expect("ERR_NO_INPUT"); + + // Create a promise toward given account. + let promise_id = env::promise_batch_create(¤t_id); + + // Deploy the contract code. + env::promise_batch_action_deploy_contract(promise_id, &input); + + env::promise_return(promise_id); +} + #[cfg(test)] mod tests { use near_sdk::test_utils::test_env::{alice, bob, carol};