Skip to content
Open
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 automation/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* [Getting Started](getting-started.md)
* [API Reference](api-reference.md)
* [Smart Contract Integration](smart-contract-integration.md)
* [Automation Dev Guide](automation_article.md)
* [Your First Automation Task](your-first-automation-task/README.md)
* [Prerequisites & Epoch Timing](your-first-automation-task/prerequisites-and-epoch-timing.md)
* [Create the Move Smart Contract](your-first-automation-task/create-the-move-smart-contract.md)
Expand Down
145 changes: 145 additions & 0 deletions automation/automation_article.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
Supra Automation
===

We all know that a smart contract needs to be triggered via an external trigger. The trigger can not come from within the blockchain itself. Many of the classical automation solution sits outside of chain, fetches every block of the target chain, checks for the condition of interest and then sends a trigger to the chain if the condition is true.

So what are the limitations of this approach?
1. By the time the automation nodes fetch the block, checks for condition and then send a trigger, the target chain might have moved ahead a few blocks. You encouter the classic _time to check vs time to execute_ problem. It is possible that by the time your trigger lands on-chain, the condition you are interested may not be _true_. Even if the resultant condition is still true, it is possible that due to this delay you might have lost a huge financial opportunity. For example, a liquidation action which is too late can result in a huge financial loss.
2. As a user, the user has to park some funds with the automation service provider for gas expense. Additionally, a user might have to trust the automation nodes to take action on their behalf, or develop a smart contract which upon receiving the trigger, acts on users' behalf.


With Supra Native automation you can bid good-bye to these limitations. The action happens as soon as the condition becomes _true_ and, it executes with the same authority as the submitter.

You might ask that Supra Automation does not seem to have `if-this-then-that` structure. Well, yes and no. From the infrastructure side, once your task becomes active, validator nodes execute the submitted task at the end of every block. Howeve, one can write the smart contract in a manner so that you achieve `if-this-then-that` effect.

```move
public entry fun my_automation_task(user: &signer) {
if (check_condition_of_interest()) {
perform_action_of_interest();
}
}
```

Now, if the condition is _false_, the action is not triggered. Typically, the gas requirements for checking condition is negligible. In all automation solution, these conditions must be check in a kind of _busy-wait_ fashion, we are just making it explicit.

Why did we design it in this fashion? Well, so that if a user wants to perform something at every block, _unconditionally_, they can still do so.


### But I only want to check my conditions once every 5 seconds

This can also be achieved via smart contract programming.

```move
struct AutomationState {
last_checked: u64,
check_interval: u64,
}

public entry fun my_automation_task(user: &signer) acquires AutomationState {
let automation_state = borrow_global<AutomationState>(signer::address_of(user));
if (timestamp::now_seconds() - automation_state.last_checked < automation_state.check_interval) {
return
};

// Now check the condition of interest
if (check_condition_of_interest()) {
perform_action_of_interest();
};
}
```


### What if the condition remains true for several blocks, I only want to execute the action 5 times

Again, this can also be achieved via clever programming.

```move
struct AutomationState{
execution_counter: u64,
task_id: Option<u64>,
}

public entry fun my_automation_task(user:&signer) acquires AutomationState {

let automation_state = borrow_global_mut<AutomationState>(signer::address_of(user));
if(automation_state.execution_counter == 0) { return };
// OR you can just stop the task
if (option::is_some(automation_state.task_id)) {
0x1::automation_registry::stop_tasks(user,vector[task_id]);
};

if(check_condition_of_interest()) {
perform_action_of_interest();
automation_state.execution_counter = automation_state.execution_counter - 1;
};
}

public entry fun change_state(user:&signer, exec_counter: u64, task_id: u64) acquires AutomationState {

let automation_state = borrow_global_mut<AutomationState>(signer::address_of(user));
automation_state.execution_counter = exec_counter;

// Ensures that the provided `task_id` corresponds to an active task registered by the caller.
assert!(automation_registry::has_sender_active_task_with_id(user_addr, task_id), ETASK_INACTIVE_OR_UNAUTHORIZED);
automation_state.task_id = option::some(task_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have not implemented validation to restrict configuration of task_id to the task owner.
Currently, it is left to the user's discretion.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nizam-supraoracles , I didn't understand, please elaborate.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason a user sets task_id here is to enable auto-cancellation once the execution limit is reached. This auto-cancel functionality is not directly available in the automation registry, so it require the user to provide the task_id when configuring the task in their contract.

Internally, we use this task_id to stop the task via: automation_registry::stop_tasks(...).

However, this call will fail if the provided task_id does not belong to the caller (i.e., the task owner).

Currently, we have not implemented validation to enforce that the provided task_id is actually owned by the sender. Ideally, we should validate this during configuration to prevent misuse or misconfiguration.

For example:

assert!(
    automation_registry::has_sender_active_task_with_id(user_addr, task_id),
    ETASK_INACTIVE_OR_UNAUTHORIZED
);

}

public entry fun init_state(user:&signer, exec_counter: u64) {

move_to(user, AutomationState { execution_counter: exec_counter, task_id: option::none()})
}
```

You do not know the `task_id` before registration. So it can be kept as `Option` and populated later via sending a regular transaction. Other part of the state can also be changed via a regular transaction.

Always validate `task_id` ownership before storing it in global state. Failing to do so may allow unauthorized task binding or inconsistent automation behavior.

In fact, by doing `init_state` via a regular transaction, before registering your automation would greatly help is reducing `max-gas` parameter that you supply at the time of registration. Why? Because, the automation registry reserves the gas for your task in the block space, therefore, the _automation fee_ is in proportion to the `max-gas`. By doing operations which creates new storage in a regular transaction, `max-gas` requirement can be greatly reduced for automaiton tasks.


### I want to change behaviour of my automation task without cancelling and re-registering

Well, good news is that your automation task has two ways in which it receives inputs. One is via parameters and other is via global storage. So the behaviour can be dynamically be changed by changing the global state. For example,

```move
struct AutomationState {
state : u64;
}

public entry fun my_automation_task(user: &signer) {

if (state == 1) {
perform_action1();
}
else if (state ==2) {
perform_action2();
}
// ...
// ...
}

public entry fun change_state(user: &signer, in_state: u64) {

let automation_state = borrow_global_mut<AutomationState>(signer::address_of(user));
automation_state.state = in_state;
}

```

With this trick, the behaviour of the automation task can be changed by sending a regular transaction that changes the input state based on which the action is performed.

Of course, depending upon the need, you may want to perform better access control to your `public entry` methods, such as ensuring that only certain white listed users can change/initialize state or perform certain action.


Note, that `my_automation_task`, the task which is registered with the registry, is always given the `signer` of the submitter.


### How do I reduce fee for my automation task?

As mentioned above, one of the very important parameter is `max-gas`. By doing accruate gas estimate of the _longest execution path_ in your automation task and supplying that estimate as `max-gas` can greatly reduce the fee charged to your task.

Creating new storage slots is one of the most expensive operations in Move. Reason being that once a global storage slot is created, it becomes a liability on the network till eternity. Even if later it is `drop`-ed, the archive will have to maintain it.

By moving storage slot creations to a regular transaction, the gas cost of _longest execution path_ can be greatly reduced.

Additionally, doing gas optimization of your automation task would also help in reduction of execution fees charged.