Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
92 changes: 92 additions & 0 deletions move/multisig/sources/shared_space.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/// Module: shared_space
module multisig::shared_space {
use std::bcs;

use sui::hash;
use sui::ed25519;
use sui::ecdsa_k1;
use sui::ecdsa_r1;

use multisig::multisig;

const ED25519_FLAG: u8 = 0;
const SECP256K1_FLAG: u8 = 1;
const SECP256R1_FLAG: u8 = 2;

const EInvalidMultisigDerivation: u64 = 0;
const EInvalidSignature: u64 = 1;
const ELengthMismatch: u64 = 2;
const EPublicKeyFlagNotSupported: u64 = 3;

public struct SharedSpace has key {
id: UID,
addr: address,
counter: u64,
pub_keys: vector<vector<u8>>,
weights: vector<u8>,
threshold: u16,
n_members: u8
}

public struct MemberProof has drop {
indexes: vector<u8>
}

public fun new_shared_space(
pub_keys: vector<vector<u8>>, weights: vector<u8>, threshold: u16, ctx: &mut TxContext
) {
let addr = ctx.sender();
assert!(multisig::check_multisig_address_eq(pub_keys, weights, threshold, addr), EInvalidMultisigDerivation);
let n_members = pub_keys.length() as u8;
transfer::transfer(
SharedSpace {
id: object::new(ctx),
addr,
counter: 0,
pub_keys,
weights,
threshold,
n_members
},
addr
);
}

/// Takes a SharedSpace object, along with the indexes and signatures of the public keys that have signed it.
/// Verifies the signatures and returns a MemberProof verifying the participants that have signed the shared space.
public fun proove_membership(shared_space: &mut SharedSpace, indexes: vector<u8>, signatures: vector<vector<u8>>): MemberProof {

let (mut i, len) = (0, indexes.length());
assert!(len == signatures.length(), ELengthMismatch);
let serialized = bcs::to_bytes(shared_space);
let msg = hash::blake2b256(&serialized);
shared_space.counter = shared_space.counter + 1;

while (i < len) {
let index = indexes[i];
// Explicitely copy the public key to not mess up the shared space
let mut pub_key = copy shared_space.pub_keys[index as u64];
let flag = pub_key.remove(0);
if (flag == ED25519_FLAG) {
assert!(ed25519::ed25519_verify(&signatures[i], &pub_key, &msg), EInvalidSignature);
} else if (flag == SECP256K1_FLAG) {
assert!(ecdsa_k1::secp256k1_verify(&signatures[i], &pub_key, &msg, 0), EInvalidSignature);
} else if (flag == SECP256R1_FLAG) {
assert!(ecdsa_r1::secp256r1_verify(&signatures[i], &pub_key, &msg, 0), EInvalidSignature);
} else {
assert!(false, EPublicKeyFlagNotSupported);
};
i = i + 1;
};

MemberProof {
indexes
}
}

public use fun member_proof_indexes as MemberProof.indexes;
public fun member_proof_indexes(proof: &MemberProof): vector<u8> {
proof.indexes
}
}

File renamed without changes.
1 change: 1 addition & 0 deletions move/only_admin_transferable_mintcap/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.publish.res.json
38 changes: 38 additions & 0 deletions move/only_admin_transferable_mintcap/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[package]
name = "only_admin_transferable_mintcap"
edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
# license = "" # e.g., "MIT", "GPL", "Apache 2.0"
# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"]

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
multisig = { local = "../multisig" }

# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`.
# Revision can be a branch, a tag, and a commit hash.
# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" }

# For local dependencies use `local = path`. Path is relative to the package root
# Local = { local = "../path/to" }

# To resolve a version conflict and force a specific version for dependency
# override use `override = true`
# Override = { local = "../conflicting/version", override = true }

[addresses]
only_admin_transferable_mintcap = "0x0"

# Named addresses will be accessible in Move as `@name`. They're also exported:
# for example, `std = "0x1"` is exported by the Standard Library.
# alice = "0xA11CE"

[dev-dependencies]
# The dev-dependencies section allows overriding dependencies for `--test` and
# `--dev` modes. You can introduce test-only dependencies here.
# Local = { local = "../path/to/dev-build" }

[dev-addresses]
# The dev-addresses section allows overwriting named addresses for the `--test`
# and `--dev` modes.
# alice = "0xB0B"

