External Code Execution
The std-lib
includes a function called run_external
that allows Sway contracts to execute arbitrary external Sway code.
This functionality enables features like upgradeable contracts and proxies.
Upgradeable Contracts
Upgradeable contracts are designed to allow the logic of a smart contract to be updated after deployment.
Consider this example proxy contract:
#[namespace(my_storage_namespace)]
storage {
target_contract: Option<ContractId> = None,
}
impl Proxy for Contract {
#[storage(write)]
fn set_target_contract(id: ContractId) {
storage.target_contract.write(Some(id));
}
#[storage(read)]
fn double_input(_value: u64) -> u64 {
let target = storage.target_contract.read().unwrap();
run_external(target)
}
}
The contract has two functions:
set_target_contract
updates thetarget_contract
variable in storage with theContractId
of an external contract.double_input
reads thetarget_contract
from storage and uses it to run external code. If thetarget_contract
has a function with the same name (double_input
), the code in the externaldouble_input
function will run. In this case, the function will return au64
.
Notice in the Proxy
example above, the storage block has a namespace
attribute. Using this attribute is considered a best practice for all proxy contracts in Sway, because it will prevent storage collisions with the implementation contract, as the implementation contract has access to both storage contexts.
Below is what an implementation contract could look like for this:
storage {
value: u64 = 0,
// to stay compatible, this has to stay the same in the next version
}
impl Implementation for Contract {
#[storage(write)]
fn double_input(value: u64) -> u64 {
let new_value = value * 2;
storage.value.write(new_value);
new_value
}
}
This contract has one function called double_input
, which calculates the input value times two, updates the value
variable in storage, and returns the new value.
How does this differ from calling a contract?
There are a couple of major differences between calling a contract directly and using the run_external
method.
First, to use run_external
, the ABI of the external contract is not required. The proxy contract has no knowledge of the external contract except for its ContractId
.
Upgradeable Contract Storage
Second, the storage context of the proxy contract is retained for the loaded code.
This means that in the examples above, the value
variable gets updated in the storage for the proxy contract.
For example, if you were to read the value
variable by directly calling the implementation contract, you would get a different result than if you read it through the proxy contract.
The proxy contract loads the code and executes it in its own context.
Fallback functions
If the function name doesn't exist in the target contract but a fallback
function does, the fallback
function will be triggered.
If there is no fallback function, the transaction will revert.
You can access function parameters for fallback functions using the call_frames
module in the std-lib
.
For example, to access the _foo
input parameter in the proxy function below, you can use the called_args
method in the fallback
function:
fn does_not_exist_in_the_target(_foo: u64) -> u64 {
run_external(TARGET)
}
#[fallback]
fn fallback() -> u64 {
use std::call_frames::*;
__log(3);
__log(called_method());
__log("double_value");
__log(called_method() == "double_value");
let foo = called_args::<u64>();
foo * 3
}
In this case, the does_not_exist_in_the_target
function will return _foo * 3
.
Limitations
Some limitations of run_external
function are:
- It can only be used with other contracts. Scripts, predicates, and library code cannot be run externally.
- If you change the implementation contract, you must maintain the same order of previous storage variables and types, as this is what has been stored in the proxy storage.
- You can't use the call stack in another call frame before you use
run_external
. You can only use the call stack within the call frame that containsrun_external
.