From 87e0a825cdc162b3f53778e9391ca8f318976495 Mon Sep 17 00:00:00 2001 From: Eugenio Paluello Date: Wed, 23 Oct 2024 15:56:18 +0200 Subject: [PATCH] feat: fee_balancer lambda --- .gitignore | 15 ++ aws_lambda/fee_balancer/Dockerfile | 29 ++++ aws_lambda/fee_balancer/app.py | 9 ++ aws_lambda/fee_balancer/cdk.json | 71 +++++++++ aws_lambda/fee_balancer/fee_balancer.py | 142 ++++++++++++++++++ .../fee_balancer/fee_balancer_lambda_stack.py | 108 +++++++++++++ aws_lambda/fee_balancer/pyproject.toml | 26 ++++ aws_lambda/fee_balancer/relayers.json | 92 ++++++++++++ 8 files changed, 492 insertions(+) create mode 100644 aws_lambda/fee_balancer/Dockerfile create mode 100644 aws_lambda/fee_balancer/app.py create mode 100644 aws_lambda/fee_balancer/cdk.json create mode 100644 aws_lambda/fee_balancer/fee_balancer.py create mode 100644 aws_lambda/fee_balancer/fee_balancer_lambda_stack.py create mode 100644 aws_lambda/fee_balancer/pyproject.toml create mode 100644 aws_lambda/fee_balancer/relayers.json diff --git a/.gitignore b/.gitignore index bcfb427e4..a6d8a152e 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,18 @@ cairo/kakarot-ssj/cache/ cairo/kakarot-ssj/scripts/libcairo_native_runtime.a __pycache__ *.snfoundry + +# aws_lambda +aws_lambda/fee_balancer/data +aws_lambda/fee_balancer/build +aws_lambda/fee_balancer/deployments +aws_lambda/fee_balancer/constants.py +aws_lambda/fee_balancer/starknet.py + +aws_lambda/relayers_observer/data +aws_lambda/relayers_observer/build +aws_lambda/relayers_observer/deployments +aws_lambda/relayers_observer/constants.py +aws_lambda/relayers_observer/starknet.py + +cdk.out diff --git a/aws_lambda/fee_balancer/Dockerfile b/aws_lambda/fee_balancer/Dockerfile new file mode 100644 index 000000000..6975c83b0 --- /dev/null +++ b/aws_lambda/fee_balancer/Dockerfile @@ -0,0 +1,29 @@ +# trunk-ignore-all(checkov/CKV_DOCKER_2) +# trunk-ignore-all(checkov/CKV_DOCKER_3) +# trunk-ignore-all(hadolint/DL3013) +# trunk-ignore-all(hadolint/DL3033) +FROM amazon/aws-lambda-python:3.10 + +RUN yum update -y && \ + yum install -y gcc gmp-devel && \ + yum clean all && \ + rm -rf /var/cache/yum + +WORKDIR /var/task + +RUN pip install --no-cache-dir uv && uv venv + +COPY pyproject.toml ./ + +RUN pip install --no-cache-dir -e '.[lambda-dependencies]' + +COPY build ./build +COPY deployments ./deployments +COPY relayers.json ./relayers.json +COPY .env ./ +COPY fee_balancer.py ./ +COPY constants.py ./kakarot_scripts/constants.py +COPY starknet.py ./kakarot_scripts/utils/starknet.py +COPY data ./kakarot_scripts/data + +CMD ["fee_balancer.lambda_handler"] diff --git a/aws_lambda/fee_balancer/app.py b/aws_lambda/fee_balancer/app.py new file mode 100644 index 000000000..1d823a738 --- /dev/null +++ b/aws_lambda/fee_balancer/app.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +import aws_cdk as cdk +from fee_balancer_lambda_stack import FeeBalancerLambdaStack + +app = cdk.App() +FeeBalancerLambdaStack(app, "FeeBalancerLambdaStack") + +app.synth() diff --git a/aws_lambda/fee_balancer/cdk.json b/aws_lambda/fee_balancer/cdk.json new file mode 100644 index 000000000..9d6f1e8cb --- /dev/null +++ b/aws_lambda/fee_balancer/cdk.json @@ -0,0 +1,71 @@ +{ + "app": "python3 app.py", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true + } +} diff --git a/aws_lambda/fee_balancer/fee_balancer.py b/aws_lambda/fee_balancer/fee_balancer.py new file mode 100644 index 000000000..84830a967 --- /dev/null +++ b/aws_lambda/fee_balancer/fee_balancer.py @@ -0,0 +1,142 @@ +import asyncio +import json +import logging +import os + +import boto3 +from web3 import Web3 + +from kakarot_scripts.utils.starknet import ( + get_balance, + get_contract, + get_eth_contract, + get_starknet_account, + wait_for_transaction, +) + +logger = logging.getLogger() +logger.setLevel("INFO") + +client = boto3.client("secretsmanager") +ssm = boto3.client("ssm") +node_url = os.getenv("NODE_URL") +web3 = Web3(Web3.HTTPProvider(node_url)) + +with open("coinbase_abi.json", "r") as f: + coinbase_abi = json.load(f) + +contract_address = os.getenv("COINBASE_CONTRACT_ADDRESS") + +# Create smart contract instance +contract = web3.eth.contract(address=contract_address, abi=coinbase_abi) + + +def lambda_handler(event, context): + return asyncio.get_event_loop().run_until_complete(check_and_fund_relayers()) + + +async def check_and_fund_relayers(): + # Load relayers information from JSON file + with open("relayers.json", "r") as f: + relayers = json.load(f) + + # Retrieve starknet secret from AWS Secrets Manager + response = client.get_secret_value(SecretId="relayers_fund_account") + secret_dict = json.loads(response["SecretString"]) + + starknet_address, starknet_private_key = next(iter(secret_dict.items())) + starknet_account = await get_starknet_account( + starknet_address, starknet_private_key + ) + eth_contract = await get_eth_contract(starknet_account) + + # Retrieve eth secret from AWS Secrets Manager + response = client.get_secret_value(SecretId="eth_coinbase_owner") + secret_dict = json.loads(response["SecretString"]) + + eth_address, eth_private_key = next(iter(secret_dict.items())) + nonce = web3.eth.get_transaction_count(eth_address) + + account_balance_before_withdraw = await get_balance( + starknet_account.starknet_address, eth_contract + ) + + # withdraw fees from coinbase contract + await withdraw_fee(starknet_address, nonce, eth_address, eth_private_key) + + account_balance = await get_balance(starknet_account.starknet_address, eth_contract) + relayers_total_balance = await get_total_balance_of_relayers(relayers, eth_contract) + + actual_fee = account_balance - account_balance_before_withdraw + + # get the prev balances + relayers_prev_total_balance = int(os.getenv("PREV_TOTAL_BALANCE")) + + # get the earning percentage + earning_percentage = int(os.getenv("EARNING_PERCENTAGE")) + + cairo_counter = get_contract("Kakarot") + + yield cairo_counter + + base_fee = await cairo_counter.functions["get_base_fee"].call() + logger.info(f"Base fee: {base_fee}") + changed_fee = base_fee + + # check if the relayers balance is less than the prev balance + if relayers_prev_total_balance + actual_fee < relayers_total_balance: + # increase the base fee of 12.5% + changed_fee += base_fee * 0.125 + # check if the relayers balance is more than the prev balance + the acceptable earning percentage + elif relayers_prev_total_balance + actual_fee > relayers_total_balance + ( + earning_percentage * actual_fee / 100 + ): + # decrease the base fee of 12.5% + changed_fee -= base_fee * 0.125 + else: + logger.info("No changes to the base fee") + + if changed_fee != base_fee: + tx = await cairo_counter.functions["set_base_fee"].invoke_v1(base_fee) + await wait_for_transaction(tx.hash) + + return { + "statusCode": 200, + } + + +async def get_total_balance_of_relayers(relayers, eth_contract, block_number): + total_balance = 0 + + for relayer in relayers: + account_balance = await get_balance( + relayer["address"], eth_contract, block_number + ) + total_balance += account_balance + + return total_balance + + +async def withdraw_fee(starknet_address, nonce, eth_address, eth_private_key): + Chain_id = web3.eth.chain_id + + # Call your function + call_function = contract.functions.withdraw( + toStarknetAddress=starknet_address + ).build_transaction({"chainId": Chain_id, "from": eth_address, "nonce": nonce}) + + # Sign transaction + signed_tx = web3.eth.account.sign_transaction( + call_function, private_key=eth_private_key + ) + + # Send transaction + send_tx = web3.eth.send_raw_transaction(signed_tx.raw_transaction) + + # Wait for transaction receipt + tx_receipt = web3.eth.wait_for_transaction_receipt(send_tx) + logger.info(tx_receipt) + + +if __name__ == "__main__": + asyncio.run(check_and_fund_relayers()) diff --git a/aws_lambda/fee_balancer/fee_balancer_lambda_stack.py b/aws_lambda/fee_balancer/fee_balancer_lambda_stack.py new file mode 100644 index 000000000..744765416 --- /dev/null +++ b/aws_lambda/fee_balancer/fee_balancer_lambda_stack.py @@ -0,0 +1,108 @@ +import os +import shutil + +from aws_cdk import Duration, Stack +from aws_cdk import aws_events as events +from aws_cdk import aws_events_targets as targets +from aws_cdk import aws_iam as iam +from aws_cdk import aws_lambda as _lambda +from constructs import Construct + + +class FeeBalancerLambdaStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + self.build_lambda_func() + + def build_lambda_func(self): + node_url = os.environ.get("NODE_URL") + if node_url is None: + raise ValueError("NODE_URL environment variable is not set") + + coinbase_contract_address = os.environ.get("COINBASE_CONTRACT_ADDRESS") + if coinbase_contract_address is None: + raise ValueError( + "COINBASE_CONTRACT_ADDRESS environment variable is not set" + ) + + prev_total_balance = os.environ.get("PREV_TOTAL_BALANCE") + if prev_total_balance is None: + raise ValueError("PREV_TOTAL_BALANCE environment variable is not set") + + earning_percentage = os.environ.get("EARNING_PERCENTAGE") + if earning_percentage is None: + raise ValueError("EARNING_PERCENTAGE environment variable is not set") + + dest_dir = "./" + shutil.copy( + "../../kakarot_scripts/constants.py", os.path.join(dest_dir, "constants.py") + ) + shutil.copy( + "../../kakarot_scripts/utils/starknet.py", + os.path.join(dest_dir, "starknet.py"), + ) + shutil.copy("../../.env", os.path.join(dest_dir, ".env")) + shutil.copytree( + "../../build", + os.path.join(dest_dir, "build"), + dirs_exist_ok=True, + ) + shutil.copytree( + "../../deployments", + os.path.join(dest_dir, "deployments"), + dirs_exist_ok=True, + ) + shutil.copytree( + "../../kakarot_scripts/data", + os.path.join(dest_dir, "data"), + dirs_exist_ok=True, + ) + self.prediction_lambda = _lambda.DockerImageFunction( + scope=self, + id="fee_balancer_lambda", + function_name="fee_balancer", + code=_lambda.DockerImageCode.from_image_asset(directory="."), + environment={ + "NODE_URL": node_url, + "COINBASE_CONTRACT_ADDRESS": coinbase_contract_address, + "PREV_TOTAL_BALANCE": prev_total_balance, + "EARNING_PERCENTAGE": earning_percentage, + }, + environment_encryption=None, + timeout=Duration.minutes(1), + ) + + secret_arns = [ + os.environ.get("RELAYERS_FUND_ACCOUNT_SECRET_ARN"), + os.environ.get("ETH_COINBASE_OWNER_SECRET_ARN"), + ] + secret_arns = [arn for arn in secret_arns if arn is not None] + + self.prediction_lambda.add_to_role_policy( + iam.PolicyStatement( + actions=["secretsmanager:GetSecretValue"], + resources=secret_arns, + ) + ) + + # param_arns = [ + # os.environ.get("RELAYERS_PREV_TOTAL_BALANCE_PARAM_ARN"), + # os.environ.get("ACCOUNT_PREV_BALANCE_PARAM_ARN"), + # os.environ.get("EARNING_PERCENTAGE_PARAM_ARN"), + # ] + # param_arns = [arn for arn in param_arns if arn is not None] + + # self.prediction_lambda.add_to_role_policy( + # iam.PolicyStatement( + # actions=["ssm:GetParameter", "ssm:PutParameter"], + # resources=param_arns, + # ) + # ) + + events.Rule( + self, + "ScheduleRule", + schedule=events.Schedule.cron(minute="0/20"), + targets=[targets.LambdaFunction(self.prediction_lambda)], + ) diff --git a/aws_lambda/fee_balancer/pyproject.toml b/aws_lambda/fee_balancer/pyproject.toml new file mode 100644 index 000000000..4bd7d88f0 --- /dev/null +++ b/aws_lambda/fee_balancer/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "fee_balancer" +version = "0.1.0" +description = "Fee Balancer Lambda function" +requires-python = ">=3.10,<3.11" + +[project.optional-dependencies] +lambda-dependencies = [ + "starknet-py==0.23.0", + "python-dotenv==0.21.0", + "web3==6", + "async_lru==2.0.4", + "cairo-lang==0.13.1", + "requests==2.32.3", + "eth_keys==0.5.1", + "boto3==1.35.36", +] + +cdk-dependencies = ["aws-cdk-lib==2.161.1", "constructs>=10.0.0,<11.0.0"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = ["fee_balancer.py", "fee_balancer_lambda_stack.py", "app.py"] diff --git a/aws_lambda/fee_balancer/relayers.json b/aws_lambda/fee_balancer/relayers.json new file mode 100644 index 000000000..878784a14 --- /dev/null +++ b/aws_lambda/fee_balancer/relayers.json @@ -0,0 +1,92 @@ +[ + { + "address": 950135147883551002471284612127692562371997031991686973344811456814871551703 + }, + { + "address": 784553024261379269037061621041983249972520011546534893666375886810285652022 + }, + { + "address": 2016715077620147159803572307858325398159758228128803672826672562207191315116 + }, + { + "address": 1662920711526725350032789559696699552222118181660561466963176525053308263625 + }, + { + "address": 3153637514465957585881265164805986785841191541018593438889626498255327177411 + }, + { + "address": 2766997136495195637890235109180516317074792508593436805931502677369166475499 + }, + { + "address": 2093590491756438567971438593464470359400203878063146755934968664136245299457 + }, + { + "address": 1156309487454888498236027337307355210771977936939547064564822895690729329261 + }, + { + "address": 2375508979001195339092705976533981949661341069936882436634727685307875319193 + }, + { + "address": 2794561348319831960405086417566258806031170000859851643881352304467811247177 + }, + { + "address": 3196938585199069588552778059786084432204759834046564491402522960867285987487 + }, + { + "address": 3249849696581527057698578089564361163387585517848746353802934812359434293529 + }, + { + "address": 1707748983747411678822551525712719303335815970545789299210384474650319917052 + }, + { + "address": 871964783514378596548163446256746702153855963543252144050276816814875674558 + }, + { + "address": 447594387324528764199053850392655034838752348231600003608193346877723837852 + }, + { + "address": 249732155503971925348549527968576754852836046059112668466387462006740968212 + }, + { + "address": 3446662058793631682698091747697837875337565532441852226591023978462097914409 + }, + { + "address": 2431027466263453465882296491914298235363836672287741575367285923951389377303 + }, + { + "address": 2667345079579245645829382805366852599258219482721527050573445324850349248720 + }, + { + "address": 1912701013260142734206290515668606917117159045812693977111553625230013542785 + }, + { + "address": 2448460999699100678054857534135769814992359258605688440449958764415834140201 + }, + { + "address": 2506017580705275985007775372164438810134467385003111950692305383782396861109 + }, + { + "address": 2467262930860607918431477448588678974558152179771189301003734902904667082699 + }, + { + "address": 238052413982080525062062834169368364942253420244711858791548911875984508405 + }, + { + "address": 1377987273889472543754659361551378469821977441767344463704291075020954302330 + }, + { + "address": 2096011801106804190494834046291183630742307755777720881520190404439626060392 + }, + { + "address": 2526397583797409088576194203532356382647146866713022053321340510362096170426 + }, + { + "address": 408885905545087652195361774298355147012912093936939629684581725287151251217 + }, + { + "address": 2036100759087474495109507324801202976882523277765123480505872484555089939206 + }, + { + "address": 1646975286751587104385756489415815854141667265954694456915839184663717755926 + } +]