You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The following is a proposal about how to build a connector that moves native ERC20 tokens in Aurora to NEAR. This proposal assumes that Aurora Cross Contract Calls are available. If this connector is done, then moving from Aurora to Ethereum (or any other blockchain) can be done by moving the tokens to NEAR, and then using NEAR <-> Ethereum bridge (when available).
Challenges
After multiple silos are introduced we should be able to use this connector to move tokens between different silos seamlessly.
Do we want the same token to have the same address on different silos? This is can be achieved for clones using CREATE2 opcode.
It should be possible to move tokens from Aurora to NEAR, or Aurora to Ethereum, or between silos with one NEAR transaction on NEAR side (potentially requiring one more transaction if it is)
See relevant comment about expected UX from the product perspective.
Design
The high-level design is similar to Fungible Token Connector. There is a Locker contract on the native domain, and there is a Factory contract on the target domain, that will create one token contract on the target domain per contract on the native domain.
Both the Locker and the Factory are decentralized and require no operator, so anyone can move any token (with their metadata). Each contract is described with pseudo-code below.
Since Aurora domains are just a smart contract on NEAR, they can interact with NEAR contract without requiring any complex bridge, but just the ability to execute cross-contract calls to retrieve and verify data on-chain.
Locker (locker): Smart contract on Aurora
Factory (factory.aurora) Smart contract on NEAR
Original Token (0xaabb) Smart contract on Aurora
Representative Token (0xaabb.factory.aurora) Smart contract on NEAR
Here is the expected workflow from scratch:
A new (custom) ERC20 token (0xaabb) is deployed to Aurora and becomes popular. (Popular enough so users want to use this token in DeFi markets outside of Aurora).
Alice (she stands for an arbitrary user with a NEAR wallet) should call factory.aurora.deploy_token("0xaabb") and this will automatically deploy and create the new contract 0xaabb.factory.aurora. This step needs to be executed exactly one time (following times will result in failure). During this step, the metadata of the token is moved automatically.
Aurora -> NEAR
Bob wants to send 12 0xaabb tokens from Aurora to NEAR.
Since a transfer call is required first Bob should give an allowance to locker contract: 0xaabb.approve("locker", 12)
Bob locks the tokens on Aurora, specifying the target on NEAR, by calling: locker.lock("0xaabb", 12, target="bob.near"). In this step, an event is emitted, and a record is stored in the state with the information provided: (token_address, amount, target_account_id). The tokens are transferred from Bob's address to locker address.
Bob mints the tokens on NEAR calling 0xaabb.factory.aurora.mint(amount, target_account_id).
5.1. On the inside this function calls locker.claim(token_address, amount, target_account_id) Notice this is a call from a NEAR smart contract to an Aurora smart contract which is possible. When claim is called, it verifies that the event is stored in memory, and in case it is, then it is deleted and returns true, otherwise it returns false.
5.2. If calling claim returns true then that exact amount is minted to the target account id. Notice that anyone can call mint on behalf of the target_account_id.
At this point, Bob already has the tokens on NEAR ecosystem and can exchange them freely using their NEAR wallet.
Single transfer
Steps 4) and 5) can be done in a single transfer, that can be even signed using an aurora wallet (for example MetaMask). This will be possible after Aurora Cross Contract Calls are in place. The operations look as follows:
Bob submits an aurora tx that is routed to async.aurora.
async.aurora routes the transaction to aurora.submit
locker.lock is invoked with relevant arguments, and it returns the promises that need to be executed next.
A promise to 0xabb.factory.aurora.mint is created with relevant arguments.
UX and Security considerations
It is important that the bundle tx doesn't fail in the middle (i.e either it succeeds or fail without any implication on the state). To achieve this we must properly estimate the amount of gas required per step, and make sure the gas attached is enough to cover the full trip. This requires exposing gas_used and gas_attached as precompiles in aurora evm.
If the transaction fails in the middle it is recoverable but provides a bad experience or very complex UI which shouldn't be necessary.
NEAR -> Aurora
Carol received 10 0xaabb from Bob on NEAR and wants to send them to Aurora. Let's say its Aurora wallet is 0xca401.
Carol calls 0xaabb.factory.aurora.burn(10, target="0xca401"). This burns the tokens and triggers a call that unlocks them on Aurora side.
A cross contract call is automatically created: locker.unlock(token="0xaabb", 10, target="0xca401") that will transfer this tokens to carol account on Aurora
This will work naturally in a single NEAR transaction. Notice this step will require that Carol has a NEAR wallet.
Aurora -> Ethereum
The main point of this section is to provide a generic interface that allows calling ft_transfer_call automatically after minting on behalf of the user. The mint function on 0xaabb.factory.aurora should have the following interface:
This will allow minting tokens on NEAR, and automatically locking them on EthereumLockerOnNEAR.
Note: It is important to properly verify the amount of gas attached is enough to cover the whole transaction.
Contracts
Locker on Aurora
classLocker:
AURORA_ASYNC='async.aurora'def__init__(self, near_factory: AccountId):
""" @near_factory: Account id pointing to the factory on NEAR. """self.near_factory=near_factoryself._internal_nonce=0# Map with balance for each user for each token that hasn't been# claimed on NEARself.not_claimed=map(nonce-> (token, amount, receipient))
deflock(self,
token: Addres,
amount: u256,
receipient: ReceipientAccountId) ->Vec<Promise>:
# Transfer the tokens from the user to the lockerself.token.transferFrom(msg.sender, amount, this)
nonce=self._internal_nonceself._internal_nonce+=1self.not_claimed[nonce] = (token, amount, receipient)
# Create a promise to call `token.factory.aurora.mint(...)`promise=PromiseDescription()
return [promise]
deflock_async(self, *args) ->Vec<Promise>:
""" Same as `lock` but only executes if it was called from `async.aurora` """assertenv.predecessor_account_id() ==Locker.AURORA_ASYNC# Make sure the attached gas is enough to cover for the whole transactionassertverify_gas(args)
returnself.lock(*args)
defclaim(self, token: Address, nonce: u256) ->Option<(amount, receipient)>:
# This can only be called from the account of the tokentoken_account_id_on_near=f'{token}.{self.near_factory}'assertenv.predecessor_account_id() ==token_account_id_on_near
(expected_token, amount, receipient) =self.not_claimed[nonce]
assertexpected_token=tokendelself.not_claimed[nonce]
return (amount, recipient)
defunlock(self, token: Address, amount: u256, receipient: Address):
# This can only be called from the account of the tokentoken_account_id_on_near=f'{token}.{self.near_factory}'assertenv.predecessor_account_id() ==token_account_id_on_near# Unlock the tokens for this recipienttoken.transfer(amount, recipient)
enumReceipientAccountId {
Mint(receipient),
MintCall(receipient, target, gas, arguments)
}
Factory on NEAR
classFactory:
# Inline bytecode of each tokenTokenByteCode=load_from_file!()
def__init__(self, aurora: AccountId, locker: Address)
# Keep track of the locker address and aurora account idself.aurora=auroraself.locker=lockerself.deployed_tokens=set()
defdeploy_token(self, token: Address):
""" Deploy a new token contract with the provided Address. This is trustless and doesn't require any sort of validation. """asserttokennotinself.deployed_tokensself.deployed_tokens.add(token)
newPromise(f'{token}.{env.current_account_id()}')
.deploy()
.init(self.aurora, self.locker)
Tokens on NEAR
classToken(NEP141):
def__init__(self,
token: Address,
aurora: AccountId,
locker: Address):
self.token=tokenself.aurora=auroraself.locker=locker# Automatic fetch and update metadataself.update_metadata()
defmint(self, nonce: u256):
(amount, receipient) =self.aurora.call(
self.locker.claim(self.token, nonce)
)
self._mint(receipient.recipient, amount)
# Check if the recipient specifies a transfer call and execute itifrequires_transfer_call(receipient):
self.transfer_call(...)
defburn(self, amount: u256, receipient: Address):
self._burn(env.predecessor_account_id(), amount)
self.aurora.call(
self.locker.unlock(self.token, amount, receipient)
)
defupdate_metadata(self):
metadata=self.aurora.call(
self.token.get_metadata()
)
self._set_metadata(metadata)
In this proposal
Additional considerations
Admin keys (pause and upgrade)
These contracts are trustless and decentralized. However, there should be an operator with the ability to upgrade and pause the contracts in case of any emergence. To prevent any malicious intentions, these controllers must be behind a DAO or at least a multi-sig.
In case there is an emergency on the token contracts, they all should be paused and upgraded at once. For Token contract I propose an interface where the binary is uploaded once to the factory, and all tokens can upgrade automatically by calling a single function that fetches the binary, and auto-upgrade.
Token storage requirements
The Tokens should not impose any storage requirement on the user. Storage requirements usually have affected User Experience while not providing huge benefits. If the contract is running out of balance for storage renting, is still the responsibility of the user to provide enough balance to cover the storage.
We should introduce gas_used and gas_attached precompiles in aurora-engine.
Security considerations
This design assumes there is no feasible way to find a collision between different Aurora addresses which is given for granted. We need to make sure this is still the case after introducing implicit account ids.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
The following is a proposal about how to build a connector that moves native ERC20 tokens in Aurora to NEAR. This proposal assumes that Aurora Cross Contract Calls are available. If this connector is done, then moving from Aurora to Ethereum (or any other blockchain) can be done by moving the tokens to NEAR, and then using NEAR <-> Ethereum bridge (when available).
Challenges
Design
The high-level design is similar to Fungible Token Connector. There is a
Locker
contract on the native domain, and there is aFactory
contract on the target domain, that will create one token contract on the target domain per contract on the native domain.Both the
Locker
and theFactory
are decentralized and require no operator, so anyone can move any token (with their metadata). Each contract is described with pseudo-code below.Since Aurora domains are just a smart contract on NEAR, they can interact with NEAR contract without requiring any complex bridge, but just the ability to execute cross-contract calls to retrieve and verify data on-chain.
locker
): Smart contract on Aurorafactory.aurora
) Smart contract on NEAR0xaabb
) Smart contract on Aurora0xaabb.factory.aurora
) Smart contract on NEARHere is the expected workflow from scratch:
0xaabb
) is deployed to Aurora and becomes popular. (Popular enough so users want to use this token in DeFi markets outside of Aurora).factory.aurora.deploy_token("0xaabb")
and this will automatically deploy and create the new contract0xaabb.factory.aurora
. This step needs to be executed exactly one time (following times will result in failure). During this step, the metadata of the token is moved automatically.Aurora -> NEAR
Bob wants to send 12
0xaabb
tokens from Aurora to NEAR.locker
contract:0xaabb.approve("locker", 12)
locker.lock("0xaabb", 12, target="bob.near")
. In this step, an event is emitted, and a record is stored in the state with the information provided:(token_address, amount, target_account_id)
. The tokens are transferred from Bob's address tolocker
address.0xaabb.factory.aurora.mint(amount, target_account_id)
.5.1. On the inside this function calls
locker.claim(token_address, amount, target_account_id)
Notice this is a call from a NEAR smart contract to an Aurora smart contract which is possible. Whenclaim
is called, it verifies that the event is stored in memory, and in case it is, then it is deleted and returnstrue
, otherwise it returnsfalse
.5.2. If calling
claim
returnstrue
then that exact amount is minted to the target account id. Notice that anyone can callmint
on behalf of thetarget_account_id
.At this point, Bob already has the tokens on NEAR ecosystem and can exchange them freely using their NEAR wallet.
Single transfer
Steps 4) and 5) can be done in a single transfer, that can be even signed using an aurora wallet (for example MetaMask). This will be possible after Aurora Cross Contract Calls are in place. The operations look as follows:
Bob submits an aurora tx that is routed to
async.aurora
.async.aurora
routes the transaction toaurora.submit
locker.lock
is invoked with relevant arguments, and it returns the promises that need to be executed next.0xabb.factory.aurora.mint
is created with relevant arguments.UX and Security considerations
It is important that the bundle tx doesn't fail in the middle (i.e either it succeeds or fail without any implication on the state). To achieve this we must properly estimate the amount of gas required per step, and make sure the gas attached is enough to cover the full trip. This requires exposing
gas_used
andgas_attached
as precompiles in auroraevm
.If the transaction fails in the middle it is recoverable but provides a bad experience or very complex UI which shouldn't be necessary.
NEAR -> Aurora
Carol received 10
0xaabb
from Bob on NEAR and wants to send them to Aurora. Let's say its Aurora wallet is0xca401
.0xaabb.factory.aurora.burn(10, target="0xca401")
. This burns the tokens and triggers a call that unlocks them on Aurora side.locker.unlock(token="0xaabb", 10, target="0xca401")
that will transfer this tokens to carol account on AuroraThis will work naturally in a single NEAR transaction. Notice this step will require that Carol has a NEAR wallet.
Aurora -> Ethereum
The main point of this section is to provide a generic interface that allows calling ft_transfer_call automatically after minting on behalf of the user. The mint function on
0xaabb.factory.aurora
should have the following interface:This will allow minting tokens on NEAR, and automatically locking them on
EthereumLockerOnNEAR
.Note: It is important to properly verify the amount of gas attached is enough to cover the whole transaction.
Contracts
Locker on Aurora
Factory on NEAR
Tokens on NEAR
In this proposal
Additional considerations
Admin keys (pause and upgrade)
These contracts are trustless and decentralized. However, there should be an operator with the ability to upgrade and pause the contracts in case of any emergence. To prevent any malicious intentions, these controllers must be behind a DAO or at least a multi-sig.
In case there is an emergency on the token contracts, they all should be paused and upgraded at once. For
Token
contract I propose an interface where the binary is uploaded once to the factory, and all tokens can upgrade automatically by calling a single function that fetches the binary, and auto-upgrade.Token storage requirements
The Tokens should not impose any storage requirement on the user. Storage requirements usually have affected User Experience while not providing huge benefits. If the contract is running out of balance for storage renting, is still the responsibility of the user to provide enough balance to cover the storage.
Multi Token
We could store all tokens on NEAR in a single contract using Multi Token Interface (NEP-246).
Gas precompiles
We should introduce
gas_used
andgas_attached
precompiles in aurora-engine.Security considerations
This design assumes there is no feasible way to find a collision between different Aurora addresses which is given for granted. We need to make sure this is still the case after introducing implicit account ids.
Beta Was this translation helpful? Give feedback.
All reactions