Skip to content

Commit 792b7b2

Browse files
committed
Improved payment, fee, and reputation handling for contract sign
1 parent 8637ae8 commit 792b7b2

File tree

5 files changed

+295
-86
lines changed

5 files changed

+295
-86
lines changed

common/src/account_transfers.rs

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ use sha2::{Digest, Sha256};
1414
use std::cell::RefCell;
1515

1616
use crate::{
17-
account_balance_add, account_balance_get, account_balance_sub, get_timestamp_ns,
18-
ledger_add_reputation_change, slice_to_32_bytes_array, DccIdentity, TokenAmountE9s,
19-
TransferError, LABEL_DC_TOKEN_TRANSFER, MINTING_ACCOUNT, MINTING_ACCOUNT_PRINCIPAL,
17+
account_balance_add, account_balance_get, account_balance_sub, amount_as_string,
18+
get_pubkey_from_principal, get_timestamp_ns, ledger_add_reputation_change,
19+
slice_to_32_bytes_array, DccIdentity, TokenAmountE9s, TransferError, LABEL_DC_TOKEN_TRANSFER,
20+
MINTING_ACCOUNT, MINTING_ACCOUNT_PRINCIPAL,
2021
};
2122

2223
thread_local! {
@@ -63,34 +64,33 @@ pub fn ledger_funds_transfer(
6364

6465
pub fn charge_fees_to_account_and_bump_reputation(
6566
ledger: &mut LedgerMap,
66-
dcc_id_charge: &DccIdentity,
67-
dcc_id_bump_reputation: &DccIdentity,
67+
dcc_id: &DccIdentity,
6868
amount_e9s: TokenAmountE9s,
6969
) -> Result<(), String> {
7070
if amount_e9s == 0 {
7171
return Ok(());
7272
}
73-
let balance_from_after =
74-
account_balance_get(&dcc_id_charge.as_icrc_compatible_account()) - amount_e9s;
73+
let from_icrc1_account = dcc_id.as_icrc_compatible_account();
74+
let balance_from_before = account_balance_get(&from_icrc1_account);
7575
match ledger_funds_transfer(
7676
ledger,
7777
// Burn 0 tokens, and transfer the entire amount_e9s to the fee accounts
7878
FundsTransfer::new(
79-
dcc_id_charge.as_icrc_compatible_account(),
79+
from_icrc1_account,
8080
MINTING_ACCOUNT,
8181
amount_e9s.into(),
8282
Some(fees_sink_accounts()),
8383
Some(get_timestamp_ns()),
8484
vec![],
8585
0,
86-
balance_from_after,
86+
balance_from_before.saturating_sub(amount_e9s),
8787
0,
8888
),
8989
) {
9090
Ok(_) => Ok(ledger_add_reputation_change(
9191
ledger,
92-
dcc_id_bump_reputation,
93-
amount_e9s.min(i64::MAX as TokenAmountE9s) as i64,
92+
dcc_id,
93+
amount_e9s as i64,
9494
)?),
9595
Err(e) => {
9696
info!("Failed to charge fees: {}", e);
@@ -101,26 +101,26 @@ pub fn charge_fees_to_account_and_bump_reputation(
101101

102102
pub fn charge_fees_to_account_no_bump_reputation(
103103
ledger: &mut LedgerMap,
104-
dcc_identity: &DccIdentity,
104+
dcc_id_charge: &DccIdentity,
105105
amount_e9s: TokenAmountE9s,
106106
) -> Result<(), String> {
107107
if amount_e9s == 0 {
108108
return Ok(());
109109
}
110-
let balance_from_after =
111-
account_balance_get(&dcc_identity.as_icrc_compatible_account()) - amount_e9s;
110+
let from_icrc1_account = dcc_id_charge.as_icrc_compatible_account();
111+
let balance_from_before = account_balance_get(&from_icrc1_account);
112112
match ledger_funds_transfer(
113113
ledger,
114114
// Burn 0 tokens, and transfer the entire amount_e9s to the fee accounts
115115
FundsTransfer::new(
116-
dcc_identity.as_icrc_compatible_account(),
116+
from_icrc1_account,
117117
MINTING_ACCOUNT,
118118
Some(amount_e9s),
119119
Some(fees_sink_accounts()),
120120
Some(get_timestamp_ns()),
121121
vec![],
122122
0,
123-
balance_from_after,
123+
balance_from_before.saturating_sub(amount_e9s),
124124
0,
125125
),
126126
) {
@@ -132,6 +132,78 @@ pub fn charge_fees_to_account_no_bump_reputation(
132132
}
133133
}
134134

135+
pub enum IncreaseReputation {
136+
None,
137+
Sender,
138+
Recipient,
139+
}
140+
141+
pub fn do_funds_transfer(
142+
ledger: &mut LedgerMap,
143+
from_dcc_id: &DccIdentity,
144+
to_icrc1_account: &IcrcCompatibleAccount,
145+
transfer_amount_e9s: TokenAmountE9s,
146+
fees_amount_e9s: TokenAmountE9s,
147+
memo: &[u8],
148+
increase_reputation: IncreaseReputation,
149+
) -> Result<String, String> {
150+
let from_icrc1_account = from_dcc_id.as_icrc_compatible_account();
151+
152+
if transfer_amount_e9s == 0 {
153+
return Ok("Nothing to transfer".to_string());
154+
}
155+
let balance_from_before = account_balance_get(&from_icrc1_account);
156+
let balance_to_before = account_balance_get(to_icrc1_account);
157+
if balance_from_before < transfer_amount_e9s + fees_amount_e9s {
158+
return Err(format!(
159+
"Not enough funds to transfer: {} < {} + {}",
160+
balance_from_before, transfer_amount_e9s, fees_amount_e9s
161+
));
162+
}
163+
let balance_from_after = balance_from_before - transfer_amount_e9s - fees_amount_e9s;
164+
match ledger_funds_transfer(
165+
ledger,
166+
FundsTransfer::new(
167+
from_icrc1_account.clone(),
168+
to_icrc1_account.clone(),
169+
Some(fees_amount_e9s),
170+
Some(fees_sink_accounts()),
171+
Some(get_timestamp_ns()),
172+
memo.to_vec(),
173+
transfer_amount_e9s,
174+
balance_from_after,
175+
balance_to_before + transfer_amount_e9s,
176+
),
177+
) {
178+
Ok(_) => {
179+
let response = format!(
180+
"Transferred {} tokens from {} \t to account {}, and charged fees {} tokens",
181+
amount_as_string(transfer_amount_e9s),
182+
from_icrc1_account,
183+
to_icrc1_account,
184+
amount_as_string(fees_amount_e9s)
185+
);
186+
match increase_reputation {
187+
IncreaseReputation::None => (),
188+
IncreaseReputation::Sender => {
189+
ledger_add_reputation_change(ledger, from_dcc_id, fees_amount_e9s as i64)?;
190+
}
191+
IncreaseReputation::Recipient => {
192+
let to_dcc_identity = to_icrc1_account
193+
.to_dcc_identity()
194+
.expect("Failed to get dcc identity from icrc1 account");
195+
ledger_add_reputation_change(ledger, &to_dcc_identity, fees_amount_e9s as i64)?;
196+
}
197+
}
198+
Ok(response)
199+
}
200+
Err(e) => {
201+
info!("Failed to charge fees: {}", e);
202+
Err(e.to_string())
203+
}
204+
}
205+
}
206+
135207
#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
136208
pub struct IcrcCompatibleAccount {
137209
pub owner: Principal,
@@ -201,6 +273,14 @@ impl IcrcCompatibleAccount {
201273
pub fn from_bytes(bytes: &[u8]) -> Result<Self, std::io::Error> {
202274
borsh::from_slice(bytes)
203275
}
276+
277+
pub fn to_dcc_identity(&self) -> Option<DccIdentity> {
278+
let owner_bytes = get_pubkey_from_principal(self.owner);
279+
if owner_bytes.is_empty() {
280+
return None;
281+
}
282+
DccIdentity::new_verifying_from_bytes(&owner_bytes).ok()
283+
}
204284
}
205285

206286
impl From<&str> for IcrcCompatibleAccount {

common/src/contract_sign_reply.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use ledger_map::LedgerMap;
66
use serde::{Deserialize, Serialize};
77

88
use crate::{
9-
amount_as_string, charge_fees_to_account_and_bump_reputation, contract_sign_fee_e9s,
10-
contracts_cache_open_remove, fn_info, ContractSignRequestPayload, DccIdentity,
9+
account_balance_get, amount_as_string, charge_fees_to_account_no_bump_reputation,
10+
contract_sign_fee_e9s, contracts_cache_open_remove, do_funds_transfer, fn_info,
11+
ledger_add_reputation_change, ContractSignRequestPayload, DccIdentity,
1112
LABEL_CONTRACT_SIGN_REPLY, LABEL_CONTRACT_SIGN_REQUEST,
1213
};
1314

@@ -117,10 +118,11 @@ pub fn do_contract_sign_reply(
117118
reply_serialized: Vec<u8>,
118119
crypto_signature: Vec<u8>,
119120
) -> Result<String, String> {
120-
let dcc_id = DccIdentity::new_verifying_from_bytes(&pubkey_bytes).unwrap();
121-
dcc_id.verify_bytes(&reply_serialized, &crypto_signature)?;
121+
let provider_dcc_id = DccIdentity::new_verifying_from_bytes(&pubkey_bytes).unwrap();
122+
provider_dcc_id.verify_bytes(&reply_serialized, &crypto_signature)?;
123+
let provider_icrc1 = provider_dcc_id.as_icrc_compatible_account();
122124

123-
fn_info!("{}", dcc_id);
125+
fn_info!("{}", provider_dcc_id);
124126

125127
let cs_reply = ContractSignReply::try_from_slice(&reply_serialized).unwrap();
126128
let contract_id = cs_reply.contract_id();
@@ -134,14 +136,44 @@ pub fn do_contract_sign_reply(
134136
if pubkey_bytes != cs_req.provider_pubkey_bytes() {
135137
return Err(format!(
136138
"Contract signing reply signed and submitted by {} does not match the provider public key {} from contract req 0x{}",
137-
dcc_id, DccIdentity::new_verifying_from_bytes(cs_req.provider_pubkey_bytes()).unwrap(), hex::encode(contract_id)
139+
provider_dcc_id, DccIdentity::new_verifying_from_bytes(cs_req.provider_pubkey_bytes()).unwrap(), hex::encode(contract_id)
138140
));
139141
}
142+
let fees_e9s = contract_sign_fee_e9s(cs_req.payment_amount_e9s());
143+
144+
let requester_dcc_id =
145+
DccIdentity::new_verifying_from_bytes(cs_req.requester_pubkey_bytes()).unwrap();
146+
let requester_icrc1 = requester_dcc_id.as_icrc_compatible_account();
147+
let requester_balance = account_balance_get(&requester_icrc1);
148+
140149
let payload = ContractSignReplyPayload::new(reply_serialized, crypto_signature);
141150
let payload_serialized = borsh::to_vec(&payload).unwrap();
142151

143-
let fees = contract_sign_fee_e9s(cs_req.payment_amount_e9s());
144-
charge_fees_to_account_and_bump_reputation(ledger, &dcc_id, &dcc_id, fees)?;
152+
if cs_reply.sign_accepted() {
153+
// Accepted: charge the requester the full amount
154+
let charge_amount_e9s = cs_req.payment_amount_e9s() + fees_e9s;
155+
if requester_balance < charge_amount_e9s {
156+
return Err(format!(
157+
"Requester {} does not have enough funds. At least {} tokens are required, and {} are available",
158+
requester_icrc1,
159+
amount_as_string(charge_amount_e9s),
160+
amount_as_string(requester_balance)
161+
));
162+
}
163+
do_funds_transfer(
164+
ledger,
165+
&requester_dcc_id,
166+
&provider_icrc1,
167+
cs_req.payment_amount_e9s(),
168+
fees_e9s,
169+
cs_req.request_memo().as_bytes(),
170+
crate::IncreaseReputation::Recipient,
171+
)?;
172+
} else {
173+
// Else, charge the provider the response fee, and revert the requester's reputation
174+
charge_fees_to_account_no_bump_reputation(ledger, &provider_dcc_id, fees_e9s)?;
175+
ledger_add_reputation_change(ledger, &requester_dcc_id, -(fees_e9s as i64))?;
176+
}
145177

146178
ledger.upsert(
147179
LABEL_CONTRACT_SIGN_REPLY,
@@ -152,7 +184,7 @@ pub fn do_contract_sign_reply(
152184
contracts_cache_open_remove(contract_id);
153185
format!(
154186
"Contract signing reply submitted! Thank you. You have been charged {} tokens as a fee, and your reputation has been bumped accordingly",
155-
amount_as_string(fees)
187+
amount_as_string(fees_e9s)
156188
)
157189
})
158190
.map_err(|e| e.to_string())

common/src/contract_sign_request.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize, Serializer};
99
use sha2::{Digest, Sha256};
1010

1111
use crate::{
12-
amount_as_string, charge_fees_to_account_and_bump_reputation, fn_info, AHashMap, DccIdentity,
13-
TokenAmountE9s, LABEL_CONTRACT_SIGN_REQUEST,
12+
account_balance_get, amount_as_string, charge_fees_to_account_and_bump_reputation, fn_info,
13+
AHashMap, DccIdentity, TokenAmountE9s, LABEL_CONTRACT_SIGN_REQUEST,
1414
};
1515

1616
pub type ContractId = Vec<u8>;
@@ -267,14 +267,29 @@ pub fn do_contract_sign_request(
267267

268268
let contract_req = ContractSignRequest::try_from_slice(&request_serialized).unwrap();
269269

270-
let fees = contract_sign_fee_e9s(contract_req.payment_amount_e9s());
270+
let fees_e9s = contract_sign_fee_e9s(contract_req.payment_amount_e9s());
271+
272+
let requester_dcc_id =
273+
DccIdentity::new_verifying_from_bytes(contract_req.requester_pubkey_bytes()).unwrap();
274+
let requester_icrc1 = requester_dcc_id.as_icrc_compatible_account();
275+
let requester_balance = account_balance_get(&requester_icrc1);
276+
let expected_min_balance = contract_req.payment_amount_e9s() + fees_e9s;
277+
278+
if requester_balance < expected_min_balance {
279+
return Err(format!(
280+
"Signing of this contract requires at least {} tokens. Requester {} has only {} tokens",
281+
amount_as_string(expected_min_balance),
282+
requester_icrc1,
283+
amount_as_string(requester_balance)
284+
));
285+
}
271286

272287
let payload = ContractSignRequestPayload::new(&request_serialized, &crypto_signature).unwrap();
273288
let payload_bytes = borsh::to_vec(&payload).unwrap();
274289

275-
charge_fees_to_account_and_bump_reputation(ledger, &dcc_id, &dcc_id, fees)?;
276-
let contract_id = payload.calc_contract_id();
290+
charge_fees_to_account_and_bump_reputation(ledger, &dcc_id, fees_e9s)?;
277291

292+
let contract_id = payload.calc_contract_id();
278293
ledger.upsert(
279294
LABEL_CONTRACT_SIGN_REQUEST,
280295
contract_id,
@@ -284,7 +299,7 @@ pub fn do_contract_sign_request(
284299
format!(
285300
"Contract signing req 0x{} submitted! Thank you. You have been charged {} tokens as a fee, and your reputation has been bumped accordingly. Please check back for a response from the provider.",
286301
hex::encode(contract_id),
287-
amount_as_string(fees)
302+
amount_as_string(fees_e9s)
288303
)
289304
}).map_err(|e| e.to_string())
290305
}

common/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub const MAX_PUBKEY_BYTES: usize = 32;
106106
pub const MEMO_BYTES_MAX: usize = 32;
107107
/// Reduction of reputations for all accounts, based on time (per block), in parts per million
108108
pub const REPUTATION_AGING_PPM: u64 = 1_000;
109-
pub const MAX_REPUTATION_INCREASE_PER_TX: i64 = DC_TOKEN_DECIMALS_DIV as i64 / 1000;
109+
pub const MAX_REPUTATION_INCREASE_PER_TX: i64 = DC_TOKEN_DECIMALS_DIV as i64 * 10; // Max 10 DC tokens per transaction
110110
pub const REWARD_HALVING_AFTER_BLOCKS: u64 = 210_000; // halve the rewards every 210000 reward distributions
111111
pub const DATA_PULL_BYTES_BEFORE_LEN: u16 = 16; // How many bytes before the pulled data should be compared as a quick sanity check
112112

0 commit comments

Comments
 (0)