Skip to content

Commit 71adb1a

Browse files
palinatolmachrv-auditorPetarMaxPetar Maksimovicrv-jenkins
authored
CSE: Include symbolic contracts corresponding to contract fields into accounts (#600)
* Draft implementation and test for `contract` field accounts inclusion * Set Version: 0.1.301 * Set Version: 0.1.301 * Set Version: 0.1.302 * Use (simple) `#lookup` constraint for `contract` field in storage * Minor formatting * Set Version: 0.1.303 * Draft implementation refactoring * Set Version: 0.1.306 * renaming contracts to avoid conflicts, removing unneeded constraints * minor streamlining * Set Version: 0.1.307 * Set Version: 0.1.308 * removing unused variable * renaming re-assigned variable * Make `_create_cse_accounts` apply recursively on `contract` field accounts * Code quality improvement * Set Version: 0.1.309 * Avoid adding new accs for constructors, add new CSE test for recursive storage building * Enforce same symbolic storage variable name between constructor and functions * tests with immutables * stabilising tests * Set Version: 0.1.309 * Update expected output, remove `Accounts` test * Update `contracts.k` expected output to reflect removal of `Accounts` * Set Version: 0.1.310 * Use default symbolic storage var name for constructor storage with CSE * Documented `_create_cse_accounts` * One other output update * Set Version: 0.1.311 * Apply refactoring from review comments * Apply refactoring from review comments, preserve `new_init_cterm` * Add test for recursive storage generation in new accounts * Update output to account for TGovernance * correction --------- Co-authored-by: devops <[email protected]> Co-authored-by: Petar Maksimović <[email protected]> Co-authored-by: Petar Maksimovic <[email protected]> Co-authored-by: rv-jenkins <[email protected]>
1 parent 304c658 commit 71adb1a

16 files changed

+1646
-106
lines changed

package/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.310
1+
0.1.311

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "kontrol"
7-
version = "0.1.310"
7+
version = "0.1.311"
88
description = "Foundry integration for KEVM"
99
authors = [
1010
"Runtime Verification, Inc. <[email protected]>",

src/kontrol/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
if TYPE_CHECKING:
66
from typing import Final
77

8-
VERSION: Final = '0.1.310'
8+
VERSION: Final = '0.1.311'

src/kontrol/prove.py

Lines changed: 165 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from kevm_pyk.utils import KDefinition__expand_macros, abstract_cell_vars, run_prover
1212
from pathos.pools import ProcessPool # type: ignore
1313
from pyk.cterm import CTerm, CTermSymbolic
14-
from pyk.kast.inner import KApply, KSequence, KSort, KToken, KVariable, Subst
14+
from pyk.kast.inner import KApply, KSequence, KSort, KVariable, Subst
1515
from pyk.kast.manip import flatten_label, set_cell
1616
from pyk.kcfg import KCFG, KCFGExplore
1717
from pyk.kore.rpc import KoreClient, TransportType, kore_server
@@ -42,6 +42,7 @@
4242

4343
from .deployment import DeploymentStateEntry
4444
from .options import ProveOptions
45+
from .solc_to_k import StorageField
4546

4647
_LOGGER: Final = logging.getLogger(__name__)
4748

@@ -520,6 +521,7 @@ def _method_to_initialized_cfg(
520521

521522
empty_config = foundry.kevm.definition.empty_config(GENERATED_TOP_CELL)
522523
kcfg, new_node_ids, init_node_id, target_node_id = _method_to_cfg(
524+
foundry,
523525
empty_config,
524526
test.contract,
525527
test.method,
@@ -555,6 +557,7 @@ def _method_to_initialized_cfg(
555557

556558

557559
def _method_to_cfg(
560+
foundry: Foundry,
558561
empty_config: KInner,
559562
contract: Contract,
560563
method: Contract.Method | Contract.Constructor,
@@ -581,9 +584,11 @@ def _method_to_cfg(
581584
program = contract_code
582585

583586
init_cterm = _init_cterm(
587+
foundry,
584588
empty_config,
585589
program=program,
586590
contract_code=bytesToken(contract_code),
591+
storage_fields=contract.fields,
587592
method=method,
588593
use_gas=use_gas,
589594
deployment_state_entries=deployment_state_entries,
@@ -652,66 +657,76 @@ def _method_to_cfg(
652657

653658

654659
def _update_cterm_from_node(cterm: CTerm, node: KCFG.Node, config_type: ConfigType) -> CTerm:
655-
new_accounts_cell = node.cterm.cell('ACCOUNTS_CELL')
656-
number_cell = node.cterm.cell('NUMBER_CELL')
657-
timestamp_cell = node.cterm.cell('TIMESTAMP_CELL')
658-
basefee_cell = node.cterm.cell('BASEFEE_CELL')
659-
chainid_cell = node.cterm.cell('CHAINID_CELL')
660-
coinbase_cell = node.cterm.cell('COINBASE_CELL')
661-
prevcaller_cell = node.cterm.cell('PREVCALLER_CELL')
662-
prevorigin_cell = node.cterm.cell('PREVORIGIN_CELL')
663-
newcaller_cell = node.cterm.cell('NEWCALLER_CELL')
664-
neworigin_cell = node.cterm.cell('NEWORIGIN_CELL')
665-
active_cell = node.cterm.cell('ACTIVE_CELL')
666-
depth_cell = node.cterm.cell('DEPTH_CELL')
667-
singlecall_cell = node.cterm.cell('SINGLECALL_CELL')
668-
gas_cell = node.cterm.cell('GAS_CELL')
669-
callgas_cell = node.cterm.cell('CALLGAS_CELL')
670-
671-
all_accounts = flatten_label('_AccountCellMap_', new_accounts_cell)
672-
673-
new_accounts = [CTerm(account, []) for account in all_accounts if (type(account) is KApply and account.is_cell)]
674-
non_cell_accounts = [account for account in all_accounts if not (type(account) is KApply and account.is_cell)]
675-
676-
new_accounts_map = {account.cell('ACCTID_CELL'): account for account in new_accounts}
677-
678-
if config_type == ConfigType.SUMMARY_CONFIG:
679-
for account_id, account in new_accounts_map.items():
680-
acct_id_cell = account.cell('ACCTID_CELL')
681-
if type(acct_id_cell) is KVariable:
682-
acct_id = acct_id_cell.name
683-
elif type(acct_id_cell) is KToken:
684-
acct_id = acct_id_cell.token
685-
else:
686-
raise RuntimeError(
687-
f'Failed to abstract storage. acctId cell is neither variable nor token: {acct_id_cell}'
688-
)
689-
new_accounts_map[account_id] = CTerm(
690-
set_cell(
691-
account.config,
692-
'STORAGE_CELL',
693-
KVariable(f'STORAGE_{acct_id}', sort=KSort('Map')),
694-
),
695-
[],
696-
)
660+
if config_type == ConfigType.TEST_CONFIG:
661+
cell_names = [
662+
'ACCOUNTS_CELL',
663+
'NUMBER_CELL',
664+
'TIMESTAMP_CELL',
665+
'BASEFEE_CELL',
666+
'CHAINID_CELL',
667+
'COINBASE_CELL',
668+
'PREVCALLER_CELL',
669+
'PREVORIGIN_CELL',
670+
'NEWCALLER_CELL',
671+
'NEWORIGIN_CELL',
672+
'ACTIVE_CELL',
673+
'DEPTH_CELL',
674+
'SINGLECALL_CELL',
675+
'GAS_CELL',
676+
'CALLGAS_CELL',
677+
]
678+
679+
cells = {name: node.cterm.cell(name) for name in cell_names}
680+
all_accounts = flatten_label('_AccountCellMap_', cells['ACCOUNTS_CELL'])
681+
682+
new_accounts = [CTerm(account, []) for account in all_accounts if (type(account) is KApply and account.is_cell)]
683+
non_cell_accounts = [account for account in all_accounts if not (type(account) is KApply and account.is_cell)]
684+
685+
new_accounts_map = {account.cell('ACCTID_CELL'): account for account in new_accounts}
686+
687+
cells['ACCOUNTS_CELL'] = KEVM.accounts(
688+
[account.config for account in new_accounts_map.values()] + non_cell_accounts
689+
)
690+
691+
new_init_cterm = CTerm(cterm.config, [])
692+
693+
for cell_name, cell_value in cells.items():
694+
new_init_cterm = CTerm(set_cell(new_init_cterm.config, cell_name, cell_value), [])
695+
696+
# config_type == ConfigType.SUMMARY_CONFIG
697+
# This means that a function is being run in isolation, that is, that the `node` we are
698+
# taking information from has come from a constructor and not a setUp function.
699+
# In this case, the <output> cell of the constructor execution becomes the program cell
700+
# of the function execution and the constraints from the constructor are propagated.
701+
else:
702+
new_program_cell = node.cterm.cell('OUTPUT_CELL')
703+
accounts_cell = cterm.cell('ACCOUNTS_CELL')
704+
contract_id = cterm.cell('ID_CELL')
705+
706+
all_accounts = flatten_label('_AccountCellMap_', accounts_cell)
707+
# TODO: Understand why there are two possible KInner representations or accounts,
708+
# one directly with `<acccount>` and the other with `AccountCellMapItem`.
709+
cell_accounts = [
710+
CTerm(account, []) for account in all_accounts if (type(account) is KApply and account.is_cell)
711+
] + [
712+
CTerm(account.args[1], [])
713+
for account in all_accounts
714+
if (type(account) is KApply and account.label.name == 'AccountCellMapItem')
715+
]
716+
other_accounts = [
717+
account
718+
for account in all_accounts
719+
if not (type(account) is KApply and (account.is_cell or account.label.name == 'AccountCellMapItem'))
720+
]
721+
cell_accounts_map = {account.cell('ACCTID_CELL'): account for account in cell_accounts}
722+
# Set the code of the active contract to the one produced by the constructor
723+
contract_account = cell_accounts_map[contract_id]
724+
contract_account = CTerm(set_cell(contract_account.config, 'CODE_CELL', new_program_cell), [])
725+
cell_accounts_map[contract_id] = contract_account
726+
new_accounts_cell = KEVM.accounts([account.config for account in cell_accounts_map.values()] + other_accounts)
697727

698-
new_accounts_cell = KEVM.accounts([account.config for account in new_accounts_map.values()] + non_cell_accounts)
699-
700-
new_init_cterm = CTerm(set_cell(cterm.config, 'ACCOUNTS_CELL', new_accounts_cell), [])
701-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'NUMBER_CELL', number_cell), [])
702-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'TIMESTAMP_CELL', timestamp_cell), [])
703-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'BASEFEE_CELL', basefee_cell), [])
704-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'CHAINID_CELL', chainid_cell), [])
705-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'COINBASE_CELL', coinbase_cell), [])
706-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'PREVCALLER_CELL', prevcaller_cell), [])
707-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'PREVORIGIN_CELL', prevorigin_cell), [])
708-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'NEWCALLER_CELL', newcaller_cell), [])
709-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'NEWORIGIN_CELL', neworigin_cell), [])
710-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'ACTIVE_CELL', active_cell), [])
711-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'DEPTH_CELL', depth_cell), [])
712-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'SINGLECALL_CELL', singlecall_cell), [])
713-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'GAS_CELL', gas_cell), [])
714-
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'CALLGAS_CELL', callgas_cell), [])
728+
new_init_cterm = CTerm(set_cell(cterm.config, 'PROGRAM_CELL', new_program_cell), [])
729+
new_init_cterm = CTerm(set_cell(new_init_cterm.config, 'ACCOUNTS_CELL', new_accounts_cell), [])
715730

