-
Notifications
You must be signed in to change notification settings - Fork 92
Description
Context
Current dynamic precompiles requires hooking all message calls inside evm execution, and requires extra storage accesses for each message call to decide if an address is a precompile or not. It's both performance heavy and requires intrusive patch into EVM implementation.
Proposal
Implement an unified bank precompile to manage all native denoms, so we can implement the ERC20 token in solidity on top of it:
interface IBankPrecompile {
function name(string memory denom) external view returns (string memory);
function symbol(string memory denom) external view returns (string memory);
function decimals(string memory denom) external view returns (uint8);
function totalSupply(string memory denom) external view returns (uint256);
function balanceOf(address account, string memory denom) external view returns (uint256);
function transferFrom(address from, address to, uint256 value, string memory denom) external returns (bool);
}
The immutable functions are open for anyone to call.
The only mutable function is transferFrom
, it only authorize two use cases:
-
msg.sender == from
user are allowed to transfer their own funds for any denom. -
msg.sender
is the deterministic contract constructed from the standard implementation with the specific denom.
This contract is allowed to manage any user's funds only for the specific denom.
It don't support gas token here for simplicity, the gas token has more complex interactions.
Standard ERC20 Implementation
This contract must be deployed in a pre-defined deterministic way, so the precompile can automatically authorize it to call the transferFrom
method:
address = create2_address(CREATE2_FACTORY, salt || bytecode || constructor_args)
Standard ERC20 Implementation In Solidity
Adapted from openzeppelin ERC20 impl
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
interface IERC20Metadata is IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}
interface IERC20Errors {
error ERC20InvalidSender(address sender);
error ERC20InvalidReceiver(address receiver);
error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
error ERC20InvalidApprover(address approver);
error ERC20InvalidSpender(address spender);
}
interface IBankPrecompile {
function name(string memory denom) external view returns (string memory);
function symbol(string memory denom) external view returns (string memory);
function decimals(string memory denom) external view returns (uint8);
function totalSupply(string memory denom) external view returns (uint256);
function balanceOf(address account, string memory denom) external view returns (uint256);
function transferFrom(address from, address to, uint256 value, string memory denom) external returns (bool);
}
contract ERC20 is IERC20, IERC20Metadata, IERC20Errors {
string public denom;
mapping(address account => mapping(address spender => uint256)) public allowance;
IBankPrecompile public immutable bank;
constructor(string memory denom_, IBankPrecompile bank_) {
denom = denom_;
bank = bank_;
}
function name() public view returns (string memory) {
return bank.name(denom);
}
function symbol() public view returns (string memory) {
return bank.symbol(denom);
}
function decimals() public view returns (uint8) {
return bank.decimals(denom);
}
function totalSupply() public view returns (uint256) {
return bank.totalSupply(denom);
}
function balanceOf(address account) public view returns (uint256) {
return bank.balanceOf(account, denom);
}
function transfer(address to, uint256 value) public returns (bool) {
_transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) public returns (bool) {
_approve(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value) public returns (bool) {
address spender = msg.sender;
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
bank.transferFrom(from, to, value, denom);
emit Transfer(from, to, value);
}
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
allowance[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance[owner][spender];
if (currentAllowance < type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}