/// Provides a way to transfer structs from one account to another in two transactions. /// Unlike many languages, Move cannot move data from one account to another with /// single-signer transactions. As of this writing, ordinary transactions can only have /// a single signer, and Move code can only store to an address (via `move_to`) if it /// can supply a reference to a signer for the destination address (there are special case /// exceptions in Genesis and DiemAccount where there can temporarily be multiple signers). /// /// Offer solves this problem by providing an `Offer` resource. To move a struct `T` from /// account A to B, account A first publishes an `Offer` resource at `address_of(A)`, /// using the `offer::create` function. /// Then account B, in a separate transaction, can move the struct `T` from the `Offer` at /// A's address to the desired destination. B accesses the resource using the `redeem` function, /// which aborts unless the `for` field is B's address (preventing other addresses from /// accessing the `T` that is intended only for B). A can also redeem the `T` value if B hasn't /// redeemed it. module std::offer { use std::signer; use std::error; /// A wrapper around value `offered` that can be claimed by the address stored in `for`. struct Offer has key { offered: Offered, for: address } /// An offer of the specified type for the account does not exist const EOFFER_DNE_FOR_ACCOUNT: u64 = 0; /// Address already has an offer of this type. const EOFFER_ALREADY_CREATED: u64 = 1; /// Address does not have an offer of this type to redeem. const EOFFER_DOES_NOT_EXIST: u64 = 2; /// Publish a value of type `Offered` under the sender's account. The value can be claimed by /// either the `for` address or the transaction sender. public fun create(account: &signer, offered: Offered, for: address) { assert!(!exists>(signer::address_of(account)), error::already_exists(EOFFER_ALREADY_CREATED)); move_to(account, Offer { offered, for }); } spec create { /// Offer a struct to the account under address `for` by /// placing the offer under the signer's address aborts_if exists>(signer::address_of(account)); ensures exists>(signer::address_of(account)); ensures global>(signer::address_of(account)) == Offer { offered: offered, for: for }; } /// Claim the value of type `Offered` published at `offer_address`. /// Only succeeds if the sender is the intended recipient stored in `for` or the original /// publisher `offer_address`. /// Also fails if there is no `Offer` published. public fun redeem(account: &signer, offer_address: address): Offered acquires Offer { assert!(exists>(offer_address), error::not_found(EOFFER_DOES_NOT_EXIST)); let Offer { offered, for } = move_from>(offer_address); let sender = signer::address_of(account); assert!(sender == for || sender == offer_address, error::invalid_argument(EOFFER_DNE_FOR_ACCOUNT)); offered } spec redeem { /// Aborts if there is no offer under `offer_address` or if the account /// cannot redeem the offer. /// Ensures that the offered struct under `offer_address` is removed. aborts_if !exists>(offer_address); aborts_if !is_allowed_recipient(offer_address, signer::address_of(account)); ensures !exists>(offer_address); ensures result == old(global>(offer_address).offered); } // Returns true if an offer of type `Offered` exists at `offer_address`. public fun exists_at(offer_address: address): bool { exists>(offer_address) } spec exists_at { aborts_if false; /// Returns whether or not an `Offer` resource is under the given address `offer_address`. ensures result == exists>(offer_address); } // Returns the address of the `Offered` type stored at `offer_address. // Fails if no such `Offer` exists. public fun address_of(offer_address: address): address acquires Offer { assert!(exists>(offer_address), error::not_found(EOFFER_DOES_NOT_EXIST)); borrow_global>(offer_address).for } spec address_of { /// Aborts is there is no offer resource `Offer` at the `offer_address`. /// Returns the address of the intended recipient of the Offer /// under the `offer_address`. aborts_if !exists>(offer_address); ensures result == global>(offer_address).for; } // ================================================================= // Module Specification spec module {} // switch documentation context back to module level /// # Access Control /// ## Creation of Offers spec schema NoOfferCreated { /// Says no offer is created for any address. Later, it is applied to all functions /// except `create` ensures forall addr: address where !old(exists>(addr)) : !exists>(addr); } spec module { /// Apply OnlyCreateCanCreateOffer to every function except `create` apply NoOfferCreated to *, * except create; } /// ## Removal of Offers spec schema NoOfferRemoved { /// Says no offer is removed for any address. Applied below to everything except `redeem` ensures forall addr: address where old(exists>(addr)) : (exists>(addr) && global>(addr) == old(global>(addr))); } spec module { /// Only `redeem` can remove an offer from the global store. apply NoOfferRemoved to *, * except redeem; } /// # Helper Functions spec module { /// Returns true if the recipient is allowed to redeem `Offer` at `offer_address` /// and false otherwise. fun is_allowed_recipient(offer_addr: address, recipient: address): bool { recipient == global>(offer_addr).for || recipient == offer_addr } } }