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

Nat Spec for ozUSD and wozUSD #6

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 107 additions & 66 deletions packages/contracts-bedrock/src/L2/OzUSD.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,57 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";

/// Rebasing token - does NOT conform to the ERC20 standard due to no event emitted on rebase
/// Reference implementation: https://vscode.blockscan.com/ethereum/0x17144556fd3424edc8fc8a4c940b2d04936d17eb
/// Need to add/redo Nat Spec also
/// @title Ozean USD (ozUSD) Token Contract
/// @notice This contract implements a rebasing token (ozUSD), where token balances are dynamic and calculated
/// based on shares controlled by each account. The total pooled USDX (protocol-controlled USDX) determines the
/// total balances; meaning that any USDX sent to this contract automatically rebases all user balances.
/// 1 USDX == 1 ozUSD.
/// @dev This contract does not fully comply with the ERC20 standard as rebasing events do not emit `Transfer` events.
/// This contract is inspired by Lido's stETH contract: https://vscode.blockscan.com/ethereum/0x17144556fd3424edc8fc8a4c940b2d04936d17eb
contract OzUSD is IERC20, ReentrancyGuard, Initializable {
string public constant name = "Ozean USD";
string public constant symbol = "ozUSD";
uint8 public constant decimals = 18;
uint256 private totalShares;

/**
* @dev ozUSD balances are dynamic and are calculated based on the accounts' shares
* and the total amount of USDX controlled by the protocol. Account shares aren't
* normalized, so the contract also stores the sum of all shares to calculate
* each account's token balance which equals to:
*
* shares[account] * _getTotalPooledUSDX() / totalShares
*/
/// @notice A mapping from addresses to shares controlled by each account.
/// @dev ozUSD balances are dynamic and are calculated based on the accounts' shares and the total amount of
/// USDX controlled by the protocol. Account shares aren't normalized, so the contract also stores the
/// sum of all shares to calculate each account's token balance which equals to:
/// shares[account] * _getTotalPooledUSDX() / totalShares
mapping(address => uint256) private shares;

/**
* @dev Allowances are denominated in tokens, not token shares.
*/
/// @notice A mapping to track token allowances for delegated spending.
/// @dev Allowances are denominated in tokens, not token shares.
mapping(address => mapping(address => uint256)) private allowances;

/**
* @notice An executed shares transfer from `sender` to `recipient`.
* @dev emitted in pair with an ERC20-defined `Transfer` event.
*/
/// @notice An executed shares transfer from `sender` to `recipient`.
/// @param from The address the shares are leaving from.
/// @param to The address receiving the shares.
/// @param sharesValue The number of shares being transferred.
/// @dev This is emitted in pair with an ERC20-defined `Transfer` event.
event TransferShares(address indexed from, address indexed to, uint256 sharesValue);

/**
* @notice An executed `burnShares` request
*
* @dev Reports simultaneously burnt shares amount and corresponding ozUSD amount.
* The ozUSD amount is calculated twice: before and after the burning incurred rebase.
*
* @param account holder of the burnt shares
* @param preRebaseTokenAmount amount of ozUSD the burnt shares corresponded to before the burn
* @param postRebaseTokenAmount amount of ozUSD the burnt shares corresponded to after the burn
* @param sharesAmount amount of burnt shares
*/
/// @notice An executed `burnShares` request
/// @param account holder of the burnt shares.
/// @param preRebaseTokenAmount amount of ozUSD the burnt shares corresponded to before the burn.
/// @param postRebaseTokenAmount amount of ozUSD the burnt shares corresponded to after the burn.
/// @param sharesAmount amount of burnt shares.
/// @dev Reports simultaneously burnt shares amount and corresponding ozUSD amount.
/// The ozUSD amount is calculated twice: before and after the burning incurred rebase.
event SharesBurnt(
address indexed account, uint256 preRebaseTokenAmount, uint256 postRebaseTokenAmount, uint256 sharesAmount
);

/// SETUP ///

constructor() {
_disableInitializers();
}

/// @notice Initializes the contract with a specific amount of shares.
/// @dev Requires the sender to send USDX equal to the number of shares specified in `_sharesAmount`.
/// @param _sharesAmount The number of shares to initialize.
function initialize(uint256 _sharesAmount) external payable initializer nonReentrant {
require(msg.value == _sharesAmount, "OzUSD: INCORRECT_VALUE");
_mintShares(address(0xdead), _sharesAmount);
Expand All @@ -64,48 +66,82 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable {

receive() external payable { }

/// @dev The `_amount` argument is the amount of tokens, not shares.
/// @notice Transfers an amount of ozUSD tokens from the caller to a recipient.
/// @param _recipient The recipient of the token transfer.
/// @param _amount The number of ozUSD tokens to transfer.
/// @return bool Returns `true` if the transfer was successful.
/// @dev The `_amount` parameter represents the number of tokens, not shares. It calculates the equivalent shares
/// and transfers those shares between the accounts.
function transfer(address _recipient, uint256 _amount) external nonReentrant returns (bool) {
_transfer(msg.sender, _recipient, _amount);
return true;
}

/// @dev The `_amount` argument is the amount of tokens, not shares.
/// @notice Transfers `ozUSD` tokens on behalf of a sender to a recipient.
/// @param _sender The account from which the tokens are transferred.
/// @param _recipient The recipient of the token transfer.
/// @param _amount The number of ozUSD tokens to transfer.
/// @return success Returns `true` if the transfer was successful.
/// @dev The `_amount` parameter represents the number of tokens, not shares. The caller must have an allowance
/// from the sender to spend the specified amount.
function transferFrom(address _sender, address _recipient, uint256 _amount) external nonReentrant returns (bool) {
_spendAllowance(_sender, msg.sender, _amount);
_transfer(_sender, _recipient, _amount);
return true;
}

/// @notice Approves a spender to spend a specific number of `ozUSD` tokens on behalf of the caller.
/// @param _spender The address authorized to spend the tokens.
/// @param _amount The number of tokens allowed to be spent.
/// @return success Returns `true` if the approval was successful.
/// @dev The `_amount` argument is the amount of tokens, not shares.
function approve(address _spender, uint256 _amount) external nonReentrant returns (bool) {
_approve(msg.sender, _spender, _amount);
return true;
}

/// @dev The `_addedValue` argument is the amount of tokens, not shares.
/// @notice Increases the allowance of a spender by a specified amount.
/// @param _spender The address authorized to spend the tokens.
/// @param _addedValue The additional amount of tokens the spender is allowed to spend.
/// @return success Returns `true` if the operation was successful.
/// @dev The `_addedValue` argument is the amount of tokens, not shares.
function increaseAllowance(address _spender, uint256 _addedValue) external nonReentrant returns (bool) {
_approve(msg.sender, _spender, allowances[msg.sender][_spender] + _addedValue);
return true;
}

/// @dev The `_subtractedValue` argument is the amount of tokens, not shares.
/// @notice Decreases the allowance of a spender by a specified amount.
/// @param _spender The address authorized to spend the tokens.
/// @param _subtractedValue The amount of tokens to subtract from the current allowance.
/// @return success Returns `true` if the operation was successful.
/// @dev The `_subtractedValue` argument is the amount of tokens, not shares.
/// Reverts if the current allowance is less than the amount being subtracted.
function decreaseAllowance(address _spender, uint256 _subtractedValue) external nonReentrant returns (bool) {
uint256 currentAllowance = allowances[msg.sender][_spender];
require(currentAllowance >= _subtractedValue, "OzUSD: ALLOWANCE_BELOW_ZERO");
_approve(msg.sender, _spender, currentAllowance - _subtractedValue);
return true;
}

/// @dev The `_sharesAmount` argument is the amount of shares, not tokens.
/// @notice Transfers `ozUSD` shares from the caller to a recipient and returns the equivalent ozUSD tokens.
/// @param _recipient The recipient of the share transfer.
/// @param _sharesAmount The number of shares to transfer.
/// @return uint256 The equivalent ozUSD token amount corresponding to the transferred shares.
/// @dev The `_sharesAmount` argument is the amount of shares, not tokens.
function transferShares(address _recipient, uint256 _sharesAmount) external nonReentrant returns (uint256) {
_transferShares(msg.sender, _recipient, _sharesAmount);
uint256 tokensAmount = getPooledUSDXByShares(_sharesAmount);
_emitTransferEvents(msg.sender, _recipient, tokensAmount, _sharesAmount);
return tokensAmount;
}

/// @dev The `_sharesAmount` argument is the amount of shares, not tokens.
/// @notice Transfers `_sharesAmount` shares from `_sender` to `_recipient` and returns the equivalent ozUSD tokens.
/// @dev Shares are transferred, and equivalent ozUSD tokens are calculated and returned.
/// @param _sender The address to transfer shares from.
/// @param _recipient The address to transfer shares to.
/// @param _sharesAmount The number of shares to transfer.
/// @return uint256 The amount of ozUSD tokens equivalent to the transferred shares.
/// @dev The `_sharesAmount` argument is the amount of shares, not tokens.
function transferSharesFrom(
address _sender,
address _recipient,
Expand All @@ -122,6 +158,10 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable {
return tokensAmount;
}

/// @notice Mints `ozUSD` to the specified `_to` address by depositing a `_usdxAmount` of USDX.
/// @dev Transfers USDX and mints new shares accordingly.
/// @param _to The address to receive the minted ozUSD.
/// @param _usdxAmount The amount of USDX to lock in exchange for ozUSD.
function mintOzUSD(address _to, uint256 _usdxAmount) external payable nonReentrant {
require(_usdxAmount != 0, "OzUSD: Amount zero");
require(msg.value == _usdxAmount, "OzUSD: Insufficient USDX transfer");
Expand All @@ -133,7 +173,11 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable {
_emitTransferAfterMintingShares(_to, newTotalShares);
}

/// @dev spender must approve contract, even if owner of coins
/// @notice Redeems ozUSD tokens by burning shares and redeeming the equivalent amount of `_ozUSDAmount` in USDX.
/// @param _from The address that owns the ozUSD to redeem.
/// @param _ozUSDAmount The amount of ozUSD to redeem.
/// @dev Spender must approve contract, even if owner of coins
/// Burns shares and transfers back the corresponding USDX.
function redeemOzUSD(address _from, uint256 _ozUSDAmount) external nonReentrant {
require(_ozUSDAmount != 0, "OzUSD: Amount zero");
_spendAllowance(_from, msg.sender, _ozUSDAmount);
Expand All @@ -149,46 +193,48 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable {

/// VIEW ///

/**
* @return the amount of tokens owned by the `_account`.
*
* @dev Balances are dynamic and equal the `_account`'s share in the amount of the
* total USDX controlled by the protocol. See `sharesOf`.
*/
/// @notice Returns the balance of ozUSD tokens owned by `_account`.
/// @param _account The address to query the balance for.
/// @return uint256 The amount of ozUSD tokens owned by `_account`.
/// @dev Balances are dynamic and equal to the _account's share of the total USDX controlled by the protocol.
/// This is calculated using the `sharesOf` function.
function balanceOf(address _account) external view returns (uint256) {
return getPooledUSDXByShares(shares[_account]);
}

/**
* @return the remaining number of tokens that `_spender` is allowed to spend
* on behalf of `_owner` through `transferFrom`. This is zero by default.
*
* @dev This value changes when `approve` or `transferFrom` is called.
*/
/// @notice Returns the remaining number of ozUSD tokens that `_spender` is allowed to spend on behalf of `_owner`.
/// @param _owner The address of the token owner.
/// @param _spender The address of the spender.
/// @return uint256 The remaining amount of ozUSD tokens that `_spender` can spend on behalf of `_owner`.
/// @dev This value is updated when `approve` or `transferFrom` is called. Defaults to zero.
function allowance(address _owner, address _spender) external view returns (uint256) {
return allowances[_owner][_spender];
}

/// @notice Returns the amount of shares owned by `_account`.
/// @param _account The address to query for shares.
/// @return uint256 The amount of shares owned by `_account`.
function sharesOf(address _account) external view returns (uint256) {
return shares[_account];
}

/// @return the amount of shares that corresponds to `_usdxAmount` protocol-controlled USDX.
/// @notice Returns the amount of shares that corresponds to the `_usdxAmount` of protocol-controlled USDX.
/// @param _usdxAmount The amount of USDX to convert to shares.
/// @return uint256 The equivalent amount of shares for `_usdxAmount`.
function getSharesByPooledUSDX(uint256 _usdxAmount) public view returns (uint256) {
return (_usdxAmount * totalShares) / _getTotalPooledUSDX();
}

/// @return the amount of USDX that corresponds to `_sharesAmount` token shares.
/// @notice Returns the amount of USDX that corresponds to `_sharesAmount` token shares.
/// @param _sharesAmount The number of shares to convert to USDX.
/// @return The equivalent amount of USDX for `_sharesAmount`.
function getPooledUSDXByShares(uint256 _sharesAmount) public view returns (uint256) {
return (_sharesAmount * _getTotalPooledUSDX()) / totalShares;
}

/**
* @return the amount of tokens in existence.
*
* @dev Always equals to `_getTotalPooledUSDX()` since token amount
* is pegged to the total amount of USDX controlled by the protocol.
*/
/// @notice Returns the total supply of ozUSD tokens in existence.
/// @return The total supply of ozUSD tokens.
/// @dev This is always equal to the total amount of USDX controlled by the protocol.
function totalSupply() external view returns (uint256) {
return _getTotalPooledUSDX();
}
Expand All @@ -203,7 +249,7 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable {
return shares[_account];
}

/// @notice Moves `_amount` tokens from `_sender` to `_recipient`.
/// @dev Moves `_amount` tokens from `_sender` to `_recipient`.
function _transfer(address _sender, address _recipient, uint256 _amount) internal {
uint256 _sharesToTransfer = getSharesByPooledUSDX(_amount);
_transferShares(_sender, _recipient, _sharesToTransfer);
Expand Down Expand Up @@ -238,10 +284,8 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable {
shares[_recipient] = shares[_recipient] + _sharesAmount;
}

/**
* @notice Creates `_sharesAmount` shares and assigns them to `_recipient`, increasing the total amount of shares.
* @dev This doesn't increase the token total supply.
*/
/// @notice Creates `_sharesAmount` shares and assigns them to `_recipient`, increasing the total amount of shares.
/// @dev This doesn't increase the token total supply.
function _mintShares(address _recipient, uint256 _sharesAmount) internal returns (uint256 newTotalShares) {
require(_recipient != address(0), "OzUSD: MINT_TO_ZERO_ADDR");

Expand All @@ -250,11 +294,8 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable {
shares[_recipient] += _sharesAmount;
}

/**
* @notice Destroys `_sharesAmount` shares from `_account`'s holdings, decreasing the total amount of shares.
* @dev This doesn't decrease the token total supply.
* @dev Suspect the pre and post Rebase amounts aren't necessary for this use-case
*/
/// @notice Destroys `_sharesAmount` shares from `_account`'s holdings, decreasing the total amount of shares.
/// @dev This doesn't decrease the token total supply.
function _burnShares(address _account, uint256 _sharesAmount) internal returns (uint256 newTotalShares) {
require(_account != address(0), "OzUSD: BURN_FROM_ZERO_ADDR");

Expand Down
19 changes: 15 additions & 4 deletions packages/contracts-bedrock/src/L2/WozUSD.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,46 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import { OzUSD } from "./OzUSD.sol";

/// Auto-compounding token of ozUSD
/// Reference implementation: https://vscode.blockscan.com/ethereum/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0
/// @title Wrapped Ozean USD (WozUSD)
/// @notice A wrapper contract for OzUSD, providing auto-compounding functionality.
/// @dev The contract wraps ozUSD into wozUSD, which represents shares of ozUSD.
/// This contract is inspired by Lido's wstETH contract: https://vscode.blockscan.com/ethereum/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0
contract WozUSD is ERC20, ReentrancyGuard {
/// @notice The instance of the ozUSD proxy contract.
OzUSD public immutable ozUSD;

constructor(OzUSD _ozUSD) ERC20("Wrapped Ozean USD", "wozUSD") {
ozUSD = _ozUSD;
}

/// @notice Wraps ozUSD into wozUSD (ozUSD shares).
/// @param _ozUSDAmount The amount of ozUSD to wrap into wozUSD.
/// @return wozUSDAmount The amount of wozUSD minted based on the wrapped ozUSD.
function wrap(uint256 _ozUSDAmount) external nonReentrant returns (uint256 wozUSDAmount) {
require(_ozUSDAmount > 0, "WozUSD: Can't wrap zero ozUSD");
ozUSD.transferFrom(msg.sender, address(this), _ozUSDAmount);
wozUSDAmount = ozUSD.getSharesByPooledUSDX(_ozUSDAmount);
_mint(msg.sender, wozUSDAmount);
}

/// @notice Unwraps wozUSD into ozUSD.
/// @param _wozUSDAmount The amount of wozUSD to unwrap into ozUSD.
/// @return ozUSDAmount The amount of ozUSD returned based on the unwrapped wozUSD.
function unwrap(uint256 _wozUSDAmount) external nonReentrant returns (uint256 ozUSDAmount) {
require(_wozUSDAmount > 0, "WozUSD: Can't unwrap zero wozUSD");
_burn(msg.sender, _wozUSDAmount);
ozUSDAmount = ozUSD.getPooledUSDXByShares(_wozUSDAmount);
ozUSD.transfer(msg.sender, ozUSDAmount);
}

/// @notice Get amount of ozUSD for a one wozUSD
/// @notice Returns the amount of ozUSD equivalent to 1 wozUSD.
/// @return uint256 The amount of ozUSD corresponding to 1 wozUSD.
function ozUSDPerToken() external view returns (uint256) {
return ozUSD.getPooledUSDXByShares(1 ether);
}

/// @notice Get amount of wozUSD for a one ozUSD
/// @notice Returns the amount of wozUSD equivalent to 1 ozUSD.
/// @return The amount of wozUSD corresponding to 1 ozUSD.
function tokensPerOzUSD() external view returns (uint256) {
return ozUSD.getSharesByPooledUSDX(1 ether);
}
Expand Down