-
Notifications
You must be signed in to change notification settings - Fork 66
fee-abstraction: clear residual allowance after eager fee collection #837
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,16 +71,98 @@ fn collect_fee_with_eager_approval_overwrites_allowance() { | |
| }); | ||
|
|
||
| let events = e.events().all(); | ||
| // approval, transfer and collect fee | ||
| assert_eq!(events.events().len(), 3); | ||
| // approval, fee transfer, residual refund and collect fee | ||
| assert_eq!(events.events().len(), 4); | ||
|
|
||
| // the unspent allowance is consumed back to the user in eager mode | ||
| let allowance = token_client.allowance(&user, &contract_address); | ||
| assert_eq!(allowance, 30); | ||
| assert_eq!(allowance, 0); | ||
|
Comment on lines
+74
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files 'packages/fee-abstraction/**' | sed -n '1,120p'
printf '%s\n' '--- test structure ---'
if command -v ast-grep >/dev/null 2>&1; then
ast-grep outline packages/fee-abstraction/src/test.rs --match '$_' --view compact 2>/dev/null | sed -n '1,160p' || true
fi
printf '%s\n' '--- relevant test lines ---'
sed -n '1,130p' packages/fee-abstraction/src/test.rs
printf '%s\n' '--- event definitions and uses ---'
rg -n -C 4 'Transfer|contractevent|events\(\)|to_xdr|fee_amount|max_fee_amount|allowance' packages/fee-abstraction packages --glob '*.rs' | sed -n '1,260p'Repository: OpenZeppelin/stellar-contracts Length of output: 23172 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- fee-abstraction imports and declarations ---'
sed -n '1,120p' packages/fee-abstraction/src/lib.rs
sed -n '1,90p' packages/fee-abstraction/src/storage.rs
cat packages/fee-abstraction/Cargo.toml
printf '%s\n' '--- all typed transfer-event references ---'
rg -n -C 5 'fungible::.*Transfer|Transfer \{|TransferEvent|FeeCollected|contractevent' packages --glob '*.rs' | sed -n '1,300p'
printf '%s\n' '--- workspace dependency declarations ---'
rg -n -C 3 'stellar_tokens|stellar-tokens|soroban-sdk' Cargo.toml packages --glob 'Cargo.toml' | sed -n '1,220p'
printf '%s\n' '--- relevant test section ---'
sed -n '1,110p' packages/fee-abstraction/src/test.rsRepository: OpenZeppelin/stellar-contracts Length of output: 37678 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- token package structure ---'
git ls-files packages/tokens/src | sed -n '1,160p'
printf '%s\n' '--- fungible module outline and event definitions ---'
if command -v ast-grep >/dev/null 2>&1; then
ast-grep outline packages/tokens/src/fungible.rs --match '$_' --view compact 2>/dev/null | sed -n '1,220p' || true
fi
rg -n -C 8 'struct Transfer|enum Transfer|emit_transfer|transfer_from|contractevent' packages/tokens/src --glob '*.rs' | sed -n '1,320p'
printf '%s\n' '--- existing typed event assertions in tests ---'
rg -n -C 8 'to_xdr\(|events\(\)\.get|events\(\)\.first|events\(\)\.events\(\)' packages --glob 'test.rs' --glob '*.rs' | sed -n '1,320p'Repository: OpenZeppelin/stellar-contracts Length of output: 50386 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- fungible event declarations ---'
rg -n -C 10 'struct Transfer|emit_transfer|contractevent' packages/tokens/src/fungible/mod.rs packages/tokens/src/fungible/storage.rs packages/tokens/src/fungible/overrides.rs
printf '%s\n' '--- fungible public exports ---'
sed -n '1,180p' packages/tokens/src/fungible/mod.rs
printf '%s\n' '--- nearby fungible event implementation ---'
python3 - <<'PY'
from pathlib import Path
p = Path("packages/tokens/src/fungible/mod.rs")
lines = p.read_text().splitlines()
for i, line in enumerate(lines):
if "struct Transfer" in line or "emit_transfer" in line or "contractevent" in line:
lo, hi = max(0, i - 8), min(len(lines), i + 35)
print(f"--- lines {lo+1}-{hi} ---")
for n in range(lo, hi):
print(f"{n+1}:{lines[n]}")
PY
printf '%s\n' '--- typed event assertion conventions around indexed events ---'
rg -l 'events\(\)\.get|events\(\)\.first|to_xdr\(' packages --glob '*.rs' |
while IFS= read -r f; do
case "$f" in
packages/tokens/*|packages/fee-abstraction/*)
rg -n -C 4 'events\(\)\.(get|first)|to_xdr\(' "$f"
;;
esac
done | sed -n '1,260p'Repository: OpenZeppelin/stellar-contracts Length of output: 39261 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
test = Path("packages/fee-abstraction/src/test.rs").read_text()
storage = Path("packages/fee-abstraction/src/storage.rs").read_text()
fungible = Path("packages/tokens/src/fungible/mod.rs").read_text()
# Confirm the test currently checks only the event count and not typed payloads.
assert "assert_eq!(events.events().len(), 4);" in test
assert "to_xdr" not in test
# Confirm the eager path emits approval, fee transfer, refund, then FeeCollected.
body = storage[storage.index("pub fn collect_fee("):storage.index("// ################## FEE TOKEN ALLOWLIST")]
sequence = [
"token_client.approve(",
"token_client.transfer_from(&e.current_contract_address(), user, fee_recipient, &fee_amount);",
"token_client.transfer_from(&e.current_contract_address(), user, user, &remaining);",
"emit_fee_collected(e, user, fee_recipient, fee_token, fee_amount);",
]
positions = [body.index(item) for item in sequence]
assert positions == sorted(positions), positions
# Confirm the refund is a non-muxed fungible Transfer event with the required fields.
assert re.search(
r"Transfer\s*\{.*?from: from\.clone\(\),.*?to: to\.clone\(\),.*?amount",
fungible,
re.S,
)
assert "Transfer { from: from.clone(), to: to.clone(), amount }.publish(e);" in fungible
assert "let remaining = max_fee_amount - fee_amount;" in body
print("The count-only assertion omits the eager refund payload.")
print("The refund is event index 2 and is serialized by stellar_tokens::fungible::Transfer.")
print("Expected refund payload: from=user, to=user, amount=max_fee_amount-fee_amount.")
PYRepository: OpenZeppelin/stellar-contracts Length of output: 389 Assert the eager refund event payload. Compare 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| let balance = token_client.balance(&recipient); | ||
| assert_eq!(balance, 20); | ||
| } | ||
|
|
||
| #[test] | ||
| fn collect_fee_with_eager_approval_consumes_only_the_affordable_residual() { | ||
| let e = Env::default(); | ||
| e.mock_all_auths_allowing_non_root_auth(); | ||
|
|
||
| let contract_address = e.register(MockContract, ()); | ||
| let user = Address::generate(&e); | ||
| let token_address = e.register(MockToken, (user.clone(),)); | ||
| let recipient = Address::generate(&e); | ||
| let other = Address::generate(&e); | ||
|
|
||
| let max_fee_amount = 50; | ||
|
|
||
| let token_client = TokenClient::new(&e, &token_address); | ||
| // the user keeps 25, which covers the fee but not the whole residual | ||
| token_client.transfer(&user, &other, &975); | ||
|
|
||
| e.as_contract(&contract_address, || { | ||
| // approve 50, spend 20 | ||
| collect_fee( | ||
| &e, | ||
| &token_address, | ||
| 20, | ||
| max_fee_amount, | ||
| 100, | ||
| &user, | ||
| &recipient, | ||
| FeeAbstractionApproval::Eager, | ||
| ); | ||
| }); | ||
|
|
||
| // only the 5 left after the fee are consumed, the rest of the allowance stays | ||
| let allowance = token_client.allowance(&user, &contract_address); | ||
| assert_eq!(allowance, 25); | ||
|
|
||
| assert_eq!(token_client.balance(&user), 5); | ||
| assert_eq!(token_client.balance(&recipient), 20); | ||
| } | ||
|
|
||
| #[test] | ||
| fn collect_fee_with_eager_approval_and_no_residual_balance() { | ||
| let e = Env::default(); | ||
| e.mock_all_auths_allowing_non_root_auth(); | ||
|
|
||
| let contract_address = e.register(MockContract, ()); | ||
| let user = Address::generate(&e); | ||
| let token_address = e.register(MockToken, (user.clone(),)); | ||
| let recipient = Address::generate(&e); | ||
| let other = Address::generate(&e); | ||
|
|
||
| let max_fee_amount = 50; | ||
|
|
||
| let token_client = TokenClient::new(&e, &token_address); | ||
| // the user keeps exactly the fee, so nothing is left to consume | ||
| token_client.transfer(&user, &other, &980); | ||
|
|
||
| e.as_contract(&contract_address, || { | ||
| // approve 50, spend 20 | ||
| collect_fee( | ||
| &e, | ||
| &token_address, | ||
| 20, | ||
| max_fee_amount, | ||
| 100, | ||
| &user, | ||
| &recipient, | ||
| FeeAbstractionApproval::Eager, | ||
| ); | ||
| }); | ||
|
|
||
| let events = e.events().all(); | ||
| // approval, fee transfer and collect fee, without a residual refund | ||
| assert_eq!(events.events().len(), 3); | ||
|
|
||
| let allowance = token_client.allowance(&user, &contract_address); | ||
| assert_eq!(allowance, 30); | ||
|
|
||
| assert_eq!(token_client.balance(&user), 0); | ||
| assert_eq!(token_client.balance(&recipient), 20); | ||
| } | ||
|
|
||
| #[test] | ||
| fn collect_fee_with_lazy_approval_no_previous() { | ||
| let e = Env::default(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: OpenZeppelin/stellar-contracts
Length of output: 50386
🏁 Script executed:
Repository: OpenZeppelin/stellar-contracts
Length of output: 50388
🏁 Script executed:
Repository: OpenZeppelin/stellar-contracts
Length of output: 24839
🏁 Script executed:
Repository: OpenZeppelin/stellar-contracts
Length of output: 14358
🏁 Script executed:
Repository: OpenZeppelin/stellar-contracts
Length of output: 329
🏁 Script executed:
Repository: OpenZeppelin/stellar-contracts
Length of output: 375
Avoid debiting the user to clear the eager allowance.
When
fee_amount < max_fee_amount, the self-transfer still requires the user balance to covermax_fee_amount - fee_amount. If the user balance equalsfee_amount, cleanup fails withInsufficientBalance, andcollect_fee_and_invokedoes not reach the target call. Use an allowance-only cleanup authorized in the eager authorization tree. Add a regression test for this balance.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @knQzx the coderabbit finding seems a valid one: if the user's balance is below
max_fee_amount - fee_amountthe whole invocation would fail. One option is usingtry_transfer_frombut then we might have to deal with residuals that are smaller thanremaining. Another option is checking the user's balance and transferring onlymin(balance, remaining).