12 changes: 12 additions & 0 deletions move/only_admin_transferable_mintcap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Shared Space

Test out a new concept where we use multisig functionality to create a shared space where objects inside it are owned by multiple people.

The objects owned by the multisig address are available from every public key(s) that can combine above threshold.

But, we use multisig contract to check which public keys have signed and issue `MemberProof` to the sender of the transaction.

Using this `MemberProof` we can authorize different combinations of public keys for different actions with these objects.

In this example we have a `MintCap` that is transferrable and updatable only by the 1st member of the multisig (`AdminAuth`),
while usable for minting only by the 2nd member (`MintAuth`).
83 changes: 83 additions & 0 deletions move/only_admin_transferable_mintcap/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/bin/bash

# check dependencies are available.
for i in jq curl sui; do
if ! command -V ${i} 2>/dev/null; then
echo "${i} is not installed"
exit 1
fi
done
cwd=$(pwd)
script_dir=$(dirname "$0")

MOVE_PACKAGE_PATH=${cwd}/${script_dir}/
GAS_BUDGET=900000000 # 0.9 SUI (Usually it is less than 0.5 SUI)

network_alias=$(sui client active-env)
envs_json=$(sui client envs --json)
NETWORK=$(echo $envs_json | jq -r --arg alias $network_alias '.[0][] | select(.alias == $alias).rpc')
addresses=$(sui client addresses --json)

echo "Checking if admin, buyer, and seller addresses are available"
has_admin=$(echo "$addresses" | jq -r '.addresses | map(contains(["admin"])) | any')
if [ "$has_admin" = "false" ]; then
echo "Did not find 'admin' in the addresses. Creating one."
sui client new-address ed25519 admin
fi
has_minter=$(echo "$addresses" | jq -r '.addresses | map(contains(["minter"])) | any')
if [ "$has_minter" = "false" ]; then
echo "Did not find 'minter' in the addresses. Creating one."
sui client new-address ed25519 minter
fi

echo "Switching to admin address"
sui client switch --address admin

echo "Checking if enough gas is available"
gas=$(sui client gas --json)
AVAILABLE=$(echo "$gas" | jq --argjson min_gas $GAS_BUDGET '.[] | select(.mistBalance > $min_gas).gasCoinId')
if [ -z "$AVAILABLE" ]; then
echo "Not enough gas to deploy contract, requesting from faucet for all addresses"
sui client faucet --address admin
sui client faucet --address minter
# If network_alias is localnet wait 2 sec
if [ "$network_alias" == "localnet" ]; then
sleep 2
else
echo "Please try again after some time."
exit 1
fi
fi

echo "Publishing"
publish_res=$(sui client publish --skip-fetch-latest-git-deps --with-unpublished-dependencies --gas-budget ${GAS_BUDGET} --json ${MOVE_PACKAGE_PATH})
echo ${publish_res} >.publish.res.json

# Check if the command succeeded (exit status 0)
if [[ "$publish_res" =~ "error" ]]; then
# If yes, print the error message and exit the script
echo "Error during move contract publishing. Details : $publish_res"
exit 1
fi

# Parse PACKAGE_ID, PUBLISHER, MINT_CAP from the publish response
published_objs=$(echo "$publish_res" | jq -r '.objectChanges[] | select(.type == "published")')
PACKAGE_ID=$(echo "$published_objs" | jq -r '.packageId')

new_objs=$(echo "$publish_res" | jq -r '.objectChanges[] | select(.type == "created")')
ADMIN_CAP=$(echo "$new_objs" | jq -r 'select(.objectType | contains("::mintcap::AdminCap")).objectId')

