Skip to content

Commit

Permalink
feat: update xToken contract & its interface
Browse files Browse the repository at this point in the history
  • Loading branch information
bhargavaparoksham committed Jan 8, 2025
1 parent 8f02c58 commit ce311bf
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 96 deletions.
27 changes: 0 additions & 27 deletions src/interfaces/IScaledBalanceToken.sol

This file was deleted.

119 changes: 69 additions & 50 deletions src/interfaces/IXToken.sol
Original file line number Diff line number Diff line change
@@ -1,56 +1,75 @@
// SPDX-License-Identifier: AGPL-3.0
// author: bhargavaparoksham

pragma solidity ^0.8.20;

import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol';
import {IScaledBalanceToken} from './IScaledBalanceToken.sol';

interface IXToken is IERC20, IScaledBalanceToken {
/**
* @dev Emitted after the mint action
* @param from The address performing the mint
* @param value The amount being
**/
event Mint(address indexed from, uint256 value);

/**
* @dev Mints `amount` xTokens to `user`
* @param user The address receiving the minted tokens
* @param amount The amount of tokens getting minted
*/
function mint(
address user,
uint256 amount
) external;

/**
* @dev Emitted after xTokens are burned
* @param user The owner of the xTokens, getting them burned
* @param value The amount being burned
**/
event Burn(address indexed user, uint256 value);

/**
* @dev Emitted during the transfer action
* @param from The user whose tokens are being transferred
* @param to The recipient
* @param value The amount being transferred
**/
event xTokenTransfer(address indexed from, address indexed to, uint256 value);

/**
* @dev Burns xTokens from `user` and sends the equivalent amount of underlying to `receiverOfUnderlying`
* @param user The owner of the xTokens, getting them burned
* @param amount The amount being burned
**/
function burn(
address user,
uint256 amount
) external;


/**
* @dev Returns the address of the underlying asset of this xToken.
**/
function ASSET_ORACLE_ADDRESS() external view returns (address);

interface IXToken is IERC20 {
/**
* @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);

/**
* @dev Returns the scaled balance of the user. The scaled balance represents the user's balance
* normalized by the underlying asset price, maintaining constant purchasing power.
* @param user The user whose balance is calculated
* @return The scaled balance of the user
**/
function scaledBalanceOf(address user) external view returns (uint256);

/**
* @dev Returns the scaled total supply of the token. Represents the total supply
* normalized by the asset price.
* @return The scaled total supply
**/
function scaledTotalSupply() external view returns (uint256);

/**
* @dev Returns the version of the xToken implementation
* @return The version number
**/
function XTOKEN_VERSION() external view returns (uint256);

/**
* @dev Returns the oracle contract address used for price feeds
* @return The address of the oracle contract
**/
function oracle() external view returns (address);

/**
* @dev Returns the pool contract address that manages this token
* @return The address of the pool contract
**/
function pool() external view returns (address);

/**
* @dev Mints `amount` xTokens to `account`
* @param account The address receiving the minted tokens
* @param amount The amount of tokens getting minted
*/
function mint(
address account,
uint256 amount
) external;

/**
* @dev Burns xTokens from `account`
* @param account The owner of the xTokens, getting burned
* @param amount The amount being burned
**/
function burn(
address account,
uint256 amount
) external;
}
163 changes: 144 additions & 19 deletions src/protocol/xToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,179 @@ pragma solidity ^0.8.20;

import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import "./AssetOracle.sol";
import "../interfaces/IXToken.sol";

contract xToken is ERC20 {
AssetOracle public oracle;
address public pool;
/**
* @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 IXToken, ERC20 {
/// @notice Reference to the oracle providing asset price feeds
AssetOracle 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() {
require(msg.sender == pool, "Only pool can call this function");
require(msg.sender == pool, "Own: Only pool can call this function");
_;
}

/**
* @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) {
require(_oracle != address(0), "Own: Oracle cannot be zero address");
oracle = AssetOracle(_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];
}

function balanceOf(address account) public view override returns (uint256) {
/**
* @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();
require(price > 0, "Invalid asset price");
return (_scaledBalances[account] * (price * 1e16)) / 1e18;
require(price > 0, "Own: Invalid asset price");
return (amount * 1e18) / (price * PRICE_SCALING);
}

function totalScaledSupply() public view returns (uint256) {
/**
* @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(IERC20, ERC20) returns (uint256) {
uint256 price = oracle.assetPrice();
require(price > 0, "Invalid asset price");
return (totalSupply() / (price * 1e16) / 1e18);
require(price > 0, "Own: Invalid asset price");
return (_scaledBalances[account] * (price * PRICE_SCALING)) / 1e18;
}

function mint(address account, uint256 amount) external onlyPool {
/**
* @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(IERC20, ERC20) returns (uint256) {
uint256 price = oracle.assetPrice();
require(price > 0, "Invalid asset price");
uint256 scaledAmount = (amount * 1e18) / (price * 1e16);
require(price > 0, "Own: Invalid asset price");
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;
emit Transfer(address(0), account, amount);
_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 price = oracle.assetPrice();
require(price > 0, "Invalid asset price");
uint256 scaledAmount = (amount * 1e18) / (price * 1e16);
require(_scaledBalances[account] >= scaledAmount, "Insufficient balance");
uint256 scaledAmount = _convertToScaledAmount(amount);
require(_scaledBalances[account] >= scaledAmount, "Own: Insufficient balance");
_scaledBalances[account] -= scaledAmount;
emit Transfer(account, address(0), amount);
_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, IERC20) returns (bool) {
require(recipient != address(0), "Own: Transfer to zero address");
uint256 scaledAmount = _convertToScaledAmount(amount);
require(_scaledBalances[msg.sender] >= scaledAmount, "Own: Insufficient balance");

_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, IERC20) returns (bool) {
require(recipient != address(0), "Own: Transfer to zero address");
uint256 currentAllowance = allowance(sender, msg.sender);
require(currentAllowance >= amount, "Own: Insufficient allowance");

uint256 scaledAmount = _convertToScaledAmount(amount);
require(_scaledBalances[sender] >= scaledAmount, "Own: Insufficient balance");

_scaledBalances[sender] -= scaledAmount;
_scaledBalances[recipient] += scaledAmount;
_approve(sender, msg.sender, currentAllowance - amount);

emit Transfer(sender, recipient, amount);
return true;
}
}

0 comments on commit ce311bf

Please sign in to comment.