-
Notifications
You must be signed in to change notification settings - Fork 2
first draft of article on automation for dev #5
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
Open
sjoshisupra
wants to merge
3
commits into
main
Choose a base branch
from
automation-article
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
|
|
||
| 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. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_idhere 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 thetask_idwhen 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_iddoes not belong to the caller (i.e., the task owner).Currently, we have not implemented validation to enforce that the provided
task_idis actually owned by the sender. Ideally, we should validate this during configuration to prevent misuse or misconfiguration.For example: