diff --git a/docs/architecture/token designs/xToken_design_1.sol b/docs/architecture/token designs/xToken_design_1.sol new file mode 100644 index 0000000..765eff8 --- /dev/null +++ b/docs/architecture/token designs/xToken_design_1.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: AGPL-3.0 +// author: bhargavaparoksham +// Token design similar to aTokens in Aave + +pragma solidity ^0.8.20; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "../../../src/interfaces/IAssetOracle.sol"; + +/** + * @title xToken Contract + * @notice This contract implements a price-scaling token that tracks an underlying real-world asset. + * @dev The token maintains scaled balances that adjust based on the underlying asset price. + * All user-facing amounts are in standard token decimals, while internal accounting uses scaled balances. + * The asset price is assumed to be in cents (1/100 of a dollar). + */ +contract xToken is ERC20 { + /** + * @dev Thrown when a caller is not the pool contract + */ + error NotPool(); + + /** + * @dev Thrown when zero address is provided where it's not allowed + */ + error ZeroAddress(); + + /** + * @dev Thrown when asset price is invalid (zero) + */ + error InvalidPrice(); + + /** + * @dev Thrown when account has insufficient balance for an operation + */ + error InsufficientBalance(); + + /** + * @dev Thrown when spender has insufficient allowance + */ + error InsufficientAllowance(); + + /** + * @dev Emitted after the mint action + * @param account The address receiving the minted tokens + * @param value The amount being minted + **/ + event Mint(address indexed account, uint256 value); + + /** + * @dev Emitted after xTokens are burned + * @param account The owner of the xTokens, getting burned + * @param value The amount being burned + **/ + event Burn(address indexed account, uint256 value); + + + /// @notice Reference to the oracle providing asset price feeds + IAssetOracle public immutable oracle; + + /// @notice Address of the pool contract that manages this token + address public immutable pool; + + /// @notice Version identifier for the xToken implementation + uint256 public constant XTOKEN_VERSION = 0x1; + + /** + * @notice Price scaling factor to convert cent prices to wei-equivalent precision + * @dev Since price is in cents (1/100 of dollar), we multiply by 1e16 to get to 1e18 precision + */ + uint256 private constant PRICE_SCALING = 1e16; + + /// @notice Mapping of scaled balances for each account + mapping(address => uint256) private _scaledBalances; + + /// @notice Total supply in scaled balance terms + uint256 private _totalScaledSupply; + + /** + * @notice Ensures the caller is the pool contract + */ + modifier onlyPool() { + if (msg.sender != pool) revert NotPool(); + _; + } + + /** + * @notice Constructs the xToken contract + * @param name The name of the token + * @param symbol The symbol of the token + * @param _oracle The address of the asset price oracle + */ + constructor(string memory name, string memory symbol, address _oracle) ERC20(name, symbol) { + if (_oracle == address(0)) revert ZeroAddress(); + oracle = IAssetOracle(_oracle); + pool = msg.sender; + } + + /** + * @notice Returns the scaled balance of an account + * @dev This balance is independent of the asset price and represents the user's share of the pool + * @param account The address of the account + * @return The scaled balance of the account + */ + function scaledBalanceOf(address account) public view returns (uint256) { + return _scaledBalances[account]; + } + + /** + * @notice Returns the total scaled supply + * @return The total scaled supply of tokens + */ + function scaledTotalSupply() public view returns (uint256) { + return _totalScaledSupply; + } + + /** + * @notice Converts a nominal token amount to its scaled equivalent + * @dev Uses the current asset price for conversion + * @param amount The amount to convert + * @return The equivalent scaled amount + */ + function _convertToScaledAmount(uint256 amount) internal view returns (uint256) { + uint256 price = oracle.assetPrice(); + if (price == 0) revert InvalidPrice(); + return (amount * 1e18) / (price * PRICE_SCALING); + } + + /** + * @notice Returns the current balance of an account in nominal terms + * @dev Converts the scaled balance to nominal terms using current asset price + * @param account The address of the account + * @return The nominal balance of the account + */ + function balanceOf(address account) public view override(ERC20) returns (uint256) { + uint256 price = oracle.assetPrice(); + if (price == 0) revert InvalidPrice(); + return (_scaledBalances[account] * (price * PRICE_SCALING)) / 1e18; + } + + /** + * @notice Returns the total supply in nominal terms + * @dev Converts the total scaled supply to nominal terms using current asset price + * @return The total supply of tokens + */ + function totalSupply() public view override(ERC20) returns (uint256) { + uint256 price = oracle.assetPrice(); + if (price == 0) revert InvalidPrice(); + return (_totalScaledSupply * (price * PRICE_SCALING)) / 1e18; + } + + /** + * @notice Mints new tokens to an account + * @dev Only callable by the pool contract + * @param account The address receiving the minted tokens + * @param amount The amount of tokens to mint (in nominal terms) + */ + function mint(address account, uint256 amount) external onlyPool { + uint256 scaledAmount = _convertToScaledAmount(amount); + _scaledBalances[account] += scaledAmount; + _totalScaledSupply += scaledAmount; + emit Mint(account, amount); + } + + /** + * @notice Burns tokens from an account + * @dev Only callable by the pool contract + * @param account The address to burn tokens from + * @param amount The amount of tokens to burn (in nominal terms) + */ + function burn(address account, uint256 amount) external onlyPool { + uint256 scaledAmount = _convertToScaledAmount(amount); + if (_scaledBalances[account] < scaledAmount) revert InsufficientBalance(); + _scaledBalances[account] -= scaledAmount; + _totalScaledSupply -= scaledAmount; + emit Burn(account, amount); + } + + /** + * @notice Transfers tokens to a recipient + * @param recipient The address receiving the tokens + * @param amount The amount of tokens to transfer (in nominal terms) + * @return success True if the transfer succeeded + */ + function transfer(address recipient, uint256 amount) public override(ERC20) returns (bool) { + if (recipient == address(0)) revert ZeroAddress(); + uint256 scaledAmount = _convertToScaledAmount(amount); + if (_scaledBalances[msg.sender] < scaledAmount) revert InsufficientBalance(); + + _scaledBalances[msg.sender] -= scaledAmount; + _scaledBalances[recipient] += scaledAmount; + + emit Transfer(msg.sender, recipient, amount); + return true; + } + + /** + * @notice Transfers tokens from one address to another using the allowance mechanism + * @param sender The address to transfer tokens from + * @param recipient The address receiving the tokens + * @param amount The amount of tokens to transfer (in nominal terms) + * @return success True if the transfer succeeded + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public override(ERC20) returns (bool) { + if (recipient == address(0)) revert ZeroAddress(); + uint256 currentAllowance = allowance(sender, msg.sender); + if (currentAllowance < amount) revert InsufficientAllowance(); + + uint256 scaledAmount = _convertToScaledAmount(amount); + if (_scaledBalances[sender] < scaledAmount) revert InsufficientBalance(); + + _scaledBalances[sender] -= scaledAmount; + _scaledBalances[recipient] += scaledAmount; + _approve(sender, msg.sender, currentAllowance - amount); + + emit Transfer(sender, recipient, amount); + return true; + } +} \ No newline at end of file diff --git a/docs/architecture/token designs/xToken_design_2.sol b/docs/architecture/token designs/xToken_design_2.sol new file mode 100644 index 0000000..2508e24 --- /dev/null +++ b/docs/architecture/token designs/xToken_design_2.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: AGPL-3.0 +// author: bhargavaparoksham +// Token design where balance remains constant matching the initial reserve deposit + +pragma solidity ^0.8.20; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import "../../../src/interfaces/IAssetOracle.sol"; + +/** + * @title xToken Contract + * @notice This contract implements a price-scaling token that tracks an underlying real-world asset. + * @dev The token maintains scaled balances that adjust based on the underlying asset price. + * All amounts are expected to be in 18 decimal precision. + * The asset price is assumed to be in 18 decimal precision. + */ +contract xToken is ERC20, ERC20Permit { + + /** + * @dev Thrown when a caller is not the pool contract + */ + error NotPool(); + + /** + * @dev Thrown when zero address is provided where it's not allowed + */ + error ZeroAddress(); + + /** + * @dev Thrown when asset price is invalid (zero) + */ + error InvalidPrice(); + + /** + * @dev Thrown when account has insufficient balance for an operation + */ + error InsufficientBalance(); + + /** + * @dev Thrown when spender has insufficient allowance + */ + error InsufficientAllowance(); + + /** + * @dev Emitted after the mint action + * @param account The address receiving the minted tokens + * @param value The amount being minted + * @param price The price at which the tokens are minted + **/ + event Mint(address indexed account, uint256 value, uint256 price); + + /** + * @dev Emitted after xTokens are burned + * @param account The owner of the xTokens, getting burned + * @param value The amount being burned + **/ + event Burn(address indexed account, uint256 value); + + /// @notice Reference to the oracle providing asset price feeds + IAssetOracle public immutable oracle; + + /// @notice Address of the pool contract that manages this token + address public immutable pool; + + /// @notice Version identifier for the xToken implementation + uint256 public constant XTOKEN_VERSION = 0x1; + + /// @notice Price precision constant + uint256 private constant PRECISION = 1e18; + + /// @notice Mapping of scaled balances for each account + mapping(address => uint256) private _scaledBalances; + + /// @notice Total supply in scaled balance terms + uint256 private _totalScaledSupply; + + /** + * @notice Ensures the caller is the pool contract + */ + modifier onlyPool() { + if (msg.sender != pool) revert NotPool(); + _; + } + + /** + * @notice Constructs the xToken contract + * @param name The name of the token + * @param symbol The symbol of the token + * @param _oracle The address of the asset price oracle + */ + constructor(string memory name, string memory symbol, address _oracle) ERC20(name, symbol) ERC20Permit(name) { + if (_oracle == address(0)) revert ZeroAddress(); + oracle = IAssetOracle(_oracle); + pool = msg.sender; + } + + /** + * @notice Returns the scaled balance of an account + * @dev This balance is independent of the asset price and represents the user's share of the pool + * @param account The address of the account + * @return The scaled balance of the account + */ + function scaledBalanceOf(address account) public view returns (uint256) { + return _scaledBalances[account]; + } + + /** + * @notice Returns the total scaled supply + * @return The total scaled supply of tokens + */ + function scaledTotalSupply() public view returns (uint256) { + return _totalScaledSupply; + } + + /** + * @notice Converts token amount to its scaled equivalent at the given price + * @param amount The amount to convert (in 18 decimal precision) + * @param price The asset price to use for conversion + * @return The equivalent scaled amount + */ + function _convertToScaledAmountWithPrice(uint256 amount, uint256 price) internal pure returns (uint256) { + if (price == 0) revert InvalidPrice(); + return Math.mulDiv(amount, PRECISION, price); + } + + /** + * @notice Returns the market value of a user's tokens + * @dev Converts the scaled balance to market value using current asset price + * @param account The address of the account + * @return The market value of user's tokens in 18 decimal precision + */ + function marketValue(address account) public view returns (uint256) { + uint256 price = oracle.assetPrice(); + if (price == 0) revert InvalidPrice(); + return _scaledBalances[account] * price; + } + + /** + * @notice Returns the total market value of all tokens + * @dev Converts the total scaled supply to nominal terms using current asset price + * @return The total market value in 18 decimal precision + */ + function totalMarketValue() public view returns (uint256) { + uint256 price = oracle.assetPrice(); + if (price == 0) revert InvalidPrice(); + return _totalScaledSupply * price; + } + + /** + * @notice Mints new tokens to an account + * @dev Only callable by the pool contract + * @param account The address receiving the minted tokens + * @param amount The amount of tokens to mint (in 18 decimal precision) + * @param price The asset price at which the minting is done + */ + function mint(address account, uint256 amount, uint256 price) external onlyPool { + uint256 scaledAmount = _convertToScaledAmountWithPrice(amount, price); + _scaledBalances[account] += scaledAmount; + _totalScaledSupply += scaledAmount; + _mint(account, amount); + emit Mint(account, amount, price); + } + + /** + * @notice Burns tokens from an account + * @dev Only callable by the pool contract + * @param account The address to burn tokens from + * @param amount The amount of tokens to burn (in 18 decimal precision) + */ + function burn(address account, uint256 amount) external onlyPool { + uint256 balance = balanceOf(account); + if (balance < amount) revert InsufficientBalance(); + uint256 scaledBalance = _scaledBalances[account]; + uint256 scaledBalanceToBurn = Math.mulDiv(scaledBalance, amount, balance); + + _scaledBalances[account] -= scaledBalanceToBurn; + _totalScaledSupply -= scaledBalanceToBurn; + _burn(account, amount); + emit Burn(account, amount); + } + + /** + * @notice Transfers tokens to a recipient + * @param recipient The address receiving the tokens + * @param amount The amount of tokens to transfer (in 18 decimal precision) + * @return success True if the transfer succeeded + */ + function transfer(address recipient, uint256 amount) public override(ERC20) returns (bool) { + if (recipient == address(0)) revert ZeroAddress(); + uint256 balance = balanceOf(msg.sender); + if (balance < amount) revert InsufficientBalance(); + + uint256 scaledBalance = _scaledBalances[msg.sender]; + uint256 scaledBalanceToTransfer = Math.mulDiv(scaledBalance, amount, balance); + + _scaledBalances[msg.sender] -= scaledBalanceToTransfer; + _scaledBalances[recipient] += scaledBalanceToTransfer; + + _transfer(msg.sender, recipient, amount); + + return true; + } + + /** + * @notice Transfers tokens from one address to another using the allowance mechanism + * @param sender The address to transfer tokens from + * @param recipient The address receiving the tokens + * @param amount The amount of tokens to transfer (in 18 decimal precision) + * @return success True if the transfer succeeded + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public override(ERC20) returns (bool) { + if (recipient == address(0)) revert ZeroAddress(); + uint256 currentAllowance = allowance(sender, msg.sender); + if (currentAllowance < amount) revert InsufficientAllowance(); + + uint256 balance = balanceOf(sender); + if (balance < amount) revert InsufficientBalance(); + + uint256 scaledBalance = _scaledBalances[sender]; + uint256 scaledBalanceToTransfer = scaledBalance * amount / balance; + + _scaledBalances[sender] -= scaledBalanceToTransfer; + _scaledBalances[recipient] += scaledBalanceToTransfer; + _approve(sender, msg.sender, currentAllowance - amount); + + _transfer(sender, recipient, amount); + + return true; + } +} \ No newline at end of file diff --git a/docs/architecture/token designs/xToken_design_3.sol b/docs/architecture/token designs/xToken_design_3.sol new file mode 100644 index 0000000..1ffc1d7 --- /dev/null +++ b/docs/architecture/token designs/xToken_design_3.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: AGPL-3.0 +// author: bhargavaparoksham +// Token design where balance remains constant matching the asset supply + +pragma solidity ^0.8.20; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import "openzeppelin-contracts/contracts/utils/math/Math.sol"; + +/** + * @title xToken Contract + * @notice This contract implements a token that tracks an underlying real-world asset. + * All amounts are expected to be in 18 decimal precision. + * The asset price is assumed to be in 18 decimal precision. + */ +contract xToken is ERC20, ERC20Permit { + + /** + * @dev Thrown when a caller is not the pool contract + */ + error NotPool(); + + /** + * @dev Thrown when zero address is provided where it's not allowed + */ + error ZeroAddress(); + + /** + * @dev Thrown when account has insufficient balance for an operation + */ + error InsufficientBalance(); + + /** + * @dev Thrown when spender has insufficient allowance + */ + error InsufficientAllowance(); + + /** + * @dev Emitted after the mint action + * @param account The address receiving the minted tokens + * @param value The amount being minted + * @param reserve The amount of reserve tokens which is backing the minted xTokens + **/ + event Mint(address indexed account, uint256 value, uint256 reserve); + + /** + * @dev Emitted after xTokens are burned + * @param account The owner of the xTokens, getting burned + * @param value The amount being burned + * @param reserve The amount of reserve tokens + **/ + event Burn(address indexed account, uint256 value, uint256 reserve); + + /// @notice Address of the pool contract that manages this token + address public immutable pool; + + /// @notice Version identifier for the xToken implementation + uint256 public constant XTOKEN_VERSION = 0x1; + + /// @notice Price precision constant + uint256 private constant PRECISION = 1e18; + + /// @notice Mapping of reserve balances for each account + mapping(address => uint256) private _reserveBalances; + + /// @notice Total supply in reserve balance terms + uint256 private _totalReserveSupply; + + /** + * @notice Ensures the caller is the pool contract + */ + modifier onlyPool() { + if (msg.sender != pool) revert NotPool(); + _; + } + + /** + * @notice Constructs the xToken contract + * @param name The name of the token + * @param symbol The symbol of the token + */ + constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) { + pool = msg.sender; + } + + /** + * @notice Returns the reserve balance of an account + * @dev This balance is independent of the asset price and represents the user's share of the reserve tokens in the pool + * @param account The address of the account + * @return The reserve balance of the account + */ + function reserveBalanceOf(address account) public view returns (uint256) { + return _reserveBalances[account]; + } + + /** + * @notice Returns the total reserve supply + * @return The total reserve supply of tokens + */ + function totalReserveSupply() public view returns (uint256) { + return _totalReserveSupply; + } + + /** + * @notice Mints new tokens to an account + * @dev Only callable by the pool contract + * @param account The address receiving the minted tokens + * @param amount The amount of tokens to mint (in 18 decimal precision) + * @param reserve The amount of reserve tokens which is backing the minted xTokens + */ + function mint(address account, uint256 amount, uint256 reserve) external onlyPool { + _reserveBalances[account] += reserve; + _totalReserveSupply += reserve; + _mint(account, amount); + emit Mint(account, amount, reserve); + } + + /** + * @notice Burns tokens from an account + * @dev Only callable by the pool contract + * @param account The address to burn tokens from + * @param amount The amount of tokens to burn (in 18 decimal precision) + * @param reserve The amount of reserve tokens to burn + */ + function burn(address account, uint256 amount, uint256 reserve) external onlyPool { + uint256 balance = balanceOf(account); + uint256 reserveBalance = _reserveBalances[account]; + if (balance < amount) revert InsufficientBalance(); + if (reserveBalance < reserve) revert InsufficientBalance(); + _reserveBalances[account] -= reserve; + _totalReserveSupply -= reserve; + _burn(account, amount); + emit Burn(account, amount, reserve); + } + + /** + * @notice Transfers tokens to a recipient + * @param recipient The address receiving the tokens + * @param amount The amount of tokens to transfer (in 18 decimal precision) + * @return success True if the transfer succeeded + */ + function transfer(address recipient, uint256 amount) public override(ERC20) returns (bool) { + if (recipient == address(0)) revert ZeroAddress(); + uint256 balance = balanceOf(msg.sender); + if (balance < amount) revert InsufficientBalance(); + + uint256 reserveBalance = _reserveBalances[msg.sender]; + uint256 reserveBalanceToTransfer = Math.mulDiv(reserveBalance, amount, balance); + + _reserveBalances[msg.sender] -= reserveBalanceToTransfer; + _reserveBalances[recipient] += reserveBalanceToTransfer; + + _transfer(msg.sender, recipient, amount); + + return true; + } + + /** + * @notice Transfers tokens from one address to another using the allowance mechanism + * @param sender The address to transfer tokens from + * @param recipient The address receiving the tokens + * @param amount The amount of tokens to transfer (in 18 decimal precision) + * @return success True if the transfer succeeded + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public override(ERC20) returns (bool) { + if (recipient == address(0)) revert ZeroAddress(); + uint256 currentAllowance = allowance(sender, msg.sender); + if (currentAllowance < amount) revert InsufficientAllowance(); + + uint256 balance = balanceOf(sender); + if (balance < amount) revert InsufficientBalance(); + + uint256 reserveBalance = _reserveBalances[sender]; + uint256 reserveBalanceToTransfer = Math.mulDiv(reserveBalance, amount, balance); + + _reserveBalances[sender] -= reserveBalanceToTransfer; + _reserveBalances[recipient] += reserveBalanceToTransfer; + _approve(sender, msg.sender, currentAllowance - amount); + + _transfer(sender, recipient, amount); + + return true; + } +} \ No newline at end of file