Skip to content

Commit c49f95d

Browse files
authored
Add Support for AirdropOp for Bulk Distribution of Tokens (#15)
* initial airdrop opcode * added airdrop tests * added chunked test
1 parent 21eea9f commit c49f95d

File tree

11 files changed

+836
-17
lines changed

11 files changed

+836
-17
lines changed

api/src/cvm/messages/airdrop.rs

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use steel::*;
2+
3+
use crate::utils;
4+
use crate::types::Hash;
5+
use crate::cvm::{
6+
CodeVmAccount,
7+
VirtualDurableNonce,
8+
VirtualTimelockAccount
9+
};
10+
11+
pub fn compact_airdrop_message(
12+
src_timelock_address: &Pubkey,
13+
dst_timelock_addresses: &[Pubkey],
14+
amount: u64,
15+
vdn: &VirtualDurableNonce,
16+
) -> Hash {
17+
let mut msg = Vec::new();
18+
19+
msg.push(b"airdrop" as &[u8]);
20+
msg.push(src_timelock_address.as_ref());
21+
msg.push(vdn.address.as_ref());
22+
msg.push(vdn.value.as_ref());
23+
24+
// Store the little-endian bytes in a local variable so it won't go out of scope
25+
let amount_bytes = amount.to_le_bytes();
26+
msg.push(&amount_bytes);
27+
28+
// Push each destination pubkey
29+
for dst_pubkey in dst_timelock_addresses {
30+
msg.push(dst_pubkey.as_ref());
31+
}
32+
33+
utils::hashv(&msg)
34+
}
35+
36+
pub fn create_airdrop_message(
37+
vm: &CodeVmAccount,
38+
src_vta: &VirtualTimelockAccount,
39+
destinations: &[Pubkey],
40+
amount: u64,
41+
vdn: &VirtualDurableNonce,
42+
) -> Hash {
43+
44+
let src_timelock_address = src_vta.get_timelock_address(
45+
&vm.get_mint(),
46+
&vm.get_authority(),
47+
vm.get_lock_duration(),
48+
);
49+
50+
let src_token_address = src_vta.get_token_address(
51+
&src_timelock_address,
52+
);
53+
54+
compact_airdrop_message(
55+
&src_token_address,
56+
destinations,
57+
amount,
58+
vdn,
59+
)
60+
}

api/src/cvm/messages/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
mod airdrop;
12
mod transfer;
23
mod withdraw;
34

5+
pub use airdrop::*;
46
pub use transfer::*;
57
pub use withdraw::*;

api/src/opcode.rs

+37
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub enum Opcode {
1515
ExternalRelayOp = 20,
1616

1717
ConditionalTransferOp = 12,
18+
19+
AirdropOp = 30,
1820
}
1921

2022
instruction!(Opcode, TransferOp);
@@ -24,6 +26,7 @@ instruction!(Opcode, ExternalTransferOp);
2426
instruction!(Opcode, ExternalWithdrawOp);
2527
instruction!(Opcode, ExternalRelayOp);
2628
instruction!(Opcode, ConditionalTransferOp);
29+
instruction!(Opcode, AirdropOp);
2730

2831
#[repr(C)]
2932
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
@@ -210,3 +213,37 @@ pub struct ParsedConditionalTransferOp {
210213
pub signature: [u8; 64],
211214
pub amount: u64,
212215
}
216+
217+
#[repr(C)]
218+
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
219+
pub struct AirdropOp {
220+
pub signature: [u8; 64],
221+
pub amount: [u8; 8], // Pack u64 as [u8; 8]
222+
pub count: u8, // Up to 255 airdrops in a single tx (but CU limit will be hit first)
223+
}
224+
225+
impl AirdropOp {
226+
/// Converts the byte array `amount` to `u64`.
227+
pub fn to_struct(&self) -> Result<ParsedAirdropOp, std::io::Error> {
228+
Ok(ParsedAirdropOp {
229+
signature: self.signature,
230+
amount: u64::from_le_bytes(self.amount),
231+
count: self.count,
232+
})
233+
}
234+
235+
/// Creates `AirdropOp` from the parsed struct by converting `u64` back to byte array.
236+
pub fn from_struct(parsed: ParsedAirdropOp) -> Self {
237+
AirdropOp {
238+
signature: parsed.signature,
239+
amount: parsed.amount.to_le_bytes(),
240+
count: parsed.count,
241+
}
242+
}
243+
}
244+
245+
pub struct ParsedAirdropOp {
246+
pub signature: [u8; 64],
247+
pub amount: u64,
248+
pub count: u8,
249+
}

program/src/instruction/exec.rs

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ pub fn process_exec(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult
6969

7070
Opcode::ConditionalTransferOp => process_conditional_transfer(&ctx, &args),
7171

72+
Opcode::AirdropOp => process_airdrop(&ctx, &args),
73+
7274
_ => Err(ProgramError::InvalidInstructionData),
7375
}?;
7476

program/src/opcode/airdrop.rs

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use code_vm_api::prelude::*;
2+
use steel::*;
3+
4+
use crate::ExecContext;
5+
6+
/*
7+
This instruction is used to transfer tokens from *one* virtual account to a
8+
number of virtual accounts. The signature of the source account is required
9+
to authorize the transfer.
10+
11+
Extra accounts required by this instruction:
12+
13+
| # | R/W | Type | Req | PDA | Name | Description |
14+
|---|-----|------------- |-----|-----|--------|--------------|
15+
|...| The same as the vm_exec instruction. |
16+
|---|-----|------------- |-----|-----|--------|--------------|
17+
| 6 | | <None> | | | | |
18+
| 7 | | <None> | | | | |
19+
| 8 | | <None> | | | | |
20+
| 9 | | <None> | | | | |
21+
|10 | | <None> | | | | |
22+
23+
24+
Instruction data:
25+
26+
0. signature: [u8;64] - The opcode to execute.
27+
1. amount: [u64] - The account_indicies of the virtual accounts to use.
28+
2. count: [u8] - The number of destinations.
29+
*/
30+
pub fn process_airdrop(
31+
ctx: &ExecContext,
32+
data: &ExecIxData,
33+
) -> ProgramResult {
34+
35+
let vm = load_vm(ctx.vm_info)?;
36+
let args = AirdropOp::try_from_bytes(&data.data)?.to_struct()?;
37+
38+
let mem_indicies = &data.mem_indicies;
39+
let mem_banks = &data.mem_banks;
40+
let num_accounts = 2 + (args.count as usize);
41+
42+
check_condition(
43+
mem_indicies.len() == num_accounts,
44+
"invalid number of memory indicies",
45+
)?;
46+
47+
check_condition(
48+
mem_banks.len() == num_accounts,
49+
"invalid number of memory banks",
50+
)?;
51+
52+
let nonce_index = mem_indicies[0];
53+
let nonce_mem = mem_banks[0];
54+
55+
let src_index = mem_indicies[1];
56+
let src_mem = mem_banks[1];
57+
58+
let vm_mem = ctx.get_banks();
59+
60+
check_condition(
61+
vm_mem[nonce_mem as usize].is_some(),
62+
"the nonce memory account must be provided",
63+
)?;
64+
65+
check_condition(
66+
vm_mem[src_mem as usize].is_some(),
67+
"the source memory account must be provided",
68+
)?;
69+
70+
let nonce_mem_info = vm_mem[nonce_mem as usize].unwrap();
71+
let src_mem_info = vm_mem[src_mem as usize].unwrap();
72+
73+
let va = try_read(&nonce_mem_info, nonce_index)?;
74+
let mut vdn = va.into_inner_nonce().unwrap();
75+
76+
let va = try_read(&src_mem_info, src_index)?;
77+
let mut src_vta = va.into_inner_timelock().unwrap();
78+
79+
let total_amount = args.amount
80+
.checked_mul(args.count as u64)
81+
.ok_or(ProgramError::ArithmeticOverflow)?;
82+
83+
if src_vta.balance < total_amount {
84+
return Err(ProgramError::InsufficientFunds);
85+
}
86+
87+
src_vta.balance = src_vta.balance
88+
.checked_sub(total_amount)
89+
.ok_or(ProgramError::ArithmeticOverflow)?;
90+
91+
let mut dst_pubkeys = Vec::new();
92+
for i in 0..args.count as usize {
93+
let dst_index = mem_indicies[2 + i];
94+
let dst_mem = mem_banks[2 + i];
95+
96+
check_condition(
97+
vm_mem[dst_mem as usize].is_some(),
98+
"a destination memory account must be provided",
99+
)?;
100+
101+
let dst_mem_info = vm_mem[dst_mem as usize].unwrap();
102+
103+
let va = try_read(&dst_mem_info, dst_index)?;
104+
let mut dst_vta = va.into_inner_timelock().unwrap();
105+
106+
// Check if this destination is actually the source.
107+
let is_same_account = (src_mem == dst_mem) && (src_index == dst_index);
108+
if is_same_account {
109+
// If the source is also in the destinations list, it receives the airdrop as well.
110+
src_vta.balance = src_vta.balance
111+
.checked_add(args.amount)
112+
.ok_or(ProgramError::ArithmeticOverflow)?;
113+
114+
} else {
115+
// Normal destination: add the airdrop to its balance
116+
dst_vta.balance = dst_vta.balance
117+
.checked_add(args.amount)
118+
.ok_or(ProgramError::ArithmeticOverflow)?;
119+
120+
// Write the updated destination back
121+
try_write(
122+
dst_mem_info,
123+
dst_index,
124+
&VirtualAccount::Timelock(dst_vta)
125+
)?;
126+
}
127+
128+
dst_pubkeys.push(dst_vta.owner);
129+
}
130+
131+
let hash = create_airdrop_message(
132+
&vm,
133+
&src_vta,
134+
&dst_pubkeys,
135+
args.amount,
136+
&vdn,
137+
);
138+
139+
sig_verify(
140+
src_vta.owner.as_ref(),
141+
args.signature.as_ref(),
142+
hash.as_ref(),
143+
)?;
144+
145+
vdn.value = vm.get_current_poh();
146+
147+
// Finally, write back the updated source (which now includes
148+
// any airdrop shares if the source was also in the destination list).
149+
try_write(
150+
src_mem_info,
151+
src_index,
152+
&VirtualAccount::Timelock(src_vta)
153+
)?;
154+
155+
try_write(
156+
nonce_mem_info,
157+
nonce_index,
158+
&VirtualAccount::Nonce(vdn)
159+
)?;
160+
161+
Ok(())
162+
}

program/src/opcode/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod airdrop;
12
mod conditional_transfer;
23
mod external_relay;
34
mod external_transfer;
@@ -6,6 +7,7 @@ mod relay;
67
mod transfer;
78
mod withdraw;
89

10+
pub use airdrop::*;
911
pub use conditional_transfer::*;
1012
pub use external_relay::*;
1113
pub use external_transfer::*;

0 commit comments

Comments
 (0)