Skip to content

Commit

Permalink
[ECO-1988] Add lottery wrapper implementation (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
alnoki authored Jul 23, 2024
1 parent b93475f commit 6c6aefc
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions cfg/cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ nazar
nextjs
nohup
octa
octas
oden
parallelizable
permissionless
Expand Down
18 changes: 18 additions & 0 deletions src/move/rewards/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[addresses]
integrator = "_"
rewards = "_"

[dependencies.EmojicoinDotFun]
local = "../emojicoin_dot_fun"

[dev-addresses]
coin_factory = "0xaaaa"
emojicoin_dot_fun = "0xc0de"
integrator = "0xccc"
rewards = "0xddd"

[package]
authors = ["Econia Labs ([email protected])"]
name = "EmojicoinDotFunRewards"
upgrade_policy = "compatible"
version = "1.0.0"
75 changes: 75 additions & 0 deletions src/move/rewards/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!---
cspell:word undergassing
cspell:word permissionlessly
-->

# `EmojicoinDotFunRewards`

This package provides an overloaded swap function that randomly rewards a fixed
percentage of swaps that exceed an arbitrary amount of volume. Funds are
autonomously disbursed from a resource account with an APT vault that can be
topped off permissionlessly.

Per the [undergassing docs], one strategy to mitigate undergassing attacks is to
make the most preferred path cost the most gas. Hence, here it is ensured that
the winning path requires the most gas per the final logical branch:

```move
let result = randomness::u64_range(0, WIN_PERCENTAGE_DENOMINATOR);
if (result < WIN_PERCENTAGE_NUMERATOR) {
// If user wins ...
```

To view the bytecode result:

```sh
aptos move compile --dev
aptos move disassemble --bytecode-path \
build/EmojicoinDotFunRewards/bytecode_modules/emojicoin_dot_fun_rewards.mv
```

Then inside `build/EmojicoinDotFunRewards/bytecode_modules`, open
`emojicoin_dot_fun_rewards.asm` and note:

```asm
B4:
56: LdU64(0)
57: LdConst[6](U64: [16, 39, 0, 0, 0, 0, 0, 0])
58: Call randomness::u64_range(u64, u64): u64
59: LdConst[7](U64: [231, 3, 0, 0, 0, 0, 0, 0])
60: Lt
61: BrFalse(85)
```

Here, the `false` (losing) branch is essentially an instant return, while the
`true` (winning) branch requires additional logic.

Note that even if the `BrFalse` instruction were to be exceptionally expensive
for the `false` branch, it would still not be able to make the losing path more
expensive in the general case, because the winning path *also* has a `BrFalse`
statement where the preferred case is the `false` outcome, specifically:

```move
let reward_amount = if (vault_balance < REWARD_AMOUNT_IN_OCTAS) vault_balance
else REWARD_AMOUNT_IN_OCTAS;
```

Hence the winning path is necessarily more expensive than the losing path
because it contains all of the same instructions as the losing path, and more,
except in the extreme hypothetical edge case where:

1. `BrFalse` for the `false` branch is set absurdly high in a gas schedule
update, such that its execution cost vastly exceeds all other instructions in
the winning path combined.
1. The user is the last lottery winner, sweeping a vault that has less than the
single rewards amount inside.

Yet even this can issue can be mitigated, by simply filling up the vault with
an integer number of standard reward amounts, because then the lottery will
never evaluate due to this statement:

```move
if (vault_balance == 0) return;
```

[undergassing docs]: https://aptos.dev/en/build/smart-contracts/randomness#undergasing-attacks-and-how-to-prevent
102 changes: 102 additions & 0 deletions src/move/rewards/sources/emojicoin_dot_fun_rewards.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
module rewards::emojicoin_dot_fun_rewards {

use std::signer;
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_framework::randomness;
use emojicoin_dot_fun::emojicoin_dot_fun::{Self, Swap};

/// Resource account address seed for the vault.
const VAULT: vector<u8> = b"Vault";

/// Flat integrator fee.
const INTEGRATOR_FEE_RATE_BPS: u8 = 100;

// APT amounts.
const REWARD_AMOUNT_IN_OCTAS: u64 = 100_000_000;
const VOLUME_THRESHOLD_IN_OCTAS: u64 = 990_000_000;

// Randomness amounts.
const WIN_PERCENTAGE_NUMERATOR: u64 = 980;
const WIN_PERCENTAGE_DENOMINATOR: u64 = 10_000;

struct RewardsVaultSignerCapability has key {
signer_capability: SignerCapability,
}

#[event]
struct EmojicoinDotFunRewardsLotteryWinner has copy, drop, store {
swap: Swap,
reward_amount: u64,
}

#[randomness]
entry fun swap_with_rewards<Emojicoin, EmojicoinLP>(
swapper: &signer,
market_address: address,
input_amount: u64,
is_sell: bool,
) acquires RewardsVaultSignerCapability {

// Simulate swap to get quote volume, then execute swap.
let swapper_address = signer::address_of(swapper);
let swap = emojicoin_dot_fun::simulate_swap(
swapper_address,
market_address,
input_amount,
is_sell,
@integrator,
INTEGRATOR_FEE_RATE_BPS,
);
let ( _, _, _, _, _, _, _, _, _, _, quote_volume, _, _, _, _, _) =
emojicoin_dot_fun::unpack_swap(swap);
emojicoin_dot_fun::swap<Emojicoin, EmojicoinLP>(
swapper,
market_address,
input_amount,
is_sell,
@integrator,
INTEGRATOR_FEE_RATE_BPS,
);

// Return if quote volume is below threshold, otherwise proceed to lottery.
if (quote_volume < VOLUME_THRESHOLD_IN_OCTAS) return;

// Get vault balance, returning without lottery if vault is empty.
let vault_signer_cap_ref =
&borrow_global<RewardsVaultSignerCapability>(@rewards).signer_capability;
let vault_address = account::get_signer_capability_address(vault_signer_cap_ref);
let vault_balance = coin::balance<AptosCoin>(vault_address);
if (vault_balance == 0) return;

// Evaluate lottery.
let result = randomness::u64_range(0, WIN_PERCENTAGE_DENOMINATOR);
if (result < WIN_PERCENTAGE_NUMERATOR) {
let reward_amount = if (vault_balance < REWARD_AMOUNT_IN_OCTAS) vault_balance
else REWARD_AMOUNT_IN_OCTAS;
let vault_signer = account::create_signer_with_capability(vault_signer_cap_ref);
aptos_account::transfer(&vault_signer, swapper_address, reward_amount);
event::emit(EmojicoinDotFunRewardsLotteryWinner{ swap, reward_amount });
};

}

fun init_module(rewards: &signer) {
let (vault_signer, signer_capability) = account::create_resource_account(rewards, VAULT);
move_to(rewards, RewardsVaultSignerCapability{ signer_capability });
coin::register<AptosCoin>(&vault_signer);
}

#[test] fun expected_value() {
let bps_per_unit = (emojicoin_dot_fun::get_BASIS_POINTS_PER_UNIT() as u64);
assert!(
WIN_PERCENTAGE_NUMERATOR * REWARD_AMOUNT_IN_OCTAS / WIN_PERCENTAGE_DENOMINATOR <
VOLUME_THRESHOLD_IN_OCTAS * (INTEGRATOR_FEE_RATE_BPS as u64) / bps_per_unit,
0
);
}

}

0 comments on commit 6c6aefc

Please sign in to comment.