Skip to content
This repository has been archived by the owner on Jan 9, 2025. It is now read-only.

Commit

Permalink
tests: add univ3 svg generation test (#1292)
Browse files Browse the repository at this point in the history
<!--- Please provide a general summary of your changes in the title
above -->

<!-- Give an estimate of the time you spent on this PR in terms of work
days.
Did you spend 0.5 days on this PR or rather 2 days?  -->

Time spent on this PR: 1d

Adds Mock UniswapV3 contracts that allow querying a dynamically
generated on-chain SVG representing a position.
The values are mocked, and due to some issues, the base64 encoding of
the image crashes with a stack underflow error (tbd).

However, the encoding of the rest takes ~27M steps

Also added a way to do library linking in the python kakarot scripts 

## Pull request type

<!-- Please try to limit your pull request to one type,
submit multiple pull requests if needed. -->

Please check the type of change your PR introduces:

- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying,
or link to a relevant issue. -->

Resolves #<Issue number>

## What is the new behavior?

<!-- Please describe the behavior or changes that are being added by
this PR. -->

-
-
-

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg"
height="34" align="absmiddle"
alt="Reviewable"/>](https://reviewable.io/reviews/kkrt-labs/kakarot/1292)
<!-- Reviewable:end -->
enitrat authored Jul 25, 2024
1 parent e3469f8 commit a41fe3b
Showing 18 changed files with 1,082 additions and 35 deletions.
10 changes: 9 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
[submodule "solidity_contracts/lib/forge-std"]
path = solidity_contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
branch = v1.3.0
[submodule "solidity_contracts/lib/kakarot-lib"]
path = solidity_contracts/lib/kakarot-lib
url = https://github.com/kkrt-labs/kakarot-lib
[submodule "solidity_contracts/lib/v3-core"]
path = solidity_contracts/lib/v3-core
url = https://github.com/Uniswap/v3-core
[submodule "solidity_contracts/lib/openzeppelin"]
path = solidity_contracts/lib/openzeppelin
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "solidity_contracts/lib/base64-sol"]
path = solidity_contracts/lib/base64-sol
url = https://github.com/Brechtpd/base64
4 changes: 2 additions & 2 deletions kakarot_scripts/constants.py
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@ class NetworkType(Enum):
"l1_rpc_url": "http://127.0.0.1:8545",
"type": NetworkType.DEV,
"check_interval": 0.01,
"max_wait": 1,
"max_wait": 3,
},
"katana": {
"name": "katana",
@@ -64,7 +64,7 @@ class NetworkType(Enum):
"l1_rpc_url": "http://127.0.0.1:8545",
"type": NetworkType.DEV,
"check_interval": 0.01,
"max_wait": 2,
"max_wait": 3,
},
"madara": {
"name": "madara",
110 changes: 99 additions & 11 deletions kakarot_scripts/utils/kakarot.py
Original file line number Diff line number Diff line change
@@ -3,9 +3,10 @@
import logging
from pathlib import Path
from types import MethodType
from typing import List, Optional, Union, cast
from typing import Any, Dict, List, Optional, Tuple, Union, cast

import rlp
from async_lru import alru_cache
from eth_abi import decode
from eth_abi.exceptions import InsufficientDataBytes
from eth_account import Account as EvmAccount
@@ -72,6 +73,7 @@ def get_solidity_artifacts(
except (NameError, FileNotFoundError):
foundry_file = toml.loads(Path("foundry.toml").read_text())

src_path = Path(foundry_file["profile"]["default"]["src"])
all_compilation_outputs = [
json.load(open(file))
for file in Path(foundry_file["profile"]["default"]["out"]).glob(
@@ -82,9 +84,7 @@ def get_solidity_artifacts(
target_compilation_output = all_compilation_outputs[0]
else:
target_solidity_file_path = list(
(Path(foundry_file["profile"]["default"]["src"]) / contract_app).glob(
f"**/{contract_name}.sol"
)
(src_path / contract_app).glob(f"**/{contract_name}.sol")
)
if len(target_solidity_file_path) != 1:
raise ValueError(
@@ -105,31 +105,57 @@ def get_solidity_artifacts(
f"found {len(target_compilation_output)} outputs:\n{target_compilation_output}"
)
target_compilation_output = target_compilation_output[0]

def process_link_references(
link_references: Dict[str, Dict[str, Any]]
) -> Dict[str, Dict[str, Any]]:
return {
Path(file_path)
.relative_to(src_path)
.parts[0]: {
library_name: references
for library_name, references in libraries.items()
}
for file_path, libraries in link_references.items()
}

return {
"bytecode": target_compilation_output["bytecode"]["object"],
"bytecode_runtime": target_compilation_output["deployedBytecode"]["object"],
"bytecode": {
"object": target_compilation_output["bytecode"]["object"],
"linkReferences": process_link_references(
target_compilation_output["bytecode"].get("linkReferences", {})
),
},
"bytecode_runtime": {
"object": target_compilation_output["deployedBytecode"]["object"],
"linkReferences": process_link_references(
target_compilation_output["deployedBytecode"].get("linkReferences", {})
),
},
"abi": target_compilation_output["abi"],
"name": contract_name,
}


def get_contract(
async def get_contract(
contract_app: str,
contract_name: str,
address=None,
caller_eoa: Optional[Account] = None,
) -> Web3Contract:

artifacts = get_solidity_artifacts(contract_app, contract_name)

bytecode, bytecode_runtime = await link_libraries(artifacts)

contract = cast(
Web3Contract,
WEB3.eth.contract(
address=to_checksum_address(address) if address is not None else address,
abi=artifacts["abi"],
bytecode=artifacts["bytecode"],
bytecode=bytecode,
),
)
contract.bytecode_runtime = HexBytes(artifacts["bytecode_runtime"])
contract.bytecode_runtime = HexBytes(bytecode_runtime)

try:
for fun in contract.functions:
@@ -140,12 +166,74 @@ def get_contract(
return contract


@alru_cache()
async def get_or_deploy_library(library_app: str, library_name: str) -> str:
"""
Deploy a solidity library if not already deployed and return its address.
Args:
----
library_app (str): The application name of the library.
library_name (str): The name of the library.
Returns:
-------
str: The deployed library address as a hexstring with the '0x' prefix.
"""
library_contract = await deploy(library_app, library_name)
logger.info(f"ℹ️ Deployed {library_name} at address {library_contract.address}")
return library_contract.address


async def link_libraries(artifacts: Dict[str, Any]) -> Tuple[str, str]:
"""
Process an artifacts bytecode by linking libraries with their deployed addresses.
Args:
----
artifacts (Dict[str, Any]): The contract artifacts containing bytecode and link references.
Returns:
-------
Tuple[str, str]: The processed bytecode and runtime bytecode.
"""

async def process_bytecode(bytecode_type: str) -> str:
bytecode_obj = artifacts[bytecode_type]
current_bytecode = bytecode_obj["object"][2:]
link_references = bytecode_obj.get("linkReferences", {})

for library_app, libraries in link_references.items():
for library_name, references in libraries.items():
library_address = await get_or_deploy_library(library_app, library_name)

for ref in references:
start, length = ref["start"] * 2, ref["length"] * 2
placeholder = current_bytecode[start : start + length]
current_bytecode = current_bytecode.replace(
placeholder, library_address[2:].lower()
)

logger.info(
f"ℹ️ Replaced {library_name} in {bytecode_type} with address 0x{library_address}"
)

return current_bytecode

bytecode = await process_bytecode("bytecode")
bytecode_runtime = await process_bytecode("bytecode_runtime")

return bytecode, bytecode_runtime


async def deploy(
contract_app: str, contract_name: str, *args, **kwargs
) -> Web3Contract:
logger.info(f"⏳ Deploying {contract_name}")
caller_eoa = kwargs.pop("caller_eoa", None)
contract = get_contract(contract_app, contract_name, caller_eoa=caller_eoa)
contract = await get_contract(contract_app, contract_name, caller_eoa=caller_eoa)
max_fee = kwargs.pop("max_fee", None)
value = kwargs.pop("value", 0)
receipt, response, success, _ = await eth_send_transaction(
4 changes: 2 additions & 2 deletions kakarot_scripts/utils/l1.py
Original file line number Diff line number Diff line change
@@ -103,10 +103,10 @@ def get_l1_contract(
L1_RPC_PROVIDER.eth.contract(
address=to_checksum_address(address) if address is not None else address,
abi=artifacts["abi"],
bytecode=artifacts["bytecode"],
bytecode=artifacts["bytecode"]["object"],
),
)
contract.bytecode_runtime = HexBytes(artifacts["bytecode_runtime"])
contract.bytecode_runtime = HexBytes(artifacts["bytecode_runtime"]["object"])

try:
for fun in contract.functions:
1 change: 1 addition & 0 deletions solidity_contracts/lib/base64-sol
Submodule base64-sol added at dcbf85
1 change: 1 addition & 0 deletions solidity_contracts/lib/openzeppelin
Submodule openzeppelin added at 8e0296
1 change: 1 addition & 0 deletions solidity_contracts/lib/v3-core
Submodule v3-core added at e3589b
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ library UniswapV2Library {
hex"ff",
factory,
keccak256(abi.encodePacked(token0, token1)),
hex"666a5b78ea0b660c426b08cb5b7427447e909408067de1a5519c772ee9a3c032" // init code hash
hex"0f5b822a8dffa6ce589a2c240d78a6a2b38a51835a97ceab40c1f301e46ba30b" // init code hash
)
)
)
29 changes: 29 additions & 0 deletions solidity_contracts/src/UniswapV3/HexStrings.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity =0.7.6;

library HexStrings {
bytes16 internal constant ALPHABET = "0123456789abcdef";

/// @notice Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
/// @dev Credit to Open Zeppelin under MIT license https://github.com/OpenZeppelin/openzeppelin-contracts/blob/243adff49ce1700e0ecb99fe522fb16cff1d1ddc/contracts/utils/Strings.sol#L55
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = ALPHABET[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}

function toHexStringNoPrefix(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length);
for (uint256 i = buffer.length; i > 0; i--) {
buffer[i - 1] = ALPHABET[value & 0xf];
value >>= 4;
}
return string(buffer);
}
}
462 changes: 462 additions & 0 deletions solidity_contracts/src/UniswapV3/NFTDescriptor.sol

Large diffs are not rendered by default.

395 changes: 395 additions & 0 deletions solidity_contracts/src/UniswapV3/NFTSVG.sol

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions solidity_contracts/src/UniswapV3/UniswapV3NFTManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
pragma solidity >=0.7.0;
pragma abicoder v2;

import "openzeppelin/token/ERC721/ERC721.sol";
import "./NFTDescriptor.sol";

contract UniswapV3NFTManager is ERC721 {
constructor() ERC721("UniswapV3 NFT Positions", "UNIV3") {}

function tokenURI(uint256 tokenId) public view override returns (string memory) {
return _mockTokenUri(tokenId);
}

function tokenURIExternal(uint256 tokenId) external returns (string memory) {
return _mockTokenUri(tokenId);
}

function _mockTokenUri(uint256 tokenId) internal view returns (string memory) {
NFTDescriptor.ConstructTokenURIParams memory params = NFTDescriptor.ConstructTokenURIParams({
tokenId: tokenId,
quoteTokenAddress: address(0xabcdef),
baseTokenAddress: address(0x123456),
quoteTokenSymbol: "ETH",
baseTokenSymbol: "USDC",
quoteTokenDecimals: 18,
baseTokenDecimals: 6,
flipRatio: false,
tickLower: -887272,
tickUpper: 887272,
tickCurrent: 387272,
tickSpacing: 1000,
fee: 3000,
poolAddress: address(0xc0de)
});

return NFTDescriptor.constructTokenURI(params);
}
}
14 changes: 7 additions & 7 deletions tests/end_to_end/PlainOpcodes/test_plain_opcodes.py
Original file line number Diff line number Diff line change
@@ -157,7 +157,7 @@ async def test_should_create_counters(
events = plain_opcodes.events.parse_events(receipt)
assert len(events["CreateAddress"]) == count
for create_event in events["CreateAddress"]:
deployed_counter = get_contract(
deployed_counter = await get_contract(
"PlainOpcodes", "Counter", address=create_event["_address"]
)
assert await deployed_counter.count() == 0
@@ -187,7 +187,7 @@ async def test_should_create_counter_and_call_in_the_same_tx(
receipt = (await plain_opcodes.createCounterAndCall())["receipt"]
events = plain_opcodes.events.parse_events(receipt)
address = events["CreateAddress"][0]["_address"]
counter = get_contract("PlainOpcodes", "Counter", address=address)
counter = await get_contract("PlainOpcodes", "Counter", address=address)
assert await counter.count() == 0

async def test_should_create_counter_and_invoke_in_the_same_tx(
@@ -196,14 +196,14 @@ async def test_should_create_counter_and_invoke_in_the_same_tx(
receipt = (await plain_opcodes.createCounterAndInvoke())["receipt"]
events = plain_opcodes.events.parse_events(receipt)
address = events["CreateAddress"][0]["_address"]
counter = get_contract("PlainOpcodes", "Counter", address=address)
counter = await get_contract("PlainOpcodes", "Counter", address=address)
assert await counter.count() == 1

class TestCreate2:
async def test_should_collision_after_selfdestruct_different_tx(
self, plain_opcodes, owner
):
contract_with_selfdestruct = get_contract(
contract_with_selfdestruct = await get_contract(
"PlainOpcodes", "ContractWithSelfdestructMethod"
)
salt = 12345
@@ -216,7 +216,7 @@ async def test_should_collision_after_selfdestruct_different_tx(
)["receipt"]
events = plain_opcodes.events.parse_events(receipt)
assert len(events["Create2Address"]) == 1
contract_with_selfdestruct = get_contract(
contract_with_selfdestruct = await get_contract(
"PlainOpcodes",
"ContractWithSelfdestructMethod",
address=events["Create2Address"][0]["_address"],
@@ -258,7 +258,7 @@ async def test_should_deploy_bytecode_at_address(
events = plain_opcodes.events.parse_events(receipt)
assert len(events["Create2Address"]) == 1

deployed_counter = get_contract(
deployed_counter = await get_contract(
"PlainOpcodes",
"Counter",
address=events["Create2Address"][0]["_address"],
@@ -297,7 +297,7 @@ async def test_should_revert_via_call(self, plain_opcodes, owner):
)
)["receipt"]

reverting_contract = get_contract(
reverting_contract = await get_contract(
"PlainOpcodes", "ContractRevertsOnMethodCall"
)

2 changes: 1 addition & 1 deletion tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ async def test_should_create_pair_only_once(self, factory, owner):
assert await factory.allPairs(0) == pair_evm_address
assert await factory.allPairsLength() == 1

pair = get_contract(
pair = await get_contract(
"UniswapV2",
"UniswapV2Pair",
address=pair_evm_address,
20 changes: 20 additions & 0 deletions tests/end_to_end/UniswapV3/test_univ3_rendering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest
import pytest_asyncio

from kakarot_scripts.utils.kakarot import deploy


@pytest_asyncio.fixture(scope="module")
async def univ3_position(owner):
return await deploy(
"UniswapV3",
"UniswapV3NFTManager",
caller_eoa=owner.starknet_contract,
)


@pytest.mark.asyncio(scope="session")
@pytest.mark.xfail(reason="Rendering the SVG takes too many steps")
class TestUniswapV3Rendering:
async def test_should_render_position(self, univ3_position):
await univ3_position.tokenURIExternal(1)
8 changes: 6 additions & 2 deletions tests/end_to_end/test_kakarot.py
Original file line number Diff line number Diff line change
@@ -174,7 +174,9 @@ class TestWriteAccountBytecode:
async def test_should_set_account_bytecode(self, new_eoa):
counter_artifacts = get_solidity_artifacts("PlainOpcodes", "Counter")
eoa = await new_eoa()
bytecode = list(bytes.fromhex(counter_artifacts["bytecode"][2:]))
bytecode = list(
bytes.fromhex(counter_artifacts["bytecode"]["object"][2:])
)

await invoke(
"kakarot", "write_account_bytecode", int(eoa.address, 16), bytecode
@@ -192,7 +194,9 @@ async def test_should_set_account_bytecode(self, new_eoa):
async def test_should_fail_not_owner(self, new_eoa, other):
counter_artifacts = get_solidity_artifacts("PlainOpcodes", "Counter")
eoa = await new_eoa()
bytecode = list(bytes.fromhex(counter_artifacts["bytecode"][2:]))
bytecode = list(
bytes.fromhex(counter_artifacts["bytecode"]["object"][2:])
)

tx_hash = await invoke(
"kakarot",
12 changes: 6 additions & 6 deletions tests/src/kakarot/test_kakarot.py
Original file line number Diff line number Diff line change
@@ -297,8 +297,8 @@ class TestEthCall:
"IAccount.is_valid_jumpdest",
lambda addr, data: [1],
)
def test_erc20_transfer(self, get_contract):
erc20 = get_contract("Solmate", "ERC20")
async def test_erc20_transfer(self, get_contract):
erc20 = await get_contract("Solmate", "ERC20")
amount = int(1e18)
initial_state = {
CONTRACT_ADDRESS: {
@@ -321,8 +321,8 @@ def test_erc20_transfer(self, get_contract):
"IAccount.is_valid_jumpdest",
lambda addr, data: [1],
)
def test_erc721_transfer(self, get_contract):
erc721 = get_contract("Solmate", "ERC721")
async def test_erc721_transfer(self, get_contract):
erc721 = await get_contract("Solmate", "ERC721")
token_id = 1337
initial_state = {
CONTRACT_ADDRESS: {
@@ -414,8 +414,8 @@ class TestLoopProfiling:
@pytest.mark.NoCI
@pytest.mark.parametrize("steps", [10, 50, 100, 200])
@SyscallHandler.patch("IAccount.is_valid_jumpdest", lambda addr, data: [1])
def test_loop_profiling(self, get_contract, steps):
plain_opcodes = get_contract("PlainOpcodes", "PlainOpcodes")
async def test_loop_profiling(self, get_contract, steps):
plain_opcodes = await get_contract("PlainOpcodes", "PlainOpcodes")
initial_state = {
CONTRACT_ADDRESS: {
"code": list(plain_opcodes.bytecode_runtime),
4 changes: 2 additions & 2 deletions tests/src/utils/test_utils.py
Original file line number Diff line number Diff line change
@@ -155,8 +155,8 @@ def test_should_unpack_felt_array_to_bytes32_array(cairo_run, data, expected):

class TestInitializeJumpdests:
@pytest.mark.slow
def test_should_return_same_as_execution_specs(self, cairo_run):
bytecode = get_contract("PlainOpcodes", "Counter").bytecode_runtime
async def test_should_return_same_as_execution_specs(self, cairo_run):
bytecode = (await get_contract("PlainOpcodes", "Counter")).bytecode_runtime
output = cairo_run("test__initialize_jumpdests", bytecode=bytecode)
assert set(output) == get_valid_jump_destinations(bytecode)

0 comments on commit a41fe3b

Please sign in to comment.