Skip to content

Commit

Permalink
tests: e2e eip 3074 tests (#1129)
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:

## 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 #1127 Resolves #1125 Closes #1128

## 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/1129)
<!-- Reviewable:end -->
  • Loading branch information
enitrat authored Apr 26, 2024
1 parent cd0ed76 commit 0e24f82
Show file tree
Hide file tree
Showing 18 changed files with 233 additions and 47 deletions.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ artifacts
build

#foundry
/cache
/out
out
cache
/broadcast

.DS_Store
Expand All @@ -33,3 +33,5 @@ logs

tests/ef_tests/test_data
!tests/ef_tests/test_data/.gitkeep

bin/
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ check-resources:
build-sol:
git submodule update --init --recursive
forge build --names --force
$(MAKE) build-sol-experimental

build-sol-experimental:
docker run --rm \
-v $$(pwd):/app/foundry \
-u $$(id -u):$$(id -g) \
ghcr.io/paradigmxyz/foundry-alphanet@sha256:64ac81c19b910e766ce750499a2c9de064dce4fa9c4fc1e42368fdd73fc48dde \
--foundry-directory /app/foundry/experimental_contracts \
--foundry-command build



install-katana:
cargo install --git https://github.com/dojoengine/dojo --locked --tag "${KATANA_VERSION}" katana
Expand Down
9 changes: 9 additions & 0 deletions experimental_contracts/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[profile.default]
src = 'src'
test = 'tests'
out = '../solidity_contracts/build'
libs = ['../lib']

[rpc_endpoints]
anvil = "http://127.0.0.1:8545"
kakarot = "http://127.0.0.1:3030"
53 changes: 53 additions & 0 deletions experimental_contracts/src/EIP3074/BaseAuth.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

/// @title BaseAuth
/// @author Anna Carroll <https://github.com/anna-carroll/3074>
abstract contract BaseAuth {
/// @notice magic byte to disambiguate EIP-3074 signature payloads
uint8 constant MAGIC = 0x04;

/// @notice produce a digest for the authorizer to sign
/// @param commit - any 32-byte value used to commit to transaction validity conditions
/// @param nonce - signer's current nonce
/// @return digest - sign the `digest` to authorize the invoker to execute the `calls`
/// @dev signing `digest` authorizes this contact to execute code on behalf of the signer
/// the logic of the inheriting contract should encode rules which respect the information within `commit`
/// @dev the authorizer includes `commit` in their signature to ensure the authorized contract will only execute intended actions(s).
/// the Invoker logic MUST implement constraints on the contract execution based on information in the `commit`;
/// otherwise, any EOA that signs an AUTH for the Invoker will be compromised
/// @dev per EIP-3074, digest = keccak256(MAGIC || paddedChainId || paddedNonce || paddedInvokerAddress || commit)
function getDigest(bytes32 commit, uint256 nonce) public view returns (bytes32 digest) {
digest =
keccak256(abi.encodePacked(MAGIC, bytes32(block.chainid), bytes32(nonce), bytes32(uint256(uint160(address(this)))), commit));
}

function authSimple(address authority, bytes32 commit, uint8 v, bytes32 r, bytes32 s)
internal
pure
returns (bool success)
{
bytes memory authArgs = abi.encodePacked(yParity(v), r, s, commit);
assembly {
success := auth(authority, add(authArgs, 0x20), mload(authArgs))
}
}

function authCallSimple(address to, bytes memory data, uint256 value, uint256 gasLimit)
internal
returns (bool success)
{
assembly {
success := authcall(gasLimit, to, value, add(data, 0x20), mload(data), 0, 0)
}
}

/// @dev Internal helper to convert `v` to `yParity` for `AUTH`
function yParity(uint8 v) private pure returns (uint8 yParity_) {
assembly {
switch lt(v, 35)
case true { yParity_ := eq(v, 28) }
default { yParity_ := mod(sub(v, 35), 2) }
}
}
}
35 changes: 35 additions & 0 deletions experimental_contracts/src/EIP3074/GasSponsorInvoker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.23;

import { BaseAuth } from "./BaseAuth.sol";

/// @title Gas Sponsor Invoker
/// @notice Invoker contract using EIP-3074 to sponsor gas for authorized transactions
contract GasSponsorInvoker is BaseAuth {
/// @notice Executes a call authorized by an external account (EOA)
/// @param authority The address of the authorizing external account
/// @param commit A 32-byte value committing to transaction validity conditions
/// @param v The recovery byte of the signature
/// @param r Half of the ECDSA signature pair
/// @param s Half of the ECDSA signature pair
/// @param to The target contract address to call
/// @param data The data payload for the call
/// @return success True if the call was successful
function sponsorCall(
address authority,
bytes32 commit,
uint8 v,
bytes32 r,
bytes32 s,
address to,
bytes calldata data
) external returns (bool success) {
// Ensure the transaction is authorized by the signer
require(authSimple(authority, commit, v, r, s), "Authorization failed");

// Execute the call as authorized by the signer
success = authCallSimple(to, data, 0, 0);
require(success, "Call execution failed");
}
}
15 changes: 15 additions & 0 deletions experimental_contracts/src/EIP3074/SenderRecorder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.23;

contract SenderRecorder {
address public lastSender;

function recordSender() external {
lastSender = msg.sender;
}

function reset() external {
lastSender = address(0);
}
}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ markers = [
"SolmateERC721",
"UniswapV2ERC20",
"UniswapV2Factory",
"EIP3074",
"AccountContract",
"Utils",
"Safe",
Expand Down
12 changes: 10 additions & 2 deletions src/kakarot/instructions/system_operations.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ from starkware.cairo.common.cairo_secp.ec_point import EcPoint
from starkware.cairo.common.cairo_secp.bigint import uint256_to_bigint
from starkware.cairo.common.cairo_secp.bigint3 import BigInt3
from starkware.cairo.common.registers import get_fp_and_pc
from starkware.cairo.common.uint256 import Uint256, uint256_lt, uint256_le
from starkware.cairo.common.uint256 import Uint256, uint256_lt, uint256_le, uint256_eq
from starkware.cairo.common.default_dict import default_dict_new
from starkware.cairo.common.dict_access import DictAccess

Expand Down Expand Up @@ -1035,7 +1035,15 @@ namespace SystemOperations {
tempvar extra_gas = access_gas_cost + create_gas_cost + transfer_gas_cost;
let evm = EVM.charge_gas(evm, extra_gas + memory_expansion.cost);

let gas = Gas.compute_message_call_gas(gas_param, evm.gas_left);
let (is_gas_zero) = uint256_eq(gas_param, Uint256(0, 0));
if (is_gas_zero != FALSE) {
let (high, low) = split_felt(evm.gas_left);
tempvar gas_limit = Uint256(low, high);
} else {
tempvar gas_limit = gas_param;
}
let gas_limit = Uint256([ap - 2], [ap - 1]);
let gas = Gas.compute_message_call_gas(gas_limit, evm.gas_left);

// Charge the fixed message call gas
let evm = EVM.charge_gas(evm, gas);
Expand Down
19 changes: 19 additions & 0 deletions tests/end_to_end/EIP3074/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest_asyncio


@pytest_asyncio.fixture(scope="package")
async def gas_sponsor_invoker(deploy_contract, owner):
return await deploy_contract(
"EIP3074",
"GasSponsorInvoker",
caller_eoa=owner.starknet_contract,
)


@pytest_asyncio.fixture(scope="package")
async def sender_recorder(deploy_contract, owner):
return await deploy_contract(
"EIP3074",
"SenderRecorder",
caller_eoa=owner.starknet_contract,
)
39 changes: 39 additions & 0 deletions tests/end_to_end/EIP3074/test_eip_3074.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest
from eth_utils import keccak

from tests.utils.helpers import ec_sign


@pytest.fixture(scope="module")
def commit():
return keccak(b"Some unique commit data")


@pytest.fixture(autouse=True)
async def cleanup(sender_recorder):
yield
await sender_recorder.reset()


@pytest.mark.asyncio(scope="package")
@pytest.mark.EIP3074
class TestEIP3074:
class TestEIP3074Integration:
async def test_should_execute_authorized_call(
self, gas_sponsor_invoker, sender_recorder, other, commit
):
initial_sender = await sender_recorder.lastSender()
assert int(initial_sender, 16) == 0
signer_nonce = await other.starknet_contract.get_nonce()
digest = await gas_sponsor_invoker.getDigest(commit, signer_nonce)
v, r_, s_ = ec_sign(digest, other.private_key)

calldata = sender_recorder.get_function_by_name(
"recordSender"
)()._encode_transaction_data()

await gas_sponsor_invoker.sponsorCall(
other.address, commit, v, r_, s_, sender_recorder.address, calldata
)
last_sender = await sender_recorder.lastSender()
assert last_sender == other.address
16 changes: 8 additions & 8 deletions tests/end_to_end/PlainOpcodes/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@


@pytest_asyncio.fixture(scope="package")
async def counter(deploy_solidity_contract, owner):
return await deploy_solidity_contract(
async def counter(deploy_contract, owner):
return await deploy_contract(
"PlainOpcodes",
"Counter",
caller_eoa=owner.starknet_contract,
)


@pytest_asyncio.fixture(scope="package")
async def caller(deploy_solidity_contract, owner):
return await deploy_solidity_contract(
async def caller(deploy_contract, owner):
return await deploy_contract(
"PlainOpcodes",
"Caller",
caller_eoa=owner.starknet_contract,
)


@pytest_asyncio.fixture(scope="package")
async def plain_opcodes(deploy_solidity_contract, counter, owner):
return await deploy_solidity_contract(
async def plain_opcodes(deploy_contract, counter, owner):
return await deploy_contract(
"PlainOpcodes",
"PlainOpcodes",
counter.address,
Expand All @@ -30,8 +30,8 @@ async def plain_opcodes(deploy_solidity_contract, counter, owner):


@pytest_asyncio.fixture(scope="package")
async def revert_on_fallbacks(deploy_solidity_contract, owner):
return await deploy_solidity_contract(
async def revert_on_fallbacks(deploy_contract, owner):
return await deploy_contract(
"PlainOpcodes",
"ContractRevertOnFallbackAndReceive",
caller_eoa=owner.starknet_contract,
Expand Down
12 changes: 4 additions & 8 deletions tests/end_to_end/PlainOpcodes/test_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
@pytest.mark.Counter
class TestCounter:
class TestCount:
async def test_should_return_0_after_deployment(
self, deploy_solidity_contract, owner
):
counter = await deploy_solidity_contract(
async def test_should_return_0_after_deployment(self, deploy_contract, owner):
counter = await deploy_contract(
"PlainOpcodes",
"Counter",
caller_eoa=owner.starknet_contract,
Expand Down Expand Up @@ -64,11 +62,9 @@ async def test_should_set_count_to_0(self, counter):
assert await counter.count() == 0

class TestDeploymentWithValue:
async def test_deployment_with_value_should_fail(
self, deploy_solidity_contract
):
async def test_deployment_with_value_should_fail(self, deploy_contract):
with evm_error():
await deploy_solidity_contract("PlainOpcodes", "Counter", value=1)
await deploy_contract("PlainOpcodes", "Counter", value=1)

class TestLoops:
@pytest.mark.parametrize("iterations", [0, 50, 100])
Expand Down
10 changes: 4 additions & 6 deletions tests/end_to_end/PlainOpcodes/test_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@


@pytest_asyncio.fixture(scope="package")
async def safe(deploy_solidity_contract, owner):
return await deploy_solidity_contract(
async def safe(deploy_contract, owner):
return await deploy_contract(
"PlainOpcodes", "Safe", caller_eoa=owner.starknet_contract
)

Expand Down Expand Up @@ -58,10 +58,8 @@ async def test_should_withdraw_call_eth(self, safe, owner, eth_balance_of):
assert owner_balance_after - owner_balance_before + gas_used == safe_balance

class TestDeploySafeWithValue:
async def test_deploy_safe_with_value(
self, safe, deploy_solidity_contract, owner
):
safe = await deploy_solidity_contract(
async def test_deploy_safe_with_value(self, safe, deploy_contract, owner):
safe = await deploy_contract(
"PlainOpcodes",
"Safe",
caller_eoa=owner.starknet_contract,
Expand Down
4 changes: 2 additions & 2 deletions tests/end_to_end/Solmate/test_erc20.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@


@pytest_asyncio.fixture(scope="module")
async def erc_20(deploy_solidity_contract, owner):
return await deploy_solidity_contract(
async def erc_20(deploy_contract, owner):
return await deploy_contract(
"Solmate",
"ERC20",
"Kakarot Token",
Expand Down
Loading

0 comments on commit 0e24f82

Please sign in to comment.