basic token bridge design (#71)
* wip: bridge messages * feature: add a WIP bridge contract for arbitrary tokens * ref: break out shared tools for devs into UsingOptics.sol * chore: add a type conversion util method * refactor: move token registry to its own contract * refactor: improve isNative using extcodsize * refactor: update slip44 to domain to match new nomenclature * doc: remove outdated TODO note * doc: add a note about the token registry's purpose * refactor: break out type casting to its own library * refactor: break Bridge code out into its own package * chore: make deps explicit in package.json files * refactor: improve clarity of mustHaveRemote * rename xfer to transfer (#89) * bug: fix a copy-paste error in bridge types Co-authored-by: Anna Carroll <anna-carroll@users.noreply.github.com>buddies-main-deployment
parent
c57b9ded00
commit
9ff8490afd
@ -0,0 +1,79 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import {ERC20} from "./OZERC20.sol"; |
||||
|
||||
import {TypeCasts} from "@celo-org/optics-sol/contracts/UsingOptics.sol"; |
||||
|
||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; |
||||
|
||||
interface BridgeTokenI { |
||||
function name() external returns (string memory); |
||||
|
||||
function symbol() external returns (string memory); |
||||
|
||||
function decimals() external returns (uint8); |
||||
|
||||
function burn(address from, uint256 amnt) external; |
||||
|
||||
function mint(address to, uint256 amnt) external; |
||||
|
||||
function setDetails( |
||||
bytes32 _name, |
||||
bytes32 _symbol, |
||||
uint8 _decimals |
||||
) external; |
||||
} |
||||
|
||||
contract BridgeToken is BridgeTokenI, Ownable, ERC20 { |
||||
/** |
||||
* @dev Returns the name of the token. |
||||
*/ |
||||
function name() public view override returns (string memory) { |
||||
return _name; |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the symbol of the token, usually a shorter version of the |
||||
* name. |
||||
*/ |
||||
function symbol() public view override returns (string memory) { |
||||
return _symbol; |
||||
} |
||||
|
||||
/** |
||||
* @dev Returns the number of decimals used to get its user representation. |
||||
* For example, if `decimals` equals `2`, a balance of `505` tokens should |
||||
* be displayed to a user as `5,05` (`505 / 10 ** 2`). |
||||
* |
||||
* Tokens usually opt for a value of 18, imitating the relationship between |
||||
* Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is |
||||
* called. |
||||
* |
||||
* NOTE: This information is only used for _display_ purposes: it in |
||||
* no way affects any of the arithmetic of the contract, including |
||||
* {IERC20-balanceOf} and {IERC20-transfer}. |
||||
*/ |
||||
function decimals() public view override returns (uint8) { |
||||
return _decimals; |
||||
} |
||||
|
||||
function burn(address from, uint256 amnt) external override onlyOwner { |
||||
_burn(from, amnt); |
||||
} |
||||
|
||||
function mint(address to, uint256 amnt) external override onlyOwner { |
||||
_mint(to, amnt); |
||||
} |
||||
|
||||
function setDetails( |
||||
bytes32 _newName, |
||||
bytes32 _newSymbol, |
||||
uint8 _newDecimals |
||||
) external override onlyOwner { |
||||
// careful with naming convention change here |
||||
_name = TypeCasts.coerceString(_newName); |
||||
_symbol = TypeCasts.coerceString(_newSymbol); |
||||
_decimals = _newDecimals; |
||||
} |
||||
} |
@ -0,0 +1,328 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity >=0.6.0 <0.8.0; |
||||
|
||||
// This is modified from "@openzeppelin/contracts/token/ERC20/IERC20.sol" |
||||
// Modifications were made to make the _name, _symbol, and _decimals fields |
||||
// internal instead of private. Getters for them were removed to silence |
||||
// solidity inheritance issues |
||||
|
||||
import "@openzeppelin/contracts/GSN/Context.sol"; |
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
||||
import "@openzeppelin/contracts/math/SafeMath.sol"; |
||||
|
||||
/** |
||||
* @dev Implementation of the {IERC20} interface. |
||||
* |
||||
* This implementation is agnostic to the way tokens are created. This means |
||||
* that a supply mechanism has to be added in a derived contract using {_mint}. |
||||
* For a generic mechanism see {ERC20PresetMinterPauser}. |
||||
* |
||||
* TIP: For a detailed writeup see our guide |
||||
* https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How |
||||
* to implement supply mechanisms]. |
||||
* |
||||
* We have followed general OpenZeppelin guidelines: functions revert instead |
||||
* of returning `false` on failure. This behavior is nonetheless conventional |
||||
* and does not conflict with the expectations of ERC20 applications. |
||||
* |
||||
* Additionally, an {Approval} event is emitted on calls to {transferFrom}. |
||||
* This allows applications to reconstruct the allowance for all accounts just |
||||
* by listening to said events. Other implementations of the EIP may not emit |
||||
* these events, as it isn't required by the specification. |
||||
* |
||||
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance} |
||||
* functions have been added to mitigate the well-known issues around setting |
||||
* allowances. See {IERC20-approve}. |
||||
*/ |
||||
contract ERC20 is Context, IERC20 { |
||||
using SafeMath for uint256; |
||||
|
||||
mapping(address => uint256) private _balances; |
||||
|
||||
mapping(address => mapping(address => uint256)) private _allowances; |
||||
|
||||
uint256 private _totalSupply; |
||||
|
||||
string internal _name; |
||||
string internal _symbol; |
||||
uint8 internal _decimals; |
||||
|
||||
/** |
||||
* @dev See {IERC20-totalSupply}. |
||||
*/ |
||||
function totalSupply() public view override returns (uint256) { |
||||
return _totalSupply; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IERC20-balanceOf}. |
||||
*/ |
||||
function balanceOf(address account) public view override returns (uint256) { |
||||
return _balances[account]; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IERC20-transfer}. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `recipient` cannot be the zero address. |
||||
* - the caller must have a balance of at least `amount`. |
||||
*/ |
||||
function transfer(address recipient, uint256 amount) |
||||
public |
||||
virtual |
||||
override |
||||
returns (bool) |
||||
{ |
||||
_transfer(_msgSender(), recipient, amount); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IERC20-allowance}. |
||||
*/ |
||||
function allowance(address owner, address spender) |
||||
public |
||||
view |
||||
virtual |
||||
override |
||||
returns (uint256) |
||||
{ |
||||
return _allowances[owner][spender]; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IERC20-approve}. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `spender` cannot be the zero address. |
||||
*/ |
||||
function approve(address spender, uint256 amount) |
||||
public |
||||
virtual |
||||
override |
||||
returns (bool) |
||||
{ |
||||
_approve(_msgSender(), spender, amount); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {IERC20-transferFrom}. |
||||
* |
||||
* Emits an {Approval} event indicating the updated allowance. This is not |
||||
* required by the EIP. See the note at the beginning of {ERC20}. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `sender` and `recipient` cannot be the zero address. |
||||
* - `sender` must have a balance of at least `amount`. |
||||
* - the caller must have allowance for ``sender``'s tokens of at least |
||||
* `amount`. |
||||
*/ |
||||
function transferFrom( |
||||
address sender, |
||||
address recipient, |
||||
uint256 amount |
||||
) public virtual override returns (bool) { |
||||
_transfer(sender, recipient, amount); |
||||
_approve( |
||||
sender, |
||||
_msgSender(), |
||||
_allowances[sender][_msgSender()].sub( |
||||
amount, |
||||
"ERC20: transfer amount exceeds allowance" |
||||
) |
||||
); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @dev Atomically increases the allowance granted to `spender` by the caller. |
||||
* |
||||
* This is an alternative to {approve} that can be used as a mitigation for |
||||
* problems described in {IERC20-approve}. |
||||
* |
||||
* Emits an {Approval} event indicating the updated allowance. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `spender` cannot be the zero address. |
||||
*/ |
||||
function increaseAllowance(address spender, uint256 addedValue) |
||||
public |
||||
virtual |
||||
returns (bool) |
||||
{ |
||||
_approve( |
||||
_msgSender(), |
||||
spender, |
||||
_allowances[_msgSender()][spender].add(addedValue) |
||||
); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @dev Atomically decreases the allowance granted to `spender` by the caller. |
||||
* |
||||
* This is an alternative to {approve} that can be used as a mitigation for |
||||
* problems described in {IERC20-approve}. |
||||
* |
||||
* Emits an {Approval} event indicating the updated allowance. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `spender` cannot be the zero address. |
||||
* - `spender` must have allowance for the caller of at least |
||||
* `subtractedValue`. |
||||
*/ |
||||
function decreaseAllowance(address spender, uint256 subtractedValue) |
||||
public |
||||
virtual |
||||
returns (bool) |
||||
{ |
||||
_approve( |
||||
_msgSender(), |
||||
spender, |
||||
_allowances[_msgSender()][spender].sub( |
||||
subtractedValue, |
||||
"ERC20: decreased allowance below zero" |
||||
) |
||||
); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @dev Moves tokens `amount` from `sender` to `recipient`. |
||||
* |
||||
* This is internal function is equivalent to {transfer}, and can be used to |
||||
* e.g. implement automatic token fees, slashing mechanisms, etc. |
||||
* |
||||
* Emits a {Transfer} event. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `sender` cannot be the zero address. |
||||
* - `recipient` cannot be the zero address. |
||||
* - `sender` must have a balance of at least `amount`. |
||||
*/ |
||||
function _transfer( |
||||
address sender, |
||||
address recipient, |
||||
uint256 amount |
||||
) internal virtual { |
||||
require(sender != address(0), "ERC20: transfer from the zero address"); |
||||
require(recipient != address(0), "ERC20: transfer to the zero address"); |
||||
|
||||
_beforeTokenTransfer(sender, recipient, amount); |
||||
|
||||
_balances[sender] = _balances[sender].sub( |
||||
amount, |
||||
"ERC20: transfer amount exceeds balance" |
||||
); |
||||
_balances[recipient] = _balances[recipient].add(amount); |
||||
emit Transfer(sender, recipient, amount); |
||||
} |
||||
|
||||
/** @dev Creates `amount` tokens and assigns them to `account`, increasing |
||||
* the total supply. |
||||
* |
||||
* Emits a {Transfer} event with `from` set to the zero address. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `to` cannot be the zero address. |
||||
*/ |
||||
function _mint(address account, uint256 amount) internal virtual { |
||||
require(account != address(0), "ERC20: mint to the zero address"); |
||||
|
||||
_beforeTokenTransfer(address(0), account, amount); |
||||
|
||||
_totalSupply = _totalSupply.add(amount); |
||||
_balances[account] = _balances[account].add(amount); |
||||
emit Transfer(address(0), account, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Destroys `amount` tokens from `account`, reducing the |
||||
* total supply. |
||||
* |
||||
* Emits a {Transfer} event with `to` set to the zero address. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `account` cannot be the zero address. |
||||
* - `account` must have at least `amount` tokens. |
||||
*/ |
||||
function _burn(address account, uint256 amount) internal virtual { |
||||
require(account != address(0), "ERC20: burn from the zero address"); |
||||
|
||||
_beforeTokenTransfer(account, address(0), amount); |
||||
|
||||
_balances[account] = _balances[account].sub( |
||||
amount, |
||||
"ERC20: burn amount exceeds balance" |
||||
); |
||||
_totalSupply = _totalSupply.sub(amount); |
||||
emit Transfer(account, address(0), amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. |
||||
* |
||||
* This internal function is equivalent to `approve`, and can be used to |
||||
* e.g. set automatic allowances for certain subsystems, etc. |
||||
* |
||||
* Emits an {Approval} event. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `owner` cannot be the zero address. |
||||
* - `spender` cannot be the zero address. |
||||
*/ |
||||
function _approve( |
||||
address owner, |
||||
address spender, |
||||
uint256 amount |
||||
) internal virtual { |
||||
require(owner != address(0), "ERC20: approve from the zero address"); |
||||
require(spender != address(0), "ERC20: approve to the zero address"); |
||||
|
||||
_allowances[owner][spender] = amount; |
||||
emit Approval(owner, spender, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Sets {decimals} to a value other than the default one of 18. |
||||
* |
||||
* WARNING: This function should only be called from the constructor. Most |
||||
* applications that interact with token contracts will not expect |
||||
* {decimals} to ever change, and may work incorrectly if it does. |
||||
*/ |
||||
function _setupDecimals(uint8 decimals_) internal { |
||||
_decimals = decimals_; |
||||
} |
||||
|
||||
/** |
||||
* @dev Hook that is called before any transfer of tokens. This includes |
||||
* minting and burning. |
||||
* |
||||
* Calling conditions: |
||||
* |
||||
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens |
||||
* will be to transferred to `to`. |
||||
* - when `from` is zero, `amount` tokens will be minted for `to`. |
||||
* - when `to` is zero, `amount` of ``from``'s tokens will be burned. |
||||
* - `from` and `to` are never both zero. |
||||
* |
||||
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. |
||||
*/ |
||||
function _beforeTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 amount |
||||
) internal virtual {} |
||||
} |
@ -0,0 +1,160 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import {BridgeMessage} from "./Types.sol"; |
||||
import {TokenRegistry} from "./TokenRegistry.sol"; |
||||
import {BridgeTokenI, BridgeToken} from "./BridgeToken.sol"; |
||||
|
||||
import { |
||||
TypeCasts, |
||||
OpticsHandlerI |
||||
} from "@celo-org/optics-sol/contracts/UsingOptics.sol"; |
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
||||
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; |
||||
|
||||
contract BridgeRouter is OpticsHandlerI, TokenRegistry { |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
using BridgeMessage for bytes29; |
||||
using SafeERC20 for IERC20; |
||||
|
||||
mapping(uint32 => bytes32) internal remotes; |
||||
|
||||
constructor() TokenRegistry() {} |
||||
|
||||
function enrollRemote(uint32 _origin, bytes32 _router) external onlyOwner { |
||||
remotes[_origin] = _router; |
||||
} |
||||
|
||||
function mustHaveRemote(uint32 _domain) |
||||
internal |
||||
view |
||||
returns (bytes32 _remote) |
||||
{ |
||||
_remote = remotes[_domain]; |
||||
require(_remote != bytes32(0), "!remote"); |
||||
} |
||||
|
||||
function isRemoteRouter(uint32 _origin, bytes32 _router) |
||||
internal |
||||
view |
||||
returns (bool) |
||||
{ |
||||
return remotes[_origin] == _router; |
||||
} |
||||
|
||||
modifier onlyRemoteRouter(uint32 _origin, bytes32 _router) { |
||||
require(isRemoteRouter(_origin, _router)); |
||||
_; |
||||
} |
||||
|
||||
function handle( |
||||
uint32 _origin, |
||||
bytes32 _sender, |
||||
bytes memory _message |
||||
) |
||||
external |
||||
override |
||||
onlyReplica |
||||
onlyRemoteRouter(_origin, _sender) |
||||
returns (bytes memory) |
||||
{ |
||||
bytes29 _msg = _message.ref(0).mustBeMessage(); |
||||
bytes29 _tokenId = _msg.tokenId(); |
||||
bytes29 _action = _msg.action(); |
||||
if (_action.isTransfer()) { |
||||
return handleTransfer(_tokenId, _action); |
||||
} |
||||
if (_action.isDetails()) { |
||||
return handleDetails(_tokenId, _action); |
||||
} |
||||
require(false, "!action"); |
||||
return hex""; |
||||
} |
||||
|
||||
function handleTransfer(bytes29 _tokenId, bytes29 _action) |
||||
internal |
||||
typeAssert(_tokenId, BridgeMessage.Types.TokenId) |
||||
typeAssert(_action, BridgeMessage.Types.Transfer) |
||||
returns (bytes memory) |
||||
{ |
||||
IERC20 _token = ensureToken(_tokenId); |
||||
|
||||
if (isNative(_token)) { |
||||
_token.safeTransfer(_action.evmRecipient(), _action.amnt()); |
||||
} else { |
||||
downcast(_token).mint(_action.evmRecipient(), _action.amnt()); |
||||
} |
||||
|
||||
return hex""; |
||||
} |
||||
|
||||
function handleDetails(bytes29 _tokenId, bytes29 _action) |
||||
internal |
||||
typeAssert(_tokenId, BridgeMessage.Types.TokenId) |
||||
typeAssert(_action, BridgeMessage.Types.Details) |
||||
returns (bytes memory) |
||||
{ |
||||
IERC20 _token = ensureToken(_tokenId); |
||||
require(!isNative(_token), "!repr"); |
||||
|
||||
downcast(_token).setDetails( |
||||
_action.name(), |
||||
_action.symbol(), |
||||
_action.decimals() |
||||
); |
||||
|
||||
return hex""; |
||||
} |
||||
|
||||
function send( |
||||
address _token, |
||||
uint32 _destination, |
||||
bytes32 _recipient, |
||||
uint256 _amnt |
||||
) external { |
||||
bytes32 _remote = mustHaveRemote(_destination); |
||||
IERC20 _tok = IERC20(_token); |
||||
|
||||
if (isNative(_tok)) { |
||||
_tok.safeTransferFrom(msg.sender, address(this), _amnt); |
||||
} else { |
||||
downcast(_tok).burn(msg.sender, _amnt); |
||||
} |
||||
|
||||
TokenId memory _tokId = tokenIdFor(_token); |
||||
bytes29 _tokenId = |
||||
BridgeMessage.formatTokenId(_tokId.domain, _tokId.id); |
||||
bytes29 _action = BridgeMessage.formatTransfer(_recipient, _amnt); |
||||
|
||||
home.enqueue( |
||||
_destination, |
||||
_remote, |
||||
BridgeMessage.formatMessage(_tokenId, _action) |
||||
); |
||||
} |
||||
|
||||
function updateDetails(address _token, uint32 _destination) external { |
||||
bytes32 _remote = mustHaveRemote(_destination); |
||||
BridgeTokenI _tok = BridgeTokenI(_token); |
||||
|
||||
TokenId memory _tokId = tokenIdFor(_token); |
||||
bytes29 _tokenId = |
||||
BridgeMessage.formatTokenId(_tokId.domain, _tokId.id); |
||||
|
||||
bytes29 _action = |
||||
BridgeMessage.formatDetails( |
||||
TypeCasts.coerceBytes32(_tok.name()), |
||||
TypeCasts.coerceBytes32(_tok.symbol()), |
||||
_tok.decimals() |
||||
); |
||||
|
||||
home.enqueue( |
||||
_destination, |
||||
_remote, |
||||
BridgeMessage.formatMessage(_tokenId, _action) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,161 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import {BridgeMessage} from "./Types.sol"; |
||||
import {BridgeTokenI, BridgeToken} from "./BridgeToken.sol"; |
||||
|
||||
import { |
||||
UsingOptics, |
||||
TypeCasts |
||||
} from "@celo-org/optics-sol/contracts/UsingOptics.sol"; |
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
||||
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
|
||||
// How the token registry works: |
||||
// We sort token types as "representation" or "native". |
||||
// Native means a contract that is originally deployed on this chain |
||||
// Representation (repr) means a token that originates on some other chain |
||||
// |
||||
// We identify tokens by a 4 byte chain ID and a 32 byte identifier in that |
||||
// chain's native address format. We leave upgradability and management of |
||||
// that identity to the token's deployers. |
||||
// |
||||
// When the router handles an incoming message, it determines whether the |
||||
// transfer is for a native asset. If not, it checks for an existing |
||||
// representation. If no such representation exists, it deploys a new |
||||
// representation token contract. It then stores the relationship in the |
||||
// "reprToCanonical" and "canonicalToRepr" mappings to ensure we can always |
||||
// perform a lookup in either direction |
||||
// |
||||
// Note that native tokens should NEVER be represented in these lookup tables. |
||||
|
||||
contract TokenRegistry is UsingOptics { |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
using BridgeMessage for bytes29; |
||||
|
||||
// simplifies the mapping type if we do it this way |
||||
struct TokenId { |
||||
uint32 domain; |
||||
bytes32 id; |
||||
} |
||||
|
||||
// We should be able to deploy a new token on demand |
||||
address tokenTemplate; |
||||
|
||||
function createClone(address _target) internal returns (address result) { |
||||
bytes20 targetBytes = bytes20(_target); |
||||
assembly { |
||||
let clone := mload(0x40) |
||||
mstore( |
||||
clone, |
||||
0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 |
||||
) |
||||
mstore(add(clone, 0x14), targetBytes) |
||||
mstore( |
||||
add(clone, 0x28), |
||||
0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 |
||||
) |
||||
result := create(0, clone, 0x37) |
||||
} |
||||
} |
||||
|
||||
// lookup tables for tokens. |
||||
|
||||
// Map the local address to the token ID. |
||||
mapping(address => TokenId) internal reprToCanonical; |
||||
|
||||
// Map the hash of the tightly-packed token ID to the address |
||||
// of the local representation. |
||||
// |
||||
// If the token is native, this MUST be address(0). |
||||
mapping(bytes32 => address) internal canonicalToRepr; |
||||
|
||||
constructor() { |
||||
tokenTemplate = address(new BridgeToken()); |
||||
} |
||||
|
||||
function setTemplate(address _newTemplate) external onlyOwner { |
||||
tokenTemplate = _newTemplate; |
||||
} |
||||
|
||||
modifier typeAssert(bytes29 _view, BridgeMessage.Types _t) { |
||||
_view.assertType(uint40(_t)); |
||||
_; |
||||
} |
||||
|
||||
function downcast(IERC20 _token) internal pure returns (BridgeTokenI) { |
||||
return BridgeTokenI(address(_token)); |
||||
} |
||||
|
||||
function tokenIdFor(address _token) |
||||
internal |
||||
view |
||||
returns (TokenId memory _id) |
||||
{ |
||||
_id = reprToCanonical[_token]; |
||||
if (_id.domain == 0) { |
||||
_id.domain = home.originDomain(); |
||||
_id.id = TypeCasts.addressToBytes32(_token); |
||||
} |
||||
} |
||||
|
||||
function isNative(IERC20 _token) internal view returns (bool) { |
||||
address _addr = address(_token); |
||||
// If this contract deployed it, it isn't native. |
||||
if (reprToCanonical[_addr].domain != 0) { |
||||
return false; |
||||
} |
||||
// Avoid returning true for non-existant contracts |
||||
uint256 _codeSize; |
||||
assembly { |
||||
_codeSize := extcodesize(_addr) |
||||
} |
||||
return _codeSize != 0; |
||||
} |
||||
|
||||
function reprFor(bytes29 _tokenId) |
||||
internal |
||||
view |
||||
typeAssert(_tokenId, BridgeMessage.Types.TokenId) |
||||
returns (IERC20) |
||||
{ |
||||
return IERC20(canonicalToRepr[_tokenId.keccak()]); |
||||
} |
||||
|
||||
function deployToken(bytes29 _tokenId) |
||||
internal |
||||
typeAssert(_tokenId, BridgeMessage.Types.TokenId) |
||||
returns (address _token) |
||||
{ |
||||
bytes32 _idHash = _tokenId.keccak(); |
||||
_token = createClone(tokenTemplate); |
||||
|
||||
// Initial details are set to a hash of the ID |
||||
BridgeTokenI(_token).setDetails(_idHash, _idHash, 18); |
||||
|
||||
reprToCanonical[_token].domain = _tokenId.domain(); |
||||
reprToCanonical[_token].id = _tokenId.id(); |
||||
canonicalToRepr[_idHash] = _token; |
||||
} |
||||
|
||||
function ensureToken(bytes29 _tokenId) |
||||
internal |
||||
typeAssert(_tokenId, BridgeMessage.Types.TokenId) |
||||
returns (IERC20) |
||||
{ |
||||
// Native |
||||
if (_tokenId.domain() == home.originDomain()) { |
||||
return IERC20(_tokenId.evmId()); |
||||
} |
||||
|
||||
// Repr |
||||
address _local = canonicalToRepr[_tokenId.keccak()]; |
||||
if (_local == address(0)) { |
||||
// DEPLO |
||||
_local = deployToken(_tokenId); |
||||
} |
||||
return IERC20(_local); |
||||
} |
||||
} |
@ -0,0 +1,237 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
|
||||
library BridgeMessage { |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
|
||||
uint256 private constant TOKEN_ID_LEN = 36; |
||||
uint256 private constant TRANSFER_LEN = 64; |
||||
uint256 private constant DETAILS_LEN = 65; |
||||
|
||||
enum Types { |
||||
Invalid, // 0 |
||||
Transfer, // 1 |
||||
Details, // 2 |
||||
TokenId, // 3 |
||||
Message // 4 |
||||
} |
||||
|
||||
modifier typeAssert(bytes29 _view, Types _t) { |
||||
_view.assertType(uint40(_t)); |
||||
_; |
||||
} |
||||
|
||||
function messageType(bytes29 _view) internal pure returns (Types) { |
||||
return Types(uint8(_view.typeOf())); |
||||
} |
||||
|
||||
function isTransfer(bytes29 _view) internal pure returns (bool) { |
||||
return messageType(_view) == Types.Transfer; |
||||
} |
||||
|
||||
function isDetails(bytes29 _view) internal pure returns (bool) { |
||||
return messageType(_view) == Types.Details; |
||||
} |
||||
|
||||
function formatTransfer(bytes32 _to, uint256 _amnt) |
||||
internal |
||||
pure |
||||
returns (bytes29) |
||||
{ |
||||
return mustBeTransfer(abi.encodePacked(_to, _amnt).ref(0)); |
||||
} |
||||
|
||||
function formatDetails( |
||||
bytes32 _name, |
||||
bytes32 _symbol, |
||||
uint8 _decimals |
||||
) internal pure returns (bytes29) { |
||||
return |
||||
mustBeDetails(abi.encodePacked(_name, _symbol, _decimals).ref(0)); |
||||
} |
||||
|
||||
function formatTokenId(uint32 _domain, bytes32 _id) |
||||
internal |
||||
pure |
||||
returns (bytes29) |
||||
{ |
||||
return mustBeTokenId(abi.encodePacked(_domain, _id).ref(0)); |
||||
} |
||||
|
||||
function formatMessage(bytes29 _tokenId, bytes29 _action) |
||||
internal |
||||
view |
||||
typeAssert(_tokenId, Types.TokenId) |
||||
returns (bytes memory) |
||||
{ |
||||
require(isDetails(_action) || isTransfer(_action), "!action"); |
||||
bytes29[] memory _views = new bytes29[](2); |
||||
_views[0] = _tokenId; |
||||
_views[1] = _action; |
||||
return TypedMemView.join(_views); |
||||
} |
||||
|
||||
function domain(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.TokenId) |
||||
returns (uint32) |
||||
{ |
||||
return uint32(_view.indexUint(0, 4)); |
||||
} |
||||
|
||||
function id(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.TokenId) |
||||
returns (bytes32) |
||||
{ |
||||
return _view.index(4, 32); |
||||
} |
||||
|
||||
function evmId(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.TokenId) |
||||
returns (address) |
||||
{ |
||||
// 4 bytes domain + 12 empty to trim for address |
||||
return _view.indexAddress(16); |
||||
} |
||||
|
||||
function recipient(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Transfer) |
||||
returns (bytes32) |
||||
{ |
||||
return _view.index(0, 32); |
||||
} |
||||
|
||||
function evmRecipient(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Transfer) |
||||
returns (address) |
||||
{ |
||||
return _view.indexAddress(12); |
||||
} |
||||
|
||||
function amnt(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Transfer) |
||||
returns (uint256) |
||||
{ |
||||
return _view.indexUint(32, 32); |
||||
} |
||||
|
||||
function name(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Details) |
||||
returns (bytes32) |
||||
{ |
||||
return _view.index(0, 32); |
||||
} |
||||
|
||||
function symbol(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Details) |
||||
returns (bytes32) |
||||
{ |
||||
return _view.index(32, 32); |
||||
} |
||||
|
||||
function decimals(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Details) |
||||
returns (uint8) |
||||
{ |
||||
return uint8(_view.indexUint(64, 1)); |
||||
} |
||||
|
||||
function tokenId(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Message) |
||||
returns (bytes29) |
||||
{ |
||||
return _view.slice(0, TOKEN_ID_LEN, uint40(Types.TokenId)); |
||||
} |
||||
|
||||
function action(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Message) |
||||
returns (bytes29) |
||||
{ |
||||
if (_view.len() == TOKEN_ID_LEN + DETAILS_LEN) { |
||||
return |
||||
_view.slice( |
||||
TOKEN_ID_LEN, |
||||
TOKEN_ID_LEN + DETAILS_LEN, |
||||
uint40(Types.Details) |
||||
); |
||||
} |
||||
return |
||||
_view.slice( |
||||
TOKEN_ID_LEN, |
||||
TOKEN_ID_LEN + TRANSFER_LEN, |
||||
uint40(Types.Transfer) |
||||
); |
||||
} |
||||
|
||||
function tryAsTransfer(bytes29 _view) internal pure returns (bytes29) { |
||||
if (_view.len() == TRANSFER_LEN) { |
||||
return _view.castTo(uint40(Types.Transfer)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function tryAsDetails(bytes29 _view) internal pure returns (bytes29) { |
||||
if (_view.len() == DETAILS_LEN) { |
||||
return _view.castTo(uint40(Types.Details)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function tryAsTokenId(bytes29 _view) internal pure returns (bytes29) { |
||||
if (_view.len() == 36) { |
||||
return _view.castTo(uint40(Types.TokenId)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function tryAsMessage(bytes29 _view) internal pure returns (bytes29) { |
||||
uint256 _len = _view.len(); |
||||
if ( |
||||
_len == TOKEN_ID_LEN + TRANSFER_LEN || |
||||
_len == TOKEN_ID_LEN + DETAILS_LEN |
||||
) { |
||||
return _view.castTo(uint40(Types.Message)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function mustBeTransfer(bytes29 _view) internal pure returns (bytes29) { |
||||
return tryAsTransfer(_view).assertValid(); |
||||
} |
||||
|
||||
function mustBeDetails(bytes29 _view) internal pure returns (bytes29) { |
||||
return tryAsDetails(_view).assertValid(); |
||||
} |
||||
|
||||
function mustBeTokenId(bytes29 _view) internal pure returns (bytes29) { |
||||
return tryAsTokenId(_view).assertValid(); |
||||
} |
||||
|
||||
function mustBeMessage(bytes29 _view) internal pure returns (bytes29) { |
||||
return tryAsMessage(_view).assertValid(); |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
require('hardhat-gas-reporter'); |
||||
require('solidity-coverage'); |
||||
|
||||
/** |
||||
* @type import('hardhat/config').HardhatUserConfig |
||||
*/ |
||||
module.exports = { |
||||
solidity: { |
||||
version: '0.7.6', |
||||
settings: { |
||||
optimizer: { |
||||
enabled: true, |
||||
runs: 999999, |
||||
}, |
||||
}, |
||||
}, |
||||
|
||||
gasReporter: { |
||||
currency: 'USD', |
||||
}, |
||||
|
||||
networks: { |
||||
localhost: { |
||||
url: 'http://localhost:8545', |
||||
}, |
||||
}, |
||||
}; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,34 @@ |
||||
{ |
||||
"name": "@celo-org/optics-bridge-sol", |
||||
"devDependencies": { |
||||
"@nomiclabs/hardhat-ethers": "^2.0.1", |
||||
"@nomiclabs/hardhat-waffle": "^2.0.1", |
||||
"chai": "^4.3.0", |
||||
"ethereum-waffle": "^3.2.2", |
||||
"ethers": "^5.0.29", |
||||
"hardhat": "^2.0.8", |
||||
"hardhat-gas-reporter": "^1.0.4", |
||||
"prettier": "^2.2.1", |
||||
"prettier-plugin-solidity": "^1.0.0-beta.4", |
||||
"solidity-coverage": "^0.7.14" |
||||
}, |
||||
"version": "0.0.0", |
||||
"description": "A bridge using optics", |
||||
"main": " ", |
||||
"directories": { |
||||
"test": "test" |
||||
}, |
||||
"scripts": { |
||||
"prettier": "prettier --write './contracts' ./test ./scripts", |
||||
"compile": "hardhat compile && npm run prettier", |
||||
"coverage": "npm run compile && hardhat coverage", |
||||
"test": "npm run compile && hardhat test" |
||||
}, |
||||
"author": "James Prestwich", |
||||
"license": "MIT OR Apache-2.0", |
||||
"dependencies": { |
||||
"@celo-org/optics-sol": "file:../optics-core", |
||||
"@openzeppelin/contracts": "^3.4.0", |
||||
"@summa-tx/memview-sol": "^1.1.1" |
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
node_modules/ |
||||
cache/ |
||||
artifacts/ |
||||
coverage/ |
||||
coverage.json |
@ -0,0 +1,23 @@ |
||||
{ |
||||
"overrides": [ |
||||
{ |
||||
"files": "*.js", |
||||
"options": { |
||||
"tabWidth": 2, |
||||
"singleQuote": true, |
||||
"trailingComma": "all" |
||||
} |
||||
}, |
||||
{ |
||||
"files": "*.sol", |
||||
"options": { |
||||
"printWidth": 80, |
||||
"tabWidth": 4, |
||||
"useTabs": false, |
||||
"singleQuote": false, |
||||
"bracketSpacing": false, |
||||
"explicitTypes": "always" |
||||
} |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,3 @@ |
||||
module.exports = { |
||||
skipFiles: ["test"], |
||||
}; |
@ -0,0 +1,85 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import "./Home.sol"; |
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol"; |
||||
import "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
|
||||
interface OpticsHandlerI { |
||||
function handle( |
||||
uint32 origin, |
||||
bytes32 sender, |
||||
bytes memory message |
||||
) external returns (bytes memory); |
||||
} |
||||
|
||||
abstract contract UsingOptics is Ownable { |
||||
mapping(address => uint32) public replicas; |
||||
Home home; |
||||
|
||||
constructor() Ownable() {} |
||||
|
||||
function isReplica(address _replica) internal view returns (bool) { |
||||
return replicas[_replica] != 0; |
||||
} |
||||
|
||||
function enrollReplica(uint32 _domain, address _replica) public onlyOwner { |
||||
replicas[_replica] = _domain; |
||||
} |
||||
|
||||
function unenrollReplica(address _replica) public onlyOwner { |
||||
replicas[_replica] = 0; |
||||
} |
||||
|
||||
function setHome(address _home) public onlyOwner { |
||||
home = Home(_home); |
||||
} |
||||
|
||||
modifier onlyReplica() { |
||||
require(isReplica(msg.sender), "!replica"); |
||||
_; |
||||
} |
||||
} |
||||
|
||||
library TypeCasts { |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
|
||||
function coerceBytes32(string memory _s) |
||||
internal |
||||
pure |
||||
returns (bytes32 _b) |
||||
{ |
||||
_b = bytes(_s).ref(0).index(0, uint8(bytes(_s).length)); |
||||
} |
||||
|
||||
// treat it as a null-terminated string of max 32 bytes |
||||
function coerceString(bytes32 _buf) |
||||
internal |
||||
pure |
||||
returns (string memory _newStr) |
||||
{ |
||||
uint8 _slen = 0; |
||||
while (_slen < 32 && _buf[_slen] != 0) { |
||||
_slen++; |
||||
} |
||||
|
||||
assembly { |
||||
_newStr := mload(0x40) |
||||
mstore(0x40, add(_newStr, 0x40)) // may end up with extra |
||||
mstore(_newStr, _slen) |
||||
mstore(add(_newStr, 0x20), _buf) |
||||
} |
||||
} |
||||
|
||||
// alignment preserving cast |
||||
function addressToBytes32(address _addr) internal pure returns (bytes32) { |
||||
return bytes32(uint256(uint160(_addr))); |
||||
} |
||||
|
||||
// alignment preserving cast |
||||
function bytes32ToAddress(bytes32 _buf) internal pure returns (address) { |
||||
return address(uint160(uint256(_buf))); |
||||
} |
||||
} |
Loading…
Reference in new issue