Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: stellar multisig #430

Merged
merged 4 commits into from
Dec 20, 2024
Merged
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
8 changes: 7 additions & 1 deletion contracts/soroban/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions contracts/soroban/contracts/multisig/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "multisig"
version = "0.0.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true, features = ["alloc"] }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
58 changes: 58 additions & 0 deletions contracts/soroban/contracts/multisig/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Stellar Multisig Contract Frontend Workflow

This README explains how to build a frontend for interacting with a Stellar Multisig contract. The frontend is provided with:
- **Multisig Contract Address**: Used to call functions like `create_proposal`, `add_approval_signature`, and `execute_proposal`.
- **Multisig Account Address**: Used to fetch and interact with relevant proposals.

---

## **Features Overview**

### 1. **Create Proposal Section**
This section allows users to create proposals to upgrade a Stellar contract.

- **Inputs**:
- **Contract Address**: Address of the contract to be upgraded.
- **Contract Hash**: Hash of the new WASM code to deploy.

- **Button**: "Create Proposal".

- **Action Flow**:
1. Build a transaction to upgrade the contract(Can be taken further reference from https://github.com/icon-project/ICON-Projects-Planning/issues/510).
2. Call the `create_proposal` method on the Multisig contract, passing:
- The built transaction in XDR format.
- The Multisig Account Address for reference.

---

### 2. **Approve Proposal Section**
This section allows users to approve proposals that are in a pending state.

- **Display**:
- A list of proposals fetched from the Multisig contract, showing:
- **Proposal ID**
- **Status** (Pending, Approved, Executed).

- **Button**: "Approve" (enabled for pending proposals).

- **Action Flow**:
1. Fetch the proposal data (XDR) from the contract(Structure can be taken reference from contract).
2. Build the transaction and sign it using the signer's private key.
3. Submit the signature and `proposal_id` to the `add_approval_signature` method to approve the proposal.

---

### 3. **Execute Proposal Section**
This section allows users to execute approved proposals.

- **Display**:
- A list of approved proposals ready for execution.

- **Button**: "Execute" (enabled for approved proposals).

- **Action Flow**:
1. Fetch transaction data and signatures from the contract for execution.
2. Build the transaction using the fetched data.
3. Submit the transaction to the Stellar network for execution.

---
105 changes: 105 additions & 0 deletions contracts/soroban/contracts/multisig/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#![no_std]
use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec};
mod states;
use states::{is_signer, ContractError, MultisigWallet, Proposal, Signature, StorageKey};
#[contract]
pub struct ProposalContract;

#[contractimpl]
impl ProposalContract {

pub fn init(env: Env) -> Result<(), ContractError> {
if states::is_initialized(&env) {
return Err(ContractError::AlreadyInitialized);
}
states::set_count(&env, 0);
Ok(())
}


pub fn add_multisig_wallet(env: Env, wallet: Address, signers: Vec<Address>, threshold: u32) -> Result<(), ContractError> {
let multisig = MultisigWallet {
signers,
threshold
};
states::set_multisig_wallet(&env, wallet, multisig);
Ok(())
}

/// Create a proposal. This proposal is identified by a proposal_id that is increased with each proposal.
/// The proposal is associated with a wallet, which is used to verify signers.
/// The proposal is approved after a number of signatures equal to the threshold of the wallet is reached.
pub fn create_proposal(env: Env, sender : Address, proposal_data: String, wallet: Address) -> Result<(), ContractError> {
sender.require_auth();
if !is_signer(&env, &wallet, sender) {
return Err(ContractError::NotAValidSigner);
}
let proposal_id = states::get_count(&env);
let proposal = Proposal {
proposal_id,
proposal_data,
approved: false,
signatures: Vec::new(&env),
wallet
};
states::increase_count(&env);
states::set_proposal(&env, proposal_id, proposal);
Ok(())
}


/// Add a signature to a proposal.
/// The proposal is approved after a number of signatures equal to the threshold of the wallet is reached.
/// The function returns an error if the proposal has expired, or if the sender is not a valid signer, or if the sender has already voted.
pub fn add_approval_signature(env: Env, proposal_id: u32, sender: Address, signature: String) -> Result<(), ContractError> {
sender.require_auth();
let key = states::get_proposal(&env, proposal_id);
if states::is_proposal_expired(&env, proposal_id) {
return Err(ContractError::ProposalExpired);
}
let mut proposal: Proposal = env.storage().temporary().get(&key).unwrap();
if !is_signer(&env, &proposal.wallet, sender.clone()) {
return Err(ContractError::NotAValidSigner);
}
let new_signature = Signature {
address:sender,
signature
};

if proposal.signatures.contains(&new_signature) {
return Err(ContractError::AlreadyVoted);
}
proposal.signatures.push_back(new_signature);
let threshold = states::get_threshold(&env, proposal.wallet.clone());
if proposal.signatures.len() >= threshold {
proposal.approved = true;
}

states::set_proposal(&env, proposal_id, proposal);
Ok(())
}

/// Returns all active proposals. A proposal is active if it has not expired.
pub fn get_active_proposals(env: Env) -> Vec<Proposal> {
let count = states::get_count(&env);
let mut proposals = Vec::new(&env);
for i in 0..count {
let key = StorageKey::Proposals(i);
if !env.storage().temporary().has(&key) {
continue;
}
let proposal: Proposal = env.storage().temporary().get(&key).unwrap();
proposals.push_back(proposal);
}
proposals
}

pub fn get_multisig_wallet(env: Env, wallet: Address) -> MultisigWallet {
states::get_multisig_wallet(&env, wallet)
}

pub fn extend_instance(e: Env) {
states::extend_instance(&e);
}

}
115 changes: 115 additions & 0 deletions contracts/soroban/contracts/multisig/src/states.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use soroban_sdk::{ Address, Env, String, Vec, contracttype};

