diff --git a/aptos/deployer/Makefile b/aptos/deployer/Makefile new file mode 100644 index 000000000..025c38ddb --- /dev/null +++ b/aptos/deployer/Makefile @@ -0,0 +1,14 @@ +-include ../../Makefile.help + +.PHONY: artifacts +artifacts: build + +.PHONY: build +## Build contract +build: + aptos move compile --save-metadata + +.PHONY: test +## Run tests +test: + aptos move test diff --git a/aptos/deployer/Move.toml b/aptos/deployer/Move.toml new file mode 100644 index 000000000..dfb2f354f --- /dev/null +++ b/aptos/deployer/Move.toml @@ -0,0 +1,14 @@ +[package] +name = 'Deployer' +version = '1.0.0' + +[dependencies.AptosFramework] +git = 'https://github.com/aptos-labs/aptos-core.git' +rev = 'main' +subdir = 'aptos-move/framework/aptos-framework' + +[dev-addresses] +deployer="0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b" + +[addresses] +deployer="_" diff --git a/aptos/deployer/sources/deployer.move b/aptos/deployer/sources/deployer.move new file mode 100644 index 000000000..dc14465ea --- /dev/null +++ b/aptos/deployer/sources/deployer.move @@ -0,0 +1,135 @@ +/// This is a deployer module that enables deploying a package to an autonomous +/// account, or a "resource account". +/// +/// By default, when an account publishes a package, the modules get deployed at +/// the deployer account's address, and retains authority over the package. +/// For applications that want to guard their upgradeability by some other +/// mechanism (such as decentralized governance), this setup is inadequate. +/// +/// The solution is to generate an autonomous account whose signer is controlled +/// by the runtime, as opposed to a private key (think program-derived addresses +/// in Solana). The package is then deployed at this address, guaranteeing that +/// effectively the program can upgrade itself in whatever way it wishes, and no +/// one else can. +/// +/// The `aptos_framework::account` module provides a way to generate such +/// resource accounts, where the account's pubkey is derived from the +/// transaction signer's account hashed together with some seed. In pseudocode: +/// +/// resource_address = sha3_256(tx_sender + seed) +/// +/// The `aptos_framework::account::create_resource_account` function creates such an +/// account, and returns a newly created signer and a `SignerCapability`, which +/// is an affine resource (i.e. can be dropped but not copied). Holding a +/// `SignerCapability` (which can be stored) grants the ability to recover the +/// signer (which cannot be stored). Thus, the program will need to hold its own +/// `SignerCapability` in storage. It is crucial that this capability is kept +/// "securely", i.e. gated behind proper access control, which essentially means +/// in a struct with a private field. This capability is retrieved and stored in +/// the module's initializer. +/// +/// So the strategy is as follows: +/// +/// 1. An account calls `deploy_derived` with the bytecode and the address seed +/// The function will create the resource account and deploy the bytecode at the +/// resource account's address. It will then temporarily lock up the +/// `SignerCapability` in `DeployingSignerCapability` together with the deployer +/// account's address. +/// +/// Then there are two options: +/// 2.a. The module has an `init_module` entry point. This is a special function +/// that gets called by the runtime immediately after the module is deployed. +/// The only argument passed to this function is the module account's signer, in +/// this case the resource account itself. The resource account can call +/// `claim_signer_capability` and retrieve the signer capability to store it in +/// storage for later. This destroys the `DeployingSignerCapability`. +/// +/// 2.b The module has a custom initializer function. This might be necessary +/// if the initializer needs additional arguments, which is not supported by +/// `init_module`. This initializer will have to be called in a separate +/// transaction (since after deploying a module, it cannot be called in the same +/// transaction). The initializer may call `claim_signer_capability` which +/// destroys the `DeployingSignerCability` and extracts the `SignerCapability` +/// from it. Note that this can _only_ be called by the deployer account. +/// +/// The `claim_signer_capability` function checks that it's called _either_ by +/// the resource account itself or the deployer of the resource account. +/// +/// 3. After the `SignerCapability` is extracted, the program can now recover +/// the signer from it and store the capability in its own storage in a secure +/// resource type. +/// +/// Note that the fact that `SignerCapability` has no copy ability means that +/// it's guaranteed to be globally unique for a given resource account (since +/// the function that creates it can only be called once as it implements replay +/// protection). Thanks to this, as long as the deployed program is successfully +/// initialized and stores its signer capability, we can be sure that only the +/// program can authorize its own upgrades. +/// +module deployer::deployer { + use aptos_framework::account; + use aptos_framework::code; + use aptos_framework::signer; + + const E_NO_DEPLOYING_SIGNER_CAPABILITY: u64 = 0; + const E_INVALID_DEPLOYER: u64 = 1; + + /// Resource for temporarily holding on to the signer capability of a newly + /// deployed program before the program claims it. + struct DeployingSignerCapability has key { + signer_cap: account::SignerCapability, + deployer: address, + } + + public entry fun deploy_derived( + deployer: &signer, + metadata_serialized: vector, + code: vector>, + seed: vector + ) acquires DeployingSignerCapability { + let deployer_address = signer::address_of(deployer); + let resource = account::create_resource_address(&deployer_address, seed); + let resource_signer: signer; + if (exists(resource)) { + // if the deploying signer capability already exists, it means that + // the resource account hasn't claimed it. This code path allows the + // deployer to upgrade the resource account's contract, but only + // before the resource account is initialised. + // You might think that this is a very niche use-case, but this + // happened when trying to deploy wormhole to aptos testnet, as the + // bytecode we published had been compiled with an older version of + // the stdlib, and had native dependency issues. These are checked + // lazily (i.e. at runtime, and not deployment time), which meant + // that the contract was effectively broken, i.e. unable to + // initialise itself, and therefore unable to upgrade. + let deploying_cap = borrow_global(resource); + resource_signer = account::create_signer_with_capability(&deploying_cap.signer_cap); + } else { + // if it doesn't exist, it means that either + // a) the account hasn't been created yet at all + // b) the account has already claimed the signer capability + // + // in the case of a), we just create it. In case of b), the account + // creation will fail, since the resource account already exist, + // effectively providing replay protection. + let signer_cap: account::SignerCapability; + (resource_signer, signer_cap) = account::create_resource_account(deployer, seed); + move_to(&resource_signer, DeployingSignerCapability { signer_cap, deployer: deployer_address }); + }; + code::publish_package_txn(&resource_signer, metadata_serialized, code); + } + + public fun claim_signer_capability( + caller: &signer, + resource: address + ): account::SignerCapability acquires DeployingSignerCapability { + assert!(exists(resource), E_NO_DEPLOYING_SIGNER_CAPABILITY); + let DeployingSignerCapability { signer_cap, deployer } = move_from(resource); + let caller_addr = signer::address_of(caller); + assert!( + caller_addr == deployer || caller_addr == resource, + E_INVALID_DEPLOYER + ); + signer_cap + } +}