diff --git a/database.sql b/database.sql index 6e26571..533f5f5 100644 --- a/database.sql +++ b/database.sql @@ -614,6 +614,32 @@ END; $$; +-- +-- Name: validate_transformation_key_length(jsonb); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.validate_transformation_key_length(object jsonb) RETURNS boolean + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN object ? 'length' AND is_jsonb_number(object -> 'length') AND (object ->> 'length')::integer > 0; +END; +$$; + +-- +-- Name: validate_transformation_key_length_optional(jsonb); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.validate_transformation_key_length_optional(object jsonb) RETURNS boolean + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN NOT object ? 'length' + OR (is_jsonb_number(object -> 'length') AND (object ->> 'length')::integer > 0); +END; +$$; + + -- -- Name: validate_transformation_key_type(jsonb, text); Type: FUNCTION; Schema: public; Owner: - -- @@ -691,8 +717,16 @@ CREATE FUNCTION public.validate_transformations_cbor_auxdata(object jsonb) RETUR LANGUAGE plpgsql AS $$ BEGIN - RETURN validate_transformation_key_type(object, 'replace') AND validate_transformation_key_offset(object) - AND validate_transformation_key_id(object); + RETURN ( + validate_transformation_key_type(object, 'replace') + AND validate_transformation_key_offset(object) + AND validate_transformation_key_length_optional(object) + AND validate_transformation_key_id(object) + ) OR ( + validate_transformation_key_type(object, 'delete') + AND validate_transformation_key_offset(object) + AND validate_transformation_key_length(object) + ); END; $$; diff --git a/json-schemas/README.md b/json-schemas/README.md index c36bfb3..04d24cc 100644 --- a/json-schemas/README.md +++ b/json-schemas/README.md @@ -26,6 +26,7 @@ Apart from the specifications in each section below, the following rules apply t - All hexadecimal value strings must be prefixed with `0x` such as addresses, constructor arguments etc. - `offset` values correspond to bytes in the bytecode and not string indexes. So `offset: 1` for the bytecode "0xab46fd" is the first byte in the bytecode corresponds to start from `46` +- `length` values (for delete transformations) are in number of bytes. ## Transformations @@ -82,7 +83,8 @@ This object contains the transformation that will be applied to the creation byt The creation transformation can only contain these as `"reason"`s and `"type"`s: - `{ "reason": "constructorArguments", "type": "insert", "offset": 999 }` -- `{ "reason": "cborAuxdata", "type": "replace", "offset": 123, id: "0" }` Needs an `id` since there can be multiple auxdata transformations e.g. factories. +- `{ "reason": "cborAuxdata", "type": "replace", "offset": 123, id: "0" }` Needs an `id` since there can be multiple auxdata transformations e.g. factories. Can also include optional `"length"`, if included it must be used instead of the length of the auxdata in the transformations. +- `{ "reason": "cborAuxdata", "type": "delete", "offset": 123, "length": 45 }` Can also have `"type": "delete"` which means the onchain bytecode does not have the cborAuxdata while the recompiled bytecode has it. - `{ "reason": "library", "type": "replace", "offset": 123, id: "sources/lib/MyLib.sol:MyLib" }` Example: @@ -101,6 +103,12 @@ Example: "offset": 1269, "reason": "cborAuxdata" }, + { + "type": "delete", + "offset": 1295, + "length": 47, + "reason": "cborAuxdata" + }, { "type": "insert", "offset": 1322, @@ -133,7 +141,8 @@ Similar to `creation_transformation`. But runtime code does not contain construc The runtime transformations can only contain these as `"reason"`s and `"type"`s: -- `{ "reason": "cborAuxdata", "type": "replace", "offset": 123, id: "0" }` Needs an `id` since there can be multiple auxdata transformations e.g. factories. +- `{ "reason": "cborAuxdata", "type": "replace", "offset": 123, id: "0" }` Needs an `id` since there can be multiple auxdata transformations e.g. factories. Can also include optional `"length"`, if included it must be used instead of the length of the auxdata in the transformations. +- `{ "reason": "cborAuxdata", "type": "delete", "offset": 123, "length": 45 }` Can also have `"type": "delete"` which means the onchain bytecode does not have the cborAuxdata while the recompiled bytecode has it. - `{ "reason": "library", "type": "replace", "offset": 123, id: "contracts/order/OrderUtils.sol:OrderUtilsLib" }` - `{ "reason": "immutable", "type": "replace", "offset": 999, id: "2473" }` Needs an `id` for referencing multiple times and there can be multiple immutable transformations. Solidity contracts have `"replace"` type, while Vyper ones have `"insert"` because they are appended to the runtime bytecode. - `{ "reason": "callProtection", "type": "replace", "offset": 1 }` @@ -220,7 +229,14 @@ Compiler settings as passed to the compiler in JSON format }, "outputSelection": { "*": { - "*": ["evm.bytecode", "evm.deployedBytecode", "devdoc", "userdoc", "metadata", "abi"] + "*": [ + "evm.bytecode", + "evm.deployedBytecode", + "devdoc", + "userdoc", + "metadata", + "abi" + ] }, "contracts/order/OrderUtils.sol": { "OrderUtils": ["*"] diff --git a/json-schemas/verified_contracts-transformations.json b/json-schemas/verified_contracts-transformations.json index a6b2718..c95c7b3 100644 --- a/json-schemas/verified_contracts-transformations.json +++ b/json-schemas/verified_contracts-transformations.json @@ -32,14 +32,16 @@ "required": [ "type", "reason", - "offset", - "id" + "offset" ], "additionalProperties": false, "properties": { "type": { "type": "string", - "value": "replace" + "enum": [ + "replace", + "delete" + ] }, "reason": { "type": "string", @@ -54,6 +56,10 @@ "id": { "type": "string", "minLength": 1 + }, + "length": { + "type": "number", + "minimum": 1 } } }, diff --git a/migrations/20260126113330_allow_delete_cbor_auxdata_transformations.sql b/migrations/20260126113330_allow_delete_cbor_auxdata_transformations.sql new file mode 100644 index 0000000..c1d7276 --- /dev/null +++ b/migrations/20260126113330_allow_delete_cbor_auxdata_transformations.sql @@ -0,0 +1,47 @@ +-- migrate:up +CREATE OR REPLACE FUNCTION validate_transformation_key_length(object jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN object ? 'length' AND is_jsonb_number(object -> 'length') AND (object ->> 'length')::integer > 0; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION validate_transformation_key_length_optional(object jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN NOT object ? 'length' + OR (is_jsonb_number(object -> 'length') AND (object ->> 'length')::integer > 0); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION validate_transformations_cbor_auxdata(object jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN ( + validate_transformation_key_type(object, 'replace') + AND validate_transformation_key_offset(object) + AND validate_transformation_key_length_optional(object) + AND validate_transformation_key_id(object) + ) OR ( + validate_transformation_key_type(object, 'delete') + AND validate_transformation_key_offset(object) + AND validate_transformation_key_length(object) + ); +END; +$$ LANGUAGE plpgsql; + +-- migrate:down +DROP FUNCTION IF EXISTS validate_transformation_key_length_optional(jsonb); +DROP FUNCTION IF EXISTS validate_transformation_key_length(jsonb); + +CREATE OR REPLACE FUNCTION validate_transformations_cbor_auxdata(object jsonb) + RETURNS boolean AS +$$ +BEGIN + RETURN validate_transformation_key_type(object, 'replace') AND validate_transformation_key_offset(object) + AND validate_transformation_key_id(object); +END; +$$ LANGUAGE plpgsql; diff --git a/tests/test_constraint_creation_transformations_json_schema.py b/tests/test_constraint_creation_transformations_json_schema.py index e9f9e26..53980c0 100644 --- a/tests/test_constraint_creation_transformations_json_schema.py +++ b/tests/test_constraint_creation_transformations_json_schema.py @@ -22,7 +22,8 @@ def test_expected_type_values(self, connection, dummy_code, dummy_contract, dumm {"reason": "constructorArguments", "type": "insert", "offset": 0}, {"reason": "library", "type": "replace", "offset": 0, "id": "file1:lib1"}, - {"reason": "cborAuxdata", "type": "replace", "offset": 0, "id": "0"} + {"reason": "cborAuxdata", "type": "replace", "offset": 0, "id": "0"}, + {"reason": "cborAuxdata", "type": "delete", "offset": 2, "length": 4} ] dummy_verified_contract.insert( connection, dummy_contract_deployment.id, dummy_compiled_contract.id) @@ -267,6 +268,20 @@ def test_valid_value(self, connection, dummy_code, dummy_contract, dummy_contrac dummy_verified_contract.insert( connection, dummy_contract_deployment.id, dummy_compiled_contract.id) + def test_valid_value_with_length(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.creation_transformations = [ + {"reason": "cborAuxdata", "type": "replace", "offset": 6, "id": "0", "length": 12} + ] + dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id) + + def test_valid_value_delete(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.creation_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 6, "length": 12} + ] + dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id) + def test_missing_key_type_fails(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): dummy_verified_contract.creation_transformations = [ {"reason": "cborAuxdata", "offset": 0, "id": "0"} @@ -325,6 +340,45 @@ def test_invalid_key_offset_value_fails(self, connection, dummy_code, dummy_cont connection, dummy_contract_deployment.id, dummy_compiled_contract.id), "creation_transformations_json_schema") + def test_missing_key_length_for_delete_fails(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.creation_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 6} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "creation_transformations_json_schema") + + @pytest.mark.parametrize("value", [None, "", [], dict()], ids=["null", "string", "array", "object"]) + def test_invalid_key_length_type_fails(self, value, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.creation_transformations = [ + {"reason": "cborAuxdata", "type": "replace", "offset": 6, "id": "0", "length": value} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "creation_transformations_json_schema") + + @pytest.mark.parametrize("value", [None, "", [], dict()], ids=["null", "string", "array", "object"]) + def test_invalid_key_length_type_for_delete_fails(self, value, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.creation_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 0, "length": value} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "creation_transformations_json_schema") + + @pytest.mark.parametrize("value", [0, -1], ids=["zero", "negative"]) + def test_invalid_key_length_value_fails(self, value, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.creation_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 6, "length": value} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "creation_transformations_json_schema") + def test_missing_key_id_fails(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): dummy_verified_contract.creation_transformations = [ {"reason": "cborAuxdata", "type": "replace", "offset": 0} diff --git a/tests/test_constraint_runtime_transformations_json_schema.py b/tests/test_constraint_runtime_transformations_json_schema.py index 56892a6..196f78f 100644 --- a/tests/test_constraint_runtime_transformations_json_schema.py +++ b/tests/test_constraint_runtime_transformations_json_schema.py @@ -23,6 +23,7 @@ def test_expected_type_values(self, connection, dummy_code, dummy_contract, dumm "offset": 0, "id": "file1:lib1"}, {"reason": "immutable", "type": "replace", "offset": 0, "id": "0"}, {"reason": "cborAuxdata", "type": "replace", "offset": 0, "id": "0"}, + {"reason": "cborAuxdata", "type": "delete", "offset": 1, "length": 2}, {"reason": "callProtection", "type": "replace", "offset": 0} ] dummy_verified_contract.insert( @@ -287,6 +288,20 @@ def test_valid_value(self, connection, dummy_code, dummy_contract, dummy_contrac dummy_verified_contract.insert( connection, dummy_contract_deployment.id, dummy_compiled_contract.id) + def test_valid_value_with_length(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.runtime_transformations = [ + {"reason": "cborAuxdata", "type": "replace", "offset": 0, "id": "0", "length": 12} + ] + dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id) + + def test_valid_value_delete(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.runtime_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 0, "length": 12} + ] + dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id) + def test_missing_key_type_fails(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): dummy_verified_contract.runtime_transformations = [ {"reason": "cborAuxdata", "offset": 0, "id": "0"} @@ -345,6 +360,45 @@ def test_invalid_key_offset_value_fails(self, connection, dummy_code, dummy_cont connection, dummy_contract_deployment.id, dummy_compiled_contract.id), "runtime_transformations_json_schema") + def test_missing_key_length_for_delete_fails(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.runtime_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 0} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "runtime_transformations_json_schema") + + @pytest.mark.parametrize("value", [None, "", [], dict()], ids=["null", "string", "array", "object"]) + def test_invalid_key_length_type_fails(self, value, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.runtime_transformations = [ + {"reason": "cborAuxdata", "type": "replace", "offset": 0, "id": "0", "length": value} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "runtime_transformations_json_schema") + + @pytest.mark.parametrize("value", [None, "", [], dict()], ids=["null", "string", "array", "object"]) + def test_invalid_key_length_type_for_delete_fails(self, value, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.runtime_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 0, "length": value} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "runtime_transformations_json_schema") + + @pytest.mark.parametrize("value", [0, -1], ids=["zero", "negative"]) + def test_invalid_key_length_value_fails(self, value, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): + dummy_verified_contract.runtime_transformations = [ + {"reason": "cborAuxdata", "type": "delete", "offset": 0, "length": value} + ] + check_constraint_fails( + lambda: dummy_verified_contract.insert( + connection, dummy_contract_deployment.id, dummy_compiled_contract.id), + "runtime_transformations_json_schema") + def test_missing_key_id_fails(self, connection, dummy_code, dummy_contract, dummy_contract_deployment, dummy_compiled_contract, dummy_verified_contract): dummy_verified_contract.runtime_transformations = [ {"reason": "cborAuxdata", "type": "replace", "offset": 0}