#[contracttype]
pub enum StorageKey {
Proposals(u32),
Count,
MultisigWallet(Address),
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct Signature {
pub address: Address,
pub signature: String
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct MultisigWallet {
pub signers: Vec<Address>,
pub threshold: u32
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct Proposal {
pub proposal_id: u32,
pub proposal_data: String,
pub approved: bool,
pub signatures: Vec<Signature>,
pub wallet: Address
}

use soroban_sdk::contracterror;

#[contracterror]
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
#[repr(u32)]
pub enum ContractError {
AlreadyVoted = 1,
NotAValidSigner = 2,
AlreadyInitialized = 3,
ProposalExpired = 4
}

const DAY_IN_LEDGERS: u32 = 17280; // assumes 5s a ledger

const LEDGER_THRESHOLD_INSTANCE: u32 = DAY_IN_LEDGERS * 30; // ~ 30 days
const LEDGER_BUMP_INSTANCE: u32 = LEDGER_THRESHOLD_INSTANCE + DAY_IN_LEDGERS; // ~ 31 days

const LEDGER_THRESHOLD_REQUEST: u32 = DAY_IN_LEDGERS * 3; // ~ 3 days
const LEDGER_BUMP_REQUEST: u32 = LEDGER_THRESHOLD_REQUEST + DAY_IN_LEDGERS; // ~ 4 days

pub fn is_signer(env: &Env, wallet: &Address, address: Address) -> bool {
let multisig: MultisigWallet = env.storage().instance().get(&StorageKey::MultisigWallet(wallet.clone())).unwrap();
multisig.signers.contains(&address)
}

pub fn set_count(env: &Env, count: u32) {
env.storage().instance().set(&StorageKey::Count, &count);
}

pub fn get_count(env: &Env) -> u32 {
env.storage().instance().get(&StorageKey::Count).unwrap()
}
pub fn increase_count(env: &Env) {
let count = get_count(env);
set_count(env, count+1);
}

pub fn set_proposal(env: &Env, proposal_id: u32, proposal: Proposal) {
let key = StorageKey::Proposals(proposal_id);
env.storage().temporary().set(&key, &proposal);
extend_temporary_request(env, &key);
}

pub fn get_proposal(env: &Env, proposal_id: u32) -> Proposal {
let key = StorageKey::Proposals(proposal_id);
env.storage().temporary().get(&key).unwrap()
}

pub fn set_multisig_wallet(env: &Env, wallet: Address, multisig: MultisigWallet) {
env.storage().persistent().set(&StorageKey::MultisigWallet(wallet), &multisig);
}

pub fn get_multisig_wallet(env: &Env, wallet: Address) -> MultisigWallet {
let multisig: MultisigWallet = env.storage().persistent().get(&StorageKey::MultisigWallet(wallet)).unwrap();
multisig
}

pub fn get_threshold(env: &Env, wallet: Address) -> u32 {
let multisig: MultisigWallet = env.storage().persistent().get(&StorageKey::MultisigWallet(wallet)).unwrap();
multisig.threshold
}

pub fn is_proposal_expired(env: &Env, proposal_id: u32) -> bool {
let key = StorageKey::Proposals(proposal_id);
!env.storage().temporary().has(&key)
}

pub fn is_initialized(env: &Env) -> bool {
env.storage().instance().has(&StorageKey::Count)
}

pub fn extend_instance(e: &Env) {
e.storage()
.instance()
.extend_ttl(LEDGER_THRESHOLD_INSTANCE, LEDGER_BUMP_INSTANCE);
}

pub fn extend_temporary_request(e: &Env, key: &StorageKey) {
e.storage()
.temporary()
.extend_ttl(key, LEDGER_THRESHOLD_REQUEST, LEDGER_BUMP_REQUEST);
}
Loading