Skip to content

[H01] Collateral owners can skip being exercised #25

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

Merged
merged 1 commit into from
Jul 30, 2020
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
70 changes: 49 additions & 21 deletions smart-contracts/contracts/core/ACOToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ contract ACOToken is ERC20 {
uint8 public strikeAssetDecimals;

/**
* @dev Underlying precision. (10 ^ underlyingDecimals)
* @dev The maximum number of accounts that can be exercised by transaction.
*/
uint256 internal underlyingPrecision;
uint256 public maxExercisedAccounts;

/**
* @dev The maximum number of accounts that can be exercised by transaction.
* @dev Underlying precision. (10 ^ underlyingDecimals)
*/
uint256 public maxExercisedAccounts;
uint256 internal underlyingPrecision;

/**
* @dev Accounts that generated tokens with a collateral deposit.
Expand Down Expand Up @@ -486,9 +486,10 @@ contract ACOToken is ERC20 {
* 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.
*/
function exercise(uint256 tokenAmount) external payable {
_exercise(msg.sender, tokenAmount);
function exercise(uint256 tokenAmount, uint256 salt) external payable {
_exercise(msg.sender, tokenAmount, salt);
}

/**
Expand All @@ -499,9 +500,10 @@ contract ACOToken is ERC20 {
* 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.
*/
function exerciseFrom(address account, uint256 tokenAmount) external payable {
_exercise(account, tokenAmount);
function exerciseFrom(address account, uint256 tokenAmount, uint256 salt) external payable {
_exercise(account, tokenAmount, salt);
}

/**
Expand Down Expand Up @@ -603,10 +605,11 @@ contract ACOToken is ERC20 {
* @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.
*/
function _exercise(address account, uint256 tokenAmount) nonReentrant internal {
function _exercise(address account, uint256 tokenAmount, uint256 salt) nonReentrant internal {
_validateAndBurn(account, tokenAmount, maxExercisedAccounts);
_exerciseOwners(account, tokenAmount);
_exerciseOwners(account, tokenAmount, salt);
(uint256 collateralAmount, uint256 fee) = getCollateralOnExercise(tokenAmount);
_transferCollateral(account, collateralAmount, fee);
}
Expand All @@ -628,22 +631,48 @@ contract ACOToken is ERC20 {
* @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) internal {
function _exerciseOwners(address exerciseAccount, uint256 tokenAmount, uint256 salt) internal {
uint256 accountsExercised = 0;
uint256 start = _collateralOwners.length;
for (uint256 i = start; i > 0; --i) {
if (tokenAmount == 0) {
break;
}
uint256 remainingAmount = _exerciseAccount(_collateralOwners[i-1], tokenAmount, exerciseAccount);
uint256 start = salt % _collateralOwners.length; // _collateralOwners.length never will be zero.
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, "ACOToken::_exerciseOwners: Too many accounts to exercise");
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]);
}
}

/**
Expand All @@ -658,6 +687,7 @@ contract ACOToken is ERC20 {
break;
}
tokenAmount = _exerciseAccount(accounts[i], tokenAmount, exerciseAccount);
_removeCollateralDataIfNecessary(accounts[i]);
}
require(tokenAmount == 0, "ACOToken::_exerciseAccounts: Invalid remaining amount");
}
Expand All @@ -684,13 +714,11 @@ contract ACOToken is ERC20 {
}

(address exerciseAsset, uint256 amount) = getBaseExerciseData(valueToTransfer);
// To guarantee that the minter will be paid at least by 1 minimum collateral value.
// To guarantee that the minter will be paid.
amount = amount.add(1);

data.amount = data.amount.sub(valueToTransfer);

_removeCollateralDataIfNecessary(account);

if (_isEther(exerciseAsset)) {
payable(account).transfer(amount);
} else {
Expand Down
4 changes: 2 additions & 2 deletions smart-contracts/contracts/interfaces/IACOToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ interface IACOToken is IERC20 {
function burnFrom(address account, uint256 tokenAmount) external;
function redeem() external;
function redeemFrom(address account) external;
function exercise(uint256 tokenAmount) external payable;
function exerciseFrom(address account, uint256 tokenAmount) external payable;
function exercise(uint256 tokenAmount, uint256 salt) external payable;
function exerciseFrom(address account, uint256 tokenAmount, uint256 salt) external payable;
function exerciseAccounts(uint256 tokenAmount, address[] calldata accounts) external payable;
function exerciseAccountsFrom(address account, uint256 tokenAmount, address[] calldata accounts) external payable;
function clear() external;
Expand Down
16 changes: 10 additions & 6 deletions smart-contracts/contracts/periphery/ACOFlashExercise.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,10 @@ contract ACOFlashExercise is IUniswapV2Callee {
* @param acoToken Address of the ACO token.
* @param tokenAmount Amount of tokens to be exercised.
* @param minimumCollateral The minimum amount of collateral accepted to be received on the flash exercise.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
*/
function flashExercise(address acoToken, uint256 tokenAmount, uint256 minimumCollateral) public {
_flashExercise(acoToken, tokenAmount, minimumCollateral, new address[](0));
function flashExercise(address acoToken, uint256 tokenAmount, uint256 minimumCollateral, uint256 salt) public {
_flashExercise(acoToken, tokenAmount, minimumCollateral, salt, new address[](0));
}

/**
Expand All @@ -148,7 +149,7 @@ contract ACOFlashExercise is IUniswapV2Callee {
address[] memory accounts
) public {
require(accounts.length > 0, "ACOFlashExercise::flashExerciseAccounts: Accounts are required");
_flashExercise(acoToken, tokenAmount, minimumCollateral, accounts);
_flashExercise(acoToken, tokenAmount, minimumCollateral, 0, accounts);
}

/**
Expand Down Expand Up @@ -183,11 +184,12 @@ contract ACOFlashExercise is IUniswapV2Callee {
uint256 tokenAmount;
uint256 ethValue = 0;
uint256 remainingAmount;
uint256 salt;
address from;
address[] memory accounts;
{
uint256 minimumCollateral;
(from, acoToken, tokenAmount, minimumCollateral, accounts) = abi.decode(data, (address, address, uint256, uint256, address[]));
(from, acoToken, tokenAmount, minimumCollateral, salt, accounts) = abi.decode(data, (address, address, uint256, uint256, uint256, address[]));

(address exerciseAddress, uint256 expectedAmount) = _getAcoExerciseData(acoToken, tokenAmount, accounts);

Expand All @@ -208,7 +210,7 @@ contract ACOFlashExercise is IUniswapV2Callee {
}

if (accounts.length == 0) {
IACOToken(acoToken).exerciseFrom{value: ethValue}(from, tokenAmount);
IACOToken(acoToken).exerciseFrom{value: ethValue}(from, tokenAmount, salt);
} else {
IACOToken(acoToken).exerciseAccountsFrom{value: ethValue}(from, tokenAmount, accounts);
}
Expand Down Expand Up @@ -249,12 +251,14 @@ contract ACOFlashExercise is IUniswapV2Callee {
* @param acoToken Address of the ACO token.
* @param tokenAmount Amount of tokens to be exercised.
* @param minimumCollateral The minimum amount of collateral accepted to be received on the flash exercise.
* @param salt Random number to calculate the start index of the array of accounts to be exercised when using standard method.
* @param accounts The array of addresses to get the deposited collateral. Whether the array is empty the exercise will be executed using the standard method.
*/
function _flashExercise(
address acoToken,
uint256 tokenAmount,
uint256 minimumCollateral,
uint256 salt,
address[] memory accounts
) internal {
address pair = getUniswapPair(acoToken);
Expand All @@ -270,7 +274,7 @@ contract ACOFlashExercise is IUniswapV2Callee {
amount1Out = expectedAmount;
}

IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(this), abi.encode(msg.sender, acoToken, tokenAmount, minimumCollateral, accounts));
IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(this), abi.encode(msg.sender, acoToken, tokenAmount, minimumCollateral, salt, accounts));
}

/**
Expand Down
16 changes: 8 additions & 8 deletions smart-contracts/test/ACOFlashExercise.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ describe("ACOFlashExercise", function() {
await buidlerEthT1003C.connect(owner).approve(flashExercise.address, val3);
let b1 = await addr2.getBalance();
let exp1 = await flashExercise.getEstimatedReturn(buidlerEthT1003C.address, val3);
await flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1);
await flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1, 1);

expect(await buidlerEthT1003C.totalCollateral()).to.equal(val1.add(val2));
expect(await buidlerEthT1003C.balanceOf(await addr1.getAddress())).to.equal(0);
Expand Down Expand Up @@ -277,7 +277,7 @@ describe("ACOFlashExercise", function() {
expect((await buidlerT1T210000P.getBaseExerciseData(amount3))[1]).to.equal(amount3);
await buidlerT1T210000P.connect(owner).approve(flashExercise.address, amount3);
let exp2 = await flashExercise.getEstimatedReturn(buidlerT1T210000P.address, amount3);
await flashExercise.connect(owner).flashExercise(buidlerT1T210000P.address, amount3, exp2);
await flashExercise.connect(owner).flashExercise(buidlerT1T210000P.address, amount3, exp2, 1);
let fee2 = value3.mul(fee).div(100000);

expect(await buidlerT1T210000P.totalCollateral()).to.equal(value1.add(value2));
Expand Down Expand Up @@ -314,7 +314,7 @@ describe("ACOFlashExercise", function() {
expect((await buidlerEthT1003P.getBaseExerciseData(a3))[1]).to.equal(a3);
await buidlerEthT1003P.connect(owner).approve(flashExercise.address, a3);
let exp3 = await flashExercise.getEstimatedReturn(buidlerEthT1003P.address, a3);
await flashExercise.connect(owner).flashExercise(buidlerEthT1003P.address, a3, exp3);
await flashExercise.connect(owner).flashExercise(buidlerEthT1003P.address, a3, exp3, 1);
let fee3 = v3.mul(fee).div(100000);

expect(await buidlerEthT1003P.totalCollateral()).to.equal(v1.add(v2).add(one));
Expand Down Expand Up @@ -576,21 +576,21 @@ describe("ACOFlashExercise", function() {
let buidlerEthT210000C = await ethers.getContractAt("ACOToken", tx.events[tx.events.length - 1].args.acoToken);

await expect(
flashExercise.connect(owner).flashExercise(buidlerEthT210000C.address, val3, 0)
flashExercise.connect(owner).flashExercise(buidlerEthT210000C.address, val3, 0, 0)
).to.be.revertedWith("ACOFlashExercise::_flashExercise: Invalid Uniswap pair");

let exp1 = await flashExercise.getEstimatedReturn(buidlerEthT1003C.address, val3);

await expect(
flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1)
flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1, 0)
).to.be.revertedWith("SafeMath: subtraction overflow");

await expect(
flashExercise.connect(owner).flashExerciseAccounts(buidlerEthT1003C.address, val3, exp1, [await addr2.getAddress(), await addr1.getAddress(), await addr3.getAddress()])
).to.be.revertedWith("SafeMath: subtraction overflow");

await expect(
flashExercise.connect(owner).flashExercise(buidlerEthT1003P.address, a1.add(a1), 0)
flashExercise.connect(owner).flashExercise(buidlerEthT1003P.address, a1.add(a1), 0, 0)
).to.be.revertedWith("ACOFlashExercise::uniswapV2Call: Insufficient collateral amount");

await expect(
Expand All @@ -600,7 +600,7 @@ describe("ACOFlashExercise", function() {
await buidlerEthT1003C.connect(owner).approve(flashExercise.address, val3);

await expect(
flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1.add(1))
flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1.add(1), 0)
).to.be.revertedWith("ACOFlashExercise::uniswapV2Call: Minimum amount not satisfied");

await expect(
Expand All @@ -610,7 +610,7 @@ describe("ACOFlashExercise", function() {
await network.provider.send("evm_increaseTime", [86400]);

await expect(
flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1)
flashExercise.connect(owner).flashExercise(buidlerEthT1003C.address, val3, exp1, 0)
).to.be.revertedWith("ACOToken::Expired");

await expect(
Expand Down
Loading