suffix=""
if [ $# -eq 0 ]; then
suffix=".localnet"
fi

cat >../../only_admin_transferable_mintcap/.env<<-_ENV
SUI_FULLNODE_URL=$NETWORK
PACKAGE_ID=$PACKAGE_ID
ADMIN_CAP=$ADMIN_CAP

_ENV

echo "Contract Deployment finished!"

80 changes: 80 additions & 0 deletions move/only_admin_transferable_mintcap/sources/example_mintcap.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/// Module: example_mintcap
/// This is an example on how to have an owned capability object
/// that the one who uses it is different than the one who can transfer it.
module only_admin_transferable_mintcap::mintcap {

use multisig::multisig;
use multisig::shared_space::MemberProof;

const ENotAuthorized: u64 = 0;

public struct MintCap has key {
id: UID,
n_mints: u64
}

public struct AdminCap has key, store {
id: UID,
}

public struct AdminAuth has drop {}
public struct MinterAuth has drop {}

public struct EnEfTee has key, store {
id: UID
}

fun init(ctx: &mut TxContext) {
transfer::public_transfer(AdminCap { id: object::new(ctx) }, ctx.sender());
}

public fun new_mint_cap(_: &AdminCap, n_mints: u64, admin_pk: vector<u8>, minter_pk: vector<u8>, ctx: &mut TxContext) {
let mut pks = vector::empty();
// The order of the public keys pushed is important as we check below the index of the signature
pks.push_back(admin_pk);
pks.push_back(minter_pk);
let mut weights = vector::empty();
weights.push_back(1);
weights.push_back(1);
let addr = multisig::derive_multisig_address_quiet(pks, weights, 1);
transfer::transfer(MintCap { id: object::new(ctx), n_mints }, addr);
}

// Called from multisig address
// Uses MemberProof to create AdminAuth
public fun authorize_admin(member_proof: MemberProof): AdminAuth {
assert!(member_proof.indexes().contains(&0), ENotAuthorized);
AdminAuth {}
}

public fun authorize_minter(member_proof: MemberProof): MinterAuth {
assert!(member_proof.indexes().contains(&1), ENotAuthorized);
MinterAuth {}
}

/// Even if MintCap is owned by 1-out-of-2 Multisig address, only Admin can update n_mints
public fun update_n_mints(_: &AdminAuth, mint_cap: &mut MintCap, new_n_mints: u64) {
mint_cap.n_mints = new_n_mints;
}

/// MintCap is owned by 1-out-of-2 Multisig address, but only Admin (1st pk) can transfer
/// Note that `new_admin_pk` or `new_minter_pk` can be the same as the old ones
public fun transfer(_: &AdminAuth, mint_cap: MintCap, new_admin_pk: vector<u8>, new_minter_pk: vector<u8>) {
let mut pks = vector::empty();
// The order of the public keys pushed is important as we check below the index of the signature
pks.push_back(new_admin_pk);
pks.push_back(new_minter_pk);
let mut weights = vector::empty();
weights.push_back(1);
weights.push_back(1);
let addr = multisig::derive_multisig_address_quiet(pks, weights, 1);
transfer::transfer(mint_cap, addr);
}

/// MintCap is owned by 1-out-of-2 Multisig address, but only Minter (2nd pk) can mint
public fun mint(_: &MinterAuth, mint_cap: &mut MintCap, ctx: &mut TxContext): EnEfTee {
assert!(mint_cap.n_mints > 0, ENotAuthorized);
mint_cap.n_mints = mint_cap.n_mints - 1;
EnEfTee { id: object::new(ctx) }
}
}
3 changes: 3 additions & 0 deletions only_admin_transferable_mintcap/.example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SUI_FULLNODE_URL=http://127.0.0.1:9000
PACKAGE_ID=0xb9fe437d62fed5394a3203e52fc39e7b1a8a4403bc3e0f6df0f127d965d7ad62
ADMIN_CAP=0x6768e3d26f90bf90d6dceb499fcf5e0e478d5759fbdb0433ec501d0d6e9a3693
1 change: 1 addition & 0 deletions only_admin_transferable_mintcap/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
6 changes: 6 additions & 0 deletions only_admin_transferable_mintcap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Instructions

1. Run ../move/only_admin_transferable_mintcap/publish.sh to publish the package with its dependencies (multisig). publish.sh will also create an admin and a minter keypairs in your sui keystore.
2. Run ./scripts/new-shared-space.ts to create a `SharedSpace` object under the admin-minter multisig address.
3. Run ./scripts/new-mintcap.ts to create a `MintCap` object under the admin-minter multisig address.
4. Run ./scripts/mint.ts to mint an `EnEfTee` signing as minter under the multisig address.
24 changes: 24 additions & 0 deletions only_admin_transferable_mintcap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "only_admin_transferable_mintcap",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@mysten/bcs": "^0.11.1",
"@mysten/sui.js": "^0.54.1",
"blake2": "^5.0.0",
"dotenv": "^16.4.5",
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/blake2": "^4.0.4",
"@types/lodash": "^4.17.4",
"@types/node": "^20.12.12"
}
}
Loading