Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add pallet-assets precompile and asset-manager contract #820

Merged
merged 17 commits into from
Nov 20, 2024
Merged
349 changes: 188 additions & 161 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ members = [
"precompiles/multi-asset-delegation",
"precompiles/services",
"precompiles/tangle-lst",
"precompiles/assets",
"tangle-subxt",
"evm-tracer",
]
Expand Down
155 changes: 155 additions & 0 deletions precompiles/assets/AssetManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IAssets.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/// @title Asset Manager Contract
/// @dev This contract manages the bridging between ERC20 tokens and native assets in the Tangle Network.
/// It allows users to deposit ERC20 tokens and receive corresponding native assets, maintaining a 1:1 relationship
/// between ERC20 tokens and native assets.
///
/// Key features:
/// - Creates and tracks native assets corresponding to ERC20 tokens
/// - Handles deposits of ERC20 tokens and mints equivalent native assets
/// - Maintains a mapping between ERC20 tokens and their corresponding asset IDs
/// - Uses the Assets precompile for native asset operations
///
/// Security considerations:
/// - Only the contract owner can manually set asset IDs
/// - Includes emergency recovery function for stuck ERC20 tokens
/// - Asset IDs are obtained from the precompile to ensure system-wide consistency
contract AssetManager is Ownable {
// Interface to the Assets precompile that handles native asset operations
IAssets public immutable assetsPrecompile;

// Maps ERC20 token addresses to their corresponding native asset IDs
mapping(address => uint256) public erc20ToAssetId;

// Events for tracking asset creation and deposits
event AssetCreated(address indexed erc20Token, uint256 indexed assetId);
event Deposited(address indexed erc20Token, uint256 indexed assetId, address indexed user, uint256 amount);

/// @dev Initializes the contract with the Assets precompile address
/// @param _assetsPrecompile The address of the Assets precompile contract
constructor(address _assetsPrecompile) {
assetsPrecompile = IAssets(_assetsPrecompile);
}

/// @notice Deposits ERC20 tokens and mints corresponding native assets
/// @dev The function performs the following steps:
/// 1. Transfers ERC20 tokens from the user to this contract
/// 2. Gets or creates a native asset ID for the ERC20 token
/// 3. Mints an equivalent amount of native assets to the user
///
/// @param erc20Token The address of the ERC20 token to deposit
/// @param amount The amount of tokens to deposit
/// @return assetId The ID of the native asset that was minted
///
/// Requirements:
/// - Amount must be greater than 0
/// - ERC20 token address must not be zero address
/// - User must have approved this contract to spend their tokens
function deposit(address erc20Token, uint256 amount) external returns (uint256) {
require(amount > 0, "Amount must be greater than 0");
require(erc20Token != address(0), "Invalid token address");

// Transfer ERC20 tokens from user to this contract
require(
IERC20(erc20Token).transferFrom(msg.sender, address(this), amount),
"Token transfer failed"
);

// Get or create asset ID
uint256 assetId = getOrCreateAssetId(erc20Token);

// Mint native assets to the user
require(
assetsPrecompile.mint(assetId, address(this), amount),
"Asset minting failed"
);

emit Deposited(erc20Token, assetId, msg.sender, amount);
return assetId;
}

/// @dev Internal function to get existing or create new asset ID for an ERC20 token
/// @param erc20Token The ERC20 token address
/// @return assetId The asset ID (either existing or newly created)
///
/// The function follows these steps:
/// 1. Checks if an asset ID already exists for the token
/// 2. If not, gets the next available asset ID from the precompile
/// 3. Creates a new native asset with this contract as admin
/// 4. Stores the ERC20 token to asset ID mapping
///
/// Note: The minimum balance for new assets is set to 1 to prevent dust attacks
function getOrCreateAssetId(address erc20Token) internal returns (uint256) {
uint256 assetId = erc20ToAssetId[erc20Token];

// If asset doesn't exist, create it
if (assetId == 0) {
// Get the next available asset ID from the precompile
assetId = assetsPrecompile.next_asset_id();

// Create the asset with this contract as admin
require(
assetsPrecompile.create(assetId, address(this), 1),
"Asset creation failed"
);

// Store the mapping
erc20ToAssetId[erc20Token] = assetId;

emit AssetCreated(erc20Token, assetId);
}

return assetId;
}

/// @notice Retrieves the native asset ID for a given ERC20 token
/// @dev Returns 0 if no asset ID exists for the token
/// @param erc20Token The ERC20 token address to query
/// @return The corresponding native asset ID, or 0 if none exists
function getAssetId(address erc20Token) external view returns (uint256) {
return erc20ToAssetId[erc20Token];
}

/// @notice Allows the owner to manually set an asset ID for an ERC20 token
/// @dev This function is restricted to the contract owner and can only be used
/// for tokens that don't already have an asset ID assigned
///
/// @param erc20Token The ERC20 token address
/// @param assetId The corresponding asset ID to assign
///
/// Requirements:
/// - Caller must be the contract owner
/// - Token must not already have an asset ID
/// - Asset ID must be greater than 0
function setAssetId(address erc20Token, uint256 assetId) external onlyOwner {
require(erc20ToAssetId[erc20Token] == 0, "Asset ID already exists");
require(assetId > 0, "Invalid asset ID");
erc20ToAssetId[erc20Token] = assetId;
emit AssetCreated(erc20Token, assetId);
}

/// @notice Emergency function to recover accidentally sent ERC20 tokens
/// @dev This function allows the owner to recover any ERC20 tokens that were
/// accidentally sent to this contract. This is a safety measure and should
/// only be used in emergency situations.
///
/// @param token The ERC20 token address to recover
///
/// Requirements:
/// - Caller must be the contract owner
/// - Contract must have a non-zero balance of the specified token
function recoverERC20(address token) external onlyOwner {
uint256 balance = IERC20(token).balanceOf(address(this));
require(balance > 0, "No tokens to recover");
require(
IERC20(token).transfer(owner(), balance),
"Token recovery failed"
);
}
}
50 changes: 50 additions & 0 deletions precompiles/assets/Assets.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IAssets {
/// @notice Create a new asset with the given parameters
/// @param id The identifier for the new asset
/// @param admin The account that will administer the asset
/// @param minBalance The minimum balance required for an account to exist for this asset
/// @return success True if the operation was successful
function create(
uint256 id,
address admin,
uint256 minBalance
) external returns (bool success);

/// @notice Start the process of destroying an asset
/// @param id The identifier of the asset to destroy
/// @return success True if the operation was successful
function startDestroy(
uint256 id
) external returns (bool success);

/// @notice Mint new tokens for an asset
/// @param id The identifier of the asset
/// @param beneficiary The account that will receive the minted tokens
/// @param amount The amount of tokens to mint
/// @return success True if the operation was successful
function mint(
uint256 id,
address beneficiary,
uint256 amount
) external returns (bool success);

/// @notice Transfer tokens from the caller to another account
/// @param id The identifier of the asset
/// @param target The account that will receive the tokens
/// @param amount The amount of tokens to transfer
/// @return success True if the operation was successful
function transfer(
uint256 id,
address target,
uint256 amount
) external returns (bool success);

// Events that should be emitted by the implementation
event Created(uint256 indexed id, address indexed admin, uint256 minBalance);
event DestroyStarted(uint256 indexed id);
event Minted(uint256 indexed id, address indexed beneficiary, uint256 amount);
event Transferred(uint256 indexed id, address indexed from, address indexed to, uint256 amount);
}
63 changes: 63 additions & 0 deletions precompiles/assets/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[package]
name = "pallet-evm-precompileset-assets"
authors = { workspace = true }
description = "A Precompile to expose Assets pallet."
edition = "2021"
version = "0.1.0"

