Skip to content

feat: trading deploy and setup #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dist/
node_modules/
out/
wheels/
__pycache__

# Files
*.egg-info
Expand Down
2 changes: 1 addition & 1 deletion cow-trader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
91 changes: 73 additions & 18 deletions cow-trader/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -29,18 +33,19 @@

# Addresses
SAFE_ADDRESS = "0xbc3c7818177dA740292659b574D48B699Fdf0816"
TOKEN_ALLOWLIST_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
TOKEN_ALLOWLIST_ADDRESS = "0x98a4351d926e6274829c3807f39D9a7037462589"
GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
TRADING_MODULE_ADDRESS = "0xF11bC1ff8Ab8Cc297e5a1f1A51B8d1792E99D648"

GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
WXDAI = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"
MONITORED_TOKENS = [GNO, COW, WXDAI]

MINIMUM_TOKEN_BALANCES = {
GNO: 5e16,
COW: 25e18,
WXDAI: 10e18,
GNO: 116,
COW: 10e18,
WXDAI: 5e18,
}


Expand All @@ -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
Expand Down Expand Up @@ -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—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]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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": "{}",
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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})
Expand Down Expand Up @@ -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],
Expand Down
29 changes: 27 additions & 2 deletions cow-trader/system_prompt.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
- 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.
4 changes: 2 additions & 2 deletions smart-contract-infra/deployments/contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"100": {
"allowlist": "0x98a4351d926e6274829c3807f39D9a7037462589",
"guard": "0xcab68170145d593F15BF398670876CcCBFe173e2",
"tradingModuleMasterCopy": "0xE0e75802Fb63B1a3b55862D9e217f902bd4e33f8",
"tradingModuleProxy": "0x920b3D833E5663CF700B17DcF119Ac697E340621"
"tradingModuleMasterCopy": "0xDdd11DC80A25563Cb416647821f3BC5Ad75fF1BA",
"tradingModuleProxy": "0xF11bC1ff8Ab8Cc297e5a1f1A51B8d1792E99D648"
},
"8453": {
"allowlist": "0x0000000000000000000000000000000000000000",
Expand Down
33 changes: 28 additions & 5 deletions smart-contract-infra/src/TradingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand All @@ -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,
Expand All @@ -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));

Expand Down
5 changes: 1 addition & 4 deletions smart-contract-infra/test/utils/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading