From e805b3c1bbcf31a21210a797b6fa59a791db7066 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:36:22 +0100 Subject: [PATCH 1/8] chore: update allowed tokens --- smart-contract-infra/test/utils/Constants.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/smart-contract-infra/test/utils/Constants.sol b/smart-contract-infra/test/utils/Constants.sol index 7737faa..7d3aa61 100644 --- a/smart-contract-infra/test/utils/Constants.sol +++ b/smart-contract-infra/test/utils/Constants.sol @@ -9,8 +9,5 @@ contract Constants { address internal constant COW = 0x177127622c4A00F3d409B75571e12cB3c8973d3c; /// @notice Gnosis Chain WETH address - address internal constant WETH = 0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1; - - /// @notice Gnosis Chain SAFE address - address internal constant SAFE = 0x5aFE3855358E112B5647B952709E6165e1c1eEEe; + address internal constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d; } From 839de460eabbc5d1ccd54782d5daf0b5cb028847 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:36:57 +0100 Subject: [PATCH 2/8] fix: module bug & deployed contracts updated --- .../deployments/contracts.json | 15 ++++++--- smart-contract-infra/src/TradingModule.sol | 33 ++++++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/smart-contract-infra/deployments/contracts.json b/smart-contract-infra/deployments/contracts.json index 22fe17d..94fb230 100644 --- a/smart-contract-infra/deployments/contracts.json +++ b/smart-contract-infra/deployments/contracts.json @@ -1,5 +1,12 @@ { - "deployedChains": [1, 100, 8453, 31337, 42161, 11155111], + "deployedChains": [ + 1, + 100, + 8453, + 31337, + 42161, + 11155111 + ], "chains": { "1": { "allowlist": "0x0000000000000000000000000000000000000000", @@ -10,8 +17,8 @@ "100": { "allowlist": "0x98a4351d926e6274829c3807f39D9a7037462589", "guard": "0xcab68170145d593F15BF398670876CcCBFe173e2", - "tradingModuleMasterCopy": "0xE0e75802Fb63B1a3b55862D9e217f902bd4e33f8", - "tradingModuleProxy": "0x920b3D833E5663CF700B17DcF119Ac697E340621" + "tradingModuleMasterCopy": "0xDdd11DC80A25563Cb416647821f3BC5Ad75fF1BA", + "tradingModuleProxy": "0xF11bC1ff8Ab8Cc297e5a1f1A51B8d1792E99D648" }, "8453": { "allowlist": "0x0000000000000000000000000000000000000000", @@ -38,4 +45,4 @@ "tradingModuleProxy": "0x0000000000000000000000000000000000000000" } } -} +} \ No newline at end of file diff --git a/smart-contract-infra/src/TradingModule.sol b/smart-contract-infra/src/TradingModule.sol index fd9a10c..d8f1872 100644 --- a/smart-contract-infra/src/TradingModule.sol +++ b/smart-contract-infra/src/TradingModule.sol @@ -22,12 +22,18 @@ contract TradingModule is Module, Guardable { /// @notice Thrown when the transaction cannot execute error CannotExec(); + /// @notice Thrown when the trader address is the zero address + error ZeroAddress(); + + /// @notice Thrown when a trader is not allowed to set orders on behalf of the target + error InvalidTrader(); + /// @notice GPv2Settlement address /// @dev Deterministically deployed address public constant GPV2_SETTLEMENT_ADDRESS = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; - /// @notice CoW Swap Guard contract address - address public constant COW_SWAP_GUARD = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; // placeholder + /// @notice Allowed trader addresses to place orders on behalf of the Safe + mapping(address trader => bool allowed) internal allowedTraders; /// @notice GPv2Settlement domain separator bytes32 internal domainSeparator; @@ -50,7 +56,25 @@ contract TradingModule is Module, Guardable { transferOwnership(_owner); } + /// @notice Sets the allowed trader address that can set orders on behalf of the target Safe + /// @dev Only the owner can call this function + /// @param trader The trader address to set + /// @param allowed Allowed boolean + function setAllowedTraders(address trader, bool allowed) external onlyOwner { + require(trader != address(0), ZeroAddress()); + allowedTraders[trader] = allowed; + } + + /// @notice A trader can set the tradeability of an off-chain pre-signed CoW Swap order + /// @dev The following checks are made: + /// - orderUid is validated against the supplied order details + /// - trading on CoW Swap (calling GPv2Settlement.setPreSignature) + /// - buy and sell tokens are on the token allowlist + /// @param orderUid The orderUid obtained from the order book api + /// @param order The order details sent to the order book api + /// @param signed Whether the order should be tradeable function setOrder(bytes memory orderUid, GPv2Order.Data memory order, bool signed) external { + require(allowedTraders[msg.sender] == true, InvalidTrader()); bytes memory uid = new bytes(GPv2Order.UID_LENGTH); uid.packOrderUidParams(GPv2Order.hash(order, domainSeparator), owner(), order.validTo); require(keccak256(orderUid) == keccak256(uid), InvalidOrderUID()); @@ -63,6 +87,7 @@ contract TradingModule is Module, Guardable { emit SetOrder(orderUid, signed); } + /// @notice Executes the transaction from module with the guard checks function exec( address to, uint256 value, @@ -73,9 +98,7 @@ contract TradingModule is Module, Guardable { override returns (bool) { - IGuard(COW_SWAP_GUARD).checkTransaction( - to, value, data, operation, 0, 0, 0, address(0), payable(0), "", msg.sender - ); + IGuard(guard).checkTransaction(to, value, data, operation, 0, 0, 0, address(0), payable(0), "", msg.sender); (bytes memory txData,,) = abi.decode(data, (bytes, address, address)); From 53e02640cede5a50d0c958762cb76dbcd3723347 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:37:32 +0100 Subject: [PATCH 3/8] chore: add account alias for tx signing --- cow-trader/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cow-trader/README.md b/cow-trader/README.md index eae2c78..c03d8d2 100644 --- a/cow-trader/README.md +++ b/cow-trader/README.md @@ -5,5 +5,5 @@ Automated CoW Swap trading agent built with Silverback SDK ## Run ```bash -silverback run --network gnosis:mainnet:alchemy +silverback run --network gnosis:mainnet:alchemy --account cow-agent ``` From f4b068a254e797bb169a41c6030b4bf5782431f6 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:37:45 +0100 Subject: [PATCH 4/8] chore: update system prompt --- cow-trader/system_prompt.txt | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/cow-trader/system_prompt.txt b/cow-trader/system_prompt.txt index 3c12d4b..c3e4fc6 100644 --- a/cow-trader/system_prompt.txt +++ b/cow-trader/system_prompt.txt @@ -1,6 +1,28 @@ -You are a trading bot on CoW Swap DEX. +You are a skilled discretionary trader that operates on CoW Swap. + +You are prepared to take on risk when you believe you have identified an entry that offers a good opportunity. + +You understand that trading is about finding opportunities where you can make money while carefully protecting yourself from big losses. + +You appreciate that trading is nuanced, and that it is about: + - Risk management: "Don't lose too much on any single trade" + - Probability Assessment: "Find trades where you can win more than you can lose" + - Capital Preservation: "Protect your money first, profits second" + - Consistency: "Aim for lots of small wins rather than a few big ones" + Market Understanding: "Know how markets behave and how easily you can buy/sell" + Your job is to analyze market conditions and decide whether to trade. +Here is some historical canonical price information for COW and GNO from the past year: +- COW + - High: 1.16 COW/WXDAI + - Low: 0.15 COW/WXDAI. +- GNO + - High: 445 GNO/WXDAI. + - Low: 140 GNO/WXDAI. +Please use this prices as additional context for interpreting the current prices. + + CONTEXT ACCESS: - get_trading_context(): Returns a TradeContext containing: - token_balances: Dict[str, str] — Your current token holdings. @@ -42,4 +64,7 @@ OUTPUT REQUIRED: buy_token: str | None - Must be a hex address from get_eligible_buy_tokens() if trading, or None if not. reasoning: str - - A concise (1 short sentence) justifying your decision, referencing aspects like market analysis, prior results, risk, or other relevant factors. \ No newline at end of file + - A concise (1 short sentence) justifying your decision, referencing aspects like market analysis, prior results, risk, or other relevant factors. + +REVIEW REQUIRED: +- Cross check the reasoning you use to justify a trade, ensure that it is accurate according to your understanding of the market and the metrics you have on this market. \ No newline at end of file From 0214debd5c47bd4e490fa2c83029e715e6b7f206 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:38:35 +0100 Subject: [PATCH 5/8] feat: prompts for auto signing and encouraging a trade --- cow-trader/bot.py | 91 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 18 deletions(-) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index 0ea7036..b929da3 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -20,6 +20,10 @@ # Initialize bot bot = SilverbackBot() +# Config +PROMPT_AUTOSIGN = bot.signer +ENCOURAGE_TRADE = os.environ.get("ENCOURAGE_TRADE", False) + # File path configuration TRADE_FILEPATH = os.environ.get("TRADE_FILEPATH", ".db/trades.csv") BLOCK_FILEPATH = os.environ.get("BLOCK_FILEPATH", ".db/block.csv") @@ -29,8 +33,9 @@ # Addresses SAFE_ADDRESS = "0xbc3c7818177dA740292659b574D48B699Fdf0816" -TOKEN_ALLOWLIST_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" +TOKEN_ALLOWLIST_ADDRESS = "0x98a4351d926e6274829c3807f39D9a7037462589" GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" +TRADING_MODULE_ADDRESS = "0xF11bC1ff8Ab8Cc297e5a1f1A51B8d1792E99D648" GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c" @@ -38,9 +43,9 @@ MONITORED_TOKENS = [GNO, COW, WXDAI] MINIMUM_TOKEN_BALANCES = { - GNO: 5e16, - COW: 25e18, - WXDAI: 10e18, + GNO: 116, + COW: 10e18, + WXDAI: 5e18, } @@ -54,7 +59,8 @@ def _load_abi(abi_name: str) -> Dict: # Contracts GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=_load_abi("GPv2Settlement")) -TOKEN_ALLOWLIST_CONTRACt = Contract(TOKEN_ALLOWLIST_ADDRESS, abi=_load_abi("TokenAllowlist")) +TOKEN_ALLOWLIST_CONTRACT = Contract(TOKEN_ALLOWLIST_ADDRESS, abi=_load_abi("TokenAllowlist")) +TRADING_MODULE_CONTRACT = Contract(TRADING_MODULE_ADDRESS, abi=_load_abi("TradingModule")) # API @@ -272,6 +278,19 @@ def get_sell_token(ctx: RunContext[AgentDependencies]) -> str | None: raise +@trading_agent.system_prompt +def encourage_trade(ctx: RunContext[AgentDependencies]) -> str: + if ENCOURAGE_TRADE: + return ( + f"I encourage you to sell {ctx.deps.sell_token}. Use the tool " + "'get_eligible_buy_tokens' to get a list of eligible buy tokens, and from that " + "list, pick the one that is most promising despite current market conditions. " + "Remember, we're experimenting and learning—feel free to consider unconventional choices." + ) + else: + return "" + + def _get_token_balances() -> Dict[str, int]: """Get balances of monitored tokens using multicall""" token_contracts = [Contract(token_address) for token_address in MONITORED_TOKENS] @@ -507,23 +526,28 @@ def _save_decisions_db(df: pd.DataFrame) -> None: # Historical log helper functions -def get_canonical_pair(token_a: str, token_b: str) -> tuple[str, str]: +def _get_canonical_pair(token_a: str, token_b: str) -> tuple[str, str]: """Return tokens in canonical order (alphabetically by address)""" return (token_a, token_b) if token_a.lower() < token_b.lower() else (token_b, token_a) -def calculate_price(sell_amount: str, buy_amount: str) -> float: +def _calculate_price(sell_amount: str, buy_amount: str) -> float: """Calculate price from amounts""" return int(sell_amount) / int(buy_amount) def _process_trade_log(log) -> Dict: - """Process trade log with price calculation""" - token_a, token_b = get_canonical_pair(log.sellToken, log.buyToken) - price = calculate_price(log.sellAmount, log.buyAmount) + """ + Process trade log and compute canonical price as: + canonical price = (quote token amount) / (base token amount) + where (token_a, token_b) is the canonical pair sorted lexicographically. + """ + token_a, token_b = _get_canonical_pair(log.sellToken, log.buyToken) - if token_a != log.sellToken: - price = 1 / price + if log.sellToken == token_a: + price = int(log.buyAmount) / int(log.sellAmount) + else: + price = int(log.sellAmount) / int(log.buyAmount) return { "block_number": log.block_number, @@ -604,7 +628,7 @@ def _construct_quote_payload( return { "sellToken": sell_token, "buyToken": buy_token, - "sellAmountBeforeFee": sell_amount, + "sellAmountBeforeFee": str(sell_amount), "from": SAFE_ADDRESS, "receiver": SAFE_ADDRESS, "appData": "{}", @@ -694,7 +718,34 @@ def _save_order(order_uid: str, order_payload: Dict, signed: bool) -> None: _save_orders_db(df) -def create_and_submit_order( +def sign_order(order_uid: str, order_payload: dict) -> None: + """Sign order via TradingModule contract""" + + BALANCE_ERC20 = "0x5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9" + KIND_SELL = "0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775" + + TRADING_MODULE_CONTRACT.setOrder( + order_uid, + ( + order_payload["sellToken"], + order_payload["buyToken"], + order_payload["receiver"], + order_payload["sellAmount"], + order_payload["buyAmount"], + order_payload["validTo"], + order_payload["appDataHash"], + order_payload["feeAmount"], + KIND_SELL, + order_payload["partiallyFillable"], + BALANCE_ERC20, + BALANCE_ERC20, + ), + True, + sender=bot.signer, + ) + + +def create_submit_and_sign_order( sell_token: str, buy_token: str, sell_amount: str, @@ -711,13 +762,14 @@ def create_and_submit_order( click.echo(f"Quote received: {quote}") order_payload = _construct_order_payload(quote) - click.echo(f"Submitting order payload: {order_payload}") - order_uid = _submit_order(order_payload) - click.echo(f"Order response: {order_uid}") + click.echo(f"Order submitted: {order_uid}") _save_order(order_uid, order_payload, False) + click.echo("Signing order...") + sign_order(order_uid, order_payload) + return order_uid, None except Exception as e: @@ -728,6 +780,9 @@ def create_and_submit_order( @bot.on_startup() def bot_startup(startup_state: StateSnapshot): """Initialize bot state and historical data""" + if PROMPT_AUTOSIGN and click.confirm("Enable autosign?"): + bot.signer.set_autosign(enabled=True) + block_db = _load_block_db() last_processed_block = block_db["last_processed_block"] _save_block_db({"last_processed_block": chain.blocks.head.number}) @@ -811,7 +866,7 @@ def exec_block(block: BlockAPI): bot.state.next_decision_block = block.number + TRADING_BLOCK_COOLDOWN if decision.valid: - order_uid, error = create_and_submit_order( + order_uid, error = create_submit_and_sign_order( sell_token=decision.sell_token, buy_token=decision.buy_token, sell_amount=trade_ctx.token_balances[decision.sell_token], From ae135d3869d01ee432b604680cb744ca94056fdd Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:39:48 +0100 Subject: [PATCH 6/8] chore: add __pycache__ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f43d14b..5f4db55 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ dist/ node_modules/ out/ wheels/ +__pycache__ # Files *.egg-info From 326a8e6de0654dce3167111072906deea780dfbf Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:40:16 +0100 Subject: [PATCH 7/8] fix: linting --- smart-contract-infra/deployments/contracts.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/smart-contract-infra/deployments/contracts.json b/smart-contract-infra/deployments/contracts.json index 94fb230..07e5e48 100644 --- a/smart-contract-infra/deployments/contracts.json +++ b/smart-contract-infra/deployments/contracts.json @@ -1,12 +1,5 @@ { - "deployedChains": [ - 1, - 100, - 8453, - 31337, - 42161, - 11155111 - ], + "deployedChains": [1, 100, 8453, 31337, 42161, 11155111], "chains": { "1": { "allowlist": "0x0000000000000000000000000000000000000000", @@ -45,4 +38,4 @@ "tradingModuleProxy": "0x0000000000000000000000000000000000000000" } } -} \ No newline at end of file +} From 3a4ddb725e7284c014b30068951a7528397c9d0c Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Thu, 13 Feb 2025 16:42:58 +0100 Subject: [PATCH 8/8] fix: line too long linting issue --- cow-trader/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index b929da3..7a46a63 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -285,7 +285,7 @@ def encourage_trade(ctx: RunContext[AgentDependencies]) -> str: f"I encourage you to sell {ctx.deps.sell_token}. Use the tool " "'get_eligible_buy_tokens' to get a list of eligible buy tokens, and from that " "list, pick the one that is most promising despite current market conditions. " - "Remember, we're experimenting and learning—feel free to consider unconventional choices." + "Remember, we're experimenting and learning—consider unconventional choices." ) else: return ""