Skip to content

Commit 8a508d9

Browse files
authored
Tweak proofs to avoid vacuous and overflow branches (#122)
Overflow checks in the property proofs are converted to using `x.checked_add(y)` instead of a custom predicate `u64::MAX - y < x` to ensure the SMT solver can handle them without having to add simplification lemmas. Some of the proofs rely on the assumption that the total supply of tokens is bound by `u64` and cannot be exceeded by any transfer or other operation moving tokens from one account to another. Cases where this would lead to overflow are skipped. This is protected by the `assumptions` feature flag in the code. This makes a number of proofs pass that previously exhibited `assert_failed` and overflow errors.
1 parent 002e50b commit 8a508d9

File tree

6 files changed

+98
-25
lines changed

6 files changed

+98
-25
lines changed

.github/workflows/rv-run-proofs.yaml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ on:
1515
description: Start symbols for proofs to run
1616
required: true
1717
type: string
18-
default: "['test_ptoken_domain_data', 'test_process_approve', 'test_process_get_account_data_size', 'test_process_initialize_immutable_owner']"
18+
default: "['test_process_get_account_data_size', 'test_process_mint_to', 'test_process_transfer']"
19+
proof_options:
20+
description: Options for proof steps and iteration count
21+
required: true
22+
type: string
23+
default: "--max-depth 10000 --max-iterations 10000"
1924
kmir:
2025
description: KMIR to work with (must exist as a docker image tag, optional)
2126
required: false
@@ -88,7 +93,7 @@ jobs:
8893
- name: "Build with stable_mir_json"
8994
run: |
9095
cd "${{ steps.vars.outputs.crate_dir }}"
91-
RUSTC=stable_mir_json cargo build --features runtime-verification
96+
RUSTC=stable_mir_json cargo build --features runtime-verification,assumptions
9297
9398
- name: "Store SMIR JSON files"
9499
uses: actions/upload-artifact@v4
@@ -178,8 +183,7 @@ jobs:
178183
kmir prove-rs --smir "artefacts/${{ needs.compile.outputs.artifact_name }}.json" \
179184
--start-symbol "${{ needs.compile.outputs.start_prefix }}${{ matrix.proof }}" \
180185
--verbose \
181-
--max-depth 2000 \
182-
--max-iterations 500 \
186+
${{ inputs.proof_options }} \
183187
--proof-dir artefacts/proof \
184188
|| echo "Runner signals proof failure"
185189
@@ -188,14 +192,19 @@ jobs:
188192
"${{ needs.compile.outputs.show_prefix }}${{ matrix.proof }}" \
189193
> artefacts/proof/${{ matrix.proof }}-full.txt
190194
195+
docker exec --workdir /workdir "${KMIR_CONTAINER_NAME}" \
196+
kmir show --statistics --leaves --proof-dir artefacts/proof \
197+
"${{ needs.compile.outputs.show_prefix }}${{ matrix.proof }}" \
198+
> artefacts/proof/${{ matrix.proof }}-stats.txt
199+
191200
- name: "Save proof tree and smir"
192201
if: always()
193202
uses: actions/upload-artifact@v4
194203
with:
195204
name: artefacts-${{ matrix.proof }}
196205
path: |
197206
artefacts/${{ needs.compile.outputs.artifact_name }}.json
198-
artefacts/proof/*-full.txt
207+
artefacts/proof/*.txt
199208
200209
- name: "Shut down docker image"
201210
if: always()

p-token/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
1414
[features]
1515
logging = []
1616
runtime-verification = []
17-
multisig = []
17+
assumptions = []
1818

1919
[dependencies]
2020
pinocchio = { workspace = true }

p-token/src/entrypoint-runtime-verification.rs

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,12 @@ pub fn test_process_transfer(
10331033
let old_src_delgated_amount = src_old.delegated_amount();
10341034
let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account
10351035

1036+
#[cfg(feature = "assumptions")]
1037+
// avoids potential overflow in destination account. assuming global supply bound by u64
1038+
if accounts[0] != accounts[1] && dst_initial_amount.checked_add(amount).is_none() {
1039+
return Err(ProgramError::Custom(99));
1040+
}
1041+
10361042
//-Process Instruction-----------------------------------------------------
10371043
let result = process_transfer(accounts, instruction_data);
10381044

@@ -1121,12 +1127,16 @@ pub fn test_process_transfer(
11211127
} else if accounts[0] != accounts[1]
11221128
&& amount != 0
11231129
&& src_new.is_native()
1124-
&& u64::MAX - amount < dst_initial_lamports
1130+
&& dst_initial_lamports.checked_add(amount).is_none()
11251131
{
11261132
// Not sure how to fund native mint
11271133
assert_eq!(result, Err(ProgramError::Custom(14)));
11281134
return result;
1129-
} else if accounts[0] != accounts[1] && amount != 0 {
1135+
}
1136+
1137+
assert!(result.is_ok());
1138+
1139+
if accounts[0] != accounts[1] && amount != 0 {
11301140
assert_eq!(src_new.amount(), src_initial_amount - amount);
11311141
assert_eq!(
11321142
get_account(&accounts[1]).amount(),
@@ -1139,8 +1149,6 @@ pub fn test_process_transfer(
11391149
}
11401150
}
11411151

1142-
assert!(result.is_ok());
1143-
11441152
// Delegate updates
11451153
if old_src_delgate == Some(*accounts[2].key()) && accounts[0] != accounts[1] {
11461154
assert_eq!(src_new.delegated_amount(), old_src_delgated_amount - amount);
@@ -1328,6 +1336,18 @@ pub fn test_process_mint_to(
13281336
let dst_init_state = dst_old.account_state();
13291337
let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account
13301338

1339+
#[cfg(feature = "assumptions")]
1340+
{
1341+
// Do not execute if adding to the account balance would overflow.
1342+
// shared::mint_to.rs,L68 is based on the assumption that initial_amount <=
1343+
// mint.supply and therefore cannot overflow because the minting itself
1344+
// would already error out.
1345+
let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) };
1346+
if initial_amount.checked_add(amount).is_none() {
1347+
return Err(ProgramError::Custom(99));
1348+
}
1349+
}
1350+
13311351
//-Process Instruction-----------------------------------------------------
13321352
let result = process_mint_to(accounts, instruction_data);
13331353

@@ -1392,7 +1412,7 @@ pub fn test_process_mint_to(
13921412
} else if amount == 0 && accounts[1].owner() != &pinocchio_token_interface::program::ID {
13931413
assert_eq!(result, Err(ProgramError::IncorrectProgramId));
13941414
return result;
1395-
} else if amount != 0 && u64::MAX - amount < initial_supply {
1415+
} else if amount != 0 && amount.checked_add(initial_supply).is_none() {
13961416
assert_eq!(result, Err(ProgramError::Custom(14)));
13971417
return result;
13981418
}
@@ -1538,6 +1558,13 @@ pub fn test_process_burn(accounts: &[AccountInfo; 3], instruction_data: &[u8; 8]
15381558
let mint_owner = *accounts[1].owner();
15391559
let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account
15401560

1561+
#[cfg(feature = "assumptions")]
1562+
// accoutn.amount() <= mint.supply(), account.delegated_amount() <= account.amount()
1563+
// otherwise processing could lead to overflows, see processor::shared::burn,L83
1564+
if !(src_init_amount <= mint_init_supply && old_src_delgated_amount <= src_init_amount) {
1565+
return Err(ProgramError::Custom(99));
1566+
}
1567+
15411568
//-Process Instruction-----------------------------------------------------
15421569
let result = process_burn(accounts, instruction_data);
15431570

@@ -1788,7 +1815,7 @@ pub fn test_process_close_account(accounts: &[AccountInfo; 3]) -> ProgramResult
17881815
} else if accounts[1].key() != &INCINERATOR_ID {
17891816
assert_eq!(result, Err(ProgramError::InvalidAccountData));
17901817
return result;
1791-
} else if u64::MAX - src_init_lamports < dst_init_lamports {
1818+
} else if dst_init_lamports.checked_add(src_init_lamports).is_none() {
17921819
assert_eq!(result, Err(ProgramError::Custom(14)));
17931820
return result;
17941821
}
@@ -1914,6 +1941,12 @@ pub fn test_process_transfer_checked(
19141941
let mint_initialised = get_mint(&accounts[1]).is_initialized();
19151942
let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account
19161943

1944+
#[cfg(feature = "assumptions")]
1945+
// avoids potential overflow in destination account. assuming global supply bound by u64
1946+
if accounts[0] != accounts[1] && dst_initial_amount.checked_add(amount).is_none() {
1947+
return Err(ProgramError::Custom(99));
1948+
}
1949+
19171950
//-Process Instruction-----------------------------------------------------
19181951
let result = process_transfer_checked(accounts, instruction_data);
19191952

@@ -2008,12 +2041,15 @@ pub fn test_process_transfer_checked(
20082041
{
20092042
assert_eq!(result, Err(ProgramError::IncorrectProgramId));
20102043
return result;
2011-
} else if accounts[0] != accounts[2] && amount != 0 {
2044+
}
2045+
assert!(result.is_ok());
2046+
2047+
if accounts[0] != accounts[2] && amount != 0 {
20122048
if src_new.is_native() && src_initial_lamports < amount {
20132049
// Not sure how to fund native mint
20142050
assert_eq!(result, Err(ProgramError::Custom(14)));
20152051
return result;
2016-
} else if src_new.is_native() && u64::MAX - amount < dst_initial_lamports {
2052+
} else if src_new.is_native() && dst_initial_lamports.checked_add(amount).is_none() {
20172053
// Not sure how to fund native mint
20182054
assert_eq!(result, Err(ProgramError::Custom(14)));
20192055
return result;
@@ -2031,7 +2067,6 @@ pub fn test_process_transfer_checked(
20312067
}
20322068
}
20332069

2034-
assert!(result.is_ok());
20352070
// Delegate updates
20362071
if old_src_delgate == Some(*accounts[3].key()) && accounts[0] != accounts[2] {
20372072
assert_eq!(src_new.delegated_amount(), old_src_delgated_amount - amount);
@@ -2242,6 +2277,13 @@ pub fn test_process_burn_checked(
22422277
let mint_owner = *accounts[1].owner();
22432278
let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account
22442279

2280+
#[cfg(feature = "assumptions")]
2281+
// accoutn.amount() <= mint.supply(), account.delegated_amount() <= account.amount()
2282+
// otherwise processing could lead to overflows, see processor::shared::burn,L83
2283+
if !(src_init_amount <= mint_init_supply && old_src_delgated_amount <= src_init_amount) {
2284+
return Err(ProgramError::Custom(99));
2285+
}
2286+
22452287
//-Process Instruction-----------------------------------------------------
22462288
let result = process_burn_checked(accounts, instruction_data);
22472289

@@ -3902,6 +3944,18 @@ fn test_process_mint_to_checked(
39023944
let dst_init_state = dst_old.account_state();
39033945
let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account
39043946

3947+
#[cfg(feature = "assumptions")]
3948+
{
3949+
// Do not execute if adding to the account balance would overflow.
3950+
// shared::mint_to.rs,L68 is based on the assumption that initial_amount <=
3951+
// mint.supply and therefore cannot overflow because the minting itself
3952+
// would already error out.
3953+
let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) };
3954+
if initial_amount.checked_add(amount).is_none() {
3955+
return Err(ProgramError::Custom(99));
3956+
}
3957+
}
3958+
39053959
//-Process Instruction-----------------------------------------------------
39063960
let result = process_mint_to_checked(accounts, instruction_data);
39073961

@@ -3970,7 +4024,7 @@ fn test_process_mint_to_checked(
39704024
} else if amount == 0 && accounts[1].owner() != &pinocchio_token_interface::program::ID {
39714025
assert_eq!(result, Err(ProgramError::IncorrectProgramId));
39724026
return result;
3973-
} else if amount != 0 && u64::MAX - amount < initial_supply {
4027+
} else if amount != 0 && initial_supply.checked_add(amount).is_none() {
39744028
assert_eq!(result, Err(ProgramError::Custom(14)));
39754029
return result;
39764030
}
@@ -4446,15 +4500,18 @@ fn test_process_withdraw_excess_lamports_account(accounts: &[AccountInfo; 3]) ->
44464500
if src_init_lamports < minimum_balance {
44474501
assert_eq!(result, Err(ProgramError::Custom(0)));
44484502
return result;
4449-
} else if u64::MAX - src_init_lamports + minimum_balance < dst_init_lamports {
4503+
} else if dst_init_lamports
4504+
.checked_add(src_init_lamports - minimum_balance)
4505+
.is_none()
4506+
{
44504507
assert_eq!(result, Err(ProgramError::Custom(0)));
44514508
return result;
44524509
}
44534510

44544511
assert_eq!(accounts[0].lamports(), minimum_balance);
44554512
assert_eq!(
44564513
accounts[1].lamports(),
4457-
dst_init_lamports + src_init_lamports - minimum_balance
4514+
dst_init_lamports + (src_init_lamports - minimum_balance)
44584515
);
44594516
assert!(result.is_ok())
44604517
}
@@ -4597,15 +4654,18 @@ fn test_process_withdraw_excess_lamports_mint(accounts: &[AccountInfo; 3]) -> Pr
45974654
} else if src_init_lamports < minimum_balance {
45984655
assert_eq!(result, Err(ProgramError::Custom(0)));
45994656
return result;
4600-
} else if u64::MAX - src_init_lamports + minimum_balance < dst_init_lamports {
4657+
} else if dst_init_lamports
4658+
.checked_add(src_init_lamports - minimum_balance)
4659+
.is_none()
4660+
{
46014661
assert_eq!(result, Err(ProgramError::Custom(0)));
46024662
return result;
46034663
}
46044664

46054665
assert_eq!(accounts[0].lamports(), minimum_balance);
46064666
assert_eq!(
46074667
accounts[1].lamports(),
4608-
dst_init_lamports + src_init_lamports - minimum_balance
4668+
dst_init_lamports + (src_init_lamports - minimum_balance)
46094669
);
46104670
assert!(result.is_ok())
46114671
}
@@ -4739,15 +4799,18 @@ fn test_process_withdraw_excess_lamports_multisig(accounts: &[AccountInfo; 3]) -
47394799
if src_init_lamports < minimum_balance {
47404800
assert_eq!(result, Err(ProgramError::Custom(0)));
47414801
return result;
4742-
} else if u64::MAX - src_init_lamports + minimum_balance < dst_init_lamports {
4802+
} else if dst_init_lamports
4803+
.checked_add(src_init_lamports - minimum_balance)
4804+
.is_none()
4805+
{
47434806
assert_eq!(result, Err(ProgramError::Custom(0)));
47444807
return result;
47454808
}
47464809

47474810
assert_eq!(accounts[0].lamports(), minimum_balance);
47484811
assert_eq!(
47494812
accounts[1].lamports(),
4750-
dst_init_lamports + src_init_lamports - minimum_balance
4813+
dst_init_lamports + (src_init_lamports - minimum_balance)
47514814
);
47524815
assert!(result.is_ok())
47534816
}

p-token/test-properties/run-proofs.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# table if -a given) with given run options (-o) and timeout (-t).
55
# Options and defaults:
66
# -t NUM : timeout in seconds (default 2h=7200)
7-
# -o STRING: prove-rs options. Default "--max-iterations 1000 --max-depth 2000"
7+
# -o STRING: prove-rs options. Default "--max-iterations 10000 --max-depth 10000"
88
# -a : run all start symbols from first table in `proofs.md` (1st column)
99
# -m : run all start symbols from multisig table in `proofs.md` (2nd column)
1010
# -c : continue existing proofs instead of reloading (which is default)
@@ -24,7 +24,7 @@ ALL_NAMES=$(sed -n -e 's/^| \(test_p[a-zA-Z0-9:_]*\) *|.*/\1/p' proofs.md)
2424
MULTISIG_NAMES=$(sed -n -e 's/^| m | \(test_p[a-zA-Z0-9:_]*\) *|.*/\1/p' proofs.md)
2525

2626
TIMEOUT=7200
27-
PROVE_OPTS="--max-iterations 1000 --max-depth 2000"
27+
PROVE_OPTS="--max-iterations 10000 --max-depth 10000"
2828
RELOAD_OPT="--reload"
2929
LOG_FILE=""
3030

p-token/test-properties/setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ ${RUSTC} --version
7575
pushd "${CRATE_DIR}" >/dev/null
7676
# Force cargo to emit artifacts under the crate's own target directory
7777
export CARGO_TARGET_DIR="${CRATE_DIR}/target"
78-
cargo clean && cargo build --features runtime-verification
78+
cargo clean && cargo build --features runtime-verification,assumptions
7979
popd >/dev/null
8080

8181
# Collect SMIR JSONs from the crate's target dir

program/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ test-sbf = []
1313
test-against-pinocchio = ["pinocchio-token-interface"]
1414
runtime-verification = []
1515
rvo = []
16+
assumptions = []
1617

1718
[dependencies]
1819
arrayref = "0.3.9"

0 commit comments

Comments
 (0)