FuelLabs / sway

🌴 Empowering everyone to build reliable and efficient smart contracts.
https://docs.fuel.network/docs/sway/
Apache License 2.0
62.79k stars 5.36k forks source link

Contract storage using code gen macro pattern #396

Closed SilentCicero closed 2 years ago

SilentCicero commented 2 years ago

Motivation

We would like a nice way to define contract storage layout, methods etc. We don't want to go too far from the rustish way of doing things. Solidity offers a "batteries includes" approach with storage whereby storage is affected by variable definition and use. While this is kinda nice, it becomes harmful as it's hard to differentiate between what is an in memory variable and an in storage variable.

This is simply meant to open up a bike shed discussion around storage for the interim.

Solution

Below is an idea for contract storage using a generator / macro like pattern.

Note, I introduce a kind of odd mapping method / concept here, where by an interpreted function signature is used to build the hash mapping. Not sure if this is a very rustish approach.

What I like about this pattern is it's a generator pattern, and all it does is generate a nice set of methods for a contract to use.

contract;

// We import the storage macro and mapping type generator.
use std::storage::*;

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

/// ABI definition for a subcurrency.
abi Token {
    // Make the balances function public.
    fn balances(account: b256) -> (balance: u64);

    // Mint new tokens and send to an address.
    // Can only be called by the contract creator.
    fn mint(receiver: b256, amount: u64);

    // Sends an amount of an existing token.
    // Can be called from any address.
    fn send(sender: b256, receiver: b256, amount: u64);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

/// The storage layout which will generate the necessary functions for storage.
/// You can think of this as essentially a proc macro like generator for storage.
/// The notation is similar to the `abi` and `struct` Rust notation.
storage Token {
    // The balances is a hash mapping, intakes fn sig, outputs mapping methods.
    balances: mapping(fn balances(account: b256) -> (balance: u64));
}

/*
// The generated set method.
fn set_balances(account: b256, balance: u64) {
    store(hash_pair(0, account, HashMethod::Sha256), balance);
}

// The generated get method.
fn get_balances(account: b256) -> (balance: u64) {
    get(hash_pair(0, account, HashMethod::Sha256), balance);
}
*/

////////////////////////////////////////
// Constants
////////////////////////////////////////

/// Address of contract creator.
const minter: b256 = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b;

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

/// Contract implements the `Token` ABI.
impl Token for Contract {
    fn balances(account: b256) -> (balance: u64) {
        get_balances(account)
    }

    fn mint(receiver: b256, amount: u64) -> bool {
        // Note: authentication is not yet implemented, for now just trust params
        if receiver == minter {
            let mut amount = get_balances(minter);
            amount = amount + amount;
            set_balances(minter, amount);

            true;
        } else {
            revert(0);
        }
    }

    fn send(sender: b256, receiver: b256, amount: u64) -> bool {
        let mut sender_amount = get_balances(sender);
        sender_amount = sender_amount - amount;
        set_balances(sender, sender_amount);

        let mut receiver_amount = get_balances(receiver);
        receiver_amount = receiver_amount + amount;
        set_balances(receiver, receiver_amount);

        true;
    }
}
sezna commented 2 years ago

related ish: #7

sezna commented 2 years ago

I like the explicit nature of what you're proposing. A couple of comments.

Rust tools generally avoid introducing things automatically into your scope, since that could lead to symbol ambiguity. In that spirit, instead of generating get_* and set_* methods, we could have storage.<data>.read() or .write(), referencing perhaps Rust's RwLock or similar for API inspiration.

storage Token {
    // The balances is a hash mapping, intakes fn sig, outputs mapping methods.
    balances: mapping(fn balances(account: b256) -> (balance: u64));
}

If we do the above, we no longer need mapping. We can just say HashMap<b256, b256> here, which is a bit more clear, and the user can actually store data that isn't a mapping/hashmap if they want to.

SilentCicero commented 2 years ago

@sezna so something like this would be more Rustish?

contract;

// We import the storage macro and mapping type generator.
use std::storage::*;

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

/// ABI definition for a subcurrency.
abi Token {
    // Make the balances function public.
    fn balances(account: b256) -> (balance: u64);

    // Mint new tokens and send to an address.
    // Can only be called by the contract creator.
    fn mint(receiver: b256, amount: u64);

    // Sends an amount of an existing token.
    // Can be called from any address.
    fn send(sender: b256, receiver: b256, amount: u64);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

/// The storage layout which will generate the necessary functions for storage.
/// You can think of this as essentially a proc macro like generator for storage.
/// The notation is similar to the `abi` and `struct` Rust notation.
storage {
    // The balances is a hash mapping, intakes fn sig, outputs mapping methods.
    balances: HashMap<b256, u64>;
}

////////////////////////////////////////
// Constants
////////////////////////////////////////

/// Address of contract creator.
const minter: b256 = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b;

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

/// Contract implements the `Token` ABI.
impl Token for Contract {
    fn balances(account: b256) -> (balance: u64) {
        storage.balances.read(account)
    }

    fn mint(receiver: b256, amount: u64) -> bool {
        // Note: authentication is not yet implemented, for now just trust params
        if receiver == minter {
            let mut amount = storage.balances.read(minter);
            amount = amount + amount;
            storage.balances.write(minter, amount);

            true;
        } else {
            revert(0);
        }
    }

    fn send(sender: b256, receiver: b256, amount: u64) -> bool {
        let mut sender_amount = storage.balances.read(sender);
        sender_amount = sender_amount - amount;
        storage.balances.write(sender, sender_amount);

        let mut receiver_amount = storage.balances.read(receiver);
        receiver_amount = receiver_amount + amount;
        storage.balances.write(receiver, receiver_amount);

        true;
    }
}
wolflo commented 2 years ago

I like both methods, but I think using storage.state_var.store(val) or similar strikes a good balance between explicitness and readability. Vyper uses a similar approach to great effect with self.state_var = val.

Another positive is that storage.state_var.store(val) avoids looking like just another method, and it seems less likely to get buried far away from the call site because of it's succinctness. We could go so far as to introduce special notation for storage-changing operations in methods, e.g. !: store!(slot, val) and requiring that any method containing a store!() be called with as storage_changing_method!().

SilentCicero commented 2 years ago

@wolflo yes, you could use and put all sorts of conditional logic or interventions in this pattern.

I think it just allows a smart-contract developer some decent facility which takes care of the hash and position management. Without polluting our grammar or introducing any high baggage concepts.

Ultimately we could do a variable notation style eventually, similar to Vyper, but I almost feel that's not a very Rustish idea.

As well, when Sway gets macro facility, it can be implemented with that and removed from the deeper compiler. So for now it's compile level, but eventually it would be macro level with an import.

nfurfaro commented 2 years ago

A few thoughts... First, I agree that it's worth making storage read & write ops slightly more verbose in exchange for making these operations more explicit and differentiated from working with in-memory variables.

So something like storage.my_value.read()/storage.my_value.write() seems pretty good from a developer's perspective. Not overly verbose, but very clear what the intent is, and super easy to search for usage of "storage" in a contract.

As for mappings/hashMaps, I think @adlerjohn mentioned that we could eventually have both storage-backed and in-memory mappings... in which case we may want to differentiate via naming, ie: mapping is storage-backed, and hashmap is in memory. That said, accessing the 2 different types would be different enough already(with the need to use storage.my_hash_map.read(val) making it quite clear this is a storage-backed map as opposed to an in-memory one.

As far as ergonomics go, it would be nice if:

SilentCicero commented 2 years ago
contract;

use std::hash::*;
use std::storage::*;
use std::block::*;

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

abi BlindAuction {
    fn highestBidder() -> (bidder: b256);

    fn commit(hash: b256) -> (success: bool);

    fn reveal(sender: b256, salt: b256, amount: u64) -> (success: bool);

    fn withdraw(sender: b256) -> (success: bool);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

storage {
    highestBidder: b256;

    highestBid: u64;

    commits: HashMap<b256, bool>;
}

// When the commit stage ends.
const commitEnds = 5;

/// Block number the auction will end.
const endInBlocks = 10;

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

impl BlindAuction for Contract {
    fn highestBidder() -> b256 {
        storage.highestBidder.read()
    }

    fn commit(hash: b256) -> bool {
        assert(blockHeight() < commitEnds);    

        storage.commits.write().insert(hash, true);

        true;
    }

    fn reveal(sender: b256, salt: b256, amount: u64) -> bool {
        assert(blockHeight() >= commitEnds);
        assert(blockHeight() < endInBlocks);

        let sub_digest = hash_pair(salt, sender, HashMethod::sha256);
        let digest = hash_pair(sub_digest, amount, HashMethod::sha256);
        let commitmentAvailable = storage.commits.read().get(digest);

        assert(commitmentAvailable);

        let highestBid = storage.highestBid.read();

        assert(amount > highestBid);

        storage.highestBid.write() = amount;
        storage.highestBidder.write() = sender;

        true;
    }

    fn withdraw(sender: b256) -> bool {
        let highestBidder = storage.highestBidder.read();

        assert(highestBidder == sender);

        let highestBid = storage.highestBid.read();

        assert(highestBid > 0);
        assert(blockHeight() >= endInBlocks);

        transfer(sender, highestBid);

        true;
    }
}

@nfurfaro just an idea of what an NFT would look like.

nfurfaro commented 2 years ago

@SilentCicero nice. I've been thinking about NFT's. We had discussed possibly renaming the token color to token_id or similar. But, I think we should reserve token_id for usage in the context of NFTs as you did above. color could be renamed to token_type perhaps...

SilentCicero commented 2 years ago

@nfurfaro yeah, in this case tokenId was just taken from an NFT example. I don't mind either so long as all uses are consistent. I don't think it precludes use in the NFT's themselves.

SilentCicero commented 2 years ago

@sezna do you have any further hesitations about actually pursuing the storage design above at this stage?

Pros: I think it's actually fairly doable, it can be completed in stages, has little grammar overhead, can be removed and redesigned at a higher level when we have a proper macro system, cleans up the storage notation tremendously.

emilyaherbert commented 2 years ago

The last iteration of the grammar looks good to me, for all the reasons that you mentioned above. As you mentioned we will likely definitely want to take another pass at it when the macro system is implemented.

My only concern is what @nfurfaro mentioned about @adlerjohn 's comment. I am generally against using naming schematics to differentiate between important concepts. i.e. I trust that Fuel's Sway developers will be able to keep naming schematics in line, but I don't trust the larger Sway developer community to 1) keep to naming schematics, nor 2) even have a strong desire to keep to naming schematics. I am in favor of a grammar-level difference between the two, although I am not sure what that should be yet. We should probably just keep this discussion open as we implement the initial concept though.

otrho commented 2 years ago

I think Alex's initial proposal was slightly different to what you've gone with there. I think he's saying you could put any value of any type into the storage 'area'; HashMap if you need a map, u64 if you just need a global number.

The read() or write() methods are generic accessors to storage, essentially providing immutable or mutable references respectively. Then the type you're accessing may have further methods for manipulation.

So if HashMap had insert() and contains_key() methods, you'd use:

storage {
    bals: HashMap<b256, u64>;
    quote_of_the_day: str,
}

// Initialise balance. 
if !storage.bals.read().contains_key(alice_addr) {
    storage.bals.write().insert(alice_addr, 100);
}

// Update quote.
storage.quote_of_the_day.write() = "Behind every great man is a woman rolling her eyes.  -- Jim Carrey.";

Maybe read() and write() are the wrong names here, and it looks a little clumsy. But the overall idea is any data structure could be used, the storage API means any writes are persisted.

sezna commented 2 years ago

Correct, what I was proposing is more generalized. So, for example, Rust's RwLock is just a wrapper that provides a gate to its interior mutability via write(), which takes no arguments. In Rust, this is used for synchronization to ensure that either one or more things are currently reading the value, xor it is being written to by exactly one thing. In Sway, this could be used to generalize persistent storage and perhaps even introduce a cache in the read() method.

Toby's code example is basically what I was suggesting.

SilentCicero commented 2 years ago

@sezna that's totally fine by me. The above example makes total sense, I now understand the syntax you are gong for.

@otrho completely fine with your example, I think that looks good.

@sezna for the HashMap, I was just suggesting in my commented section one way to do the internal key design using a position and a the necessary key item.

@emilyaherbert I feel at this stage after reviewing Tobys suggestion that the above for now would be completely fine. Having these things clearly marked off as storage is really helpful in understanding the cost or use of things.

The above notation I'm seeing I really like, I'd like to push to move us closer toward implementing this if we don't have any major outstanding complaints at this stage.

I think this would really moves things a long much faster having this storage module in Sway.

SilentCicero commented 2 years ago
contract;

// We import the storage macro and mapping type generator.
use std::storage::*;

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

/// ABI definition for a subcurrency.
abi Token {
    // Make the balances function public.
    fn balanceOf(account: b256) -> (balance: u64);

    // Mint new tokens and send to an address.
    // Can only be called by the contract creator.
    fn mint(receiver: b256, amount: u64) -> (success: bool);

    // Sends an amount of an existing token.
    // Can be called from any address.
    fn send(sender: b256, receiver: b256, amount: u64) -> (success: bool);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

/// The storage layout which will generate the necessary functions for storage.
/// You can think of this as essentially a proc macro like generator for storage.
/// The notation is similar to the `abi` and `struct` Rust notation.
storage {
    // The balances is a hash mapping, intakes fn sig, outputs mapping methods.
    balances: HashMap<b256, u64>;
}

////////////////////////////////////////
// Constants
////////////////////////////////////////

/// Address of contract creator.
const minter: b256 = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b;

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

/// Contract implements the `Token` ABI.
impl Token for Contract {
    fn balanceOf(account: b256) -> (balance: u64) {
        storage.balances.read().get(account)
    }

    fn mint(receiver: b256, amount: u64) -> bool {
        assert(receiver == minter);

        // Note: authentication is not yet implemented, for now just trust params
        let mut amount = storage.balances.read().get(minter);
        amount = amount + amount;
        storage.balances.write().insert(minter, amount);

        true;
    }

    fn send(sender: b256, receiver: b256, amount: u64) -> bool {
        let mut sender_amount = storage.balances.read().get(sender);

        assert(sender_amount >= amount);

        sender_amount = sender_amount - amount;
        storage.balances.write().insert(sender, sender_amount);

        let mut receiver_amount = storage.balances.read().get(receiver);
        receiver_amount = receiver_amount + amount;
        storage.balances.write().insert(receiver, receiver_amount);

        true;
    }
}
SilentCicero commented 2 years ago
contract;

use std::hash::*;
use std::storage::*;
use std::block::*;
use std::token::*;
use std::contract::*;

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

abi UniswapV1 {
    fn deposit(gas: u64, amount: u64, color: b256, sender: b256) -> (success: bool);

    fn add_liquidity(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        min_liquidity: u64,
        max_tokens: u64,
        deadline: u64,
    ) -> (liquidity: u64);

    fn removeLiquidity(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        min_tokenA: u64,
        min_tokenB: u64,
        deadline: u64,
    ) -> (tokenA: u64, tokenB: u64);

    fn swap_with_minimum(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        min_tokens: u64,
        deadline: u64) -> (tokens_recieved: u64);

    fn swap_with_maximum(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        max_tokens: u64,
        deadline: u64) -> (tokens_recieved: u64);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

storage {
    deposits: HashMap<b256, u64>;
}

const tokenA = b256(0);
const tokenB = b256(1);

const minimumLiquidity = 100;

fn deposit_key(sender: b256, color: b256) -> b256 {
    hash_pair(sender, color, HashMethod::sha256);
}

fn get_input_price(
    input_amount: u64,
    input_reserve: u64,
    output_reserve: u64,
) -> u64 {
    assert(input_reserve > 0 && output_reserve > 0);
    let input_amount_with_fee = input_amount * 997;
    let numerator = input_amount_with_fee * output_reserve;
    let denominator = (input_reserve * 1000) * input_amount_with_fee;
    numerator / denominator
}

fn get_output_price(
    output_amount: u64,
    input_reserve: u64,
    output_reserve: u64,
) -> u64 {
    assert(input_reserve > 0 && output_reserve > 0);
    let numerator = input_reserve * output_reserve * 1000;
    let denominator = (output_reserve - output_amount) * 997;
    numerator / denominator + 1
}

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

impl UniswapV1 for Contract {
    fn deposit(gas: u64, amount: u64, color: b256, sender: b256) {
        assert(color == tokenA || color == tokenB);

        let key = deposit_key(sender, color);
        let totalAmount = storage.deposits.read().get(key) + amount;

        storage.deposits.write().insert(key, totalAmount);

        true
    }

    fn add_liquidity(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        min_liquidity: u64,
        max_tokens: u64,
        deadline: u64,
    ) -> (liquidity: u64) {
        assert(color == tokenA || color == tokenB);
        assert(deadline > block_height());

        let total_liquidity = balance(address(), address());

        let mut tokenADeposit = 0;
        let mut tokenBDeposit = 0;

        if color == tokenA {
            let depositBKey = deposit_key(sender, tokenB);
            tokenADeposit = amount;
            tokenBDeposit = storage.deposits.read().get(depositBKey);
            storage.deposits.write().clear(depositBKey);
        } else {
            let depositAKey = deposit_key(sender, tokenA);
            tokenADeposit = storage.deposits.read().get(depositAKey);
            tokenBDeposit = amount;
            storage.deposits.write().clear(depositAKey);
        }

        if total_liquidity > 0 {
            assert(min_liquidity > 0);

            let tokenAReserve = balance(address(), tokenA);
            let tokenBReserve = balance(address(), tokenB);

            let total_amount = tokenADeposit * tokenBReserve / tokenAReserve + 1;
            let liquidity_minted = tokenADeposit * total_liquidity / tokenAReserve;

            assert(max_tokens >= tokenBReserve && liquidity_minted >= min_liquidity);

            mint(liquidity_minted);
            transfer_to_output(address(), liquidity_minted, sender, 0);

            return liquidity_minted;
        } else {
            assert(tokenADeposit > minimumLiquidity);

            let token_amount = max_tokens;
            let initial_liquidity = balance(address(), tokenA);

            mint(initial_liquidity);
            transfer_to_output(address(), initial_liquidity, sender, 0);

            return initial_liquidity;
        }
    }

    fn removeLiquidity(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        min_tokenA: u64,
        min_tokenB: u64,
        deadline: u64,
    ) -> (tokenA: u64, tokenB: u64) {
        assert((amount > 0 && deadline > block_height()) && (min_tokenA > 0 and min_tokenB > 0));
        assert(color == address());

        let total_liquidity = balance(address(), address());

        assert(total_liquidity > 0);
        let tokenAReserve = balance(address(), tokenA);
        let tokenBReserve = balance(address(), tokenB);

        let tokenA_amount = amount * tokenAReserve / total_liquidity;
        let tokenB_amount = amount * tokenBReserve / total_liquidity;

        assert(tokenA_amount >= min_tokenA && tokenB_amount >= min_tokenB);

        burn(amount);

        transfer_to_output(tokenA, tokenA_amount, sender, 0);
        transfer_to_output(tokenB, tokenB_amount, sender, 1);

        tokenA = tokenA_amount;
        tokenB = tokenB_amount;
    }

    fn swap_with_minimum(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        min_tokens: u64,
        deadline: u64,
    ) -> u64 {
        assert(deadline >= block_height() && (amount > 0 && min_tokens > 0));
        assert(color == tokenA || color == tokenB);
        let tokenAReserve = balance(address(), tokenA);
        let tokenBReserve = balance(address(), tokenB);

        let mut tokens_bought = 0;

        if (color == tokenA) {
            tokens_bought = get_input_price(amount, tokenAReserve, tokenBReserve);

            assert(tokens_bought >= min_tokens);

            transfer_to_output(tokenB, tokens_bought, sender, 0);
        } else {
            tokens_bought = get_input_price(amount, tokenBReserve, tokenAReserve);

            assert(tokens_bought >= min_tokens);

            transfer_to_output(tokenA, tokens_bought, sender, 0);
        }

        tokens_bought
    }

    fn swap_with_maximum(
        gas: u64,
        amount: u64,
        color: b256,
        sender: b256,
        max_tokens: u64,
        deadline: u64,
    ) -> u64 {
        assert(deadline >= block_height() && (amount > 0 && max_tokens > 0));
        assert(color == tokenA || color == tokenB);
        let tokenAReserve = balance(address(), tokenA);
        let tokenBReserve = balance(address(), tokenB);
        let mut tokens_bought = 0;

        if (color == tokenA) {
            tokens_bought = getOutputPrice(amount, tokenAReserve, tokenBReserve);

            let refund = max_tokens - tokens_bought;
            if refund > 0 {
                transfer_to_output(tokenA, refund, recipient, 1);
            }

            transfer_to_output(tokenB, tokens_bought, sender, 0);
        } else {
            tokens_bought = getOutputPrice(amount, tokenBReserve, tokenAReserve);

            let refund = max_tokens - tokens_bought;
            if refund > 0 {
                transfer_to_output(tokenB, refund, recipient, 1);
            }

            transfer_to_output(tokenA, tokens_bought, sender, 0);
        }

        tokens_bought
    }
}

/*
Methods that dont exist:
Contract:
address(),

Block:
block_height(),

Storage:
storage,
storage.[hashmap].clear

Tokens:
balance(a, b),
mint,
burn,
transfer_to_output(a,b,c,d),

Liquidity Providing Script:
`deposit` token A
`add_liquidity` with token B recieve LP token

Liquidity Removal Script:
`remove_liquidity` with LP token recieve token A and B

Swap Token with Minimum
`swap_with_minimum` input token X recieve token Y with minimum

Swap Token with Minimum
`swap_with_maximum` input token X recieve token Y with maximum
*/
SilentCicero commented 2 years ago
contract;

// We import the storage macro and types.
use std::storage::*;
use std::asserts::*;

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

/// ABI definition for a subcurrency.
abi NonFungibleToken {
    // The owner of the token.
    fn ownerOf(tokenId: b256) -> (owner: b256);

    // Burn tokens.
    fn burn(receiver: b256, tokenId: b256) -> bool;

    // Mint new tokens and send to an address.
    // Can only be called by the contract creator.
    fn mint(receiver: b256, to: b256, tokenId: b256) -> (status: bool);

    // Sends an amount of an existing token.
    // Can be called from any address.
    fn transfer(sender: b256, receiver: b256, tokenId: b256) -> (status: bool);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

/// The storage layout which will generate the necessary functions for storage.
/// You can think of this as essentially a proc macro like generator for storage.
/// The notation is similar to the `abi` and `struct` Rust notation.
storage {
    owners: HashMap<b256, b256>;
}

////////////////////////////////////////
// Constants
////////////////////////////////////////

/// Address of contract creator.
const minter: b256 = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b;

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

/// Contract implements the `Token` ABI.
impl NonFungibleToken for Contract {
    fn ownerOf(tokenId: b256) -> (owner: b256) {
        storage.owners.read().get(tokenId)
    }

    fn mint(receiver: b256, to: b256, tokenId: b256) -> bool {
        let mut owner = storage.owners.read().get(tokenId);

        assert(receiver == minter && owner == b256(0));

        // Note: authentication is not yet implemented, for now just trust params.
        storage.owners.write().insert(tokenId, to);

        true
    }

    fn burn(receiver: b256, tokenId: b256) -> bool {
        let mut owner = storage.owners.read().get(tokenId);

        assert(receiver == minter);

        // Note: authentication is not yet implemented, for now just trust params.
        storage.owners.write().clear(tokenId);

        true
    }

    fn transfer(sender: b256, receiver: b256, tokenId: b256) -> bool {
        let mut owner = storage.owners.read().get(tokenId);

        assert(sender == owner);

        // Note: authentication is not yet implemented, for now just trust params.
        storage.owners.write().insert(tokenId, receiver);

        true
    }
}
SilentCicero commented 2 years ago
contract;

use std::storage::*;
use std::assert::*;
use std::hash::*;
use std::witness::*;

enum ProposalTypes {
    ChangeThreshold,
    AddOwner,
    TransferToOutput,
}

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

abi DAO {
    fn info() -> (nonce: u64, threshold: u64);

    fn change_threshold(threshold: u64) -> bool;

    fn add_owner(owner: b256, weight: u64) -> bool;

    fn transfer_to_output(color: b256, receiver: b256, amount: u64) -> bool;
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

storage {
    threshold: u64;

    nonce: u64;

    owners: HashMap<b256, u64>;
}

////////////////////////////////////////
// Helper methods
////////////////////////////////////////

fn proposal_builder(kind: ProposalTypes, data: b256) -> (proposal: b256) {
    let nonce = storage.nonce.read();
    let base = hash_pair(kind, nonce, HashMethod::sha256)
    hash_pair(base, data, HashMethod::sha256)
}

fn proposal_passed(kind: ProposalTypes, data: b256) -> bool {
    let proposal = proposal_builder(kind, data);
    let threshold = storage.threshold.read();
    let second_witness:bytes = witness(0);

    let mut proposal_weight = 0;
    let mut index = 0;

    while proposal_weight < threshold {
        let owner = ec_recover(proposal, b512(second_witness[index, index + 65]));
        let owner_weight = storage.owners.read().get(owner);

        proposal_weight = proposal_weight + owner_weight;
        index = index + 1;
    }

    true
}

fn clear_proposal() -> bool {
    let nonce = storage.nonce.read();
    storage.nonce.write() = nonce + 1;
    true
}

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

impl DAO for Contract {
    fn info() -> (u64, u64) {
        let nonce = storage.nonce.read();
        let threshold = storage.threshold.read();

        (nonce, threshold)
    }

    fn change_threshold(threshold: u64) -> bool {
        assert(proposal_passed(ProposalTypes.ChangeThreshold, hash(threshold)));

        storae.threshold.write() = threshold;
        clear_proposal()
    }

    fn add_owner(owner: b256, weight: u64) -> bool {
        assert(proposal_passed(ProposalTypes.AddOwner, hash_pair(owner, weight, HashMethod::sha256)));

        storage.proposals.write().insert(owner, weight);
        clear_proposal()
    }

    fn transfer_to_output(color: b256, receiver: b256, amount: u64) -> bool {
        let base = hash_pair(color, receiver, HashMethod::sha256);

        assert(proposal_passed(ProposalTypes.TransferToOutput, hash_pair(base, amount, HashMethod::sha256)));

        transfer_to_output(color, receiver, amount);
        clear_proposal()
    }
}
SilentCicero commented 2 years ago
contract;

use std::storage::*;
use std::assert::*;
use std::hash::*;
use std::witness::*;

enum ProposalTypes {
    ChangeThreshold,
    AddOwner,
    TransferToOutput,
}

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

abi TokenDAO {
    fn info() -> (nonce: u64, threshold: u64);

    fn lock(gas: u64, color: u64, amount: u64, receiver: b256) -> bool;

    fn unlock(receiver: b256) -> bool;

    fn change_threshold(threshold: u64) -> bool;

    fn transfer_to_output(color: b256, receiver: b256, amount: u64) -> bool;
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

storage {
    threshold: u64;

    nonce: u64;

    owners: HashMap<b256, u64>;
}

////////////////////////////////////////
// Helper methods
////////////////////////////////////////

fn proposal_builder(kind: ProposalTypes, data: b256) -> (proposal: b256) {
    let nonce = storage.nonce.read();
    let base = hash_pair(kind, nonce, HashMethod::sha256)
    hash_pair(base, data, HashMethod::sha256)
}

fn proposal_passed(kind: ProposalTypes, data: b256) -> bool {
    let proposal = proposal_builder(kind, data);
    let threshold = storage.threshold.read();
    let second_witness:bytes = witness(0);

    let mut proposal_weight = 0;
    let mut index = 0;

    while proposal_weight < threshold {
        let owner = ec_recover(proposal, b512(second_witness[index, index + 65]));
        let owner_weight = storage.owners.read().get(owner);

        proposal_weight = proposal_weight + owner_weight;
        index = index + 1;
    }

    true
}

fn clear_proposal() -> bool {
    let nonce = storage.nonce.read();
    storage.nonce.write() = nonce + 1;
    true
}

////////////////////////////////////////
// Constants
////////////////////////////////////////

const voting_token_color = b256(0);

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

impl TokenDAO for Contract {
    fn info() -> (u64, u64) {
        let nonce = storage.nonce.read();
        let threshold = storage.threshold.read();

        (nonce, threshold)
    }

    fn lock(gas: u64, color: u64, amount: u64, receiver: b256) -> bool {
        assert(color == voting_token_color);

        let owner_weight = storage.owners.read().get(receiver);
        storage.owners.write().insert(receiver, owner_weight + amount);

        true
    }

    fn unlock(receiver: b256) -> bool {
        let owner_weight = storage.owners.read().get(receiver);

        transfer_to_output(voting_token_color, receiver, owner_weight);

        true
    }

    fn change_threshold(threshold: u64) -> bool {
        assert(proposal_passed(ProposalTypes.ChangeThreshold, hash(threshold)));

        storae.threshold.write() = threshold;
        clear_proposal()
    }

    fn transfer_to_output(color: b256, receiver: b256, amount: u64) -> bool {
        let base = hash_pair(color, receiver, HashMethod::sha256);

        assert(proposal_passed(ProposalTypes.TransferToOutput, hash_pair(base, amount, HashMethod::sha256)));

        transfer_to_output(color, receiver, amount);
        clear_proposal()
    }
}
nfurfaro commented 2 years ago

I wonder if we can simplify the reading of storage mappings. so instead of this: let owner_weight = storage.owners.read().get(receiver); perhaps we could just do let owner_weight = storage.owners(receiver).read();

Going a step further, we could make the .read() the default when using the storage word, and only requires specifying the operation when writing, though I suppose making things very explicit has it's benefits even with some added verbosity.:

let nonce = storage.nonce;

// passing the key directly vs calling .read().get()
let owner_weight = storage.owners(receiver);

storage.owners(receiver).write() = 42;
nfurfaro commented 2 years ago

How should we handle mappings/hashmaps (still not sure what our naming convention will be for these) where the key is a wrapper-type? for example, the address type is currently implemented as:

pub struct Address {
    value: b256,
}

If we want to use an address as a key like so: balances: HashMap<Address, u64>; , would the mapping implementation hash the entire Address struct as a key when it's actually only the inner value we care about?

sezna commented 2 years ago

The size of address is equal to the size of its inner value, so that's actually the same thing. Structs have no overhead, they are just the aggregate size of the containing fields. So, balances: HashMap<Address, u64> would work the same as if you used the inner type.

sezna commented 2 years ago

I wonder if we can simplify the reading of storage mappings. so instead of this: let owner_weight = storage.owners.read().get(receiver); perhaps we could just do let owner_weight = storage.owners(receiver).read();

Going a step further, we could make the .read() the default when using the storage word, and only requires specifying the operation when writing, though I suppose making things very explicit has it's benefits even with some added verbosity.:

let nonce = storage.nonce;

// passing the key directly vs calling .read().get()
let owner_weight = storage.owners(receiver);

storage.owners(receiver).write() = 42;

This assumes that it is a mapping, but what if you are storing something that isn't a mapping?

nfurfaro commented 2 years ago

@sezna Yes, my original question was:

I wonder if we can simplify the reading of storage mappings...

from this: storage.owners.read().get(receiver); to this: storage.owners(receiver);

For a non-mapping storage value, this would be fine: let nonce = storage.nonce;

sezna commented 2 years ago

Hm, that would require some compiler or macro magic since types and functions are strictly typed, we can't change signatures on the fly. But we could use a macro, eventually, to say "hey if this is a hashmap, go ahead and implement this short-circuit method". But macros are not implemented yet

SilentCicero commented 2 years ago

This is a fun one, map any address registered to a unique number. Indexing does the rest.

contract;

use std::storage::*;
use std::logs::*;
use std::auth::*;

enum Events {
    AddressRegistered
}

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

abi NoncENS {
    fn register(owner: Address) -> (address_id: u64);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

storage {
    counter: u64;
}

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

impl NoncENS for Contract {
    // Give each address registered a unique number.
    fn register(owner: Address) -> (u64) {
        // Read the current nonce.
        let nonce = storage.nonce.read();

        // Give the address a nonce.
        let address_id = nonce;

        // Update the new nonce.
        storage.nonce.write() = nonce + 1;

        // Log the address ID with the address for indexing.
        log_data(Events.AddressRegistered, address_id, owner);

        // Return the new address id.
        address_id
    }
}
SilentCicero commented 2 years ago

MasterChef concept.

contract;

use std::storage::*;
use std::logs::*;
use std::auth::*;
use std::token::*;
use std::block::*;

////////////////////////////////////////
// Struct declarations
////////////////////////////////////////

struct Pool {
    lpToken: Address;
    allocPoint: u64; // How many allocation points assigned to this pool. SUSHIs to distribute per block.
    lastRewardBlock: u64; // Last block number that SUSHIs distribution occurs.
    accSushiPerShare: u64;
}

struct User {
    amount: u64; // How many LP tokens the user has provided.
    rewardDebt: u64; // Reward debt. See explanation below.
}

////////////////////////////////////////
// ABI declarations
////////////////////////////////////////

abi MasterChef {
    fn deposit(pool_id: u64);
    fn withdraw(pool_id: u64, amount: u64);
    fn update_pool(pool_id: u64);
}

////////////////////////////////////////
// Storage declarations
////////////////////////////////////////

storage {
    num_pools: u64;

    pools: HashMap<u64, Pool>;

    users: HashMap<(u64, Address), User>;
}

////////////////////////////////////////
// Constant definitions
////////////////////////////////////////

const divisor = 1e12; // Might need to be revised for u64 numbers.

const dev_address = 0x;

const BONUS_MULTIPLIER = 10; // Might need to be revised for u64 numbers.

////////////////////////////////////////
// ABI definitions
////////////////////////////////////////

fn get_multiplier(from: u64, to: u64) -> u64 {
    let bonusEndBlock = storage.bonusEndBlock.read();

    if to <= bonusEndBlock {
        (to - from) * BONUS_MULTIPLIER;
    } else {
        if from >= bonusEndBlock {
            to - from
        } else {
            ((bonusEndBlock - from) * BONUS_MULTIPLIER) + (to - bonusEndBlock);
        }
    }
}

impl MasterChef for Contract {
    fn update_pool(pool_id: u64) {
        let pool = storage.pools.read().get(pool_id);

         if block_number() <= pool.lastRewardBlock {
            return;
        }

        let lpSupply = balance(pool.lpToken, address());

        if lpSupply == 0 {
            pool.lastRewardBlock = block_number();
            return;
        }

        let multiplier = get_multiplier(pool.lastRewardBlock, block_number());

        let sushiReward = (multiplier * sushiPerBlock * pool.allocPoint) 
            / totalAllocPoint;

        mint(dev_address, sushiReward / 10);

        pool.accSushiPerShare = pool.accSushiPerShare + (sushiReward * divisor)
            / lpSupply;
        pool.lastRewardBlock = block_number();

        storage.pools.write().insert(pool_id, pool);
    }

    fn deposit(pool_id: u64) {
        let pool = storage.pools.read().get(pool_id);
        let user = storage.users.read().get((pool_id, sender()));
        assert(pool.lpToken == token_id());

        update_pool(pool_id);

        if user.amount > 0 {
            let pending = (
                (user.amount * pool.accSushiPerShare) / divisor
            ) - user.rewardDebt;

            mint(sender(), pending);
        }

        user.amount = user.amount + amount();
        user.rewardDebt = (user.amount * pool.accSushiPerShare) / divisor;

        storage.users.write().insert((pool_id, sender()), user);
    }

    fn withdraw(pool_id: u64, amount: u64) {
        let pool = storage.pools.read().get(pool_id);
        let user = storage.users.read().get((pool_id, sender()));

        assert(user.amount >= amount);

        update_pool(pool_id);

        let pending = (
            (user.amount * pool.accSushiPerShare) / divisor
        ) - user.rewardDebt;

        mint(sender(), pending);

        user.amount = user.amount - amount;
        user.rewardDebt = (user.amount * pool.accSushiPerShare) / divisor;

        transfer_to_output(pool.lpToken, sender(), amount);

        storage.users.write().insert((pool_id, sender()), user);
    }
}
adlerjohn commented 2 years ago

Storage has been implemented now. Bikeshedding over mapping vs re-using HashMap can be done in a separate issue.