Skip to content
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
71 changes: 71 additions & 0 deletions contracts/vesting_contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub enum DataKey {
TotalShares,
TotalStaked,
StakingContract,
VotingDelegate(Address),
DelegatedBeneficiaries(Address),
}

#[contracttype]
Expand Down Expand Up @@ -276,6 +278,63 @@ impl VestingContract {
Self::get_vault_internal(&env, vault_id)
}

pub fn get_user_vaults(env: Env, user: Address) -> Vec<u64> {
env.storage().instance().get(&DataKey::UserVaults(user)).unwrap_or(Vec::new(&env))
}

pub fn get_voting_power(env: Env, user: Address) -> i128 {
// If this user has delegated their power to someone else, they have 0
if env.storage().instance().has(&DataKey::VotingDelegate(user.clone())) {
return 0;
}

let mut total_power = Self::calculate_user_own_power(&env, &user);

// Add power from others who delegated to this user
let delegators: Vec<Address> = env.storage().instance().get(&DataKey::DelegatedBeneficiaries(user)).unwrap_or(vec![&env]);
for delegator in delegators.iter() {
total_power += Self::calculate_user_own_power(&env, &delegator);
}

total_power
}

pub fn delegate_voting_power(env: Env, beneficiary: Address, representative: Address) {
beneficiary.require_auth();

// 1. Get current representative if any
let old_representative: Option<Address> = env.storage().instance().get(&DataKey::VotingDelegate(beneficiary.clone()));

// 2. If same as before, do nothing
if let Some(ref old) = old_representative {
if old == &representative {
return;
}

// Remove from old representative's list
let mut old_list: Vec<Address> = env.storage().instance().get(&DataKey::DelegatedBeneficiaries(old.clone())).unwrap_or(vec![&env]);
if let Some(idx) = old_list.first_index_of(&beneficiary) {
old_list.remove(idx);
env.storage().instance().set(&DataKey::DelegatedBeneficiaries(old.clone()), &old_list);
}
}

// 3. Update to new representative
// If representative is beneficiary itself, it means undelegate
if beneficiary == representative {
env.storage().instance().remove(&DataKey::VotingDelegate(beneficiary.clone()));
} else {
env.storage().instance().set(&DataKey::VotingDelegate(beneficiary.clone()), &representative);

// Add to new representative's list
let mut new_list: Vec<Address> = env.storage().instance().get(&DataKey::DelegatedBeneficiaries(representative.clone())).unwrap_or(vec![&env]);
if !new_list.contains(&beneficiary) {
new_list.push_back(beneficiary.clone());
env.storage().instance().set(&DataKey::DelegatedBeneficiaries(representative), &new_list);
}
}
}

// --- Internal Helpers ---

