-
Notifications
You must be signed in to change notification settings - Fork 0
How to use CronCat
This document is intended for folks who understand how CronCat works and are interested in integrating it.
For those who might be seeking a "how it works" article, here are a couple of great resources:
To build, optimize, and deploy the contracts:
just build
just optimize
just deploy
For an end user, project, or blockchain, CronCat brings automation by allowing accounts or smart contracts to create tasks. These tasks execute with the help of an agent network.
There are different kinds of tasks that can be created. By specifying a task interval and boundaries, several use cases become possible. This will be explored in the following section.
Later in this page, we'll cover how the agent works, how anyone can sign up (on-chain) to be an agent, and everything else you'll need to know.
A CronCat task is created by an end user or another contract to schedule the execution of an action at a later time. This can be done once, in a recurring fashion, only when certain conditions are met, etc.
When a task is created, it's given several important pieces of information:
- Interval: when and how often the task should execute
- Boundary
- Actions
- Queries & Transforms
- Fee
The interval of a task determines when it should and should not run. There are currently four types of intervals:
- Once: execute this task once. It's not recurring. As soon as the task is executed and the DAO and agent are paid the fees specified in the CronCat configuration, return the remainder to the task creator.
- Immediate: execute the task immediately. A task with and immediate interval can be recurring, depending on the the set boundaries. Some tasks may wish to include queries and transforms, which determine whether a task should execute.
- Block: execute at a given block height. Depending on the boundaries on the task, this can be recurring. For instance, setting a Block interval of 100 means the task will execute every 100 blocks, if the boundary and query/transforms allow.
- Cron: executes the task at a given time. This uses the cron spec that looks something like this:
19 */1 * * *
. That cron string says, "execute this task every hour at the 19th minute." There are helpful websites to interpret cron strings, like this one.
A boundary specifies a window when a task window begins and/or ends. Boundaries are optional, and when null
is provided, the task executes any time.
Let's take a toy example: perhaps an art project wants to mint NFTs every hour for the month of February. A start and end boundary can be specified.
Boundaries have two types:
- Height: the block height to start/end
- Time: a timestamp (unsigned 64-bit integer representing nanoseconds since Unix epoch)
Actions are what a task executes. A task can have one or more actions. An action can be any Cosmos SDK message that is allowed by CronCat. For security purposes, not all messages are allowed. You may see the allowed messages in the cw-croncat-core
crate. Please feel free to reach out (DM on Twitter, please.) if there are exciting use cases to unlock from other messages.
Queries and transforms are an incredibly powerful aspect to tasks, so we'll take some time explaining this one.
A query and transform can work in coordination to "insert" a value into an action. For example, a CronCat member created this script, which automatically executes a DAO DAO proposal after it passes.
How it works is this… A single action is specified. It calls a DAO smart contract at the method execute
, passing these parameters: {"proposal_id":""}
. Note that typically, this message would be sent with a proposal_id
, but it is empty. That's because the proposal_id
will be filled in using queries and transforms.
First, we start with the query. Without getting too far in the weeds, the CronCat manager has an association with a "rules" smart contract. (Note: this is subject to change in the future as CronCat explores gas efficiencies.) There's a query set up to find passed DAO proposals. For those unfamiliar with DAO DAO, a proposal is submitted, members vote on it and if it passes, there is a final step to "execute" the task, which any account can do. The task created by script linked earlier will automatically execute the proposal, but it starts by querying to find proposals whose status is passed
, which means they're not executed yet.
Once the query returns a proposal ID, the transform section of the task is utilized. Since there can be multiple actions and multiple queries, the transform section uses indices to determine where the returned query value should be inserted. In the case of the script in question, it fills in the aforementioned proposal_id
.
A fee is included with each task. This fee will cover the gas costs of executing the transaction, plus paying the decentralized agent and DAO a modest percentage. For tasks that are meant to be recurring, this fee is tracked and automatically reduced upon each execution. When a recurring task no longer has the balance necessary to cover a subsequent execution (or the boundaries deem it should end) the remaining balance is returned to the task creator.
The manager coordinates how tasks get assigned to agents, and whether those tasks are getting done.
The proxy_call
method is an important piece of the manager. For trivial tasks — those without queries and transforms — agents call proxy_call
with no arguments. For tasks that do "if-this-then-that" with their tasks through CronCat queries and transforms, an argument is sent to proxy_call
providing the task_hash
to the task that has been determined to be ready to execute. (Of course, on the smart contract level all criteria is checked again before any execution occurs.)
See Manager README
Keeps track of the agents:
- which are active
- which are pending
- the agent's "track record" for fulfilling tasks
It also contains configuration that can only be updated by the CronCat factory contract, controlled solely by the CronCat DAO. This configuration includes items like:
-
min_tasks_per_agent
This is essentially used as a ratio, "one agent should handle this many tasks, if not more." It is used to determine when to let a pending agent into the active set.
-
agent_nomination_duration
When tasks increase such that a new agent can be let in, we cannot guarantee that the first pending agent is still online and will respond. We must wait a given duration before opening up the slot to the next person in line, and the next… This field determines that duration that we wait.
-
min_coins_for_agent_registration
There's a small check during agent registration to ensure there's a minimal, base amount making them capable of performing as an agent. This will use the
denom
associated with the Manager contract. -
agents_eject_threshold
If an agent is offline or otherwise unresponsive, they may be ejected. It's possible this value will remove active agents if there was an oversaturation and the number of tasks decreases pass some threshold.
See Agent README
Keeps track of the tasks that are created and acts as the entry point for create_task
which is an important function. End users and smart contracts can create tasks, giving criteria on when and how and what it should execute.
A task can be removed by the owner. (There is also a failsafe to remove a task from a CronCat DAO vote.)
A task can also be "refilled" with native tokens. This is particularly useful for recurring tasks. The method to add native tokens does not exist on the Task contract but instead on the Manager, at refill_task_balance
and providing the task_hash
. There is also an intuitive twin command for cw20s, at refill_task_cw20_balance
.
When a task is removed, the owner is returned the amount of funds owed to them. At this time, that is the amount they included upon task creation.
Keeps track of the CronCat contracts and their versions.
The intent is for the factory to set the owner to the CronCat DAO on DAO DAO. When a new version is ready to be released, we currently truncate semantic versioning to just contain a major and minor version, then allow the factory to instantiate it. The updated contract will be stored, and the code_id
provided (along with other fields) to the deploy
method.
The factory's address is propagated to the various contracts, and they'll store it in their state, so they can reference factory's state they need to reference.
For instance, during task creation the Task contract, it checks the state of the Factory contract to get the Manager contract's address, so it can send a message to it during the response.
Agents are off-chain daemons that can run on low-power devices. The agents are the ones who fulfill the tasks on-chain. They have to register to become an agent. This places them in a pending queue, with other potential agents. As the number of tasks increases, a ratio (saved in configuration) determines whether another pending agent should be allowed to become active. Being in the active queue means being responsible for fulfilling tasks. If an agent stops being productive, it will be removed. Agents earn rewards based on a proportion of the gas spent each time they fulfill a task, so it's worthwhile.
The agent's status is an enum.
- Active — agent is in the active set
- Pending — agent is in the pending queue
- Nominated — agent may now call the
check_in_agent
method in order to be placed in the active set
An agent registers (starting in the pending queue) when it calls register_agent
on the Agent contract.
Note: agents can also be removed from the active set if the number of tasks drops enough to where it's recommended to return to a smaller subset.
Why enforce/recommend a smaller group of agents? In order to have agents be sufficiently incentivized to keep their daemon running, this decision was made in order to avoid a highly unprofitable situation for the agent community.
When the agent daemon is running it will be doing a few things. The most significant interaction is when the agent fulfills a task. From a high-level, this is like sending in a "blank transaction" with gas. The gas will be used to perform cross-contract calls as described in the CronCat task. The agent is then rewarded with the gas they sent plus an extra percentage. The stored amount is called the reward, and the agent can claim the reward by calling a method. This method will be on the CronCat Manager contract and not the Agent. This was a deliberate decision. Anyway, the agents withdraws rewards by calling the Manager at withdraw_agent_rewards
.
When the agent fulfills a task, it means they've checked that there are tasks to do, and then sends execute messages for the tasks they're on the hook for. Important: the agent doesn't know which task they'll execute, just that a task(s) exist that your agent is on the hook for. There are basic stats saved on agent participation, to ensure active agents remain active.
The agent queries the Agent contract at get_agent_tasks
. This will return the number of "Cron tasks" as well as the number of "Block tasks" which refer to the two categories of tasks we have. (At the time of this writing, and in discussion to explore approaches for future releases.)
Let's say the agent queries and hears there are 10 + 9 total tasks to execute, the next step is to call proxy_call
on the Manager contract. (See previous section about proxy_call
having an optional argument for event-based tasks.)
Another thing an agent daemon does is query its status every few blocks. It checks its status by querying the Agent contract at get_agent
and providing one parameter account_id
with the agent's address.
Here we're using junod
which is the Juno daemon that can act as a helpful CLI of sorts to query, execute, sign transactions, etc.
The scripts creates a task that will call CronCat itself at the tick
method. We share this example on purpose since it's a special task that can only be created by the "owner" of the smart contract to avoid potential security issues. You can see the tick
method under the actions
array, inside the msg
» wasm
» execute
section, there's a nested msg
key containing: eyAidGljayI6IHt9IH0K
which is { "tick": {} }
base64-encoded.
#!/bin/bash
TICK_TASK='{
"create_task": {
"task": {
"interval": {
"block": 100
},
"boundary": null,
"stop_on_fail": false,
"actions": [
{
"msg": {
"wasm": {
"execute": {
"contract_addr": "stars1p0a8da8t7d39lcahlex3vpqxv8vank3lxkqe27j3hll5mse3kcjsupj08r",
"msg": "eyJteV9tZXRob2QiOnsiYXJnMSI6ImZvbyJ9fQ==",
"funds": []
}
}
},
"gas_limit": 300000
}
],
"queries": null,
"transforms": null
}
}
}'
starsd tx wasm execute stars15sv4c3mner64rm748rq593hdfq376uklyzan4c6a7sjzap6t7rqsp84gkk "$TICK_TASK" --node https://rpc.elgafar-1.stargaze-apis.com:443 --chain-id elgafar-1 --gas-prices 0.025ustars --gas-adjustment 1.7 --gas auto -b block --from youruser -o json -y --amount 1000000ustars | jq | head -n 42
You can check that the number of tasks have increased by querying the Task contract at tasks_total
:
starsd q wasm contract-state smart stars15sv4c3mner64rm748rq593hdfq376uklyzan4c6a7sjzap6t7rqsp84gkk '{"tasks_total":{}}' --node https://rpc.elgafar-1.stargaze-apis.com:443 --chain-id elgafar-1
Other dApps can create CronCat tasks from their smart contracts, allowing them to automate logic, clean up state, etc.
Since CronCat architecture includes a factory, we'll show an example of the following scenario…
The CronCat factory exists on chain, where the factory address has been saved to another dApp's contract state.
The other dApp, in this case, is a simple contract that creates a CronCat task to call toggle
on another smart contract that stores and changes a boolean in its state.
A user calls a contract, providing the croncat_factory_address
and the address to the smart contract they wish to call
We'll show a snippet below, that's taken from the full example here.
let tasks_name: String = String::from("tasks");
// Ask the CronCat Factory contract what the latest task contract address is
// then we'll call `create_task` on the provided Task contract
let query_factory_msg = LatestContract {
contract_name: tasks_name.clone(),
};
let latest_contract_res: ContractMetadataResponse = deps.querier.query_wasm_smart(&croncat_factory_address, &query_factory_msg)?;
// Check validity of result
if latest_contract_res.metadata.is_none() {
return Err(ContractError::CustomError {
code: "NO_SUCH_CONTRACT_NAME_ON_FACTORY".to_string(),
msg: format!("Did not find contract '{}' on factory contract {}", tasks_name, croncat_factory_address),
})
}
let tasks_address = latest_contract_res.metadata.unwrap().contract_addr;
let croncat_task = TaskRequest {
interval: Interval::Block(1),
boundary: None,
stop_on_fail: false,
actions: vec![Action {
msg: Wasm(Execute {
contract_addr: boolean_address.clone().into_string(),
msg: to_binary(
&BooleanContractExecuteMsg::Toggle {},
)?,
funds: vec![],
}),
gas_limit: Some(150_000), // fine tune gas here
}],
queries: None,
transforms: None,
cw20: None,
};
let create_task_msg = Wasm(Execute {
contract_addr: String::from(tasks_address.clone()),
msg: to_binary(
&CreateTask {
task: Box::new(croncat_task),
},
)?,
funds: info.funds,
});
let sub_message = SubMsg::reply_on_error(create_task_msg, REPLY_CRONCAT_TASK_CREATION);
Ok(Response::new()
.add_attribute("croncat_factory_address", croncat_factory_address)
.add_attribute("boolean_address", boolean_address)
.add_attribute("tasks_address", tasks_address)
.add_submessage(sub_message)
)
Let's explain what's happening here… So the various CronCat contracts are created by the Factory contract. A dApp that wishes to integrate with CronCat only needs to know the CronCat factory address. The versioning system will handle all kinds of queries, but the easy one we'll call in this example is the latest_contract
method on the Factory contract, providing it the contract_name
of "tasks" to it.
This query returns the contract address of the most recent version of the tasks
contract.
We knew the contract's name was "tasks" in this case. If you wish, you can see all the contract names by calling the Factory's method contract_names
, which follows the pagination pattern, offering optional from_index
and limit
.
Now that our example contract has the address of the Task contract, it can create the task. It creates a Task with one Action. This Action is an execute contract call with a message containing information on the method and the params to call. In our example, this message calls the toggle
method with no parameters, so it's:
{
"toggle": {}
}
Note that CosmWasm's Rust macros make it easy for us to use to_binary
on our structs and enums annotated with #[cw_serde]
, making the creation of a Task simple, as shown here:
let my_wasm_execute_msg = Wasm(Execute {
contract_addr: tasks_contract_address,
msg: to_binary( // Takes care of encoding
&CreateTask {
task: Box::new(task_request),
},
)?,
funds: info.funds,
});
The example then wraps the details of the Action into a TaskRequest
object, and sends that payload as the value to the first and only argument (task
) to the Task contract's create_task
method.