diff --git a/.gitmodules b/.gitmodules
index 5312e13ab..b3caf1fcb 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -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
diff --git a/kakarot_scripts/constants.py b/kakarot_scripts/constants.py
index 4223d3c60..3547a6703 100644
--- a/kakarot_scripts/constants.py
+++ b/kakarot_scripts/constants.py
@@ -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",
diff --git a/kakarot_scripts/utils/kakarot.py b/kakarot_scripts/utils/kakarot.py
index 6756b3170..b2179fc54 100644
--- a/kakarot_scripts/utils/kakarot.py
+++ b/kakarot_scripts/utils/kakarot.py
@@ -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(
diff --git a/kakarot_scripts/utils/l1.py b/kakarot_scripts/utils/l1.py
index 6aed79011..ea54fbea8 100644
--- a/kakarot_scripts/utils/l1.py
+++ b/kakarot_scripts/utils/l1.py
@@ -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:
diff --git a/solidity_contracts/lib/base64-sol b/solidity_contracts/lib/base64-sol
new file mode 160000
index 000000000..dcbf852ba
--- /dev/null
+++ b/solidity_contracts/lib/base64-sol
@@ -0,0 +1 @@
+Subproject commit dcbf852ba545b3d15de0ac0ef88dce934c090c8e
diff --git a/solidity_contracts/lib/openzeppelin b/solidity_contracts/lib/openzeppelin
new file mode 160000
index 000000000..8e0296096
--- /dev/null
+++ b/solidity_contracts/lib/openzeppelin
@@ -0,0 +1 @@
+Subproject commit 8e0296096449d9b1cd7c5631e917330635244c37
diff --git a/solidity_contracts/lib/v3-core b/solidity_contracts/lib/v3-core
new file mode 160000
index 000000000..e3589b192
--- /dev/null
+++ b/solidity_contracts/lib/v3-core
@@ -0,0 +1 @@
+Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899
diff --git a/solidity_contracts/src/UniswapV2Router/libraries/UniswapV2Library.sol b/solidity_contracts/src/UniswapV2Router/libraries/UniswapV2Library.sol
index 744fb6de1..b8b813c19 100644
--- a/solidity_contracts/src/UniswapV2Router/libraries/UniswapV2Library.sol
+++ b/solidity_contracts/src/UniswapV2Router/libraries/UniswapV2Library.sol
@@ -33,7 +33,7 @@ library UniswapV2Library {
hex"ff",
factory,
keccak256(abi.encodePacked(token0, token1)),
- hex"666a5b78ea0b660c426b08cb5b7427447e909408067de1a5519c772ee9a3c032" // init code hash
+ hex"0f5b822a8dffa6ce589a2c240d78a6a2b38a51835a97ceab40c1f301e46ba30b" // init code hash
)
)
)
diff --git a/solidity_contracts/src/UniswapV3/HexStrings.sol b/solidity_contracts/src/UniswapV3/HexStrings.sol
new file mode 100644
index 000000000..3ff683a29
--- /dev/null
+++ b/solidity_contracts/src/UniswapV3/HexStrings.sol
@@ -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);
+ }
+}
diff --git a/solidity_contracts/src/UniswapV3/NFTDescriptor.sol b/solidity_contracts/src/UniswapV3/NFTDescriptor.sol
new file mode 100644
index 000000000..f58628808
--- /dev/null
+++ b/solidity_contracts/src/UniswapV3/NFTDescriptor.sol
@@ -0,0 +1,462 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity >=0.7.0;
+pragma abicoder v2;
+
+import "v3-core/contracts/interfaces/IUniswapV3Pool.sol";
+import "v3-core/contracts/libraries/TickMath.sol";
+import "v3-core/contracts/libraries/BitMath.sol";
+import "v3-core/contracts/libraries/FullMath.sol";
+import "openzeppelin/utils/Strings.sol";
+import "openzeppelin/math/SafeMath.sol";
+import "openzeppelin/math/SignedSafeMath.sol";
+import "base64-sol/base64.sol";
+import "./HexStrings.sol";
+import "./NFTSVG.sol";
+
+library NFTDescriptor {
+ using TickMath for int24;
+ using Strings for uint256;
+ using SafeMath for uint256;
+ using SafeMath for uint160;
+ using SafeMath for uint8;
+ using SignedSafeMath for int256;
+ using HexStrings for uint256;
+
+ uint256 constant sqrt10X128 = 1076067327063303206878105757264492625226;
+
+ struct ConstructTokenURIParams {
+ uint256 tokenId;
+ address quoteTokenAddress;
+ address baseTokenAddress;
+ string quoteTokenSymbol;
+ string baseTokenSymbol;
+ uint8 quoteTokenDecimals;
+ uint8 baseTokenDecimals;
+ bool flipRatio;
+ int24 tickLower;
+ int24 tickUpper;
+ int24 tickCurrent;
+ int24 tickSpacing;
+ uint24 fee;
+ address poolAddress;
+ }
+
+ function constructTokenURI(ConstructTokenURIParams memory params) external view returns (string memory) {
+ string memory name =
+ string(abi.encodePacked("Uniswap V3 ", params.quoteTokenSymbol, "/", params.baseTokenSymbol));
+ string memory descriptionPartOne = generateDescriptionPartOne(
+ escapeQuotes(params.quoteTokenSymbol),
+ escapeQuotes(params.baseTokenSymbol),
+ addressToString(params.poolAddress)
+ );
+ string memory descriptionPartTwo = generateDescriptionPartTwo(
+ params.tokenId.toString(),
+ escapeQuotes(params.baseTokenSymbol),
+ addressToString(params.quoteTokenAddress),
+ addressToString(params.baseTokenAddress),
+ feeToPercentString(params.fee)
+ );
+ bytes memory tmp1 = bytes(generateSVGImage(params));
+ string memory image = Base64.encode(tmp1);
+
+ return string(
+ abi.encodePacked(
+ "data:application/json;base64,",
+ Base64.encode(
+ bytes(
+ abi.encodePacked(
+ '{"name":"',
+ name,
+ '", "description":"',
+ descriptionPartOne,
+ descriptionPartTwo,
+ '", "image": "',
+ "data:image/svg+xml;base64,",
+ // image,
+ '"}'
+ )
+ )
+ )
+ )
+ );
+ }
+
+ function escapeQuotes(string memory symbol) internal pure returns (string memory) {
+ bytes memory symbolBytes = bytes(symbol);
+ uint8 quotesCount = 0;
+ for (uint8 i = 0; i < symbolBytes.length; i++) {
+ if (symbolBytes[i] == '"') {
+ quotesCount++;
+ }
+ }
+ if (quotesCount > 0) {
+ bytes memory escapedBytes = new bytes(symbolBytes.length + (quotesCount));
+ uint256 index;
+ for (uint8 i = 0; i < symbolBytes.length; i++) {
+ if (symbolBytes[i] == '"') {
+ escapedBytes[index++] = "\\";
+ }
+ escapedBytes[index++] = symbolBytes[i];
+ }
+ return string(escapedBytes);
+ }
+ return symbol;
+ }
+
+ function generateDescriptionPartOne(
+ string memory quoteTokenSymbol,
+ string memory baseTokenSymbol,
+ string memory poolAddress
+ ) private pure returns (string memory) {
+ return string(
+ abi.encodePacked(
+ "This NFT represents a liquidity position in a Uniswap V3 ",
+ quoteTokenSymbol,
+ "-",
+ baseTokenSymbol,
+ " pool. ",
+ "The owner of this NFT can modify or redeem the position.\\n",
+ "\\nPool Address: ",
+ poolAddress,
+ "\\n",
+ quoteTokenSymbol
+ )
+ );
+ }
+
+ function generateDescriptionPartTwo(
+ string memory tokenId,
+ string memory baseTokenSymbol,
+ string memory quoteTokenAddress,
+ string memory baseTokenAddress,
+ string memory feeTier
+ ) private pure returns (string memory) {
+ return string(
+ abi.encodePacked(
+ " Address: ",
+ quoteTokenAddress,
+ "\\n",
+ baseTokenSymbol,
+ " Address: ",
+ baseTokenAddress,
+ "\\nFee Tier: ",
+ feeTier,
+ "\\nToken ID: ",
+ tokenId,
+ "\\n\\n",
+ unicode"⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure token addresses match the expected tokens, as token symbols may be imitated."
+ )
+ );
+ }
+
+ function generateName(ConstructTokenURIParams memory params, string memory feeTier)
+ private
+ pure
+ returns (string memory)
+ {
+ return string(
+ abi.encodePacked(
+ "Uniswap - ",
+ feeTier,
+ " - ",
+ escapeQuotes(params.quoteTokenSymbol),
+ "/",
+ escapeQuotes(params.baseTokenSymbol),
+ " - ",
+ tickToDecimalString(
+ !params.flipRatio ? params.tickLower : params.tickUpper,
+ params.tickSpacing,
+ params.baseTokenDecimals,
+ params.quoteTokenDecimals,
+ params.flipRatio
+ ),
+ "<>",
+ tickToDecimalString(
+ !params.flipRatio ? params.tickUpper : params.tickLower,
+ params.tickSpacing,
+ params.baseTokenDecimals,
+ params.quoteTokenDecimals,
+ params.flipRatio
+ )
+ )
+ );
+ }
+
+ struct DecimalStringParams {
+ // significant figures of decimal
+ uint256 sigfigs;
+ // length of decimal string
+ uint8 bufferLength;
+ // ending index for significant figures (function works backwards when copying sigfigs)
+ uint8 sigfigIndex;
+ // index of decimal place (0 if no decimal)
+ uint8 decimalIndex;
+ // start index for trailing/leading 0's for very small/large numbers
+ uint8 zerosStartIndex;
+ // end index for trailing/leading 0's for very small/large numbers
+ uint8 zerosEndIndex;
+ // true if decimal number is less than one
+ bool isLessThanOne;
+ // true if string should include "%"
+ bool isPercent;
+ }
+
+ function generateDecimalString(DecimalStringParams memory params) private pure returns (string memory) {
+ bytes memory buffer = new bytes(params.bufferLength);
+ if (params.isPercent) {
+ buffer[buffer.length - 1] = "%";
+ }
+ if (params.isLessThanOne) {
+ buffer[0] = "0";
+ buffer[1] = ".";
+ }
+
+ // add leading/trailing 0's
+ for (uint256 zerosCursor = params.zerosStartIndex; zerosCursor < params.zerosEndIndex.add(1); zerosCursor++) {
+ buffer[zerosCursor] = bytes1(uint8(48));
+ }
+ // add sigfigs
+ while (params.sigfigs > 0) {
+ if (params.decimalIndex > 0 && params.sigfigIndex == params.decimalIndex) {
+ buffer[params.sigfigIndex--] = ".";
+ }
+ buffer[params.sigfigIndex--] = bytes1(uint8(uint256(48).add(params.sigfigs % 10)));
+ params.sigfigs /= 10;
+ }
+ return string(buffer);
+ }
+
+ function tickToDecimalString(
+ int24 tick,
+ int24 tickSpacing,
+ uint8 baseTokenDecimals,
+ uint8 quoteTokenDecimals,
+ bool flipRatio
+ ) internal pure returns (string memory) {
+ if (tick == (TickMath.MIN_TICK / tickSpacing) * tickSpacing) {
+ return !flipRatio ? "MIN" : "MAX";
+ } else if (tick == (TickMath.MAX_TICK / tickSpacing) * tickSpacing) {
+ return !flipRatio ? "MAX" : "MIN";
+ } else {
+ uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);
+ if (flipRatio) {
+ sqrtRatioX96 = uint160(uint256(1 << 192).div(sqrtRatioX96));
+ }
+ return fixedPointToDecimalString(sqrtRatioX96, baseTokenDecimals, quoteTokenDecimals);
+ }
+ }
+
+ function sigfigsRounded(uint256 value, uint8 digits) private pure returns (uint256, bool) {
+ bool extraDigit;
+ if (digits > 5) {
+ value = value.div((10 ** (digits - 5)));
+ }
+ bool roundUp = value % 10 > 4;
+ value = value.div(10);
+ if (roundUp) {
+ value = value + 1;
+ }
+ // 99999 -> 100000 gives an extra sigfig
+ if (value == 100000) {
+ value /= 10;
+ extraDigit = true;
+ }
+ return (value, extraDigit);
+ }
+
+ function adjustForDecimalPrecision(uint160 sqrtRatioX96, uint8 baseTokenDecimals, uint8 quoteTokenDecimals)
+ private
+ pure
+ returns (uint256 adjustedSqrtRatioX96)
+ {
+ uint256 difference = abs(int256(baseTokenDecimals).sub(int256(quoteTokenDecimals)));
+ if (difference > 0 && difference <= 18) {
+ if (baseTokenDecimals > quoteTokenDecimals) {
+ adjustedSqrtRatioX96 = sqrtRatioX96.mul(10 ** (difference.div(2)));
+ if (difference % 2 == 1) {
+ adjustedSqrtRatioX96 = FullMath.mulDiv(adjustedSqrtRatioX96, sqrt10X128, 1 << 128);
+ }
+ } else {
+ adjustedSqrtRatioX96 = sqrtRatioX96.div(10 ** (difference.div(2)));
+ if (difference % 2 == 1) {
+ adjustedSqrtRatioX96 = FullMath.mulDiv(adjustedSqrtRatioX96, 1 << 128, sqrt10X128);
+ }
+ }
+ } else {
+ adjustedSqrtRatioX96 = uint256(sqrtRatioX96);
+ }
+ }
+
+ function abs(int256 x) private pure returns (uint256) {
+ return uint256(x >= 0 ? x : -x);
+ }
+
+ // @notice Returns string that includes first 5 significant figures of a decimal number
+ // @param sqrtRatioX96 a sqrt price
+ function fixedPointToDecimalString(uint160 sqrtRatioX96, uint8 baseTokenDecimals, uint8 quoteTokenDecimals)
+ internal
+ pure
+ returns (string memory)
+ {
+ uint256 adjustedSqrtRatioX96 = adjustForDecimalPrecision(sqrtRatioX96, baseTokenDecimals, quoteTokenDecimals);
+ uint256 value = FullMath.mulDiv(adjustedSqrtRatioX96, adjustedSqrtRatioX96, 1 << 64);
+
+ bool priceBelow1 = adjustedSqrtRatioX96 < 2 ** 96;
+ if (priceBelow1) {
+ // 10 ** 43 is precision needed to retrieve 5 sigfigs of smallest possible price + 1 for rounding
+ value = FullMath.mulDiv(value, 10 ** 44, 1 << 128);
+ } else {
+ // leave precision for 4 decimal places + 1 place for rounding
+ value = FullMath.mulDiv(value, 10 ** 5, 1 << 128);
+ }
+
+ // get digit count
+ uint256 temp = value;
+ uint8 digits;
+ while (temp != 0) {
+ digits++;
+ temp /= 10;
+ }
+ // don't count extra digit kept for rounding
+ digits = digits - 1;
+
+ // address rounding
+ (uint256 sigfigs, bool extraDigit) = sigfigsRounded(value, digits);
+ if (extraDigit) {
+ digits++;
+ }
+
+ DecimalStringParams memory params;
+ if (priceBelow1) {
+ // 7 bytes ( "0." and 5 sigfigs) + leading 0's bytes
+ params.bufferLength = uint8(uint8(7).add(uint8(43).sub(digits)));
+ params.zerosStartIndex = 2;
+ params.zerosEndIndex = uint8(uint256(43).sub(digits).add(1));
+ params.sigfigIndex = uint8(params.bufferLength.sub(1));
+ } else if (digits >= 9) {
+ // no decimal in price string
+ params.bufferLength = uint8(digits.sub(4));
+ params.zerosStartIndex = 5;
+ params.zerosEndIndex = uint8(params.bufferLength.sub(1));
+ params.sigfigIndex = 4;
+ } else {
+ // 5 sigfigs surround decimal
+ params.bufferLength = 6;
+ params.sigfigIndex = 5;
+ params.decimalIndex = uint8(digits.sub(5).add(1));
+ }
+ params.sigfigs = sigfigs;
+ params.isLessThanOne = priceBelow1;
+ params.isPercent = false;
+
+ return generateDecimalString(params);
+ }
+
+ // @notice Returns string as decimal percentage of fee amount.
+ // @param fee fee amount
+ function feeToPercentString(uint24 fee) internal pure returns (string memory) {
+ if (fee == 0) {
+ return "0%";
+ }
+ uint24 temp = fee;
+ uint256 digits;
+ uint8 numSigfigs;
+ while (temp != 0) {
+ if (numSigfigs > 0) {
+ // count all digits preceding least significant figure
+ numSigfigs++;
+ } else if (temp % 10 != 0) {
+ numSigfigs++;
+ }
+ digits++;
+ temp /= 10;
+ }
+
+ DecimalStringParams memory params;
+ uint256 nZeros;
+ if (digits >= 5) {
+ // if decimal > 1 (5th digit is the ones place)
+ uint256 decimalPlace = digits.sub(numSigfigs) >= 4 ? 0 : 1;
+ nZeros = digits.sub(5) < (numSigfigs.sub(1)) ? 0 : digits.sub(5).sub(numSigfigs.sub(1));
+ params.zerosStartIndex = numSigfigs;
+ params.zerosEndIndex = uint8(params.zerosStartIndex.add(nZeros).sub(1));
+ params.sigfigIndex = uint8(params.zerosStartIndex.sub(1).add(decimalPlace));
+ params.bufferLength = uint8(nZeros.add(numSigfigs.add(1)).add(decimalPlace));
+ } else {
+ // else if decimal < 1
+ nZeros = uint256(5).sub(digits);
+ params.zerosStartIndex = 2;
+ params.zerosEndIndex = uint8(nZeros.add(params.zerosStartIndex).sub(1));
+ params.bufferLength = uint8(nZeros.add(numSigfigs.add(2)));
+ params.sigfigIndex = uint8((params.bufferLength).sub(2));
+ params.isLessThanOne = true;
+ }
+ params.sigfigs = uint256(fee).div(10 ** (digits.sub(numSigfigs)));
+ params.isPercent = true;
+ params.decimalIndex = digits > 4 ? uint8(digits.sub(4)) : 0;
+
+ return generateDecimalString(params);
+ }
+
+ function addressToString(address addr) internal pure returns (string memory) {
+ return (uint256(addr)).toHexString(20);
+ }
+
+ function generateSVGImage(ConstructTokenURIParams memory params) internal pure returns (string memory svg) {
+ NFTSVG.SVGParams memory svgParams = NFTSVG.SVGParams({
+ quoteToken: addressToString(params.quoteTokenAddress),
+ baseToken: addressToString(params.baseTokenAddress),
+ poolAddress: params.poolAddress,
+ quoteTokenSymbol: params.quoteTokenSymbol,
+ baseTokenSymbol: params.baseTokenSymbol,
+ feeTier: feeToPercentString(params.fee),
+ tickLower: params.tickLower,
+ tickUpper: params.tickUpper,
+ tickSpacing: params.tickSpacing,
+ overRange: overRange(params.tickLower, params.tickUpper, params.tickCurrent),
+ tokenId: params.tokenId,
+ color0: tokenToColorHex(uint256(params.quoteTokenAddress), 136),
+ color1: tokenToColorHex(uint256(params.baseTokenAddress), 136),
+ color2: tokenToColorHex(uint256(params.quoteTokenAddress), 0),
+ color3: tokenToColorHex(uint256(params.baseTokenAddress), 0),
+ x1: scale(getCircleCoord(uint256(params.quoteTokenAddress), 16, params.tokenId), 0, 255, 16, 274),
+ y1: scale(getCircleCoord(uint256(params.baseTokenAddress), 16, params.tokenId), 0, 255, 100, 484),
+ x2: scale(getCircleCoord(uint256(params.quoteTokenAddress), 32, params.tokenId), 0, 255, 16, 274),
+ y2: scale(getCircleCoord(uint256(params.baseTokenAddress), 32, params.tokenId), 0, 255, 100, 484),
+ x3: scale(getCircleCoord(uint256(params.quoteTokenAddress), 48, params.tokenId), 0, 255, 16, 274),
+ y3: scale(getCircleCoord(uint256(params.baseTokenAddress), 48, params.tokenId), 0, 255, 100, 484)
+ });
+
+ return NFTSVG.generateSVG(svgParams);
+ }
+
+ function overRange(int24 tickLower, int24 tickUpper, int24 tickCurrent) private pure returns (int8) {
+ if (tickCurrent < tickLower) {
+ return -1;
+ } else if (tickCurrent > tickUpper) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ function scale(uint256 n, uint256 inMn, uint256 inMx, uint256 outMn, uint256 outMx)
+ private
+ pure
+ returns (string memory)
+ {
+ return (n.sub(inMn).mul(outMx.sub(outMn)).div(inMx.sub(inMn)).add(outMn)).toString();
+ }
+
+ function tokenToColorHex(uint256 token, uint256 offset) internal pure returns (string memory str) {
+ return string((token >> offset).toHexStringNoPrefix(3));
+ }
+
+ function getCircleCoord(uint256 tokenAddress, uint256 offset, uint256 tokenId) internal pure returns (uint256) {
+ return (sliceTokenHex(tokenAddress, offset) * tokenId) % 255;
+ }
+
+ function sliceTokenHex(uint256 token, uint256 offset) internal pure returns (uint256) {
+ return uint256(uint8(token >> offset));
+ }
+}
diff --git a/solidity_contracts/src/UniswapV3/NFTSVG.sol b/solidity_contracts/src/UniswapV3/NFTSVG.sol
new file mode 100644
index 000000000..e5de224e5
--- /dev/null
+++ b/solidity_contracts/src/UniswapV3/NFTSVG.sol
@@ -0,0 +1,395 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity >=0.7.6;
+
+import "openzeppelin/utils/Strings.sol";
+import "v3-core/contracts/libraries/BitMath.sol";
+import "base64-sol/base64.sol";
+
+/// @title NFTSVG
+/// @notice Provides a function for generating an SVG associated with a Uniswap NFT
+library NFTSVG {
+ using Strings for uint256;
+
+ string constant curve1 = "M1 1C41 41 105 105 145 145";
+ string constant curve2 = "M1 1C33 49 97 113 145 145";
+ string constant curve3 = "M1 1C33 57 89 113 145 145";
+ string constant curve4 = "M1 1C25 65 81 121 145 145";
+ string constant curve5 = "M1 1C17 73 73 129 145 145";
+ string constant curve6 = "M1 1C9 81 65 137 145 145";
+ string constant curve7 = "M1 1C1 89 57.5 145 145 145";
+ string constant curve8 = "M1 1C1 97 49 145 145 145";
+
+ struct SVGParams {
+ string quoteToken;
+ string baseToken;
+ address poolAddress;
+ string quoteTokenSymbol;
+ string baseTokenSymbol;
+ string feeTier;
+ int24 tickLower;
+ int24 tickUpper;
+ int24 tickSpacing;
+ int8 overRange;
+ uint256 tokenId;
+ string color0;
+ string color1;
+ string color2;
+ string color3;
+ string x1;
+ string y1;
+ string x2;
+ string y2;
+ string x3;
+ string y3;
+ }
+
+ function generateSVG(SVGParams memory params) internal pure returns (string memory svg) {
+ /*
+ address: "0xe8ab59d3bcde16a29912de83a90eb39628cfc163",
+ msg: "Forged in SVG for Uniswap in 2021 by 0xe8ab59d3bcde16a29912de83a90eb39628cfc163",
+ sig: "0x2df0e99d9cbfec33a705d83f75666d98b22dea7c1af412c584f7d626d83f02875993df740dc87563b9c73378f8462426da572d7989de88079a382ad96c57b68d1b",
+ version: "2"
+ */
+ return string(
+ abi.encodePacked(
+ generateSVGDefs(params),
+ generateSVGBorderText(
+ params.quoteToken, params.baseToken, params.quoteTokenSymbol, params.baseTokenSymbol
+ ),
+ generateSVGCardMantle(params.quoteTokenSymbol, params.baseTokenSymbol, params.feeTier),
+ generageSvgCurve(params.tickLower, params.tickUpper, params.tickSpacing, params.overRange),
+ generateSVGPositionDataAndLocationCurve(params.tokenId.toString(), params.tickLower, params.tickUpper),
+ generateSVGRareSparkle(params.tokenId, params.poolAddress),
+ ""
+ )
+ );
+ }
+
+ function generateSVGDefs(SVGParams memory params) private pure returns (string memory svg) {
+ svg = string(
+ abi.encodePacked(
+ '"
+ )
+ )
+ ),
+ '"/>"
+ )
+ )
+ ),
+ '"/>"
+ )
+ )
+ ),
+ '" />',
+ '"
+ )
+ )
+ ),
+ '" /> ',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ' ',
+ '',
+ '',
+ ''
+ )
+ );
+ }
+
+ function generateSVGBorderText(
+ string memory quoteToken,
+ string memory baseToken,
+ string memory quoteTokenSymbol,
+ string memory baseTokenSymbol
+ ) private pure returns (string memory svg) {
+ svg = string(
+ abi.encodePacked(
+ '',
+ '',
+ baseToken,
+ unicode" • ",
+ baseTokenSymbol,
+ ' ',
+ ' ',
+ baseToken,
+ unicode" • ",
+ baseTokenSymbol,
+ ' ',
+ '',
+ quoteToken,
+ unicode" • ",
+ quoteTokenSymbol,
+ ' ',
+ quoteToken,
+ unicode" • ",
+ quoteTokenSymbol,
+ ' '
+ )
+ );
+ }
+
+ function generateSVGCardMantle(string memory quoteTokenSymbol, string memory baseTokenSymbol, string memory feeTier)
+ private
+ pure
+ returns (string memory svg)
+ {
+ svg = string(
+ abi.encodePacked(
+ ' ',
+ quoteTokenSymbol,
+ "/",
+ baseTokenSymbol,
+ '',
+ feeTier,
+ "",
+ ''
+ )
+ );
+ }
+
+ function generageSvgCurve(int24 tickLower, int24 tickUpper, int24 tickSpacing, int8 overRange)
+ private
+ pure
+ returns (string memory svg)
+ {
+ string memory fade = overRange == 1 ? "#fade-up" : overRange == -1 ? "#fade-down" : "#none";
+ string memory curve = getCurve(tickLower, tickUpper, tickSpacing);
+ svg = string(
+ abi.encodePacked(
+ ''
+ '' '',
+ '',
+ '',
+ '',
+ generateSVGCurveCircle(overRange)
+ )
+ );
+ }
+
+ function getCurve(int24 tickLower, int24 tickUpper, int24 tickSpacing)
+ internal
+ pure
+ returns (string memory curve)
+ {
+ int24 tickRange = (tickUpper - tickLower) / tickSpacing;
+ if (tickRange <= 4) {
+ curve = curve1;
+ } else if (tickRange <= 8) {
+ curve = curve2;
+ } else if (tickRange <= 16) {
+ curve = curve3;
+ } else if (tickRange <= 32) {
+ curve = curve4;
+ } else if (tickRange <= 64) {
+ curve = curve5;
+ } else if (tickRange <= 128) {
+ curve = curve6;
+ } else if (tickRange <= 256) {
+ curve = curve7;
+ } else {
+ curve = curve8;
+ }
+ }
+
+ function generateSVGCurveCircle(int8 overRange) internal pure returns (string memory svg) {
+ string memory curvex1 = "73";
+ string memory curvey1 = "190";
+ string memory curvex2 = "217";
+ string memory curvey2 = "334";
+ if (overRange == 1 || overRange == -1) {
+ svg = string(
+ abi.encodePacked(
+ ''
+ )
+ );
+ } else {
+ svg = string(
+ abi.encodePacked(
+ '',
+ ''
+ )
+ );
+ }
+ }
+
+ function generateSVGPositionDataAndLocationCurve(string memory tokenId, int24 tickLower, int24 tickUpper)
+ private
+ pure
+ returns (string memory svg)
+ {
+ string memory tickLowerStr = tickToString(tickLower);
+ string memory tickUpperStr = tickToString(tickUpper);
+ uint256 str1length = bytes(tokenId).length + 4;
+ uint256 str2length = bytes(tickLowerStr).length + 10;
+ uint256 str3length = bytes(tickUpperStr).length + 10;
+ (string memory xCoord, string memory yCoord) = rangeLocation(tickLower, tickUpper);
+ svg = string(
+ abi.encodePacked(
+ ' ',
+ '',
+ 'ID: ',
+ tokenId,
+ "",
+ ' ',
+ '',
+ 'Min Tick: ',
+ tickLowerStr,
+ "",
+ ' ',
+ '',
+ 'Max Tick: ',
+ tickUpperStr,
+ "" '',
+ '',
+ '',
+ ''
+ )
+ );
+ }
+
+ function tickToString(int24 tick) private pure returns (string memory) {
+ string memory sign = "";
+ if (tick < 0) {
+ tick = tick * -1;
+ sign = "-";
+ }
+ return string(abi.encodePacked(sign, uint256(tick).toString()));
+ }
+
+ function rangeLocation(int24 tickLower, int24 tickUpper) internal pure returns (string memory, string memory) {
+ int24 midPoint = (tickLower + tickUpper) / 2;
+ if (midPoint < -125_000) {
+ return ("8", "7");
+ } else if (midPoint < -75_000) {
+ return ("8", "10.5");
+ } else if (midPoint < -25_000) {
+ return ("8", "14.25");
+ } else if (midPoint < -5_000) {
+ return ("10", "18");
+ } else if (midPoint < 0) {
+ return ("11", "21");
+ } else if (midPoint < 5_000) {
+ return ("13", "23");
+ } else if (midPoint < 25_000) {
+ return ("15", "25");
+ } else if (midPoint < 75_000) {
+ return ("18", "26");
+ } else if (midPoint < 125_000) {
+ return ("21", "27");
+ } else {
+ return ("24", "27");
+ }
+ }
+
+ function generateSVGRareSparkle(uint256 tokenId, address poolAddress) private pure returns (string memory svg) {
+ if (isRare(tokenId, poolAddress)) {
+ svg = string(
+ abi.encodePacked(
+ '',
+ '',
+ ''
+ )
+ );
+ } else {
+ svg = "";
+ }
+ }
+
+ function isRare(uint256 tokenId, address poolAddress) internal pure returns (bool) {
+ bytes32 h = keccak256(abi.encodePacked(tokenId, poolAddress));
+ return uint256(h) < type(uint256).max / (1 + BitMath.mostSignificantBit(tokenId) * 2);
+ }
+}
diff --git a/solidity_contracts/src/UniswapV3/UniswapV3NFTManager.sol b/solidity_contracts/src/UniswapV3/UniswapV3NFTManager.sol
new file mode 100644
index 000000000..1a3dd0a3e
--- /dev/null
+++ b/solidity_contracts/src/UniswapV3/UniswapV3NFTManager.sol
@@ -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);
+ }
+}
diff --git a/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py b/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py
index bc78500a4..5b019df2b 100644
--- a/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py
+++ b/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py
@@ -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"
)
diff --git a/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py b/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py
index eadb49a84..6bd61ac43 100644
--- a/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py
+++ b/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py
@@ -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,
diff --git a/tests/end_to_end/UniswapV3/test_univ3_rendering.py b/tests/end_to_end/UniswapV3/test_univ3_rendering.py
new file mode 100644
index 000000000..3075df7cf
--- /dev/null
+++ b/tests/end_to_end/UniswapV3/test_univ3_rendering.py
@@ -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)
diff --git a/tests/end_to_end/test_kakarot.py b/tests/end_to_end/test_kakarot.py
index 295f9bdfc..d3daf7e90 100644
--- a/tests/end_to_end/test_kakarot.py
+++ b/tests/end_to_end/test_kakarot.py
@@ -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",
diff --git a/tests/src/kakarot/test_kakarot.py b/tests/src/kakarot/test_kakarot.py
index 3c902063a..5487952e4 100644
--- a/tests/src/kakarot/test_kakarot.py
+++ b/tests/src/kakarot/test_kakarot.py
@@ -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),
diff --git a/tests/src/utils/test_utils.py b/tests/src/utils/test_utils.py
index 871a114db..d1bb4f3a3 100644
--- a/tests/src/utils/test_utils.py
+++ b/tests/src/utils/test_utils.py
@@ -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)