fn require_admin(env: &Env) {
Expand Down Expand Up @@ -381,6 +440,18 @@ impl VestingContract {
env.storage().instance().set(&DataKey::UserVaults(user.clone()), &vaults);
}

fn calculate_user_own_power(env: &Env, user: &Address) -> i128 {
let vault_ids = env.storage().instance().get(&DataKey::UserVaults(user.clone())).unwrap_or(vec![env]);
let mut total_power: i128 = 0;
for id in vault_ids.iter() {
let vault = Self::get_vault_internal(env, id);
let balance = vault.total_amount - vault.released_amount;
let weight = if vault.is_irrevocable { 100 } else { 50 };
total_power += (balance * weight) / 100;
}
total_power
}

fn calculate_claimable(env: &Env, id: u64, vault: &Vault) -> i128 {
if env.storage().instance().has(&DataKey::VaultMilestones(id)) {
let milestones: Vec<Milestone> = env.storage().instance().get(&DataKey::VaultMilestones(id)).expect("No milestones");
Expand Down
93 changes: 93 additions & 0 deletions contracts/vesting_contracts/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,96 @@ fn test_batch_operations() {
assert_eq!(ids.get(0).unwrap(), 1);
assert_eq!(ids.get(1).unwrap(), 2);
}

#[test]
fn test_voting_power() {
let (env, _, client, _, _) = setup();
let beneficiary = Address::generate(&env);
let now = env.ledger().timestamp();

// Irrevocable vault: 1000 tokens (100% weight = 1000 power)
client.create_vault_full(
&beneficiary,
&1000i128,
&now,
&(now + 1000),
&0i128,
&false, // is_revocable = false => is_irrevocable = true
&false,
&0u64,
);

// Revocable vault: 1000 tokens (50% weight = 500 power)
client.create_vault_full(
&beneficiary,
&1000i128,
&now,
&(now + 1000),
&0i128,
&true, // is_revocable = true => is_irrevocable = false
&false,
&0u64,
);

// Total power should be 1000 + 500 = 1500
assert_eq!(client.get_voting_power(&beneficiary), 1500);
}

#[test]
fn test_delegated_voting_power() {
let (env, _, client, _, _) = setup();
let beneficiary_a = Address::generate(&env);
let beneficiary_b = Address::generate(&env);
let representative = Address::generate(&env);
let now = env.ledger().timestamp();

// A: 1000 power (irrevocable)
client.create_vault_full(
&beneficiary_a,
&1000i128,
&now,
&(now + 1000),
&0i128,
&false,
&false,
&0u64,
);

// B: 500 power (revocable)
client.create_vault_full(
&beneficiary_b,
&1000i128,
&now,
&(now + 1000),
&0i128,
&true,
&false,
&0u64,
);

// Initial check
assert_eq!(client.get_voting_power(&beneficiary_a), 1000);
assert_eq!(client.get_voting_power(&beneficiary_b), 500);
assert_eq!(client.get_voting_power(&representative), 0);

// A delegates to B
client.delegate_voting_power(&beneficiary_a, &beneficiary_b);
assert_eq!(client.get_voting_power(&beneficiary_a), 0);
assert_eq!(client.get_voting_power(&beneficiary_b), 1500); // 500 + 1000

// B delegates to representative
client.delegate_voting_power(&beneficiary_b, &representative);
assert_eq!(client.get_voting_power(&beneficiary_b), 0);
// Note: C only gets B's own power (500) because A is not a direct delegator of C in current implementation
// This is fine as per simple requirements.
assert_eq!(client.get_voting_power(&representative), 500);

// A redelegates to representative
client.delegate_voting_power(&beneficiary_a, &representative);
assert_eq!(client.get_voting_power(&representative), 1500); // 1000 + 500

// A undelegates
client.delegate_voting_power(&beneficiary_a, &beneficiary_a);
assert_eq!(client.get_voting_power(&beneficiary_a), 1000);
assert_eq!(client.get_voting_power(&representative), 500); // Only B left
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
{
"generators": {
"address": 3,
"nonce": 0,
"mux_id": 0
},
"auth": [
[],
[],
[]
],
"ledger": {
"protocol_version": 25,
"sequence_number": 0,
"timestamp": 0,
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
"base_reserve": 0,
"min_persistent_entry_ttl": 4096,
"min_temp_entry_ttl": 16,
"max_entry_ttl": 6312000,
"ledger_entries": [
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent",
"val": {
"contract_instance": {
"executable": {
"wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"storage": [
{
"key": {
"vec": [
{
"symbol": "AdminAddress"
}
]
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
}
},
{
"key": {
"vec": [
{
"symbol": "AdminBalance"
}
]
},
"val": {
"i128": "1000000"
}
},
{
"key": {
"vec": [
{
"symbol": "InitialSupply"
}
]
},
"val": {
"i128": "1000000"
}
},
{
"key": {
"vec": [
{
"symbol": "IsDeprecated"
}
]
},
"val": {
"bool": false
}
},
{
"key": {
"vec": [
{
"symbol": "IsPaused"
}
]
},
"val": {
"bool": false
}
},
{
"key": {
"vec": [
{
"symbol": "TotalShares"
}
]
},
"val": {
"i128": "0"
}
},
{
"key": {
"vec": [
{
"symbol": "TotalStaked"
}
]
},
"val": {
"i128": "0"
}
},
{
"key": {
"vec": [
{
"symbol": "VaultCount"
}
]
},
"val": {
"u64": "0"
}
}
]
}
}
}
},
"ext": "v0"
},
"live_until": 4095
},
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_code": {
"ext": "v0",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"code": ""
}
},
"ext": "v0"
},
"live_until": 4095
}
]
},
"events": []
}
Loading