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

Only allow auctions to be started at most once a month per token #254

Merged
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
4 changes: 4 additions & 0 deletions contracts/ColonyNetworkAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ contract ColonyNetworkAuction is ColonyNetworkStorage {
event AuctionCreated(address auction, address token, uint256 quantity);

function startTokenAuction(address _token) public {
uint lastAuctionTimestamp = recentAuctions[_token];
require(lastAuctionTimestamp == 0 || now - lastAuctionTimestamp >= 30 days, "colony-auction-start-too-soon");
address clny = IColony(metaColony).getToken();
DutchAuction auction = new DutchAuction(clny, _token);
uint availableTokens = ERC20Extended(_token).balanceOf(this);
ERC20Extended(_token).transfer(auction, availableTokens);
auction.start();
recentAuctions[_token] = now;
emit AuctionCreated(address(auction), _token, availableTokens);
}
}
Expand Down
3 changes: 3 additions & 0 deletions contracts/ColonyNetworkStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,7 @@ contract ColonyNetworkStorage is DSAuth, DSMath {
uint256 reputationRootHashNNodes;
// Mapping containing how much has been staked by each user
mapping (address => uint) stakedBalances;

// Mapping containing the last auction start timestamp for a token address
mapping (address => uint) recentAuctions;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea to map token address to auction address and get the start time off that. This would give us a mapping for the latest token auction (which is not maintained on-chain currently but probably should be) and additionally avoid duplicating the startTime value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the auction contracts selfdestruct when complete, we'd have to check that there was still a contract there before trying to call startTime on it, otherwise we'd revert. That's fairly straightforward to do in assembly with the extcodesize call though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes, you're right. But we probably won't be able to determine if a month has gone by if the auction contract is destroyed, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, also true, I hadn't considered that an auction could finish within the 30 days.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave it as is then. At most we might add the auction contract address record to this mapping at some point.

}
53 changes: 24 additions & 29 deletions test/colony-network-auction.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ contract("ColonyNetworkAuction", accounts => {
let clnyNeededForMaxPriceAuctionSellout;
let clny;
let token;
let createAuctionTxReceipt;

before(async () => {
quantity = new BN(10).pow(new BN(36)).muln(3);
Expand All @@ -42,7 +43,8 @@ contract("ColonyNetworkAuction", accounts => {
token = await Token.new(...args);
await token.mint(quantity.toString());
await token.transfer(colonyNetwork.address, quantity.toString());
const { logs } = await colonyNetwork.startTokenAuction(token.address);
const { logs, receipt } = await colonyNetwork.startTokenAuction(token.address);
createAuctionTxReceipt = receipt;
const auctionAddress = logs[0].args.auction;
tokenAuction = DutchAuction.at(auctionAddress);
});
Expand All @@ -66,16 +68,12 @@ contract("ColonyNetworkAuction", accounts => {
it("should fail with zero quantity", async () => {
const args = getTokenArgs();
const otherToken = await Token.new(...args);
const { logs } = await colonyNetwork.startTokenAuction(otherToken.address);
const auctionAddress = logs[0].args.auction;
tokenAuction = DutchAuction.at(auctionAddress);
await checkErrorRevert(tokenAuction.start());
await checkErrorRevert(colonyNetwork.startTokenAuction(otherToken.address));
});
});

describe("when starting an auction", async () => {
it("should set the `quantity` correctly and minPrice to 1", async () => {
await tokenAuction.start();
const quantityNow = await tokenAuction.quantity.call();
assert.equal(quantityNow.toString(10), quantity.toString());

Expand All @@ -91,36 +89,31 @@ contract("ColonyNetworkAuction", accounts => {
const { logs } = await colonyNetwork.startTokenAuction(otherToken.address);
const auctionAddress = logs[0].args.auction;
tokenAuction = DutchAuction.at(auctionAddress);
await tokenAuction.start();
const minPrice = await tokenAuction.minPrice.call();
assert.equal(minPrice.toString(10), 10);
});

it("should set the `startTime` correctly", async () => {
const tx = await tokenAuction.start();
const txReceiptBlockNumber = tx.receipt.blockNumber;
const blockTime = await getBlockTime(txReceiptBlockNumber);
const createAuctionTxBlockNumber = createAuctionTxReceipt.blockNumber;
const blockTime = await getBlockTime(createAuctionTxBlockNumber);

const startTime = await tokenAuction.startTime.call();
const startTime = await tokenAuction.startTime();
assert.equal(startTime.toNumber(), blockTime);
});

it("should set the `started` property correctly", async () => {
await tokenAuction.start();

const started = await tokenAuction.started.call();
assert.isTrue(started);
});

it("should fail starting the auction twice", async () => {
await tokenAuction.start();
await checkErrorRevert(tokenAuction.start());
await checkErrorRevert(colonyNetwork.startTokenAuction(token.address));
});

it("cannot bid before the auction is open", async () => {
await giveUserCLNYTokens(colonyNetwork, BIDDER_1, "1000000000000000000");
await clny.approve(tokenAuction.address, "1000000000000000000", { from: BIDDER_1 });
await checkErrorRevert(tokenAuction.bid("1000000000000000000", { from: BIDDER_1 }));
it("should fail if the last auction for the same token started less than 30 days", async () => {
await token.mint(quantity.toString());
await token.transfer(colonyNetwork.address, quantity.toString());
await checkErrorRevert(colonyNetwork.startTokenAuction(token.address));
});

const auctionProps = [
Expand Down Expand Up @@ -180,8 +173,6 @@ contract("ColonyNetworkAuction", accounts => {

auctionProps.forEach(async auctionProp => {
it(`should correctly calculate price and remaining CLNY amount to end auction at duration ${auctionProp.duration}`, async () => {
await tokenAuction.start();

await forwardTime(auctionProp.duration, this);
const currentPrice = await tokenAuction.price.call();
// Expect up to 1% error margin because of forwarding block time inaccuracies
Expand All @@ -198,13 +189,21 @@ contract("ColonyNetworkAuction", accounts => {
assert.isTrue(differenceQuantity.lte(errorMarginQuantity));
});
});
});

describe("when bidding", async () => {
beforeEach(async () => {
await tokenAuction.start();
it("should succeed if the last auction for the same token was started at least 30 days ago", async () => {
const previousAuctionStartTime = await tokenAuction.startTime();
// 30 days (in seconds)
await forwardTime(30 * 24 * 60 * 60, this);

await token.mint(quantity.toString());
await token.transfer(colonyNetwork.address, quantity.toString());
await colonyNetwork.startTokenAuction(token.address);
const newAuctionStartTime = await tokenAuction.startTime();
assert.notEqual(previousAuctionStartTime, newAuctionStartTime);
});
});

describe("when bidding", async () => {
it("can bid", async () => {
await giveUserCLNYTokens(colonyNetwork, BIDDER_1, "1000000000000000000");
await clny.approve(tokenAuction.address, "1000000000000000000", { from: BIDDER_1 });
Expand Down Expand Up @@ -288,7 +287,6 @@ contract("ColonyNetworkAuction", accounts => {

describe("when finalizing auction", async () => {
beforeEach(async () => {
await tokenAuction.start();
await giveUserCLNYTokens(colonyNetwork, BIDDER_1, clnyNeededForMaxPriceAuctionSellout.toString());
await clny.approve(tokenAuction.address, clnyNeededForMaxPriceAuctionSellout.toString(), { from: BIDDER_1 });
await tokenAuction.bid(clnyNeededForMaxPriceAuctionSellout.toString(), { from: BIDDER_1 });
Expand Down Expand Up @@ -351,7 +349,6 @@ contract("ColonyNetworkAuction", accounts => {
await clny.approve(tokenAuction.address, bidAmount2.toString(), { from: BIDDER_2 });
await clny.approve(tokenAuction.address, bidAmount3.toString(), { from: BIDDER_3 });

await tokenAuction.start();
await tokenAuction.bid(bidAmount1.toString(), { from: BIDDER_1 }); // Bids at near max price of 1e36 CLNY per 1e18 Tokens
await forwardTime(1382400, this); // Gets us near price of 1e20 CLNY per 1e18 Tokens
await tokenAuction.bid(bidAmount2.toString(), { from: BIDDER_2 });
Expand Down Expand Up @@ -400,7 +397,6 @@ contract("ColonyNetworkAuction", accounts => {
});

it("should set the bid amount to 0", async () => {
await tokenAuction.start();
await giveUserCLNYTokens(colonyNetwork, BIDDER_1, clnyNeededForMaxPriceAuctionSellout.toString());
await clny.approve(tokenAuction.address, clnyNeededForMaxPriceAuctionSellout.toString(), { from: BIDDER_1 });
await tokenAuction.bid(clnyNeededForMaxPriceAuctionSellout.toString(), { from: BIDDER_1 });
Expand All @@ -413,7 +409,6 @@ contract("ColonyNetworkAuction", accounts => {

describe("when closing the auction", async () => {
beforeEach(async () => {
await tokenAuction.start();
await giveUserCLNYTokens(colonyNetwork, BIDDER_1, clnyNeededForMaxPriceAuctionSellout.toString());
await clny.approve(tokenAuction.address, clnyNeededForMaxPriceAuctionSellout.toString(), { from: BIDDER_1 });
await tokenAuction.bid(clnyNeededForMaxPriceAuctionSellout.toString(), { from: BIDDER_1 });
Expand Down