@kaio-xyz/solana-edge-gateway
v1.0.1
Published
This is the main entry point to the KAIO's Gateway protocol on Solana. The majority of token and control operations originate from this program.
Readme
Solana Gateway Bridge
This is the main entry point to the KAIO's Gateway protocol on Solana. The majority of token and control operations originate from this program.
Limitations
Solana handles state updates different from other chains due to its account model. Therefore, some of the instructions i.e. sync_addresses, sync_credential, burn_and_bridge and mint require specific handling.
sync_addresses
EVM-based spec uses encoding that combines address updates across all chains. Since the limitations described in investor registry README, solana-version requires the encoding struct to submit address update per chain id in separate instructions calls.
// EVM version
struct InvestorAddresses {
bytes32 investorId; // Unique investor ID
// A list of concatenated bytes describing a change in multi-chain address
// Format: bytes1 action ++ uint16 chainId ++ bytes accountAddress
// for `action`: 0b0 -> Add, 0b1 -> Remove
// Big-endiannes is used for encoding
bytes[] addressChanges;
}in contrast to
// Solana version
struct InvestorAddresses {
// Unique investor ID
bytes32 investorId;
// Unique chain ID
uint16 chainId;
// Address size for that chain
uint8 addressSize;
// A list of concatenated bytes describing a change in multi-chain address
// Format `addressChanges`` := bytes1 action ++ bytes accountAddress
// where `action`: 0b0 -> Add, 0b1 -> Remove
// Big-endiannes is used for encoding
bytes[] addressChanges;
}Secondly, similar to native token program. The investor registry requires manual creation of a PDA to store address per investor per chain before actually putting the data in.
sync_credential
Similar to addresses, credentials program requires the PDA creation directly through the program before storing data there.
burn_and_bridge & mint
Similar to instructions listed above, it requires creation of token account PDA before a call submission. Furthermore, it requires address and credential account to be provided in the account list. Therefore, these accounts need to exist before submitting an instruction call.
Instructions
initialize
Initializes the main state PDA (state) of the program. Can only called once. Only admin can this instruction.
Arguments
supported_chains: Vec<u16>: List of chain selectors that are initially supported
Accounts
authority(Signer, mutable): Authority with admin rolestate(Account, mutable): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
admin_role(Account): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.admin]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.admin: 32-byte role identifier for admin role from roles account
roles(Account): PDA owned by role-registry program with seeds:["roles"]"roles": String constant for the main roles configuration account
roles_program(Program): Role registry programsystem_program(Program): System program
Errors
Unauthorized: Authority does not have admin role
generate_uid
Generates a unique identifier (UID) based on:
- Counter index (u128) stored in the Gateway's
state - Chain selectors and investor ID provided as arguments
This is a view-only function that calculates the UID without persisting any state changes. The actual UID account creation and counter increment happens in burn_and_bridge.
The program emits event:
pub struct GeneratedUid {
uid: [u8; 32],
}Arguments
source_chain_selector: u16: Source chain identifierdest_chain_selector: u16: Destination chain identifierinvestor_id: [u8; 32]: Investor identifier
Accounts
state(Account): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
Errors
IndexOverflow: Counter has reached maximum value (u128::MAX)
mint
Called by the bridge with TokenTransfer struct. This instruction handles the incoming bridge transfer.
pub struct TokenTransfer {
pub chain_selector: u16,
pub instrument_id: [u8; 32],
pub investor_id: [u8; 32],
pub receiver: Vec<u8>,
pub amount: u64,
}Only Bridge or Admin role holder can call this instruction.
Emit the following event:
pub struct TokenMintedEvent {
pub uid: [u8; 32],
pub transfer_details: TokenTransfer,
}Arguments
uid: [u8; 32]: Unique identifier for the operationtransfer_details: TokenTransfer: Transfer details including amounts and addresses
Accounts
authority(Signer, mutable): Authority with admin or bridge rolestate(Account): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
canonical_message_id(Account, mutable): PDA with seeds:["edge_gateway", "uid", uid]"edge_gateway": String constant for gateway state account"uid": String constant for UID accountsuid: 32-byte unique identifier from arguments- This account is created with
init_if_neededto handle both new transfers and reverts
registry(Account): PDA owned by investor-registry program with seeds:["registry"]"registry": String constant for registry state account
access_control(Account): PDA owned by security-token-hook program with seeds:["access_control", mint.key()]"access_control": String constant for access control accountmint.key(): Public key of the mint account (32 bytes)
instrument_mint(Account): PDA with seeds:["instrument_mint", transfer_details.instrument_id]"instrument_mint": String constant for instrument mint mappingtransfer_details.instrument_id: Instrument identifier from transfer details (32 bytes)
investor_id(Account): PDA owned by investor-id program with seeds:["id", token_account.owner]"id": String constant for investor ID accountstoken_account.owner: Public key of the token account owner (32 bytes)
credentials(Account): PDA owned by credentials program with seeds:["attestation", investor_id.get_id()]"attestation": String constant for attestation accountsinvestor_id.get_id(): 32-byte investor ID from investor_id account
addresses(Account): PDA owned by investor-registry program with seeds:["investor_addresses", chain_selector.to_be_bytes(), investor_id.get_id()]"investor_addresses": String constant for investor address accountschain_selector.to_be_bytes(): 2-byte chain selector in big-endian formatinvestor_id.get_id(): 32-byte investor ID from investor_id account
Multiple role verification accounts with similar PDA patterns as other instructions
Token program accounts (
mint,token_account,associated_token_program,token_program)
Errors
Paused: Gateway operations are pausedUnauthorized: Authority does not have admin or bridge roleUidUsed: The provided UID has already been used for another operationForeignChain: Transfer details specify a different chain than the native chainNonInvestor: Target account does not belong to an investorDifferentInvestor: Investor ID in transfer details doesn't match token account ownerInvalidReceiver: Receiver address doesn't match token account ownerInvalidMint: Provided mint doesn't match expected instrument mint
burn_and_bridge
Receives Solana serialised token transfer details. This instructions is called and signed by the investor and indicates the request to bridge assets to a different chain.
The UID for this operation is dynamically generated based on the gateway's counter, chain selectors, and investor ID rather than being passed as an argument.
Emit the following event:
pub struct TokenBurnedEvent {
pub uid: [u8; 32],
pub transfer_details: TokenTransfer,
}Arguments
transfer_details: TokenTransfer: Transfer details including amounts and addresses
Accounts
authority(Signer, mutable): Authority with gateway role (investor)state(Account, mutable): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account- Counter is incremented after successful operation
canonical_message_id(Account, mutable): PDA with seeds:["edge_gateway", "uid", generated_uid]"edge_gateway": String constant for gateway state account"uid": String constant for UID accountsgenerated_uid: Dynamically generated 32-byte unique identifier- This account is created with
initas it's the first time writing to this UID
registry(Account): PDA owned by investor-registry program with seeds:["registry"]"registry": String constant for registry state account
access_control(Account): PDA owned by security-token-hook program with seeds:["access_control", mint.key()]"access_control": String constant for access control accountmint.key(): Public key of the mint account (32 bytes)
instrument_mint(Account): PDA with seeds:["instrument_mint", transfer_details.instrument_id]"instrument_mint": String constant for instrument mint mappingtransfer_details.instrument_id: Instrument identifier from transfer details (32 bytes)
investor_id(Account): PDA owned by investor-id program with seeds:["id", token_account.owner]"id": String constant for investor ID accountstoken_account.owner: Public key of the token account owner (32 bytes)
credentials(Account): PDA owned by credentials program with seeds:["attestation", investor_id.get_id()]"attestation": String constant for attestation accountsinvestor_id.get_id(): 32-byte investor ID from investor_id account
addresses(Account): PDA owned by investor-registry program with seeds:["investor_addresses", transfer_details.chain_selector.to_be_bytes(), investor_id.get_id()]"investor_addresses": String constant for investor address accountstransfer_details.chain_selector.to_be_bytes(): 2-byte chain selector from transfer details in big-endian formatinvestor_id.get_id(): 32-byte investor ID from investor_id account
admin_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.admin]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.admin: 32-byte role identifier for admin role from roles account
bridge_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.bridge]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.bridge: 32-byte role identifier for bridge role from roles account
roles(Account): PDA owned by role-registry program with seeds:["roles"]"roles": String constant for the main roles configuration account
mint(Account): The mint account for the tokentoken_account(Account, mutable): Token account to burn fromassociated_token_program(Program): Associated Token Programtoken_program(Program): Token Programroles_program(Program): Role registry programinvestor_id_program(Program): Investor ID programinvestor_registry_program(Program): Investor registry programcredentials_program(Program): Credentials programsecurity_token_hook_program(Program): Security token hook program
Errors
Paused: Gateway operations are pausedUnauthorized: Authority does not have gateway roleUidUsed: The provided UID has already been used for another operationNativeChain: Transfer details specify the native chain (should be foreign)UnsupportedChain: Target chain is not in the supported chains listNonInvestor: Source account does not belong to an investorInvalidMint: Provided mint doesn't match expected instrument mintInvalidReceiver: Receiver address matches token account owner (should be different for bridging)DifferentInvestor: Target address doesn't belong to the same investor
set_chain_support
Sets the chain support. Only Admin role holder can call this function.
Arguments
chain_selector: u16: Chain identifier to modify support foris_supported: bool: Whether to enable or disable support
Accounts
authority(Signer, mutable): Authority with admin or bridge rolestate(Account, mutable): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
admin_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.admin]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.admin: 32-byte role identifier for admin role from roles account
roles(Account): PDA owned by role-registry program with seeds:["roles"]"roles": String constant for the main roles configuration account
roles_program(Program): Role registry program
Errors
Paused: Gateway operations are pausedUnauthorized: Authority does not have admin or bridge roleChainNotFound: Attempting to remove support for a chain that is not currently supported
sync_credential
Receives a Attestation struct to update the on-chain copy. Calls the credentials program via CPI.
Only Admin or Bridge can call this instruction.
Emits event:
pub struct CredentialSyncedEvent {
pub attestation_id: [u8; 32],
pub investor_id: [u8; 32],
}Arguments
attestation: Attestation: Credential attestation to synchronize
Accounts
authority(Signer, mutable): Authority with admin, bridge, or gateway rolestate(Account): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
canonical_message_id(Account, mutable): PDA with seeds:["edge_gateway", "uid", attestation.id]"edge_gateway": String constant for gateway state account"uid": String constant for UID accountsattestation.id: 32-byte attestation identifier from attestation data- This account is created with
initas credentials should only be synced once
credential(Account, mutable): PDA owned by credentials program with seeds:["attestation", attestation.data.investor_id]"attestation": String constant for attestation accountsattestation.data.investor_id: 32-byte investor identifier from attestation data- Account ownership is validated via
seeds::program = credentials_program.key()
admin_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.admin]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.admin: 32-byte role identifier for admin role from roles account
bridge_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.bridge]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.bridge: 32-byte role identifier for bridge role from roles account
roles(Account): PDA owned by role-registry program with seeds:["roles"]"roles": String constant for the main roles configuration account
roles_program(Program): Role registry programcredentials_program(Program): Credentials program
Errors
Paused: Gateway operations are pausedUnauthorized: Authority does not have admin or bridge roleUidUsed: The provided UID has already been used for another operation
sync_addresses
Applies the changes to the address list for a specific foreign chain. It accepts or removes specific based on AddressChanges. The foreign addresses are used in validation checks for the bridge operations.
Only Admin and Bridge role holder can call this instruction.
pub struct AddressChanges {
pub investor_id: Bytes32,
pub chain_selector: u16,
pub changes: Vec<Action>,
}
pub enum Action {
Add(Vec<u8>),
Remove(Vec<u8>),
}Arguments
uid: [u8; 32]: Unique identifier for the operationaddress_changes: AddressChanges: Address changes to synchronize
Accounts
authority(Signer, mutable): Authority with admin, bridge, or gateway rolestate(Account): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
canonical_message_id(Account, mutable): PDA with seeds:["edge_gateway", "uid", uid]"edge_gateway": String constant for gateway state account"uid": String constant for UID accountsuid: 32-byte unique identifier from arguments- This account is created with
initas address syncs should only happen once
registry(Account): PDA owned by investor-registry program with seeds:["registry"]"registry": String constant for registry state account
addresses(Account, mutable): PDA owned by investor-registry program with seeds:["investor_addresses", address_changes.chain_selector.to_be_bytes(), address_changes.investor_id]"investor_addresses": String constant for investor address accountsaddress_changes.chain_selector.to_be_bytes(): 2-byte chain selector from address changes in big-endian formataddress_changes.investor_id: 32-byte investor identifier from address changes- Account ownership is validated via
seeds::program = investor_registry_program.key()
admin_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.admin]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.admin: 32-byte role identifier for admin role from roles account
bridge_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.bridge]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.bridge: 32-byte role identifier for bridge role from roles account
roles(Account): PDA owned by role-registry program with seeds:["roles"]"roles": String constant for the main roles configuration account
roles_program(Program): Role registry programinvestor_registry_program(Program): Investor registry programsystem_program(Program): System program
Errors
Paused: Gateway operations are pausedUnauthorized: Authority does not have admin or bridge roleUidUsed: The provided UID has already been used for another operation
set_instrument_mint
Creates a new instrument mint PDA that associates a mint account with an instrument identifier. This instruction initializes the mapping between a mint and its corresponding instrument. The instrument ID must match the one already configured in the token's access control.
Only Admin and Token Manager role holders can call this instruction.
Arguments
instrument_id: [u8; 32]: Instrument identifier to associate with mint
Accounts
authority(Signer, mutable): Authority with admin or token manager rolemint(Account, mutable): The mint account to associate with the instrumentinstrument_mint(Account, mutable): PDA with seeds:["instrument_mint", instrument_id]"instrument_mint": String constant for instrument mint mappinginstrument_id: Instrument identifier from arguments (32 bytes)- This account will be initialized with the mint address
state(Account): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
token_access_control(Account, mutable): PDA owned by security-token-hook program with seeds:["access_control", mint.key()]"access_control": String constant for access control accountmint.key(): Public key of the mint account (32 bytes)
admin_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.admin]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.admin: 32-byte role identifier for admin role from roles account
token_manager(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.token_manager]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.token_manager: 32-byte role identifier for token manager role from roles account
roles(Account): PDA owned by role-registry program with seeds:["roles"]"roles": String constant for the main roles configuration account
roles_program(Program): Role registry programhook_program(Program): Security token hook programsystem_program(Program): System program
Errors
Paused: Gateway operations are pausedUnauthorized: Authority does not have admin or token manager roleMismatchedInstrumentId: Provided instrument ID doesn't match the token's access control configuration
update_instrument_mint
Updates the instrument mint configuration by creating a new instrument mint PDA with a different instrument ID and closing the old one. This is used when an instrument needs to be reconfigured with a new identifier while maintaining the same mint account.
Only Admin and Token Manager role holders can call this instruction.
Arguments
_old_instrument_id: [u8; 32]: Previous instrument identifier (used for PDA seeds)new_instrument_id: [u8; 32]: New instrument identifier to associate with the mint
Accounts
authority(Signer, mutable): Authority with admin or token manager rolemint(Account, mutable): The mint account being updatedold_instrument_mint(Account, mutable): PDA with seeds:["instrument_mint", old_instrument_id]"instrument_mint": String constant for instrument mint mappingold_instrument_id: Previous instrument identifier (32 bytes)- This account will be closed and rent returned to authority
new_instrument_mint(Account, mutable): PDA with seeds:["instrument_mint", new_instrument_id]"instrument_mint": String constant for instrument mint mappingnew_instrument_id: New instrument identifier (32 bytes)- This account will be initialized with the mint address
state(Account): PDA with seeds:["edge_gateway"]"edge_gateway": String constant for gateway state account
token_access_control(Account, mutable): PDA owned by security-token-hook program with seeds:["access_control", mint.key()]"access_control": String constant for access control accountmint.key(): Public key of the mint account (32 bytes)
admin_role(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.admin]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.admin: 32-byte role identifier for admin role from roles account
token_manager(Account, optional): PDA owned by role-registry program with seeds:["assigned_roles", authority.key(), roles.token_manager]"assigned_roles": String constant for role assignment accountsauthority.key(): Public key of the authority account (32 bytes)roles.token_manager: 32-byte role identifier for token manager role from roles account
roles(Account): PDA owned by role-registry program with seeds:["roles"]"roles": String constant for the main roles configuration account
roles_program(Program): Role registry programhook_program(Program): Security token hook programsystem_program(Program): System program
Errors
Paused: Gateway operations are pausedUnauthorized: Authority does not have admin or token manager roleSameInstrumentIds: New instrument ID is the same as the current one
Protocol Pause
The pause/unpause functionality for protocol operations is managed by the role-registry program. All edge-gateway instructions (except for generate_uid and initialize) check the protocol pause state through the role registry before execution.
Tests
Integration tests are located in edge-gateway.ts
