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

[ECO-2360] Add claim link module to rewards package #320

Merged
merged 25 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
5 changes: 4 additions & 1 deletion src/move/rewards/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ emojicoin_dot_fun = "0xc0de"
integrator = "0xccc"
rewards = "0xddd"

[dev-dependencies.BlackHeartCoinFactory]
local = "../test_coin_factories/black_heart"

[package]
authors = ["Econia Labs ([email protected])"]
name = "EmojicoinDotFunRewards"
upgrade_policy = "compatible"
version = "1.0.1"
version = "1.1.0"
10 changes: 9 additions & 1 deletion src/move/rewards/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ aptos move publish \
--profile $PROFILE
```

## Fund the vault
## Fund the vaults

```sh
REWARDS=0xaaa...
PROFILE=my-profile
N_CLAIM_LINK_REDEMPTIONS_TO_FUND=10
N_REWARDS_TO_FUND_PER_TIER="u64:[1500,500,200,50,5,1]"
```

Expand All @@ -53,3 +54,10 @@ aptos move run \
--function-id $REWARDS::emojicoin_dot_fun_rewards::fund_tiers \
--profile $PROFILE
```

```sh
aptos move run \
--args $N_CLAIM_LINK_REDEMPTIONS_TO_FUND \
--function-id $REWARDS::emojicoin_dot_fun_claim_link::fund_vault \
--profile $PROFILE
```
336 changes: 336 additions & 0 deletions src/move/rewards/sources/emojicoin_dot_fun_claim_link.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
// cspell:word funder
module rewards::emojicoin_dot_fun_claim_link {

use aptos_framework::account::{Self, SignerCapability};
use aptos_framework::aptos_account;
use aptos_framework::aptos_coin::AptosCoin;
use aptos_framework::coin;
use aptos_framework::event;
use aptos_std::ed25519::{Self, ValidatedPublicKey};
use aptos_std::simple_map::SimpleMap;
use aptos_std::smart_table::{Self, SmartTable};
use emojicoin_dot_fun::emojicoin_dot_fun::{Self, Swap};
use std::option::{Self, Option};
use std::bcs;
use std::signer;

const INTEGRATOR_FEE_RATE_BPS: u8 = 100;
const NIL: address = @0x0;
const DEFAULT_CLAIM_AMOUNT: u64 = 10_000_000;
alnoki marked this conversation as resolved.
Show resolved Hide resolved
const VAULT: vector<u8> = b"Claim link vault";

/// Signer does not correspond to admin.
const E_NOT_ADMIN: u64 = 0;
/// Admin to remove address does not correspond to admin.
const E_ADMIN_TO_REMOVE_IS_NOT_ADMIN: u64 = 1;
/// Public key of claim link private key is not in manifest.
const E_INVALID_CLAIM_LINK: u64 = 2;
/// Claim link has already been claimed.
const E_CLAIM_LINK_ALREADY_CLAIMED: u64 = 3;
/// Vault has insufficient funds.
const E_VAULT_INSUFFICIENT_FUNDS: u64 = 4;
/// Public key does not pass Ed25519 validation.
const E_INVALID_PUBLIC_KEY: u64 = 5;
/// Signature does not pass Ed25519 validation.
const E_INVALID_SIGNATURE: u64 = 6;

struct Vault has key {
/// Addresses of signers who can mutate the manifest.
admins: vector<address>,
/// In octas.
claim_amount: u64,
/// Map from claim link public key to address of claimant, `NIL` if unclaimed.
manifest: SmartTable<ValidatedPublicKey, address>,
/// Approves transfers from the vault.
signer_capability: SignerCapability
}

#[event]
struct EmojicoinDotFunClaimLinkRedemption has copy, drop, store {
claimant: address,
claim_amount: u64,
swap: Swap
}

#[view]
public fun admins(): vector<address> acquires Vault {
Vault[@rewards].admins
}

#[view]
public fun claim_amount(): u64 acquires Vault {
Vault[@rewards].claim_amount
}

#[view]
public fun public_key_is_in_manifest(public_key_bytes: vector<u8>): bool acquires Vault {
Vault[@rewards].manifest.contains(validate_public_key_bytes(public_key_bytes))
}

#[view]
public fun public_key_claimant(public_key_bytes: vector<u8>): address acquires Vault {
*Vault[@rewards].manifest.borrow(validate_public_key_bytes(public_key_bytes))
}

#[view]
public fun public_keys(): vector<ValidatedPublicKey> acquires Vault {
Vault[@rewards].manifest.keys()
}

#[view]
public fun public_keys_paginated(
starting_bucket_index: u64, starting_vector_index: u64, num_public_keys_to_get: u64
): (vector<ValidatedPublicKey>, Option<u64>, Option<u64>) acquires Vault {
Vault[@rewards].manifest.keys_paginated(
starting_bucket_index, starting_vector_index, num_public_keys_to_get
)
}

#[view]
public fun manifest_to_simple_map(): SimpleMap<ValidatedPublicKey, address> acquires Vault {
Vault[@rewards].manifest.to_simple_map()
}

#[view]
public fun vault_balance(): u64 acquires Vault {
coin::balance<AptosCoin>(
account::get_signer_capability_address(&Vault[@rewards].signer_capability)
)
}

#[view]
public fun vault_signer_address(): address acquires Vault {
account::get_signer_capability_address(&Vault[@rewards].signer_capability)
}

public entry fun add_admin(admin: &signer, new_admin: address) acquires Vault {
let admins_ref_mut = &mut borrow_vault_mut_checked(admin).admins;
admins_ref_mut.push_back(new_admin);
}

public entry fun add_public_keys(
admin: &signer, public_keys_as_bytes: vector<vector<u8>>
) acquires Vault {
let manifest_ref_mut = &mut borrow_vault_mut_checked(admin).manifest;
public_keys_as_bytes.for_each(|public_key_bytes| {
manifest_ref_mut.add(validate_public_key_bytes(public_key_bytes), NIL);
});
}

public entry fun fund_vault(funder: &signer, n_claims: u64) acquires Vault {
let amount = n_claims * Vault[@rewards].claim_amount;
aptos_account::transfer(
funder,
account::get_signer_capability_address(&Vault[@rewards].signer_capability),
amount
);
}

/// `signature_bytes` is generated by signing the claimant's address with the claim link private
/// key, and can be verified by `public_key_bytes`, the corresponding claim link public key.
public entry fun redeem<Emojicoin, EmojicoinLP>(
claimant: &signer,
signature_bytes: vector<u8>,
public_key_bytes: vector<u8>,
market_address: address,
min_output_amount: u64
) acquires Vault {

// Verify signature.
let validated_public_key = validate_public_key_bytes(public_key_bytes);
let claimant_address = signer::address_of(claimant);
assert!(
ed25519::signature_verify_strict(
&ed25519::new_signature_from_bytes(signature_bytes),
&ed25519::public_key_to_unvalidated(&validated_public_key),
bcs::to_bytes(&claimant_address)
),
E_INVALID_SIGNATURE
);

// Verify public key is eligible for claim.
let vault_ref_mut = &mut Vault[@rewards];
let manifest_ref_mut = &mut vault_ref_mut.manifest;
assert!(manifest_ref_mut.contains(validated_public_key), E_INVALID_CLAIM_LINK);
assert!(
*manifest_ref_mut.borrow(validated_public_key) == NIL,
E_CLAIM_LINK_ALREADY_CLAIMED
);

// Check vault balance.
let vault_signer_cap_ref = &vault_ref_mut.signer_capability;
let vault_address = account::get_signer_capability_address(vault_signer_cap_ref);
let claim_amount = vault_ref_mut.claim_amount;
assert!(
coin::balance<AptosCoin>(vault_address) >= claim_amount,
E_VAULT_INSUFFICIENT_FUNDS
);

// Update manifest, transfer APT to claimant.
*manifest_ref_mut.borrow_mut(validated_public_key) = claimant_address;
let vault_signer = account::create_signer_with_capability(vault_signer_cap_ref);
aptos_account::transfer(&vault_signer, claimant_address, claim_amount);

// Invoke swap, emit event.
let swap_event =
emojicoin_dot_fun::simulate_swap<Emojicoin, EmojicoinLP>(
claimant_address,
market_address,
claim_amount,
false,
@integrator,
INTEGRATOR_FEE_RATE_BPS
);
emojicoin_dot_fun::swap<Emojicoin, EmojicoinLP>(
claimant,
market_address,
claim_amount,
false,
@integrator,
INTEGRATOR_FEE_RATE_BPS,
min_output_amount
);
event::emit(
EmojicoinDotFunClaimLinkRedemption {
claimant: claimant_address,
claim_amount,
swap: swap_event
}
);

}

public entry fun remove_admin(admin: &signer, admin_to_remove: address) acquires Vault {
let admins_ref_mut = &mut borrow_vault_mut_checked(admin).admins;
let (admin_to_remove_is_admin, admin_to_remove_index) =
admins_ref_mut.index_of(&admin_to_remove);
assert!(admin_to_remove_is_admin, E_ADMIN_TO_REMOVE_IS_NOT_ADMIN);
admins_ref_mut.remove(admin_to_remove_index);
}

public entry fun remove_public_keys(
admin: &signer, public_keys_as_bytes: vector<vector<u8>>
) acquires Vault {
let manifest_ref_mut = &mut borrow_vault_mut_checked(admin).manifest;
public_keys_as_bytes.for_each(|public_key_bytes| {
let validated_public_key = validate_public_key_bytes(public_key_bytes);
if (manifest_ref_mut.contains(validated_public_key)
&& *manifest_ref_mut.borrow(validated_public_key) == NIL) {
manifest_ref_mut.remove(validated_public_key);
}
});
}

public entry fun set_claim_amount(admin: &signer, claim_amount: u64) acquires Vault {
borrow_vault_mut_checked(admin).claim_amount = claim_amount;
}

public entry fun withdraw_from_vault(admin: &signer, amount: u64) acquires Vault {
aptos_account::transfer(
&account::create_signer_with_capability(
&borrow_vault_mut_checked(admin).signer_capability
),
signer::address_of(admin),
amount
);
}

fun init_module(rewards: &signer) {
let (vault_signer, signer_capability) =
account::create_resource_account(rewards, VAULT);
move_to(
rewards,
Vault {
admins: vector[signer::address_of(rewards)],
claim_amount: DEFAULT_CLAIM_AMOUNT,
manifest: smart_table::new(),
signer_capability
}
);
coin::register<AptosCoin>(&vault_signer);
}

inline fun borrow_vault_mut_checked(admin: &signer): &mut Vault {
let vault_ref_mut = &mut Vault[@rewards];
assert!(vault_ref_mut.admins.contains(&signer::address_of(admin)), E_NOT_ADMIN);
vault_ref_mut
}

inline fun validate_public_key_bytes(public_key_bytes: vector<u8>): ValidatedPublicKey {
let validated_public_key_option =
ed25519::new_validated_public_key_from_bytes(public_key_bytes);
assert!(option::is_some(&validated_public_key_option), E_INVALID_PUBLIC_KEY);
option::destroy_some(validated_public_key_option)
}

#[test_only]
use aptos_framework::account::{create_signer_for_test as get_signer};
#[test_only]
use black_cat_market::coin_factory::{
Emojicoin as BlackCatEmojicoin,
EmojicoinLP as BlackCatEmojicoinLP
};

#[test_only]
const CLAIMANT: address = @0x1111;

#[test]
fun test_general_flow() acquires Vault {
// Initialize black cat market, have it undergo state transition.
emojicoin_dot_fun::tests::init_package_then_exact_transition();

// Get claim link private, public keys.
let (claim_link_private_key, claim_link_validated_public_key) =
ed25519::generate_keys();
let claim_link_validated_public_key_bytes =
ed25519::validated_public_key_to_bytes(&claim_link_validated_public_key);

// Initialize module, manifest, vault.
let rewards_signer = get_signer(@rewards);
init_module(&rewards_signer);
add_public_keys(
&rewards_signer,
vector[claim_link_validated_public_key_bytes]
);
emojicoin_dot_fun::test_acquisitions::mint_aptos_coin_to(
@rewards, DEFAULT_CLAIM_AMOUNT
);
let n_redemptions = 1;
fund_vault(&rewards_signer, n_redemptions);

// Get expected proceeds from swap.
let swap_event =
emojicoin_dot_fun::simulate_swap<BlackCatEmojicoin, BlackCatEmojicoinLP>(
CLAIMANT,
@black_cat_market,
DEFAULT_CLAIM_AMOUNT,
false,
@integrator,
INTEGRATOR_FEE_RATE_BPS
);
let (_, _, _, _, _, _, _, _, net_proceeds, _, _, _, _, _, _, _, _, _) =
emojicoin_dot_fun::unpack_swap(swap_event);
assert!(net_proceeds > 0);

// Redeem a claim link.
redeem<BlackCatEmojicoin, BlackCatEmojicoinLP>(
&get_signer(CLAIMANT),
ed25519::signature_to_bytes(
&ed25519::sign_arbitrary_bytes(
&claim_link_private_key, bcs::to_bytes(&CLAIMANT)
)
),
claim_link_validated_public_key_bytes,
@black_cat_market,
1
);

// Verify claimaint's emojicoin balance.
assert!(coin::balance<BlackCatEmojicoin>(CLAIMANT) == net_proceeds);

// Check vault balance, manifest.
assert!(vault_balance() == 0);
assert!(public_key_claimant(claim_link_validated_public_key_bytes) == CLAIMANT);

}
}
Loading
Loading