forked from Liquifact/Liquifact-contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexternal_calls.rs
More file actions
222 lines (210 loc) · 9.88 KB
/
Copy pathexternal_calls.rs
File metadata and controls
222 lines (210 loc) · 9.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
//! Hardened wrappers around cross-contract calls used by this escrow.
//!
//! This crate only performs **token** transfers on the address stored under
//! [`crate::DataKey::FundingToken`] after initialization. That address must be a **standard**
//! [SEP-41](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md)-style
//! token with no fee-on-transfer or balance-deficit behavior: post-transfer balance **deltas** must
//! match the requested `amount` exactly on both sides.
//! ## Balance-delta invariants
//!
//! All transfers enforce strict pre/post balance checks to ensure mathematical conservation of value:
//! - **Sender**: balance must decrease by exactly `amount`
//! - **Recipient**: balance must increase by exactly `amount`
//! - **Muxed mapping**: recipient address is wrapped in [`MuxedAddress`] for Stellar compatibility
//! - **Safe failure**: any deviation causes immediate panic with descriptive error message
//!
//! The invariants are enforced through atomic balance verification:
//! 1. Capture pre-transfer balances for both parties
//! 2. Execute the transfer using standard SEP-41 interface
//! 3. Capture post-transfer balances and calculate exact deltas
//! 4. Assert mathematical equality: `sender_delta == recipient_delta == amount`
//!
//! ## Test reality and verification
//!
//! The test suite validates these invariants through:
//! - Standard token transfers with exact delta verification
//! - Edge cases including zero/negative amounts and insufficient balance
//! - Multiple transfer scenarios to ensure cumulative consistency
//! - Mocked token scenarios (where feasible) to detect divergence
//!
//! ## Out-of-scope token economics
//!
//! Malicious, rebasing, or "hook" tokens are **explicitly out of scope** and will cause safe-failure
//! panics at the balance-check boundary. If such tokens bypass these checks, they must be excluded
//! by governance allowlists and integration review. Fee-on-transfer tokens are not supported.
//!
//! Specifically excluded:
//! - Tokens with transfer fees (fee-on-transfer)
//! - Rebasing tokens that change total supply
//! - Tokens with hooks or callbacks that modify balances
//! - Tokens with non-standard balance accounting
//!
//! ## Governance allowlists
//!
//! Integration review and governance allowlists are the primary defense mechanisms against
//! out-of-scope token economics. The balance-delta checks serve as a technical safety net,
//! but proper token selection through governance processes remains essential.
//!
//! # Soroban execution and "reentrancy"
//!
//! Unlike many EVM environments, Soroban does not allow the classic pattern of an external call
//! immediately re-entering the same contract mid-host-function in an interleaved way: the token
//! host function runs to completion before this contract resumes. **Still** treat the token as
//! adversarial for **correctness of balances**: always record pre/post balances around transfers so
//! integration bugs and non-compliant tokens are caught at the host boundary.
//!
//! ## Reviewer timeline (host-call boundary)
//!
//! `transfer_funding_token_with_balance_checks` follows this sequence:
//! 1. Read sender/recipient balances before transfer.
//! 2. Invoke SEP-41 `transfer` on the configured token contract.
//! 3. Soroban host executes that token call to completion, then returns.
//! 4. Read sender/recipient balances after transfer.
//! 5. Assert exact conservation (`spent == amount` and `received == amount`).
//!
//! Security takeaway: this is not relying on "non-reentrancy" as a magic property. It enforces
//! post-call accounting invariants at the external-call boundary where token behavior is observed.
use crate::{ensure, fail, EscrowError};
use soroban_sdk::{token::TokenClient, Address, Env, MuxedAddress};
/// Transfer `amount` of `token_addr` from `from` (typically this escrow contract) to `treasury`,
/// then verify SEP-41-style conservation: sender decreases and recipient increases by exactly
/// `amount`.
///
/// This function performs strict balance-delta verification through atomic balance checks:
/// 1. Records pre-transfer balances for both sender and recipient
/// 2. Executes transfer using [`MuxedAddress::from`] for Stellar compatibility
/// 3. Records post-transfer balances and calculates exact deltas
/// 4. Asserts mathematical equality: `sender_delta == recipient_delta == amount`
///
/// The invariants enforced ensure mathematical conservation of value and detect:
/// - Fee-on-transfer tokens (sender delta > amount)
/// - Rebasing/malicious tokens (recipient delta != amount)
/// - Balance manipulation or integration bugs
///
/// # Arguments
///
/// * `env` - The Soroban environment
/// * `token_addr` - Address of the SEP-41 token contract
/// * `from` - Address transferring from (usually this escrow contract)
/// * `treasury` - Address receiving the tokens
/// * `amount` - Amount to transfer (must be positive)
///
/// # Errors
///
/// Emits typed [`EscrowError`] codes if `amount` is not positive, sender balance is insufficient,
/// balance deltas do not equal `amount`, or balance delta calculation underflows.
///
/// # Security Considerations
///
/// This function assumes the token contract follows standard SEP-41 semantics without
/// fee-on-transfer, rebasing, or hook behaviors. Non-compliant tokens will cause this
/// function to fail with a typed error, serving as a safety boundary. Such tokens should be
/// excluded through governance allowlists and integration review processes.
pub fn transfer_funding_token_with_balance_checks(
env: &Env,
token_addr: &Address,
from: &Address,
treasury: &Address,
amount: i128,
) {
ensure(env, amount > 0, EscrowError::TransferAmountNotPositive);
let token = TokenClient::new(env, token_addr);
let from_before = token.balance(from);
let treasury_before = token.balance(treasury);
ensure(
env,
from_before >= amount,
EscrowError::InsufficientTokenBalanceBeforeTransfer,
);
token.transfer(from, MuxedAddress::from(treasury.clone()), &amount);
let from_after = token.balance(from);
let treasury_after = token.balance(treasury);
let spent = from_before
.checked_sub(from_after)
.unwrap_or_else(|| fail(env, EscrowError::SenderBalanceUnderflow));
let received = treasury_after
.checked_sub(treasury_before)
.unwrap_or_else(|| fail(env, EscrowError::RecipientBalanceUnderflow));
ensure(
env,
spent == amount,
EscrowError::SenderBalanceDeltaMismatch,
);
ensure(
env,
received == amount,
EscrowError::RecipientBalanceDeltaMismatch,
);
}
/// Transfer `amount` of `token` from `from` (external payer) to `to_contract` (this escrow contract),
/// then verify SEP-41-style conservation: recipient increases by exactly `amount` and sender decreases
/// (sender delta is non-positive).
///
/// This function performs strict balance-delta verification through atomic balance checks:
/// 1. Records pre-transfer balances for both sender (external payer) and recipient (escrow contract)
/// 2. Executes transfer using [`MuxedAddress::from`] for Stellar compatibility
/// 3. Records post-transfer balances and calculates exact deltas
/// 4. Asserts:
/// - Recipient delta equals exactly `amount`
/// - Sender delta is non-positive (sender spent at least some tokens)
///
/// The invariants enforced ensure mathematical conservation of value and detect:
/// - Fee-on-transfer tokens (recipient delta < amount)
/// - Rebasing/malicious tokens (recipient delta != amount or sender delta > 0)
/// - Balance manipulation or integration bugs
///
/// # Arguments
///
/// * `env` - The Soroban environment
/// * `token` - Address of the SEP-41 token contract
/// * `from` - Address transferring from (external payer, e.g., investor)
/// * `to_contract` - Address receiving the tokens (this escrow contract)
/// * `amount` - Amount to transfer (must be positive)
///
/// # Errors
///
/// Emits typed [`EscrowError`] codes if `amount` is not positive, sender balance is insufficient,
/// balance deltas do not match expectations, or balance delta calculation underflows.
///
/// # Security Considerations
///
/// This function assumes the token contract follows standard SEP-41 semantics without
/// fee-on-transfer, rebasing, or hook behaviors. Non-compliant tokens will cause this
/// function to fail with a typed error, serving as a safety boundary. Such tokens should be
/// excluded through governance allowlists and integration review processes.
pub fn transfer_into_escrow_with_balance_checks(
env: &Env,
token: &Address,
from: &Address,
to_contract: &Address,
amount: i128,
) {
ensure(env, amount > 0, EscrowError::TransferAmountNotPositive);
let token_client = TokenClient::new(env, token);
let from_before = token_client.balance(from);
let contract_before = token_client.balance(to_contract);
ensure(
env,
from_before >= amount,
EscrowError::InsufficientTokenBalanceBeforeTransfer,
);
token_client.transfer(from, MuxedAddress::from(to_contract.clone()), &amount);
let from_after = token_client.balance(from);
let contract_after = token_client.balance(to_contract);
let spent = from_before
.checked_sub(from_after)
.unwrap_or_else(|| fail(env, EscrowError::SenderBalanceUnderflow));
let received = contract_after
.checked_sub(contract_before)
.unwrap_or_else(|| fail(env, EscrowError::RecipientBalanceUnderflow));
ensure(
env,
received == amount,
EscrowError::RecipientBalanceDeltaMismatch,
);
ensure(
env,
spent >= 0, // sender delta is non-positive (sender spent at least some tokens)
EscrowError::SenderBalanceDeltaMismatch,
);
}