716731
# Adding constraints from the initial cterm and initial node
717732
for constraint in cterm.constraints + node.cterm.constraints:
@@ -773,9 +788,11 @@ def _init_account(address: int) -> None:
773788

774789

775790
def _init_cterm(
791+
foundry: Foundry,
776792
empty_config: KInner,
777793
program: bytes,
778794
contract_code: KInner,
795+
storage_fields: tuple[StorageField, ...],
779796
method: Contract.Method | Contract.Constructor,
780797
use_gas: bool,
781798
config_type: ConfigType,
@@ -827,6 +844,8 @@ def _init_cterm(
827844
'TRACEDATA_CELL': KApply('.List'),
828845
}
829846

847+
storage_constraints: list[KApply] = []
848+
830849
if config_type == ConfigType.TEST_CONFIG or active_symbolik:
831850
init_account_list = _create_initial_account_list(contract_code, deployment_state_entries)
832851
init_subst_test = {
@@ -844,13 +863,18 @@ def _init_cterm(
844863
}
845864
init_subst.update(init_subst_test)
846865
else:
847-
# Symbolic accounts of all relevant contracts
848-
# Status: Currently, only the executing contract
849-
# TODO: Add all other accounts belonging to relevant contracts
850-
accounts: list[KInner] = [
851-
Foundry.symbolic_account(Foundry.symbolic_contract_prefix(), contract_code),
852-
KVariable('ACCOUNTS_REST', sort=KSort('AccountCellMap')),
853-
]
866+
accounts: list[KInner] = []
867+
868+
if isinstance(method, Contract.Constructor):
869+
# Symbolic account for the contract being executed
870+
accounts.append(Foundry.symbolic_account(Foundry.symbolic_contract_prefix(), contract_code))
871+
else:
872+
# Symbolic accounts of all relevant contracts
873+
accounts, storage_constraints = _create_cse_accounts(
874+
foundry, storage_fields, Foundry.symbolic_contract_prefix(), contract_code
875+
)
876+
877+
accounts.append(KVariable('ACCOUNTS_REST', sort=KSort('AccountCellMap')))
854878

855879
init_subst_accounts = {'ACCOUNTS_CELL': KEVM.accounts(accounts)}
856880
init_subst.update(init_subst_accounts)
@@ -881,6 +905,9 @@ def _init_cterm(
881905
mlEqualsFalse(KApply('_==Int_', [KVariable(contract_id, sort=KSort('Int')), Foundry.address_CHEATCODE()]))
882906
)
883907

908+
for constraint in storage_constraints:
909+
init_cterm = init_cterm.add_constraint(constraint)
910+
884911
# The calling contract is assumed to be in the present accounts for non-tests
885912
if not (config_type == ConfigType.TEST_CONFIG or active_symbolik):
886913
init_cterm.add_constraint(
@@ -893,8 +920,8 @@ def _init_cterm(
893920
)
894921

895922
if isinstance(method, Contract.Constructor) and len(arg_constraints) > 0:
896-
for constraint in arg_constraints:
897-
init_cterm = init_cterm.add_constraint(mlEqualsTrue(constraint))
923+
for arg_constraint in arg_constraints:
924+
init_cterm = init_cterm.add_constraint(mlEqualsTrue(arg_constraint))
898925

899926
init_cterm = KEVM.add_invariant(init_cterm)
900927

@@ -922,6 +949,75 @@ def _create_initial_account_list(
922949
return init_account_list
923950

924951

952+
def _create_cse_accounts(
953+
foundry: Foundry, storage_fields: tuple[StorageField, ...], contract_name: str, contract_code: KInner
954+
) -> tuple[list[KInner], list[KApply]]:
955+
"""
956+
Recursively generates a list of new accounts corresponding to `contract` fields, each having <code> and <storage> cell (partially) set up.
957+
Args:
958+
foundry (Foundry): The Foundry object containing the information about contracts in the project.
959+
storage_fields (tuple[StorageField, ...]): A tuple of StorageField objects representing the contract's storage fields.
960+
contract_name (str): The name of the contract being executed to be used in the account-related symbolic variables.
961+
contract_code (KInner): The KInner object representing the contract's runtime bytecode.
962+
Returns:
963+
tuple[list[KInner], list[KApply]]:
964+
- A list of accounts to be included in the initial configuration.
965+
- A list of constraints on symbolic account IDs.
966+
"""
967+
968+
new_accounts: list[KInner] = []
969+
new_account_constraints: list[KApply] = []
970+
971+
storage_map = KVariable(contract_name + '_STORAGE', sort=KSort('Map'))
972+
new_accounts.append(Foundry.symbolic_account(contract_name, contract_code, storage_map))
973+
974+
for field in storage_fields:
975+
if field.data_type.startswith('contract '):
976+
contract_type = field.data_type.split(' ')[1]
977+
for full_contract_name, contract_obj in foundry.contracts.items():
978+
# TODO: this is not enough, it is possible that the same contract comes with
979+
# src% and test%, in which case we don't know automatically which one to choose
980+
if full_contract_name.split('%')[-1] == contract_type:
981+
contract_account_code = bytesToken(bytes.fromhex(contract_obj.deployed_bytecode))
982+
contract_account_name = 'CONTRACT-' + field.label.upper()
983+
984+
# TODO: handle the case where the field has a non-zero offset
985+
# maxUInt160 &Int #lookup ( CONTRACT_STORAGE:Map , 0 )
986+
contract_storage_lookup = KApply(
987+
'_&Int_',
988+
[
989+
intToken(1461501637330902918203684832716283019655932542975),
990+
KEVM.lookup(storage_map, intToken(field.slot)),
991+
],
992+
)
993+
new_account_constraints.append(
994+
mlEqualsTrue(
995+
KApply('_==Int_', [contract_storage_lookup, KVariable(contract_account_name + '_ID')])
996+
)
997+
)
998+
999+
# New contract account is not the cheatcode contract
1000+
new_account_constraints.append(
1001+
mlEqualsFalse(
1002+
KApply(
1003+
'_==Int_',
1004+
[
1005+
KVariable(contract_account_name + '_ID', sort=KSort('Int')),
1006+
Foundry.address_CHEATCODE(),
1007+
],
1008+
)
1009+
)
1010+
)
1011+
1012+
contract_accounts, contract_constraints = _create_cse_accounts(
1013+
foundry, contract_obj.fields, contract_account_name, contract_account_code
1014+
)
1015+
new_accounts.extend(contract_accounts)
1016+
new_account_constraints.extend(contract_constraints)
1017+
1018+
return new_accounts, new_account_constraints
1019+
1020+
9251021
def _final_cterm(
9261022
empty_config: KInner,
9271023
program: KInner,

src/tests/integration/test-data/cse-lemmas.k

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module CSE-LEMMAS
3131
[simplification]
3232

3333
// Function selector does not match
34-
rule { B:Bytes #Equals B1:Bytes +Bytes B2:Bytes } => #Bottom
34+
rule { B:Bytes #Equals B1:Bytes +Bytes _:Bytes } => #Bottom
3535
requires #range(B, 0, 4) =/=K #range (B1, 0, 4)
3636
[simplification(60), concrete(B, B1)]
3737

@@ -40,6 +40,20 @@ module CSE-LEMMAS
4040
requires 4 <=Int lengthBytes(B) andBool #range(B, 0, 4) ==K B1
4141
[simplification(60), concrete(B, B1)]
4242

43+
// Bitwise inequalities
44+
rule [bitwise-and-maxUInt-lt-l]:
45+
A <Int X &Int Y => false
46+
requires 0 <=Int X andBool 0 <=Int Y
47+
andBool X +Int 1 ==Int 2 ^Int log2Int(X +Int 1)
48+
andBool X +Int 1 <=Int A
49+
[concrete(X), simplification, preserves-definedness]
50+
51+
rule [bitwise-and-maxUInt-le-r]:
52+
X &Int Y <=Int A => true
53+
requires 0 <=Int X andBool 0 <=Int Y
54+
andBool X +Int 1 ==Int 2 ^Int log2Int(X +Int 1)
55+
andBool X +Int 1 <=Int A
56+
[concrete(X), simplification, preserves-definedness]
4357

4458
endmodule
4559

src/tests/integration/test-data/foundry-dependency-all

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ ArithmeticContract.add_sub_external(uint256,uint256,uint256)
77
CSETest.test_add_const(uint256,uint256)
88
CSETest.test_identity(uint256,uint256)
99
ConstructorTest.test_contract_call()
10+
ContractFieldTest.testEscrowToken()
11+
TGovernance.getEscrowTokenTotalSupply()
1012
Identity.applyOp(uint256)
1113
Identity.identity(uint256)
1214
ImportedContract.set(uint256)

0 commit comments

Comments
 (0)