[dependencies]
paste = { workspace = true }

# Moonbeam
precompile-utils = { workspace = true }

# Substrate
frame-support = { workspace = true }
frame-system = { workspace = true }
pallet-assets = { workspace = true }
pallet-balances = { workspace = true }
pallet-timestamp = { workspace = true }
parity-scale-codec = { workspace = true, features = [ "max-encoded-len" ] }
scale-info = { workspace = true, features = [ "derive" ] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }
tangle-primitives = { workspace = true }

# Frontier
fp-evm = { workspace = true }
pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] }

[dev-dependencies]
derive_more = { workspace = true, features = ["full"] }
hex-literal = { workspace = true }
libsecp256k1 = { workspace = true }
serde = { workspace = true }
sha3 = { workspace = true }

precompile-utils = { workspace = true, features = [ "std", "testing" ] }

pallet-timestamp = { workspace = true, features = [ "std" ] }
parity-scale-codec = { workspace = true, features = [ "max-encoded-len", "std" ] }
scale-info = { workspace = true, features = [ "derive" ] }
sp-runtime = { workspace = true, features = [ "std" ] }

[features]
default = [ "std" ]
std = [
"fp-evm/std",
"frame-support/std",
"frame-system/std",
"pallet-assets/std",
"pallet-balances/std",
"pallet-evm/std",
"pallet-timestamp/std",
"parity-scale-codec/std",
"precompile-utils/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"tangle-primitives/std",
]
Loading
Loading