An implementation of the Axelar cross-chain contracts in Move for the Sui blockchain.
Generated docs can be found here.
Install Sui as shown here. We recommend using the pre-built binaries from the Sui releases page. The version of Sui that should be used can be found here.
Install node.js 18+
Build all Move packages
npm ci
npm run build
Run tests for all Move packages
npm run test
If golden test data needs to be updated (such as if public interfaces have changed), then run
GOLDEN_TESTS=true npm run test
To run code coverage, a Sui debug binary needs to be built locally. You can also see coverage reports from the GH actions.
brew install libpq
brew link --force libpq
npm run coverage
See .coverage.info
for the coverage report.
Install the Move
extension in VS Code. It should come pre-installed with move-analyzer
.
Move Book: https://move-book.com Move Examples: https://examples.sui.io
Sui framework dependency is pinned to a specific mainnet release for all packages for consistency.
Official Sui deployment and operations scripts can be found here.
Please check the release process for more details.
The gateway lives in a few modules but has all of its storage in a single shared object called AxelarSigners
.
Relaying to Sui is a bit complicated: the concept of 'smart contracts' is quite warped compared to EVM chains.
Firstly, all persistent storage stems from Objects
that have the key
property and are either shared or owned by an account. These objects have a specific type
, which is defined in a certain module, and only that module can access their storage (either to read or to write to it). This means that instead of calling a smart contract with some data, in Sui one needs to call a module, pass the Objects that are to be modified as arguments, alongside extra arguments that specify the kind of change that should occur. This additionally means that the concept of msg.sender
does not apply to applications, and a certain capability
object that functions like a 'key' to unlock certain functionality needs to be used instead.
The second issue is the lack of interfaces whatsoever. It is impossible for a module to ever call a module that is published at a later time. This means applications that want to interface with future applications must be called by those future applications, but need to call pre-existing ones. To expand on this, we expect contract calls that are received to potentially modify the storage of multiple objects in a single call, which makes it impossible to require modules to implement a 'standardized' function that a relayer will call, because the number of arguments required varies depending on the application (or the type of call).
Finally, we do not want to require the payload of incoming calls to have a certain format, because that would mean that working applications that want to exapnd to Sui need to redesign their working protocoll to accomodate Sui, discouraging them from doing so.
First of all, as we mentioned before, for applications to 'validate' themselves with other applications need to use a capability
object. This object will be called Channel
, and it will hold information about the executed contract calls as well. It has a field called id
which specifies the 'address' of the application for the purposed of incoming and outgoing extenral calls. This id
has to match the id
of a shared object that is passed in the channel creation method (alongside a witness for security). This shared object can easily be querried by the relayer to get call fullfilment information. Specifically:
- The shared object has to have a field called
get_call_info_object_ids
that is avector<address>
. - The module that defined the shared object type has to implement a function called
get_call_info
, which has no types, and takes the incoming callpayload
as the first argument, followed by a number of shared objects whose ids are specified by theget_call_info_object_ids
mentioned above. This function has to return astd::ascii::String
which is the JSON encoded call data to fullfill the contract call. - This calldata has the following 3 fields:
target
: the target method, in the form ofpackage_id::module_name::function_name
.arguments
: an array of arguments that can be:contractCall
: theApprovedMessage
object (see below).pure:${info}
: a pure argument specified by$info
.obj:${objectId}
: a shared object with the specifiedid
.
typeArguments
: a list of types to be passed to the function called
The ITS on sui is supposed to be able to receive 3 messages:
-
Register Coin: The payload will be abi encoded data that looks like this:
4
:uint256
, fixed,tokenId
:bytes32
, fixed,name
:string
, variable,symbol
:string
, variable,decimals
:uint8
, fixed,distributor
:bytes
, variable,mintTo
:bytes
, variable,mintAmount
:uint256
, variable,operator
:bytes
, variable, Don't worry about the distributor and operator for now -
Receive coin: The payload will be abi encoded data that looks like this:
1
,uint256
, fixed,tokenId
,bytes32
, fixed,destinationAddress
,bytes
, variable (has to be converted to address),amount
,uint256
, fixed, -
Receive coin with data: The payload will be abi encoded data that looks like this:
2
,uint256
, fixed,tokenId
,bytes32
, fixed,destinationAddress
, bytes, variable (has to be converted to address),amount
,uint256
, fixed,sourceChain
,string
, variable,sourceAddress
,bytes
, variable This needs to return the coin object only if called with the right capability (another channel) that proves the caller is thedestinationAddress
ITS also needs to be able to send 2 calls, the call to receive coin and the call to receive coin with data, only id the right coin object is received. Since coins are only u64 some conversion might need to happen when receiving coins (decimals of 18 are too large for Sui to handle).
This module and the object it creates (CoinManagement<T>
) will tell if a coin is registered as a mint/burn or lock unlock token.
To create a CoinManagement
object one has to call
mint_burn<T>
, passing in aTreasuryCap<T>
.lock_unlock<T>
.lock_unlock_funded<T>
passing in some initialCoin<T>
to lock.
A distributor can also be added before registerring a coin (this is not completely flushed out)
This module and the object it creates CoinInfo<T>
will tell the ITS the information (name, symbol, decimals) for this coin. This information cannot (necesairily) be validated on chain because of how coin
is implemented, which means that we have to accept whatever the registrar tells us for it. If the CoinMetadata<T>
exists, we can also take it and create a 'validated' version of CoinInfo<T>
.
This module is responsible for creating tokenIds for both registerred and 'unregistered' (coins that are given to the ITS expecting a remote incoming DEPLOY_INTERCHAIN_TOKEN
message) coins. We might just go back to using addresses because the UX would slightly improve, but doing it this way improves code readablility and is more in line with what Sui tries to do.
This module is responsible for managing all of the storage needs of the ITS
This is the module that anyone would directly interract with. It needs to be able to do the following
register_coin<T>
: This function takes theITS
object and mutates it by adding a coin with the specifiedCoinManagement<T>
andCoinInfo<T>
.