Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
pragma solidity ^0.6.6;
import "./ERC20.sol";
import "../libs/Address.sol";
import "../libs/ACONameFormatter.sol";
/**
* @title ACOToken
* @dev The implementation of the ACO token.
* The token is ERC20 compliant.
*/
contract ACOToken is ERC20 {
using Address for address;
/**
* @dev Struct to store the accounts that generated tokens with a collateral deposit.
*/
struct TokenCollateralized {
/**
* @dev Current amount of tokens.
*/
uint256 amount;
/**
* @dev Index on the collateral owners array.
*/
uint256 index;
}
/**
* @dev Emitted when collateral is deposited on the contract.
* @param account Address of the collateral owner.
* @param amount Amount of collateral deposited.
*/
event CollateralDeposit(address indexed account, uint256 amount);
/**
* @dev Emitted when collateral is withdrawn from the contract.
* @param account Address of the account.
* @param recipient Address of the collateral destination.
* @param amount Amount of collateral withdrawn.
* @param fee The fee amount charged on the withdrawal.
*/
event CollateralWithdraw(address indexed account, address indexed recipient, uint256 amount, uint256 fee);
/**
* @dev Emitted when the collateral is used on an assignment.
* @param from Address of the account of the collateral owner.
* @param to Address of the account that exercises tokens to get the collateral.
* @param paidAmount Amount paid to the collateral owner.
* @param tokenAmount Amount of tokens used to exercise.
*/
event Assigned(address indexed from, address indexed to, uint256 paidAmount, uint256 tokenAmount);
/**
* @dev Emitted when the collateralized token is transferred.
* @param from Address of the account of the collateral owner.
* @param to Address of the account to get the collateralized tokens.
* @param tokenCollateralizedAmount Amount of collateralized tokens transferred.
*/
event TransferCollateralOwnership(address indexed from, address indexed to, uint256 tokenCollateralizedAmount);
/**
* @dev The ERC20 token address for the underlying asset (0x0 for Ethereum).
*/
address public underlying;
/**
* @dev The ERC20 token address for the strike asset (0x0 for Ethereum).
*/
address public strikeAsset;
/**
* @dev Address of the fee destination charged on the exercise.
*/
address payable public feeDestination;
/**
* @dev True if the type is CALL, false for PUT.
*/
bool public isCall;
/**
* @dev The strike price for the token with the strike asset precision.
*/
uint256 public strikePrice;
/**
* @dev The UNIX time for the token expiration.
*/
uint256 public expiryTime;
/**
* @dev The total amount of collateral on the contract.
*/
uint256 public totalCollateral;
/**
* @dev The fee value. It is a percentage value (100000 is 100%).
*/
uint256 public acoFee;
/**
* @dev Symbol of the underlying asset.
*/
string public underlyingSymbol;
/**
* @dev Symbol of the strike asset.
*/
string public strikeAssetSymbol;
/**
* @dev Decimals for the underlying asset.
*/
uint8 public underlyingDecimals;
/**
* @dev Decimals for the strike asset.
*/
uint8 public strikeAssetDecimals;
/**
* @dev The maximum number of accounts that can be exercised by transaction.
*/
uint256 public maxExercisedAccounts;
/**
* @dev Underlying precision. (10 ^ underlyingDecimals)
*/
uint256 internal underlyingPrecision;
/**
* @dev Accounts that generated tokens with a collateral deposit.
*/
mapping(address => TokenCollateralized) internal tokenData;
/**
* @dev Array with all accounts with collateral deposited.
*/
address[] internal _collateralOwners;
/**
* @dev Internal data to control the reentrancy.
*/
bool internal _notEntered;
/**
* @dev Selector for ERC20 transfer function.
*/
bytes4 internal _transferSelector;
/**
* @dev Selector for ERC20 transfer from function.
*/
bytes4 internal _transferFromSelector;
/**
* @dev Modifier to check if the token is not expired.
* It is executed only while the token is not expired.
*/
modifier notExpired() {
require(_notExpired(), "ACOToken::Expired");
_;
}
/**
* @dev Modifier to prevent a contract from calling itself during the function execution.
*/
modifier nonReentrant() {
require(_notEntered, "ACOToken::Reentry");
_notEntered = false;
_;
_notEntered = true;
}
/**
* @dev Function to initialize the contract.
* It should be called when creating the token.
* It must be called only once. The first `require` is to guarantee that behavior.
* @param _underlying Address of the underlying asset (0x0 for Ethereum).
* @param _strikeAsset Address of the strike asset (0x0 for Ethereum).
* @param _isCall True if the type is CALL, false for PUT.
* @param _strikePrice The strike price with the strike asset precision.
* @param _expiryTime The UNIX time for the token expiration.
* @param _acoFee Value of the ACO fee. It is a percentage value (100000 is 100%).
* @param _feeDestination Address of the fee destination charged on the exercise.
* @param _maxExercisedAccounts The maximum number of accounts that can be exercised by transaction.
*/
function init(
address _underlying,
address _strikeAsset,
bool _isCall,
uint256 _strikePrice,
uint256 _expiryTime,
uint256 _acoFee,
address payable _feeDestination,
uint256 _maxExercisedAccounts
) public {
require(underlying == address(0) && strikeAsset == address(0) && strikePrice == 0, "ACOToken::init: Already initialized");
require(_expiryTime > now, "ACOToken::init: Invalid expiry");
require(_strikePrice > 0, "ACOToken::init: Invalid strike price");
require(_underlying != _strikeAsset, "ACOToken::init: Same assets");
require(_acoFee <= 500, "ACOToken::init: Invalid ACO fee"); // Maximum is 0.5%
require(_isEther(_underlying) || _underlying.isContract(), "ACOToken::init: Invalid underlying");
require(_isEther(_strikeAsset) || _strikeAsset.isContract(), "ACOToken::init: Invalid strike asset");
require(_maxExercisedAccounts >= 25 && _maxExercisedAccounts <= 150, "ACOToken::init: Invalid number to max exercised accounts");
underlying = _underlying;
strikeAsset = _strikeAsset;
isCall = _isCall;
strikePrice = _strikePrice;
expiryTime = _expiryTime;
acoFee = _acoFee;
feeDestination = _feeDestination;
maxExercisedAccounts = _maxExercisedAccounts;
underlyingDecimals = _getAssetDecimals(_underlying);
require(underlyingDecimals < 78, "ACOToken::init: Invalid underlying decimals");
strikeAssetDecimals = _getAssetDecimals(_strikeAsset);
underlyingSymbol = _getAssetSymbol(_underlying);
strikeAssetSymbol = _getAssetSymbol(_strikeAsset);
underlyingPrecision = 10 ** uint256(underlyingDecimals);
_transferSelector = bytes4(keccak256(bytes("transfer(address,uint256)")));
_transferFromSelector = bytes4(keccak256(bytes("transferFrom(address,address,uint256)")));
_notEntered = true;
}
/**
* @dev Function to guarantee that the contract will not receive ether directly.
*/
receive() external payable {
revert();
}
/**
* @dev Function to get the token name.
*/
function name() public view override returns(string memory) {
return _name();
}
/**
* @dev Function to get the token symbol, that it is equal to the name.
*/
function symbol() public view override returns(string memory) {
return _name();
}
/**
* @dev Function to get the token decimals, that it is equal to the underlying asset decimals.
*/
function decimals() public view override returns(uint8) {
return underlyingDecimals;
}
/**
* @dev Function to get the current amount of collateral for an account.
* @param account Address of the account.
* @return The current amount of collateral.
*/
function currentCollateral(address account) public view returns(uint256) {
return getCollateralAmount(currentCollateralizedTokens(account));
}
/**
* @dev Function to get the current amount of unassignable collateral for an account.
* After expiration, the unassignable collateral is equal to the account's collateral balance.
* @param account Address of the account.
* @return The respective amount of unassignable collateral.
*/
function unassignableCollateral(address account) public view returns(uint256) {
return getCollateralAmount(unassignableTokens(account));
}
/**
* @dev Function to get the current amount of assignable collateral for an account.
* After expiration, the assignable collateral is zero.
* @param account Address of the account.
* @return The respective amount of assignable collateral.
*/
function assignableCollateral(address account) public view returns(uint256) {
return getCollateralAmount(assignableTokens(account));
}
/**
* @dev Function to get the current amount of collateralized tokens for an account.
* @param account Address of the account.
* @return The current amount of collateralized tokens.
*/
function currentCollateralizedTokens(address account) public view returns(uint256) {
return tokenData[account].amount;
}
/**
* @dev Function to get the current amount of unassignable tokens for an account.
* After expiration, the unassignable tokens is equal to the account's collateralized tokens.
* @param account Address of the account.
* @return The respective amount of unassignable tokens.
*/
function unassignableTokens(address account) public view returns(uint256) {
if (balanceOf(account) > tokenData[account].amount || !_notExpired()) {
return tokenData[account].amount;
} else {
return balanceOf(account);
}
}
/**
* @dev Function to get the current amount of assignable tokens for an account.
* After expiration, the assignable tokens is zero.
* @param account Address of the account.
* @return The respective amount of assignable tokens.
*/
function assignableTokens(address account) public view returns(uint256) {
if (_notExpired()) {
return _getAssignableAmount(account);
} else {
return 0;
}
}
/**
* @dev Function to get the equivalent collateral amount for a token amount.
* @param tokenAmount Amount of tokens.
* @return The respective amount of collateral.
*/
function getCollateralAmount(uint256 tokenAmount) public view returns(uint256) {
if (isCall) {
return tokenAmount;
} else if (tokenAmount > 0) {
return _getTokenStrikePriceRelation(tokenAmount);
} else {
return 0;
}
}
/**
* @dev Function to get the equivalent token amount for a collateral amount.
* @param collateralAmount Amount of collateral.
* @return The respective amount of tokens.
*/
function getTokenAmount(uint256 collateralAmount) public view returns(uint256) {
if (isCall) {
return collateralAmount;
} else if (collateralAmount > 0) {
return collateralAmount.mul(underlyingPrecision).div(strikePrice);
} else {
return 0;
}
}
/**
* @dev Function to get the number of addresses that have collateral deposited.
* @return The number of addresses.
*/
function numberOfAccountsWithCollateral() public view returns(uint256) {
return _collateralOwners.length;
}
/**
* @dev Function to get the base data for exercise of an amount of token.
* To call the exercise the value returned must be added by the number of accounts that could be exercised:
* - using the ´exercise´ or ´exerciseFrom´ functions it will be equal to `maxExercisedAccounts`.
* - using the ´exerciseAccounts´ or `exerciseAccountsFrom` functions it will be equal to the number of accounts sent as function argument.
* @param tokenAmount Amount of tokens.
* @return The asset and the respective base amount that should be sent to get the collateral.
*/
function getBaseExerciseData(uint256 tokenAmount) public view returns(address, uint256) {
if (isCall) {
return (strikeAsset, _getTokenStrikePriceRelation(tokenAmount));
} else {
return (underlying, tokenAmount);
}
}
/**
* @dev Function to get the collateral to be received on an exercise and the respective fee.
* @param tokenAmount Amount of tokens.
* @return The collateral to be received and the respective fee.
*/
function getCollateralOnExercise(uint256 tokenAmount) public view returns(uint256, uint256) {
uint256 collateralAmount = getCollateralAmount(tokenAmount);
uint256 fee = collateralAmount.mul(acoFee).div(100000);
collateralAmount = collateralAmount.sub(fee);
return (collateralAmount, fee);
}
/**
* @dev Function to get the collateral asset.
* @return The address of the collateral asset.
*/
function collateral() public view returns(address) {
if (isCall) {
return underlying;
} else {
return strikeAsset;
}
}
/**
* @dev Function to mint tokens with Ether deposited as collateral.
* NOTE: The function only works when the token is NOT expired yet.
* @return The amount of tokens minted.
*/
function mintPayable() external payable returns(uint256) {
require(_isEther(collateral()), "ACOToken::mintPayable: Invalid call");
return _mintToken(msg.sender, msg.value);
}
/**
* @dev Function to mint tokens with Ether deposited as collateral to an informed account.
* However, the minted tokens are assigned to the transaction sender.
* NOTE: The function only works when the token is NOT expired yet.
* @param account Address of the account that will be the collateral owner.
* @return The amount of tokens minted.
*/
function mintToPayable(address account) external payable returns(uint256) {
require(_isEther(collateral()), "ACOToken::mintToPayable: Invalid call");
return _mintToken(account, msg.value);
}
/**
* @dev Function to mint tokens with ERC20 deposited as collateral.
* NOTE: The function only works when the token is NOT expired yet.
* @param collateralAmount Amount of collateral deposited.
* @return The amount of tokens minted.
*/
function mint(uint256 collateralAmount) external returns(uint256) {
address _collateral = collateral();
require(!_isEther(_collateral), "ACOToken::mint: Invalid call");
_transferFromERC20(_collateral, msg.sender, address(this), collateralAmount);
return _mintToken(msg.sender, collateralAmount);
}
/**
* @dev Function to mint tokens with ERC20 deposited as collateral to an informed account.
* However, the minted tokens are assigned to the transaction sender.
* NOTE: The function only works when the token is NOT expired yet.
* @param account Address of the account that will be the collateral owner.
* @param collateralAmount Amount of collateral deposited.
* @return The amount of tokens minted.
*/
function mintTo(address account, uint256 collateralAmount) external returns(uint256) {
address _collateral = collateral();
require(!_isEther(_collateral), "ACOToken::mintTo: Invalid call");
_transferFromERC20(_collateral, msg.sender, address(this), collateralAmount);
return _mintToken(account, collateralAmount);
}
/**
* @dev Function to burn tokens and get the collateral, not assigned, back.
* NOTE: The function only works when the token is NOT expired yet.
* @param tokenAmount Amount of tokens to be burned.
* @return The amount of collateral transferred.
*/
function burn(uint256 tokenAmount) external returns(uint256) {
return _burn(msg.sender, tokenAmount);
}
/**
* @dev Function to burn tokens from a specific account and send the collateral to its address.
* The token allowance must be respected.
* The collateral is sent to the transaction sender.
* NOTE: The function only works when the token is NOT expired yet.
* @param account Address of the account.
* @param tokenAmount Amount of tokens to be burned.
* @return The amount of collateral transferred.
*/
function burnFrom(address account, uint256 tokenAmount) external returns(uint256) {
return _burn(account, tokenAmount);
}
/**
* @dev Function to get the collateral, not assigned, back.
* NOTE: The function only works when the token IS expired.
* @return The amount of collateral transferred.
*/
function redeem() external returns(uint256) {
return _redeem(msg.sender);
}
/**
* @dev Function to get the collateral from a specific account sent back to its address .
* The token allowance must be respected.
* The collateral is sent to the transaction sender.
* NOTE: The function only works when the token IS expired.
* @param account Address of the account.
* @return The amount of collateral transferred.
*/
function redeemFrom(address account) external returns(uint256) {
require(tokenData[account].amount <= allowance(account, msg.sender), "ACOToken::redeemFrom: Allowance too low");
return _redeem(account);
}
/**
* @dev Function to exercise the tokens, paying to get the equivalent collateral.
* The paid amount is sent to the collateral owners that were assigned.
* NOTE: The function only works when the token is NOT expired.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
* @return The amount of collateral transferred.
*/
function exercise(uint256 tokenAmount, uint256 salt) external payable returns(uint256) {
return _exercise(msg.sender, tokenAmount, salt);
}
/**
* @dev Function to exercise the tokens from an account, paying to get the equivalent collateral.
* The token allowance must be respected.
* The paid amount is sent to the collateral owners that were assigned.
* The collateral is transferred to the transaction sender.
* NOTE: The function only works when the token is NOT expired.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
* @return The amount of collateral transferred.
*/
function exerciseFrom(address account, uint256 tokenAmount, uint256 salt) external payable returns(uint256) {
return _exercise(account, tokenAmount, salt);
}
/**
* @dev Function to exercise the tokens, paying to get the equivalent collateral.
* The paid amount is sent to the collateral owners (on accounts list) that were assigned.
* NOTE: The function only works when the token is NOT expired.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get collateral from.
* @return The amount of collateral transferred.
*/
function exerciseAccounts(uint256 tokenAmount, address[] calldata accounts) external payable returns(uint256) {
return _exerciseFromAccounts(msg.sender, tokenAmount, accounts);
}
/**
* @dev Function to transfer collateralized tokens.
* @param recipient Address of the destination.
* @param tokenCollateralizedAmount Amount of collateralized tokens to be transferred.
*/
function transferCollateralOwnership(address recipient, uint256 tokenCollateralizedAmount) external {
require(recipient != address(0), "ACOToken::transferCollateralOwnership: Invalid recipient");
require(tokenCollateralizedAmount > 0, "ACOToken::transferCollateralOwnership: Invalid amount");
TokenCollateralized storage senderData = tokenData[msg.sender];
senderData.amount = senderData.amount.sub(tokenCollateralizedAmount);
_removeCollateralDataIfNecessary(msg.sender);
TokenCollateralized storage recipientData = tokenData[recipient];
if (_hasCollateral(recipientData)) {
recipientData.amount = recipientData.amount.add(tokenCollateralizedAmount);
} else {
tokenData[recipient] = TokenCollateralized(tokenCollateralizedAmount, _collateralOwners.length);
_collateralOwners.push(recipient);
}
emit TransferCollateralOwnership(msg.sender, recipient, tokenCollateralizedAmount);
}
/**
* @dev Function to exercise the tokens from a specific account, paying to get the equivalent collateral sent to its address.
* The token allowance must be respected.
* The paid amount is sent to the collateral owners (on accounts list) that were assigned.
* The collateral is transferred to the transaction sender.
* NOTE: The function only works when the token is NOT expired.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get the deposited collateral.
* @return The amount of collateral transferred.
*/
function exerciseAccountsFrom(address account, uint256 tokenAmount, address[] calldata accounts) external payable returns(uint256) {
return _exerciseFromAccounts(account, tokenAmount, accounts);
}
/**
* @dev Internal function to redeem respective collateral from an account.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @return The amount of collateral transferred.
*/
function _redeemCollateral(address account, uint256 tokenAmount) internal returns(uint256) {
require(_accountHasCollateral(account), "ACOToken::_redeemCollateral: No collateral available");
require(tokenAmount > 0, "ACOToken::_redeemCollateral: Invalid token amount");
TokenCollateralized storage data = tokenData[account];
data.amount = data.amount.sub(tokenAmount);
_removeCollateralDataIfNecessary(account);
return _transferCollateral(account, getCollateralAmount(tokenAmount), 0);
}
/**
* @dev Internal function to mint tokens.
* The tokens are minted for the transaction sender.
* @param account Address of the account.
* @param collateralAmount Amount of collateral deposited.
* @return The amount of tokens minted.
*/
function _mintToken(address account, uint256 collateralAmount) nonReentrant notExpired internal returns(uint256) {
require(collateralAmount > 0, "ACOToken::_mintToken: Invalid collateral amount");
if (!_accountHasCollateral(account)) {
tokenData[account].index = _collateralOwners.length;
_collateralOwners.push(account);
}
uint256 tokenAmount = getTokenAmount(collateralAmount);
require(tokenAmount != 0, "ACOToken::_mintToken: Invalid token amount");
tokenData[account].amount = tokenData[account].amount.add(tokenAmount);
totalCollateral = totalCollateral.add(collateralAmount);
emit CollateralDeposit(account, collateralAmount);
super._mintAction(msg.sender, tokenAmount);
return tokenAmount;
}
/**
* @dev Internal function to transfer collateral.
* When there is a fee, the calculated fee is also transferred to the destination fee address.
* The collateral destination is always the transaction sender address.
* @param account Address of the account.
* @param collateralAmount Amount of collateral to be transferred.
* @param fee Amount of fee charged.
* @return The amount of collateral transferred.
*/
function _transferCollateral(address account, uint256 collateralAmount, uint256 fee) internal returns(uint256) {
totalCollateral = totalCollateral.sub(collateralAmount.add(fee));
address _collateral = collateral();
if (_isEther(_collateral)) {
payable(msg.sender).transfer(collateralAmount);
if (fee > 0) {
feeDestination.transfer(fee);
}
} else {
_transferERC20(_collateral, msg.sender, collateralAmount);
if (fee > 0) {
_transferERC20(_collateral, feeDestination, fee);
}
}
emit CollateralWithdraw(account, msg.sender, collateralAmount, fee);
return collateralAmount;
}
/**
* @dev Internal function to exercise the tokens from an account.
* @param account Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
* @return The amount of collateral transferred.
*/
function _exercise(address account, uint256 tokenAmount, uint256 salt) nonReentrant internal returns(uint256) {
_validateAndBurn(account, tokenAmount, maxExercisedAccounts);
_exerciseOwners(account, tokenAmount, salt);
(uint256 collateralAmount, uint256 fee) = getCollateralOnExercise(tokenAmount);
return _transferCollateral(account, collateralAmount, fee);
}
/**
* @dev Internal function to exercise the tokens from an account.
* @param account Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get the collateral from.
* @return The amount of collateral transferred.
*/
function _exerciseFromAccounts(address account, uint256 tokenAmount, address[] memory accounts) nonReentrant internal returns(uint256) {
_validateAndBurn(account, tokenAmount, accounts.length);
_exerciseAccounts(account, tokenAmount, accounts);
(uint256 collateralAmount, uint256 fee) = getCollateralOnExercise(tokenAmount);
return _transferCollateral(account, collateralAmount, fee);
}
/**
* @dev Internal function to exercise the assignable tokens from the stored list of collateral owners.
* @param exerciseAccount Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
*/
function _exerciseOwners(address exerciseAccount, uint256 tokenAmount, uint256 salt) internal {
uint256 accountsExercised = 0;
uint256 start = salt.mod(_collateralOwners.length);
uint256 index = start;
uint256 count = 0;
while (tokenAmount > 0 && count < _collateralOwners.length) {
uint256 remainingAmount = _exerciseAccount(_collateralOwners[index], tokenAmount, exerciseAccount);
if (remainingAmount < tokenAmount) {
accountsExercised++;
require(accountsExercised < maxExercisedAccounts || remainingAmount == 0, "ACOToken::_exerciseOwners: Too many accounts to exercise");
}
tokenAmount = remainingAmount;
++index;
if (index == _collateralOwners.length) {
index = 0;
}
++count;
}
require(tokenAmount == 0, "ACOToken::_exerciseOwners: Invalid remaining amount");
uint256 indexOnModifyIteration;
bool shouldModifyIteration = false;
if (index == 0) {
index = _collateralOwners.length;
} else if (index <= start) {
indexOnModifyIteration = index - 1;
shouldModifyIteration = true;
index = _collateralOwners.length;
}
for (uint256 i = 0; i < count; ++i) {
--index;
if (shouldModifyIteration && index < start) {
index = indexOnModifyIteration;
shouldModifyIteration = false;
}
_removeCollateralDataIfNecessary(_collateralOwners[index]);
}
}
/**
* @dev Internal function to exercise the assignable tokens from an accounts list.
* @param exerciseAccount Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get the collateral from.
*/
function _exerciseAccounts(address exerciseAccount, uint256 tokenAmount, address[] memory accounts) internal {
for (uint256 i = 0; i < accounts.length; ++i) {
if (tokenAmount == 0) {
break;
}
tokenAmount = _exerciseAccount(accounts[i], tokenAmount, exerciseAccount);
_removeCollateralDataIfNecessary(accounts[i]);
}
require(tokenAmount == 0, "ACOToken::_exerciseAccounts: Invalid remaining amount");
}
/**
* @dev Internal function to exercise the assignable tokens from an account and transfer to its address the respective payment.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @param exerciseAccount Address of the account that is exercising.
* @return Remaining amount of tokens.
*/
function _exerciseAccount(address account, uint256 tokenAmount, address exerciseAccount) internal returns(uint256) {
uint256 available = _getAssignableAmount(account);
if (available > 0) {
TokenCollateralized storage data = tokenData[account];
uint256 valueToTransfer;
if (available < tokenAmount) {
valueToTransfer = available;
tokenAmount = tokenAmount.sub(available);
} else {
valueToTransfer = tokenAmount;
tokenAmount = 0;
}
(address exerciseAsset, uint256 amount) = getBaseExerciseData(valueToTransfer);
// To guarantee that the minter will be paid.
amount = amount.add(1);
data.amount = data.amount.sub(valueToTransfer);
if (_isEther(exerciseAsset)) {
payable(account).transfer(amount);
} else {
_transferERC20(exerciseAsset, account, amount);
}
emit Assigned(account, exerciseAccount, amount, valueToTransfer);
}
return tokenAmount;
}
/**
* @dev Internal function to validate the exercise operation and burn the respective tokens.
* @param account Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param maximumNumberOfAccounts The maximum number of accounts that can be exercised.
*/
function _validateAndBurn(address account, uint256 tokenAmount, uint256 maximumNumberOfAccounts) notExpired internal {
require(tokenAmount > 0, "ACOToken::_validateAndBurn: Invalid token amount");
// Whether an account has deposited collateral it only can exercise the extra amount of unassignable tokens.
if (_accountHasCollateral(account)) {
require(tokenAmount <= balanceOf(account).sub(tokenData[account].amount), "ACOToken::_validateAndBurn: Token amount not available");
}
_callBurn(account, tokenAmount);
(address exerciseAsset, uint256 expectedAmount) = getBaseExerciseData(tokenAmount);
expectedAmount = expectedAmount.add(maximumNumberOfAccounts);
if (_isEther(exerciseAsset)) {
require(msg.value == expectedAmount, "ACOToken::_validateAndBurn: Invalid ether amount");
} else {
require(msg.value == 0, "ACOToken::_validateAndBurn: No ether expected");
_transferFromERC20(exerciseAsset, msg.sender, address(this), expectedAmount);
}
}
/**
* @dev Internal function to calculate the token strike price relation.
* @param tokenAmount Amount of tokens.
* @return Calculated value with strike asset precision.
*/
function _getTokenStrikePriceRelation(uint256 tokenAmount) internal view returns(uint256) {
return tokenAmount.mul(strikePrice).div(underlyingPrecision);
}
/**
* @dev Internal function to get the collateral sent back from an account.
* Function to be called when the token IS expired.
* @param account Address of the account.
* @return The amount of collateral transferred.
*/
function _redeem(address account) nonReentrant internal returns(uint256) {
require(!_notExpired(), "ACOToken::_redeem: Token not expired yet");
uint256 collateralAmount = _redeemCollateral(account, tokenData[account].amount);
super._burnAction(account, balanceOf(account));
return collateralAmount;
}
/**
* @dev Internal function to burn tokens from an account and get the collateral, not assigned, back.
* @param account Address of the account.
* @param tokenAmount Amount of tokens to be burned.
* @return The amount of collateral transferred.
*/
function _burn(address account, uint256 tokenAmount) nonReentrant notExpired internal returns(uint256) {
uint256 collateralAmount = _redeemCollateral(account, tokenAmount);
_callBurn(account, tokenAmount);
return collateralAmount;
}
/**
* @dev Internal function to burn tokens.
* @param account Address of the account.
* @param tokenAmount Amount of tokens to be burned.
*/
function _callBurn(address account, uint256 tokenAmount) internal {
if (account == msg.sender) {
super._burnAction(account, tokenAmount);
} else {
super._burnFrom(account, tokenAmount);
}
}
/**
* @dev Internal function to get the amount of assignable token from an account.
* @param account Address of the account.
* @return The assignable amount of tokens.
*/
function _getAssignableAmount(address account) internal view returns(uint256) {
if (tokenData[account].amount > balanceOf(account)) {
return tokenData[account].amount.sub(balanceOf(account));
} else {
return 0;
}
}
/**
* @dev Internal function to remove the token data with collateral if its total amount was assigned.
* @param account Address of account.
*/
function _removeCollateralDataIfNecessary(address account) internal {
TokenCollateralized storage data = tokenData[account];
if (!_hasCollateral(data)) {
uint256 lastIndex = _collateralOwners.length - 1;
if (lastIndex != data.index) {
address last = _collateralOwners[lastIndex];
tokenData[last].index = data.index;
_collateralOwners[data.index] = last;
}
_collateralOwners.pop();
delete tokenData[account];
}
}
/**
* @dev Internal function to get if the token is not expired.
* @return Whether the token is NOT expired.
*/
function _notExpired() internal view returns(bool) {
return now < expiryTime;
}
/**
* @dev Internal function to get if an account has collateral deposited.
* @param account Address of the account.
* @return Whether the account has collateral deposited.
*/
function _accountHasCollateral(address account) internal view returns(bool) {
return _hasCollateral(tokenData[account]);
}
/**
* @dev Internal function to get if an account has collateral deposited.
* @param data Token data from an account.
* @return Whether the account has collateral deposited.
*/
function _hasCollateral(TokenCollateralized storage data) internal view returns(bool) {
return data.amount > 0;
}
/**
* @dev Internal function to get if the address is for Ethereum (0x0).
* @param _address Address to be checked.
* @return Whether the address is for Ethereum.
*/
function _isEther(address _address) internal pure returns(bool) {
return _address == address(0);
}
/**
* @dev Internal function to get the token name.
* The token name is assembled with the token data:
* ACO UNDERLYING_SYMBOL-STRIKE_PRICE_STRIKE_ASSET_SYMBOL-TYPE-EXPIRYTIME
* @return The token name.
*/
function _name() internal view returns(string memory) {
return string(abi.encodePacked(
"ACO ",
underlyingSymbol,
"-",
ACONameFormatter.formatNumber(strikePrice, strikeAssetDecimals),
strikeAssetSymbol,
"-",
ACONameFormatter.formatType(isCall),
"-",
ACONameFormatter.formatTime(expiryTime)
));
}
/**
* @dev Internal function to the asset decimals.
* @param asset Address of the asset.
* @return The asset decimals.
*/
function _getAssetDecimals(address asset) internal view returns(uint8) {
if (_isEther(asset)) {
return uint8(18);
} else {
(bool success, bytes memory returndata) = asset.staticcall(abi.encodeWithSignature("decimals()"));
require(success, "ACOToken::_getAssetDecimals: Invalid asset decimals");
return abi.decode(returndata, (uint8));
}
}
/**
* @dev Internal function to the asset symbol.
* @param asset Address of the asset.
* @return The asset symbol.
*/
function _getAssetSymbol(address asset) internal view returns(string memory) {
if (_isEther(asset)) {
return "ETH";
} else {
(bool success, bytes memory returndata) = asset.staticcall(abi.encodeWithSignature("symbol()"));
require(success, "ACOToken::_getAssetSymbol: Invalid asset symbol");
return abi.decode(returndata, (string));
}
}
/**
* @dev Internal function to transfer ERC20 tokens.
* @param token Address of the token.
* @param recipient Address of the transfer destination.
* @param amount Amount to transfer.
*/
function _transferERC20(address token, address recipient, uint256 amount) internal {
(bool success, bytes memory returndata) = token.call(abi.encodeWithSelector(_transferSelector, recipient, amount));
require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "ACOToken::_transferERC20");
}
/**
* @dev Internal function to call transferFrom on ERC20 tokens.
* @param token Address of the token.
* @param sender Address of the sender.
* @param recipient Address of the transfer destination.
* @param amount Amount to transfer.
*/
function _transferFromERC20(address token, address sender, address recipient, uint256 amount) internal {
(bool success, bytes memory returndata) = token.call(abi.encodeWithSelector(_transferFromSelector, sender, recipient, amount));
require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "ACOToken::_transferFromERC20");
}
}