diff --git a/.github/scripts/check-contract-versions-old.py b/.github/scripts/check-contract-versions-old.py new file mode 100755 index 000000000..6d384b368 --- /dev/null +++ b/.github/scripts/check-contract-versions-old.py @@ -0,0 +1,593 @@ +#!/usr/bin/env python3 +""" +Script to check contract version changes by comparing compiled bytecode between PR and base branch. + +This script: +1. Takes two directory paths (PR branch and base branch) already checked out by GitHub Actions +2. Builds contracts in both directories using `nix develop .#contracts -c yarn build` +3. Compares the compiled bytecode in X.compiled.json files between branches +4. For contracts with different bytecode, extracts CONTRACT_VERSION from source files +5. Fails if contracts have different bytecode but same CONTRACT_VERSION +6. Ensures contract version updates are enforced when contracts actually change + +Usage: + python check-contract-versions.py --pr-dir PR_DIR --base-dir BASE_DIR [--verbose] +""" + +import sys +import os +import argparse +import subprocess +import json +import re +from pathlib import Path + + +def run_command(cmd, cwd=None, check=True): + """Run a shell command and return the result.""" + try: + result = subprocess.run( + cmd, + shell=True, + cwd=cwd, + check=check, + capture_output=True, + text=True + ) + return result + except subprocess.CalledProcessError as e: + print(f"Command failed in {cwd}: {' '.join(cmd) if isinstance(cmd, list) else cmd}") + print(f"stdout: {e.stdout}") + print(f"stderr: {e.stderr}") + raise + + +def build_contracts(contracts_dir, verbose=False): + """Build contracts using nix develop and yarn build.""" + if verbose: + print(f"Building contracts in {contracts_dir}...") + + build_cmd = "nix develop .#contracts -c yarn build" + result = run_command(build_cmd, cwd=contracts_dir) + + if verbose: + print(f"Build completed in {contracts_dir}") + if result.stdout: + print(f"Build output: {result.stdout}") + + return result + + +def get_compiled_contracts(contracts_dir): + """Get all compiled.json files from the build directory.""" + build_dir = os.path.join(contracts_dir, 'build') + compiled_contracts = {} + + if not os.path.exists(build_dir): + print(f"Warning: Build directory not found at {build_dir}") + return compiled_contracts + + for file_name in os.listdir(build_dir): + if file_name.endswith('.compiled.json'): + contract_name = file_name.replace('.compiled.json', '') + compiled_file_path = os.path.join(build_dir, file_name) + + try: + with open(compiled_file_path, 'r', encoding='utf-8') as f: + compiled_data = json.load(f) + compiled_contracts[contract_name] = compiled_data + except Exception as e: + print(f"Error reading {compiled_file_path}: {e}") + + return compiled_contracts + + +def find_contracts_with_different_bytecode(pr_contracts, base_contracts, verbose=False): + """Find contracts that have different compiled bytecode.""" + contracts_with_changes = [] + + # Get all contract names from both branches + all_contracts = set(pr_contracts.keys()) | set(base_contracts.keys()) + + for contract_name in all_contracts: + pr_compiled = pr_contracts.get(contract_name) + base_compiled = base_contracts.get(contract_name) + + if verbose: + print(f"Comparing contract: {contract_name}") + + # Check if bytecode differs + bytecode_differs = False + + if pr_compiled and base_compiled: + # Compare the compiled bytecode/hex data + pr_hex = pr_compiled.get('hex', '') + base_hex = base_compiled.get('hex', '') + + if pr_hex != base_hex: + bytecode_differs = True + if verbose: + print(f" Bytecode differs (lengths: PR={len(pr_hex)}, base={len(base_hex)})") + else: + if verbose: + print(f" Bytecode identical") + + elif pr_compiled and not base_compiled: + # New contract + bytecode_differs = True + if verbose: + print(f" New contract") + + elif not pr_compiled and base_compiled: + # Deleted contract - we don't need to check versions for this + if verbose: + print(f" Deleted contract") + else: + # Neither exists - shouldn't happen but handle gracefully + if verbose: + print(f" Contract not found in either branch") + + if bytecode_differs: + contracts_with_changes.append(contract_name) + + return contracts_with_changes + + +def extract_entrypoint_from_compile_ts(file_path): + """Extract the entrypoint path from a .compile.ts file.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Look for entrypoint: 'path/to/contract.tolk' + pattern = r"entrypoint:\s*['\"]([^'\"]+)['\"]" + match = re.search(pattern, content) + + if match: + return match.group(1) + + return None + + except Exception as e: + print(f"Error reading {file_path}: {e}") + return None + + +def find_contract_entrypoint(contract_name, contracts_dir): + """Find the entrypoint for a specific contract by reading its .compile.ts file.""" + wrappers_dir = os.path.join(contracts_dir, 'wrappers') + compile_file = os.path.join(wrappers_dir, f"{contract_name}.compile.ts") + + if os.path.exists(compile_file): + return extract_entrypoint_from_compile_ts(compile_file) + + return None + + +def extract_version_from_content(content): + """Extract CONTRACT_VERSION from file content.""" + pattern = r'const CONTRACT_VERSION = "([^"]+)";' + match = re.search(pattern, content, re.MULTILINE | re.DOTALL) + + if match: + return match.group(1) + + return None + + +def get_file_content(file_path): + """Get the content of a file if it exists.""" + try: + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + print(f"Error reading file {file_path}: {e}") + + return None + + +def check_contract_versions(pr_dir, base_dir, contracts_with_changes, verbose=False): + """ + Check contract versions for contracts with different bytecode. + + Returns: + list: List of violations found + """ + violations = [] + + pr_contracts_dir = os.path.join(pr_dir, 'contracts') + base_contracts_dir = os.path.join(base_dir, 'contracts') + + for contract_name in contracts_with_changes: + if verbose: + print(f"\nChecking versions for contract: {contract_name}") + + # Find entrypoints for this contract in both branches + pr_entrypoint = find_contract_entrypoint(contract_name, pr_contracts_dir) + base_entrypoint = find_contract_entrypoint(contract_name, base_contracts_dir) + + # Get current version from PR branch + current_version = None + if pr_entrypoint: + pr_contract_path = os.path.join(pr_contracts_dir, pr_entrypoint) + pr_content = get_file_content(pr_contract_path) + if pr_content: + current_version = extract_version_from_content(pr_content) + + # Get base version from base branch + base_version = None + if base_entrypoint: + base_contract_path = os.path.join(base_contracts_dir, base_entrypoint) + base_content = get_file_content(base_contract_path) + if base_content: + base_version = extract_version_from_content(base_content) + + if verbose: + print(f" PR entrypoint: {pr_entrypoint} -> version: {current_version}") + print(f" Base entrypoint: {base_entrypoint} -> version: {base_version}") + + # Check for violations - contracts with different bytecode should have different versions + if current_version and base_version: + if current_version == base_version: + violations.append({ + 'contract': contract_name, + 'pr_entrypoint': pr_entrypoint, + 'base_entrypoint': base_entrypoint, + 'current_version': current_version, + 'base_version': base_version, + 'violation': 'Contract bytecode changed but version unchanged' + }) + else: + print(f"✅ {contract_name}: Bytecode and version both updated ({base_version} -> {current_version})") + elif current_version and not base_version: + print(f"✅ {contract_name}: New contract with version {current_version}") + elif not current_version and base_version: + print(f"⚠️ {contract_name}: CONTRACT_VERSION removed but bytecode changed (was {base_version})") + elif not current_version and not base_version: + print(f"⚠️ {contract_name}: Bytecode changed but no CONTRACT_VERSION found in either version") + else: + # This shouldn't happen, but just in case + print(f"⚠️ {contract_name}: Unexpected version state") + + return violations + + +def main(): + parser = argparse.ArgumentParser( + description="Check contract version changes by comparing compiled bytecode" + ) + parser.add_argument( + '--pr-dir', + required=True, + help='Directory containing PR branch code' + ) + parser.add_argument( + '--base-dir', + required=True, + help='Directory containing base branch code' + ) + parser.add_argument( + '--verbose', + action='store_true', + help='Enable verbose output' + ) + + args = parser.parse_args() + + # Validate directories exist + if not os.path.isdir(args.pr_dir): + print(f"Error: PR directory {args.pr_dir} does not exist") + sys.exit(1) + + if not os.path.isdir(args.base_dir): + print(f"Error: Base directory {args.base_dir} does not exist") + sys.exit(1) + + pr_contracts_dir = os.path.join(args.pr_dir, 'contracts') + base_contracts_dir = os.path.join(args.base_dir, 'contracts') + + # Build contracts in both directories + print("Building contracts in PR branch...") + try: + build_contracts(pr_contracts_dir, args.verbose) + except subprocess.CalledProcessError: + print("❌ Failed to build contracts in PR branch") + sys.exit(1) + + print("Building contracts in base branch...") + try: + build_contracts(base_contracts_dir, args.verbose) + except subprocess.CalledProcessError: + print("❌ Failed to build contracts in base branch") + sys.exit(1) + + # Get compiled contracts from both branches + print("Reading compiled contracts...") + pr_compiled = get_compiled_contracts(pr_contracts_dir) + base_compiled = get_compiled_contracts(base_contracts_dir) + + if args.verbose: + print(f"PR branch compiled contracts: {list(pr_compiled.keys())}") + print(f"Base branch compiled contracts: {list(base_compiled.keys())}") + + # Find contracts with different bytecode + contracts_with_changes = find_contracts_with_different_bytecode( + pr_compiled, base_compiled, args.verbose + ) + + if not contracts_with_changes: + print("✅ No contracts with different bytecode found - skipping version check") + sys.exit(0) + + print(f"\nFound {len(contracts_with_changes)} contracts with different bytecode:") + for contract in contracts_with_changes: + print(f" - {contract}") + + # Check contract versions for contracts with different bytecode + violations = check_contract_versions( + args.pr_dir, args.base_dir, contracts_with_changes, args.verbose + ) + + if violations: + print("\n❌ Contract version violations found:") + for violation in violations: + print(f"\n Contract: {violation['contract']}") + print(f" Issue: {violation['violation']}") + print(f" PR version: {violation['current_version']} (entrypoint: {violation['pr_entrypoint']})") + print(f" Base version: {violation['base_version']} (entrypoint: {violation['base_entrypoint']})") + + print(f"\n💡 To fix this:") + print(f" Update the CONTRACT_VERSION constant in the contract files with changed bytecode.") + print(f" Bytecode changes indicate functional contract modifications that require version updates.") + + sys.exit(1) + else: + print("\n✅ All contract version checks passed!") + sys.exit(0) + + +if __name__ == "__main__": + main() + +import sys +import os +import argparse +import re +from pathlib import Path + + +def extract_entrypoint_from_compile_ts(file_path): + """Extract the entrypoint path from a .compile.ts file.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Look for entrypoint: 'path/to/contract.tolk' + pattern = r"entrypoint:\s*['\"]([^'\"]+)['\"]" + match = re.search(pattern, content) + + if match: + return match.group(1) + + return None + + except Exception as e: + print(f"Error reading {file_path}: {e}") + return None + + +def find_contract_entrypoints(contracts_dir): + """Find all .compile.ts files in contracts/wrappers directory.""" + wrappers_dir = os.path.join(contracts_dir, 'wrappers') + compile_files = {} + + if not os.path.exists(wrappers_dir): + print(f"Warning: wrappers directory not found at {wrappers_dir}") + return compile_files + + for file_name in os.listdir(wrappers_dir): + if file_name.endswith('.compile.ts'): + compile_file_path = os.path.join(wrappers_dir, file_name) + entrypoint = extract_entrypoint_from_compile_ts(compile_file_path) + + if entrypoint: + # The entrypoint is relative to contracts/ directory + contract_name = file_name.replace('.compile.ts', '') + compile_files[contract_name] = entrypoint + + return compile_files + + +def get_contract_entrypoints(pr_dir, base_dir, verbose=False): + """Get contract entrypoints from both PR and base branches.""" + pr_contracts_dir = os.path.join(pr_dir, 'contracts') + base_contracts_dir = os.path.join(base_dir, 'contracts') + + pr_entrypoints = find_contract_entrypoints(pr_contracts_dir) + base_entrypoints = find_contract_entrypoints(base_contracts_dir) + + if verbose: + print(f"PR branch entrypoints: {pr_entrypoints}") + print(f"Base branch entrypoints: {base_entrypoints}") + + # Find all unique contract names + all_contracts = set(pr_entrypoints.keys()) | set(base_entrypoints.keys()) + + contracts_to_check = [] + + for contract_name in all_contracts: + pr_entrypoint = pr_entrypoints.get(contract_name) + base_entrypoint = base_entrypoints.get(contract_name) + + # Check if the contract exists in both branches and if entrypoints differ OR + # if contract exists in only one branch (new/deleted) + if pr_entrypoint != base_entrypoint: + contracts_to_check.append({ + 'name': contract_name, + 'pr_entrypoint': pr_entrypoint, + 'base_entrypoint': base_entrypoint + }) + + if verbose: + if pr_entrypoint and base_entrypoint: + print(f"Contract {contract_name}: entrypoint changed from {base_entrypoint} to {pr_entrypoint}") + elif pr_entrypoint and not base_entrypoint: + print(f"Contract {contract_name}: new contract with entrypoint {pr_entrypoint}") + elif not pr_entrypoint and base_entrypoint: + print(f"Contract {contract_name}: removed contract (was {base_entrypoint})") + + return contracts_to_check + + +def extract_version_from_content(content): + """Extract CONTRACT_VERSION from file content.""" + pattern = r'const CONTRACT_VERSION = "([^"]+)";' + match = re.search(pattern, content, re.MULTILINE | re.DOTALL) + + if match: + return match.group(1) + + return None + + +def get_file_content(file_path): + """Get the content of a file if it exists.""" + try: + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + print(f"Error reading file {file_path}: {e}") + + return None + + +def check_contract_versions(pr_dir, base_dir, contracts_to_check, verbose=False): + """ + Check contract versions for contracts with changed entrypoints. + + Returns: + list: List of violations found + """ + violations = [] + + for contract_info in contracts_to_check: + contract_name = contract_info['name'] + pr_entrypoint = contract_info['pr_entrypoint'] + base_entrypoint = contract_info['base_entrypoint'] + + if verbose: + print(f"\nChecking contract: {contract_name}") + + # Get current version from PR branch + current_version = None + if pr_entrypoint: + pr_contract_path = os.path.join(pr_dir, 'contracts', pr_entrypoint) + pr_content = get_file_content(pr_contract_path) + if pr_content: + current_version = extract_version_from_content(pr_content) + + # Get base version from base branch + base_version = None + if base_entrypoint: + base_contract_path = os.path.join(base_dir, 'contracts', base_entrypoint) + base_content = get_file_content(base_contract_path) + if base_content: + base_version = extract_version_from_content(base_content) + + if verbose: + print(f" PR entrypoint: {pr_entrypoint} -> version: {current_version}") + print(f" Base entrypoint: {base_entrypoint} -> version: {base_version}") + + # Check for violations + if current_version and base_version: + if current_version == base_version: + violations.append({ + 'contract': contract_name, + 'pr_entrypoint': pr_entrypoint, + 'base_entrypoint': base_entrypoint, + 'current_version': current_version, + 'base_version': base_version, + 'violation': 'Contract version unchanged despite contract modifications' + }) + else: + print(f"✅ {contract_name}: Version updated from {base_version} to {current_version}") + elif current_version and not base_version: + print(f"✅ {contract_name}: New contract with version {current_version}") + elif not current_version and base_version: + print(f"⚠️ {contract_name}: CONTRACT_VERSION removed (was {base_version})") + elif not current_version and not base_version: + print(f"ℹ️ {contract_name}: No CONTRACT_VERSION found in either version") + else: + # This shouldn't happen, but just in case + print(f"⚠️ {contract_name}: Unexpected version state") + + return violations + + +def main(): + parser = argparse.ArgumentParser( + description="Check contract version changes in .tolk files by reading compile.ts entrypoints" + ) + parser.add_argument( + '--pr-dir', + required=True, + help='Directory containing PR branch code' + ) + parser.add_argument( + '--base-dir', + required=True, + help='Directory containing base branch code' + ) + parser.add_argument( + '--verbose', + action='store_true', + help='Enable verbose output' + ) + + args = parser.parse_args() + + # Validate directories exist + if not os.path.isdir(args.pr_dir): + print(f"Error: PR directory {args.pr_dir} does not exist") + sys.exit(1) + + if not os.path.isdir(args.base_dir): + print(f"Error: Base directory {args.base_dir} does not exist") + sys.exit(1) + + print("Finding contract entrypoints from compile.ts files...") + contracts_to_check = get_contract_entrypoints(args.pr_dir, args.base_dir, args.verbose) + + if not contracts_to_check: + print("✅ No contract entrypoints changed - skipping contract version check") + sys.exit(0) + + print(f"\nFound {len(contracts_to_check)} contracts with changed entrypoints:") + for contract_info in contracts_to_check: + print(f" - {contract_info['name']}: {contract_info['base_entrypoint']} -> {contract_info['pr_entrypoint']}") + + # Check contract versions + violations = check_contract_versions(args.pr_dir, args.base_dir, contracts_to_check, args.verbose) + + if violations: + print("\n❌ Contract version violations found:") + for violation in violations: + print(f"\n Contract: {violation['contract']}") + print(f" Issue: {violation['violation']}") + print(f" PR entrypoint: {violation['pr_entrypoint']} (version: {violation['current_version']})") + print(f" Base entrypoint: {violation['base_entrypoint']} (version: {violation['base_version']})") + + print(f"\n💡 To fix this:") + print(f" Update the CONTRACT_VERSION constant in the modified contract files.") + print(f" Every contract change should increment the version to ensure proper deployment tracking.") + + sys.exit(1) + else: + print("\n✅ All contract version checks passed!") + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/scripts/check-contract-versions.py b/.github/scripts/check-contract-versions.py new file mode 100755 index 000000000..81fcdec12 --- /dev/null +++ b/.github/scripts/check-contract-versions.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +""" +Script to check contract version changes by comparing compiled bytecode between PR and base branch. + +This script: +1. Takes two directory paths (PR branch and base branch) already checked out by GitHub Actions +2. Builds contracts in both directories using `nix develop .#contracts -c yarn build` +3. Compares the compiled bytecode in X.compiled.json files between branches +4. For contracts with different bytecode, extracts CONTRACT_VERSION from source files +5. Fails if contracts have different bytecode but same CONTRACT_VERSION +6. Ensures contract version updates are enforced when contracts actually change + +Usage: + python check-contract-versions.py --pr-dir PR_DIR --base-dir BASE_DIR [--verbose] +""" + +import sys +import os +import argparse +import subprocess +import json +import re +from pathlib import Path + + +def run_command(cmd, cwd=None, check=True): + """Run a shell command and return the result.""" + try: + result = subprocess.run( + cmd, + shell=True, + cwd=cwd, + check=check, + capture_output=True, + text=True + ) + return result + except subprocess.CalledProcessError as e: + print(f"Command failed in {cwd}: {cmd}") + print(f"stdout: {e.stdout}") + print(f"stderr: {e.stderr}") + raise + + +def build_contracts(contracts_dir, verbose=False): + """Build contracts using nix develop and yarn build.""" + if verbose: + print(f"Installing dependencies in {contracts_dir}...") + dependency_install_command = "nix develop .#contracts -c yarn" + dependency_install_result = run_command(dependency_install_command, cwd=contracts_dir) + if verbose: + print(f"Dependencies installed in {contracts_dir}") + if dependency_install_result.stdout: + print(f"Install output: {dependency_install_result.stdout}") + + if verbose: + print(f"Building contracts in {contracts_dir}...") + + build_cmd = "nix develop .#contracts -c yarn build" + contract_build_result = run_command(build_cmd, cwd=contracts_dir) + + if verbose: + print(f"Build completed in {contracts_dir}") + if contract_build_result.stdout: + print(f"Build output: {contract_build_result.stdout}") + + return contract_build_result + + +def get_compiled_contracts(contracts_dir): + """Get all compiled.json files from the build directory.""" + build_dir = os.path.join(contracts_dir, 'build') + compiled_contracts = {} + + if not os.path.exists(build_dir): + print(f"Warning: Build directory not found at {build_dir}") + return compiled_contracts + + for file_name in os.listdir(build_dir): + if file_name.endswith('.compiled.json'): + contract_name = file_name.replace('.compiled.json', '') + compiled_file_path = os.path.join(build_dir, file_name) + + try: + with open(compiled_file_path, 'r', encoding='utf-8') as f: + compiled_data = json.load(f) + compiled_contracts[contract_name] = compiled_data + except Exception as e: + print(f"Error reading {compiled_file_path}: {e}") + + return compiled_contracts + + +def find_contracts_with_different_bytecode(pr_contracts, base_contracts, verbose=False): + """Find contracts that have different compiled bytecode.""" + contracts_with_changes = [] + + # Get all contract names from both branches + all_contracts = set(pr_contracts.keys()) | set(base_contracts.keys()) + + for contract_name in all_contracts: + pr_compiled = pr_contracts.get(contract_name) + base_compiled = base_contracts.get(contract_name) + + if verbose: + print(f"Comparing contract: {contract_name}") + + # Check if bytecode differs + + if pr_compiled and base_compiled: + # Compare the compiled bytecode/hex data + pr_hex = pr_compiled.get('hex', '') + base_hex = base_compiled.get('hex', '') + + if pr_hex != base_hex: + contracts_with_changes.append(contract_name) + if verbose: + print(f" Bytecode differs (lengths: PR={len(pr_hex)}, base={len(base_hex)})") + else: + if verbose: + print(f" Bytecode identical") + + elif pr_compiled and not base_compiled: + # New contract + bytecode_differs = True + if verbose: + print(f" New contract") + + elif not pr_compiled and base_compiled: + # Deleted contract - we don't need to check versions for this + if verbose: + print(f" Deleted contract") + else: + # Neither exists - shouldn't happen but handle gracefully + if verbose: + print(f" Contract not found in either branch") + + return contracts_with_changes + + +def extract_entrypoint_from_compile_ts(file_path): + """Extract the entrypoint path from a .compile.ts file.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Look for entrypoint: 'path/to/contract.tolk' + pattern = r"entrypoint:\s*['\"]([^'\"]+)['\"]" + match = re.search(pattern, content) + + if match: + return match.group(1) + + return None + + except Exception as e: + print(f"Error reading {file_path}: {e}") + return None + + +def find_contract_entrypoint(contract_name, contracts_dir): + """Find the entrypoint for a specific contract by reading its .compile.ts file.""" + wrappers_dir = os.path.join(contracts_dir, 'wrappers') + compile_file = os.path.join(wrappers_dir, f"{contract_name}.compile.ts") + + if os.path.exists(compile_file): + return extract_entrypoint_from_compile_ts(compile_file) + + return None + + +def extract_version_from_content(content): + """Extract CONTRACT_VERSION from file content.""" + pattern = r'const CONTRACT_VERSION = "([^"]+)";' + match = re.search(pattern, content, re.MULTILINE | re.DOTALL) + + if match: + return match.group(1) + + return None + + +def get_file_content(file_path): + """Get the content of a file if it exists.""" + try: + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + print(f"Error reading file {file_path}: {e}") + + return None + + +def check_contract_versions(pr_dir, base_dir, contracts_with_changes, verbose=False): + """ + Check contract versions for contracts with different bytecode. + + Returns: + list: List of violations found + """ + violations = [] + + pr_contracts_dir = os.path.join(pr_dir, 'contracts') + base_contracts_dir = os.path.join(base_dir, 'contracts') + + for contract_name in contracts_with_changes: + if verbose: + print(f"\nChecking versions for contract: {contract_name}") + + # Find entrypoints for this contract in both branches + pr_entrypoint = find_contract_entrypoint(contract_name, pr_contracts_dir) + base_entrypoint = find_contract_entrypoint(contract_name, base_contracts_dir) + + # Get current version from PR branch + current_version = None + if pr_entrypoint: + pr_contract_path = os.path.join(pr_contracts_dir, pr_entrypoint) + pr_content = get_file_content(pr_contract_path) + if pr_content: + current_version = extract_version_from_content(pr_content) + + # Get base version from base branch + base_version = None + if base_entrypoint: + base_contract_path = os.path.join(base_contracts_dir, base_entrypoint) + base_content = get_file_content(base_contract_path) + if base_content: + base_version = extract_version_from_content(base_content) + + if verbose: + print(f" PR entrypoint: {pr_entrypoint} -> version: {current_version}") + print(f" Base entrypoint: {base_entrypoint} -> version: {base_version}") + + # Check for violations - contracts with different bytecode should have different versions + if current_version and base_version: + if current_version == base_version: + violations.append({ + 'contract': contract_name, + 'pr_entrypoint': pr_entrypoint, + 'base_entrypoint': base_entrypoint, + 'current_version': current_version, + 'base_version': base_version, + 'violation': 'Contract bytecode changed but version unchanged' + }) + else: + print(f"✅ {contract_name}: Bytecode and version both updated ({base_version} -> {current_version})") + elif current_version and not base_version: + print(f"✅ {contract_name}: New contract with version {current_version}") + elif not current_version and base_version: + violations.append({ + 'contract': contract_name, + 'pr_entrypoint': pr_entrypoint, + 'base_entrypoint': base_entrypoint, + 'current_version': current_version, + 'base_version': 'None', + 'violation': 'Contract bytecode changed and version was removed' + }) + elif not current_version and not base_version: + print(f"⚠️ {contract_name}: Bytecode changed but no CONTRACT_VERSION found in either version") + else: + # This shouldn't happen, but just in case + print(f"⚠️ {contract_name}: Unexpected version state") + + return violations + + +def main(): + parser = argparse.ArgumentParser( + description="Check contract version changes by comparing compiled bytecode" + ) + parser.add_argument( + '--pr-dir', + required=True, + help='Directory containing PR branch code' + ) + parser.add_argument( + '--base-dir', + required=True, + help='Directory containing base branch code' + ) + parser.add_argument( + '--verbose', + action='store_true', + help='Enable verbose output' + ) + + args = parser.parse_args() + + # Validate directories exist + if not os.path.isdir(args.pr_dir): + print(f"Error: PR directory {args.pr_dir} does not exist") + sys.exit(1) + + if not os.path.isdir(args.base_dir): + print(f"Error: Base directory {args.base_dir} does not exist") + sys.exit(1) + + pr_contracts_dir = os.path.join(args.pr_dir, 'contracts') + base_contracts_dir = os.path.join(args.base_dir, 'contracts') + + # Build contracts in both directories + print("Building contracts in PR branch...") + try: + build_contracts(pr_contracts_dir, args.verbose) + except subprocess.CalledProcessError: + print("❌ Failed to build contracts in PR branch") + sys.exit(1) + + print("Building contracts in base branch...") + try: + build_contracts(base_contracts_dir, args.verbose) + except subprocess.CalledProcessError: + print("❌ Failed to build contracts in base branch") + sys.exit(1) + + # Get compiled contracts from both branches + print("Reading compiled contracts...") + pr_compiled = get_compiled_contracts(pr_contracts_dir) + base_compiled = get_compiled_contracts(base_contracts_dir) + + if args.verbose: + print(f"PR branch compiled contracts: {list(pr_compiled.keys())}") + print(f"Base branch compiled contracts: {list(base_compiled.keys())}") + + # Find contracts with different bytecode + contracts_with_changes = find_contracts_with_different_bytecode( + pr_compiled, base_compiled, args.verbose + ) + + if not contracts_with_changes: + print("✅ No contracts with different bytecode found - skipping version check") + sys.exit(0) + + print(f"\nFound {len(contracts_with_changes)} contracts with different bytecode:") + for contract in contracts_with_changes: + print(f" - {contract}") + + # Check contract versions for contracts with different bytecode + violations = check_contract_versions( + args.pr_dir, args.base_dir, contracts_with_changes, args.verbose + ) + + if violations: + print("\n❌ Contract version violations found:") + for violation in violations: + print(f"\n Contract: {violation['contract']}") + print(f" Issue: {violation['violation']}") + print(f" PR version: {violation['current_version']} (entrypoint: {violation['pr_entrypoint']})") + print(f" Base version: {violation['base_version']} (entrypoint: {violation['base_entrypoint']})") + + print(f"\n💡 To fix this:") + print(f" Update the CONTRACT_VERSION constant in the contract files with changed bytecode.") + print(f" Bytecode changes indicate functional contract modifications that require version updates.") + + sys.exit(1) + else: + print("\n✅ All contract version checks passed!") + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/contracts-build.yml b/.github/workflows/contracts-build.yml index 3ff50126e..f546d9c27 100644 --- a/.github/workflows/contracts-build.yml +++ b/.github/workflows/contracts-build.yml @@ -40,3 +40,37 @@ jobs: nix_path: nixpkgs=channel:nixos-unstable - name: Run build run: nix build -v .#contracts + + check-contract-versions: + name: Verify Contract Version Updates + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Check out PR code + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + with: + path: "pr-branch" + + - name: Check out base branch code + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: "base-branch" + + - name: Set up Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.9" + + - name: Install Nix + uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Check contract versions + run: | + pr-branch/.github/scripts/check-contract-versions.py \ + --pr-dir pr-branch \ + --base-dir base-branch \ + --verbose diff --git a/contracts/contracts/ccip/fee_quoter/contract.tolk b/contracts/contracts/ccip/fee_quoter/contract.tolk index 44cd8dd76..91b27a60b 100644 --- a/contracts/contracts/ccip/fee_quoter/contract.tolk +++ b/contracts/contracts/ccip/fee_quoter/contract.tolk @@ -6,6 +6,8 @@ import "../../lib/upgrades/type_and_version"; import "../../lib/utils"; import "messages" +const CONTRACT_VERSION = "0.0.1"; + const VAL_1E5: uint256 = 100000; const VAL_1E14: uint256 = 100000000000000; const VAL_1E16: uint256 = 10000000000000000; @@ -23,6 +25,7 @@ const CHAIN_FAMILY_SELECTOR_APTOS: uint32 = 0xac77ffec; const CHAIN_FAMILY_SELECTOR_SUI: uint32 = 0xc4e05953; struct Storage { + id: uint32; ownable: Ownable2Step; maxFeeJuelsPerMsg: uint96; @@ -416,7 +419,7 @@ get fun staticConfig(): (uint96, address, uint64) { } get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.ccip.FeeQuoter", "1.0.0"); + return ("com.chainlink.ton.ccip.FeeQuoter", CONTRACT_VERSION); } get fun destChainSelectors(): tuple { diff --git a/contracts/contracts/ccip/merkle_root.tolk b/contracts/contracts/ccip/merkle_root.tolk index 97cf5b194..9eebdad09 100644 --- a/contracts/contracts/ccip/merkle_root.tolk +++ b/contracts/contracts/ccip/merkle_root.tolk @@ -3,6 +3,8 @@ import "../lib/upgrades/type_and_version.tolk"; import "../deployable/types.tolk"; import "../lib/utils.tolk"; +const CONTRACT_VERSION = "0.0.1"; + // State stores the general state machine indicating which phase we're in // TODO: we might be able to merge this into execution_states const STATE_UNTOUCHED: uint8 = 0; @@ -176,5 +178,5 @@ fun _execute(st: MerkleRoot_Storage, mesage: Any2TVMRampMessage) { // IDEA: we could incentivise receivers to call back on success by sharing the rent refund get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.ccip.MerkleRoot", "1.0.0"); + return ("com.chainlink.ton.ccip.MerkleRoot", CONTRACT_VERSION); } diff --git a/contracts/contracts/ccip/offramp.tolk b/contracts/contracts/ccip/offramp.tolk index 50c35da76..da336bb1c 100644 --- a/contracts/contracts/ccip/offramp.tolk +++ b/contracts/contracts/ccip/offramp.tolk @@ -8,6 +8,8 @@ import "../lib/upgrades/type_and_version.tolk" import "../lib/utils.tolk" import "fee_quoter/messages" +const CONTRACT_VERSION = "0.0.1"; + struct Storage { id: uint32; ownable: Ownable2Step; @@ -551,5 +553,5 @@ get fun allSourceChainConfigs(): map { fun applySourceChainConfigUpdates() {} get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.ccip.OffRamp", "1.0.0"); - } + return ("com.chainlink.ton.ccip.OffRamp", CONTRACT_VERSION); +} diff --git a/contracts/contracts/ccip/onramp.tolk b/contracts/contracts/ccip/onramp.tolk index 146ffc320..f804de80f 100644 --- a/contracts/contracts/ccip/onramp.tolk +++ b/contracts/contracts/ccip/onramp.tolk @@ -9,6 +9,8 @@ import "ccipsend_executor/messages" import "../lib/jetton/messages" import "../lib/jetton/messages_extended" +const CONTRACT_VERSION = "0.0.1"; + const CCIP_MESSAGE_SENT_TOPIC: int = stringCrc32("CCIPMessageSent"); struct CCIPMessageSent { @@ -16,6 +18,8 @@ struct CCIPMessageSent { } struct OnRamp_Storage { + id: uint32; + testExtraField: bool; ownable: Ownable2Step; // static config @@ -380,5 +384,5 @@ fun withdrawFeeTokens() { } get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.ccip.OnRamp", "1.0.0"); + return ("com.chainlink.ton.ccip.OnRamp", CONTRACT_VERSION); } diff --git a/contracts/contracts/ccip/rmn_remote.tolk b/contracts/contracts/ccip/rmn_remote.tolk index bb5ddbd72..ad556f8b8 100644 --- a/contracts/contracts/ccip/rmn_remote.tolk +++ b/contracts/contracts/ccip/rmn_remote.tolk @@ -2,6 +2,8 @@ import "types.tolk"; import "../lib/access/ownable_2step.tolk"; import "../lib/upgrades/type_and_version.tolk"; +const CONTRACT_VERSION = "0.0.1"; + struct Storage { ownable: Ownable2Step; } @@ -19,5 +21,5 @@ fun onInternalMessage(msgCell: cell, msgBody: slice) { get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.ccip.RMNRemote", "1.0.0"); + return ("com.chainlink.ton.ccip.RMNRemote", CONTRACT_VERSION); } diff --git a/contracts/contracts/ccip/router.tolk b/contracts/contracts/ccip/router.tolk index 4c9a3c9c1..b2caca33a 100644 --- a/contracts/contracts/ccip/router.tolk +++ b/contracts/contracts/ccip/router.tolk @@ -6,7 +6,10 @@ import "../lib/utils.tolk"; import "../lib/jetton/messages" import "../lib/jetton/messages_extended" +const CONTRACT_VERSION = "0.0.1"; + struct Storage { + id: uint32; ownable: Ownable2Step; // TODO: expand this with versions support @@ -114,8 +117,8 @@ fun ccipSend(msg: CCIPSend, sender: address) { } get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.ccip.Router", "1.0.0"); - } + return ("com.chainlink.ton.ccip.Router", CONTRACT_VERSION); +} get fun onRamp(destChainSelector: uint64): address { val st = lazy Storage.load(); diff --git a/contracts/contracts/examples/counter.tolk b/contracts/contracts/examples/counter.tolk index ae16aa5ac..9afb849a1 100644 --- a/contracts/contracts/examples/counter.tolk +++ b/contracts/contracts/examples/counter.tolk @@ -5,6 +5,8 @@ import "../lib/upgrades/type_and_version.tolk" import "../lib/utils.tolk" import "../lib/access/ownable_2step.tolk" +const CONTRACT_VERSION = "1.1.1"; + /// Counter contract + event emission (Tolk example) /// Message to set the counter value. struct (0x00000004) SetCount { @@ -155,6 +157,6 @@ get fun value(): int { /// Gets the current type and version of the contract. get fun typeAndVersion(): (slice, slice) { - return TypeAndVersion { typeStr: "com.chainlink.ton.examples.Counter", versionStr: "1.1.0" } + return TypeAndVersion { typeStr: "com.chainlink.ton.examples.Counter", versionStr: CONTRACT_VERSION } .typeAndVersion(); } diff --git a/contracts/contracts/mcms/mcms.tolk b/contracts/contracts/mcms/mcms.tolk index 65858a61f..bde42053e 100644 --- a/contracts/contracts/mcms/mcms.tolk +++ b/contracts/contracts/mcms/mcms.tolk @@ -6,6 +6,8 @@ import "../lib/utils"; import "../lib/access/ownable_2step.tolk"; import "../lib/crypto/merkle_proof.tolk"; +const CONTRACT_VERSION = "0.0.1"; + /// This is a multi-sig contract that supports signing many transactions (called "ops" in /// the context of this contract to prevent confusion with transactions on the underlying chain) /// targeting many chains with a single set of signatures. Authorized ops along with some metadata @@ -1393,7 +1395,7 @@ fun onBouncedMessage(in: InMessageBounced) { // --- Getters --- get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.mcms.MCMS", "1.0.0"); + return ("com.chainlink.ton.mcms.MCMS", CONTRACT_VERSION); } /// @see .getId> diff --git a/contracts/contracts/mcms/rbac_timelock.tolk b/contracts/contracts/mcms/rbac_timelock.tolk index 469f8bc48..1532f49c5 100644 --- a/contracts/contracts/mcms/rbac_timelock.tolk +++ b/contracts/contracts/mcms/rbac_timelock.tolk @@ -5,6 +5,8 @@ import "@stdlib/tvm-dicts"; import "../lib/utils"; import "../lib/access/access_control"; +const CONTRACT_VERSION = "0.0.1"; + /** * Contract module which acts as a timelocked controller with role-based * access control. When set as the owner of an `Ownable` smart contract, it @@ -1367,7 +1369,7 @@ fun Timelock.hookTrait__AccessControl(self): AccessControl{ // --- Getters --- get fun typeAndVersion(): (slice, slice) { - return ("com.chainlink.ton.mcms.Timelock", "1.0.0"); + return ("com.chainlink.ton.mcms.Timelock", CONTRACT_VERSION); } /// @see .getId> diff --git a/contracts/tests/Counter.spec.ts b/contracts/tests/Counter.spec.ts index f1f04217d..2f876ebf0 100644 --- a/contracts/tests/Counter.spec.ts +++ b/contracts/tests/Counter.spec.ts @@ -46,7 +46,7 @@ describe('Counter', () => { it('should have type and version', async () => { const typeAndVersion = await bind.counter.getTypeAndVersion() expect(typeAndVersion.type).toBe('com.chainlink.ton.examples.Counter') - expect(typeAndVersion.version).toBe('1.1.0') + expect(typeAndVersion.version).toBe('1.1.1') }) it('should have the right code and hash', async () => { diff --git a/contracts/tests/ccip/CCIPRouter.spec.ts b/contracts/tests/ccip/CCIPRouter.spec.ts index 4cd75f168..668b5e65e 100644 --- a/contracts/tests/ccip/CCIPRouter.spec.ts +++ b/contracts/tests/ccip/CCIPRouter.spec.ts @@ -50,6 +50,7 @@ describe('Router', () => { // Mock UpdatePrices Message handler let routerCode = await compile('Router') let data: rt.Storage = { + id: 0, ownable: { owner: deployer.address, pendingOwner: null, @@ -73,6 +74,7 @@ describe('Router', () => { let code = await compile('FeeQuoter') let data: FeeQuoterStorage = { + id: 0, ownable: { owner: deployer.address, pendingOwner: null, @@ -171,6 +173,7 @@ describe('Router', () => { { let code = await compile('OnRamp') let data: or.OnRampStorage = { + id: 0, ownable: { owner: deployer.address, pendingOwner: null, diff --git a/contracts/tests/ccip/helpers/SetUp.ts b/contracts/tests/ccip/helpers/SetUp.ts index 369109b78..702075889 100644 --- a/contracts/tests/ccip/helpers/SetUp.ts +++ b/contracts/tests/ccip/helpers/SetUp.ts @@ -18,6 +18,7 @@ export const setupTestFeeQuoter = async ( let code = await compile('FeeQuoter') let data: FeeQuoterStorage = { + id: 0, ownable: { owner: deployer.address, pendingOwner: null, diff --git a/contracts/wrappers/ccip/FeeQuoter.ts b/contracts/wrappers/ccip/FeeQuoter.ts index ca9888883..e54a4e3bd 100644 --- a/contracts/wrappers/ccip/FeeQuoter.ts +++ b/contracts/wrappers/ccip/FeeQuoter.ts @@ -20,6 +20,7 @@ import { asSnakeData, fromSnakeData } from '../../src/utils' import { loadMap, loadDict, UMapToBuilder } from '../../src/utils/dict' export type FeeQuoterStorage = { + id: number ownable: ownable2step.Data maxFeeJuelsPerMsg: bigint linkToken: Address @@ -236,6 +237,7 @@ export const builder = { const contractData: CellCodec = { encode: (data: FeeQuoterStorage): Builder => { return beginCell() + .storeUint(data.id, 32) .storeBuilder(ownable2step.builder.data.traitData.encode(data.ownable)) .storeUint(data.maxFeeJuelsPerMsg, 96) .storeAddress(data.linkToken) @@ -245,6 +247,7 @@ export const builder = { .storeDict(data.destChainConfigs) }, load: (src: Slice): FeeQuoterStorage => { + const id = src.loadUint(32) const ownable = ownable2step.builder.data.traitData.load(src) const maxFeeJuelsPerMsg = src.loadUintBig(96) const linkToken = src.loadAddress() @@ -275,6 +278,7 @@ export const builder = { } return { + id, ownable, maxFeeJuelsPerMsg, linkToken, diff --git a/contracts/wrappers/ccip/OnRamp.ts b/contracts/wrappers/ccip/OnRamp.ts index 39da895a9..1469bbb92 100644 --- a/contracts/wrappers/ccip/OnRamp.ts +++ b/contracts/wrappers/ccip/OnRamp.ts @@ -18,6 +18,7 @@ import { CellCodec } from '../utils' import * as rt from './Router' export type OnRampStorage = { + id: number ownable: ownable2step.Data chainSelector: bigint config: { @@ -63,6 +64,7 @@ export const builder = { encode: function (data: OnRampStorage): Builder { return ( beginCell() + .storeUint(data.id, 32) .storeBuilder(ownable2step.builder.data.traitData.encode(data.ownable)) .storeUint(data.chainSelector, 64) // Cell diff --git a/contracts/wrappers/ccip/Router.ts b/contracts/wrappers/ccip/Router.ts index 92633f5c2..58f6bae11 100644 --- a/contracts/wrappers/ccip/Router.ts +++ b/contracts/wrappers/ccip/Router.ts @@ -17,6 +17,7 @@ import { CellCodec } from '../utils' import { asSnakeData, fromSnakeData } from '../../src/utils' export type Storage = { + id: number ownable: ownable2step.Data onRamps: Dictionary @@ -122,6 +123,7 @@ export const builder = { const contractData: CellCodec = { encode: (config: Storage): Builder => { return beginCell() + .storeUint(config.id, 32) .storeAddress(config.ownable.owner) .storeMaybeBuilder( config.ownable.pendingOwner @@ -134,6 +136,7 @@ export const builder = { load: (src: Slice): Storage => { return { + id: src.loadUint(32), ownable: ownable2step.builder.data.traitData.load(src.loadRef().beginParse()), onRamps: Dictionary.empty(Dictionary.Keys.BigUint(64)), } diff --git a/deployment/ccip/config/deploy.go b/deployment/ccip/config/deploy.go index 529307a42..f7f5fa901 100644 --- a/deployment/ccip/config/deploy.go +++ b/deployment/ccip/config/deploy.go @@ -38,6 +38,7 @@ type FeeToken struct { } type FeeQuoterParams struct { + ID uint32 MaxFeeJuelsPerMsg *big.Int TokenPriceStalenessThreshold uint64 FeeTokens map[TokenSymbol]FeeToken @@ -57,6 +58,7 @@ func (f FeeQuoterParams) Validate() error { } type OffRampParams struct { + ID uint32 ChainSelector uint64 PermissionlessExecutionThreshold uint32 } @@ -72,6 +74,7 @@ func (o OffRampParams) Validate() error { } type OnRampParams struct { + ID uint32 ChainSelector uint64 AllowlistAdmin *address.Address FeeAggregator *address.Address diff --git a/deployment/ccip/cs_test_helpers.go b/deployment/ccip/cs_test_helpers.go index b2c657525..bf5034ce4 100644 --- a/deployment/ccip/cs_test_helpers.go +++ b/deployment/ccip/cs_test_helpers.go @@ -81,7 +81,7 @@ var ( } ) -func DeployChainContractsConfig(t *testing.T, env cldf.Environment, chainSelector uint64, contractVersion string) DeployCCIPContractsCfg { +func DeployChainContractsConfig(t *testing.T, env cldf.Environment, chainSelector uint64, contractVersion string, idForContracts uint32) DeployCCIPContractsCfg { tonChain := env.BlockChains.TonChains()[chainSelector] deployer := tonChain.Wallet @@ -94,6 +94,7 @@ func DeployChainContractsConfig(t *testing.T, env cldf.Environment, chainSelecto TonChainSelector: chainSelector, Params: config.ChainContractParams{ FeeQuoterParams: config.FeeQuoterParams{ + ID: idForContracts, MaxFeeJuelsPerMsg: big.NewInt(1), TokenPriceStalenessThreshold: 0, FeeTokens: map[config.TokenSymbol]config.FeeToken{ @@ -104,10 +105,12 @@ func DeployChainContractsConfig(t *testing.T, env cldf.Environment, chainSelecto }, }, OffRampParams: config.OffRampParams{ + ID: idForContracts, ChainSelector: tonChain.Selector, PermissionlessExecutionThreshold: 0, }, OnRampParams: config.OnRampParams{ + ID: idForContracts, ChainSelector: ChainSelEVMTest90000001, // TODO: // AllowlistAdmin: &address.Address{}, diff --git a/deployment/ccip/operation/fee_quoter.go b/deployment/ccip/operation/fee_quoter.go index 2b63eada3..74504eca8 100644 --- a/deployment/ccip/operation/fee_quoter.go +++ b/deployment/ccip/operation/fee_quoter.go @@ -5,12 +5,13 @@ import ( "math/big" "github.com/Masterminds/semver/v3" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-ton/deployment/ccip/config" "github.com/smartcontractkit/chainlink-ton/deployment/ccip/utils" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/common" @@ -48,6 +49,7 @@ func deployFeeQuoter(b operations.Bundle, deps TonDeps, in DeployFeeQuoterInput) conn := tracetracking.NewSignedAPIClient(deps.TonChain.Client, *deps.TonChain.Wallet) storage := feequoter.Storage{ + ID: in.Params.ID, Ownable: common.Ownable2Step{ Owner: deps.TonChain.WalletAddress, PendingOwner: nil, diff --git a/deployment/ccip/operation/offramp.go b/deployment/ccip/operation/offramp.go index 3554fa959..8605f7b7a 100644 --- a/deployment/ccip/operation/offramp.go +++ b/deployment/ccip/operation/offramp.go @@ -6,11 +6,12 @@ import ( "fmt" "github.com/Masterminds/semver/v3" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-ton/deployment/ccip/utils" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/common" "github.com/smartcontractkit/chainlink-ton/pkg/ccip/bindings/offramp" @@ -19,6 +20,7 @@ import ( ) type DeployOffRampInput struct { + ID uint32 ChainSelector uint64 FeeQuoter *address.Address PermissionlessExecutionThresholdSeconds uint32 @@ -49,7 +51,7 @@ func deployOffRamp(b operations.Bundle, deps TonDeps, in DeployOffRampInput) (De conn := tracetracking.NewSignedAPIClient(deps.TonChain.Client, *deps.TonChain.Wallet) storage := offramp.Storage{ - ID: 0, + ID: in.ID, Ownable: common.Ownable2Step{ Owner: deps.TonChain.WalletAddress, PendingOwner: nil, diff --git a/deployment/ccip/operation/onramp.go b/deployment/ccip/operation/onramp.go index e3428a92f..c256aaf34 100644 --- a/deployment/ccip/operation/onramp.go +++ b/deployment/ccip/operation/onramp.go @@ -18,6 +18,7 @@ import ( ) type DeployOnRampInput struct { + ID uint32 ChainSelector uint64 FeeQuoter *address.Address FeeAggregator *address.Address @@ -52,6 +53,7 @@ func deployOnRamp(b operations.Bundle, deps TonDeps, in DeployOnRampInput) (Depl conn := tracetracking.NewSignedAPIClient(deps.TonChain.Client, *deps.TonChain.Wallet) storage := onramp.Storage{ + ID: in.ID, Ownable: common.Ownable2Step{ Owner: deps.TonChain.WalletAddress, PendingOwner: nil, diff --git a/deployment/ccip/sequence/deploy_ccip.go b/deployment/ccip/sequence/deploy_ccip.go index 173b5ab88..dfaf49e87 100644 --- a/deployment/ccip/sequence/deploy_ccip.go +++ b/deployment/ccip/sequence/deploy_ccip.go @@ -106,6 +106,7 @@ func deployCCIPSequence(b operations.Bundle, deps operation.TonDeps, in DeployCC output.FeeQuoterAddress = deployFeeQuoterReport.Output.Address onrampInput := operation.DeployOnRampInput{ + ID: in.CCIPConfig.OnRampParams.ID, ChainSelector: in.CCIPConfig.OnRampParams.ChainSelector, FeeQuoter: deployFeeQuoterReport.Output.Address, FeeAggregator: in.CCIPConfig.OnRampParams.FeeAggregator, @@ -120,6 +121,7 @@ func deployCCIPSequence(b operations.Bundle, deps operation.TonDeps, in DeployCC output.OnRampAddress = deployOnRampReport.Output.Address offrampInput := operation.DeployOffRampInput{ + ID: in.CCIPConfig.OffRampParams.ID, ChainSelector: in.CCIPConfig.OffRampParams.ChainSelector, FeeQuoter: deployFeeQuoterReport.Output.Address, PermissionlessExecutionThresholdSeconds: in.CCIPConfig.OffRampParams.PermissionlessExecutionThreshold, diff --git a/integration-tests/deployment/cs_test.go b/integration-tests/deployment/cs_test.go index 68529f828..882eac76d 100644 --- a/integration-tests/deployment/cs_test.go +++ b/integration-tests/deployment/cs_test.go @@ -35,6 +35,7 @@ import ( inmemorystore "github.com/smartcontractkit/chainlink-ton/pkg/logpoller/backend/db/inmemory" "github.com/smartcontractkit/chainlink-ton/pkg/logpoller/backend/loader/account" "github.com/smartcontractkit/chainlink-ton/pkg/logpoller/backend/txparser" + "github.com/smartcontractkit/chainlink-ton/pkg/ton/hash" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/ton" @@ -67,7 +68,7 @@ func TestDeploy(t *testing.T) { test_utils.FundWallets(t, tonChain.Client, []*address.Address{deployer.Address()}, []tlb.Coins{tlb.MustFromTON("1000")}) time.Sleep(5 * time.Second) - cs := commonchangeset.Configure(ton_ops.DeployCCIPContracts{}, ton_ops.DeployChainContractsConfig(t, env, chainSelector, sequence.ContractsLocalVersion)) + cs := commonchangeset.Configure(ton_ops.DeployCCIPContracts{}, ton_ops.DeployChainContractsConfig(t, env, chainSelector, sequence.ContractsLocalVersion, hash.CRC32("github.com/smartcontractkit/chainlink-ton/integration-tests/deployment/cs_test.TestDeploy"))) env, _, err := commonchangeset.ApplyChangesets(t, env, []commonchangeset.ConfiguredChangeSet{cs}) require.NoError(t, err, "failed to deploy ccip") diff --git a/pkg/ccip/bindings/feequoter/fee_quoter.go b/pkg/ccip/bindings/feequoter/fee_quoter.go index 111628bde..6aa318308 100644 --- a/pkg/ccip/bindings/feequoter/fee_quoter.go +++ b/pkg/ccip/bindings/feequoter/fee_quoter.go @@ -12,6 +12,7 @@ import ( ) type Storage struct { + ID uint32 `tlb:"## 32"` Ownable common.Ownable2Step `tlb:"."` MaxFeeJuelsPerMsg *big.Int `tlb:"## 96"` LinkToken *address.Address `tlb:"addr"` diff --git a/pkg/ccip/bindings/onramp/onramp.go b/pkg/ccip/bindings/onramp/onramp.go index 1dd0fe24f..cf9684f45 100644 --- a/pkg/ccip/bindings/onramp/onramp.go +++ b/pkg/ccip/bindings/onramp/onramp.go @@ -111,6 +111,7 @@ func (c *DynamicConfig) FromResult(result *ton.ExecutionResult) error { // Storage represents the storage structure for the CCIP onramp contract. type Storage struct { + ID uint32 `tlb:"## 32"` Ownable common.Ownable2Step `tlb:"."` ChainSelector uint64 `tlb:"## 64"` Config DynamicConfig `tlb:"^"` diff --git a/pkg/ccip/bindings/onramp/onramp_test.go b/pkg/ccip/bindings/onramp/onramp_test.go index 79abc6856..afcc61924 100644 --- a/pkg/ccip/bindings/onramp/onramp_test.go +++ b/pkg/ccip/bindings/onramp/onramp_test.go @@ -167,6 +167,7 @@ func TestStorage(t *testing.T) { ExecutorCode := b.EndCell() s := Storage{ + ID: 43, Ownable: common.Ownable2Step{ Owner: dummyAddr, }, @@ -186,6 +187,7 @@ func TestStorage(t *testing.T) { var decoded Storage err = tlb.LoadFromCell(&decoded, c.BeginParse()) require.NoError(t, err) + require.Equal(t, s.ID, decoded.ID) require.Equal(t, s.Ownable.Owner, decoded.Ownable.Owner) require.Equal(t, s.ChainSelector, decoded.ChainSelector) require.Equal(t, s.Config, decoded.Config)