From 2d64ba83876a14e503c1879b7812427c44d51633 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 19 May 2021 14:35:22 -0700 Subject: [PATCH 1/6] Add UINT256_MAX to ColonyExtension.sol --- contracts/extensions/ColonyExtension.sol | 2 ++ contracts/extensions/FundingQueue.sol | 1 - contracts/extensions/OneTxPayment.sol | 1 - contracts/extensions/VotingReputation.sol | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/extensions/ColonyExtension.sol b/contracts/extensions/ColonyExtension.sol index c2e29c8384..c9ec705d99 100644 --- a/contracts/extensions/ColonyExtension.sol +++ b/contracts/extensions/ColonyExtension.sol @@ -26,6 +26,8 @@ import "./../colony/ColonyDataTypes.sol"; abstract contract ColonyExtension is DSAuth, DSMath { + uint256 constant UINT256_MAX = 2**256 - 1; + event ExtensionInitialised(); address resolver; // Align storage with EtherRouter diff --git a/contracts/extensions/FundingQueue.sol b/contracts/extensions/FundingQueue.sol index 4e72d7bcbb..c70c948c7b 100644 --- a/contracts/extensions/FundingQueue.sol +++ b/contracts/extensions/FundingQueue.sol @@ -38,7 +38,6 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs { // Constants uint256 constant HEAD = 0; - uint256 constant UINT256_MAX = (2 ** 256) - 1; uint256 constant STAKE_FRACTION = WAD / 1000; // 0.1% uint256 constant COOLDOWN_PERIOD = 14 days; diff --git a/contracts/extensions/OneTxPayment.sol b/contracts/extensions/OneTxPayment.sol index aacc900d2b..28195389bc 100644 --- a/contracts/extensions/OneTxPayment.sol +++ b/contracts/extensions/OneTxPayment.sol @@ -26,7 +26,6 @@ import "./ColonyExtension.sol"; contract OneTxPayment is ColonyExtension { event OneTxPaymentMade(address agent, uint256 fundamentalId, uint256 nPayouts); - uint256 constant UINT256_MAX = 2**256 - 1; ColonyDataTypes.ColonyRole constant ADMINISTRATION = ColonyDataTypes.ColonyRole.Administration; ColonyDataTypes.ColonyRole constant FUNDING = ColonyDataTypes.ColonyRole.Funding; diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 3ee74d51e7..4729e04e4a 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -39,7 +39,6 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { event MotionEventSet(uint256 indexed motionId, uint256 eventIndex); // Constants - uint256 constant UINT256_MAX = 2**256 - 1; uint256 constant UINT128_MAX = 2**128 - 1; uint256 constant NAY = 0; From d5898a8e59dded986304d611613c30e485471658 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 19 May 2021 13:23:53 -0700 Subject: [PATCH 2/6] Add userLimit to CoinMachine --- contracts/extensions/CoinMachine.sol | 29 ++++- .../colony-arbitrary-transactions.js | 2 +- test/extensions/coin-machine.js | 100 ++++++++++++++---- 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/contracts/extensions/CoinMachine.sol b/contracts/extensions/CoinMachine.sol index 6df5f1de45..06ca3e1898 100644 --- a/contracts/extensions/CoinMachine.sol +++ b/contracts/extensions/CoinMachine.sol @@ -61,6 +61,11 @@ contract CoinMachine is ColonyExtension { bool evolvePrice; // If we should evolve the price or not + uint256 soldTotal; // Total tokens sold by the coin machine + uint256 userLimit; // Limit any address can buy of the total amount (as WAD percentage) + + mapping(address => uint256) soldUser; // Tokens sold to a particular user + // Modifiers modifier onlyRoot() { @@ -133,6 +138,7 @@ contract CoinMachine is ColonyExtension { uint256 _windowSize, uint256 _targetPerPeriod, uint256 _maxPerPeriod, + uint256 _userLimit, uint256 _startingPrice, address _whitelist ) @@ -147,6 +153,7 @@ contract CoinMachine is ColonyExtension { require(_windowSize <= 511, "coin-machine-window-too-large"); require(_targetPerPeriod > 0, "coin-machine-target-too-small"); require(_maxPerPeriod >= _targetPerPeriod, "coin-machine-max-too-small"); + require(_userLimit <= WAD, "coin-machine-limit-too-large"); token = _token; purchaseToken = _purchaseToken; @@ -160,6 +167,8 @@ contract CoinMachine is ColonyExtension { targetPerPeriod = _targetPerPeriod; maxPerPeriod = _maxPerPeriod; + userLimit = _userLimit; + activePrice = _startingPrice; activePeriod = getCurrentPeriod(); @@ -191,7 +200,10 @@ contract CoinMachine is ColonyExtension { ); uint256 tokenBalance = getTokenBalance(); - uint256 numTokens = min(min(_numTokens, maxPerPeriod - activeSold), tokenBalance); + uint256 maxPurchase = getMaxPurchase(msg.sender); + uint256 remainingTokens = sub(maxPerPeriod, activeSold); + + uint256 numTokens = min(maxPurchase, min(tokenBalance, min(remainingTokens, _numTokens))); uint256 totalCost = wmul(numTokens, activePrice); activeIntake = add(activeIntake, totalCost); @@ -199,6 +211,12 @@ contract CoinMachine is ColonyExtension { assert(activeSold <= maxPerPeriod); + // Do userLimit bookkeeping (only if needed) + if (userLimit < WAD) { + soldTotal = add(soldTotal, numTokens); + soldUser[msg.sender] = add(soldUser[msg.sender], numTokens); + } + // Check if we've sold out if (numTokens >= tokenBalance) { setPriceEvolution(false); } @@ -341,6 +359,15 @@ contract CoinMachine is ColonyExtension { ); } + /// @notice Get the maximum amount of tokens a user can purchase + function getMaxPurchase(address _user) public view returns (uint256) { + // ((max(soldTotal, targetPerPeriod) * userLimit) - soldUser) / (1 - userLimit) + return (userLimit == WAD) ? UINT256_MAX : wdiv( + sub(wmul(max(soldTotal, targetPerPeriod), userLimit), soldUser[_user]), + sub(WAD, userLimit) + ); + } + /// @notice Get the address of the whitelist (if exists) function getWhitelist() public view returns (address) { return whitelist; diff --git a/test/contracts-network/colony-arbitrary-transactions.js b/test/contracts-network/colony-arbitrary-transactions.js index 71ac440f0b..9f096f8681 100644 --- a/test/contracts-network/colony-arbitrary-transactions.js +++ b/test/contracts-network/colony-arbitrary-transactions.js @@ -163,7 +163,7 @@ contract("Colony Arbitrary Transactions", (accounts) => { const coinMachineAddress = await colonyNetwork.getExtensionInstallation(COIN_MACHINE, colony.address); const coinMachine = await CoinMachine.at(coinMachineAddress); - await coinMachine.initialise(token.address, ethers.constants.AddressZero, 60 * 60, 10, WAD, WAD, WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, ethers.constants.AddressZero, 60 * 60, 10, WAD, WAD, WAD, WAD, ADDRESS_ZERO); await token.mint(coinMachine.address, WAD); const action = await encodeTxData(coinMachine, "buyTokens", [WAD]); diff --git a/test/extensions/coin-machine.js b/test/extensions/coin-machine.js index 5b98b1d143..e96e5ae839 100644 --- a/test/extensions/coin-machine.js +++ b/test/extensions/coin-machine.js @@ -50,6 +50,7 @@ contract("Coin Machine", (accounts) => { const USER0 = accounts[0]; const USER1 = accounts[1]; + const USER2 = accounts[2]; const ADDRESS_ZERO = ethers.constants.AddressZero; @@ -123,7 +124,7 @@ contract("Coin Machine", (accounts) => { it("can send unsold tokens back to the colony", async () => { await token.mint(coinMachine.address, WAD, { from: USER0 }); - await coinMachine.initialise(token.address, ADDRESS_ZERO, 60 * 60, 10, WAD, WAD, WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, ADDRESS_ZERO, 60 * 60, 10, WAD, WAD, WAD, WAD, ADDRESS_ZERO); await colony.uninstallExtension(COIN_MACHINE, { from: USER0 }); @@ -132,11 +133,15 @@ contract("Coin Machine", (accounts) => { }); it("can initialise", async () => { - await expectEvent(coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, 0, ADDRESS_ZERO), "ExtensionInitialised", []); + await expectEvent( + coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, WAD, 0, ADDRESS_ZERO), + "ExtensionInitialised", + [] + ); }); it("can handle a large windowSize", async () => { - await coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, 0, ADDRESS_ZERO); + await coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, WAD, 0, ADDRESS_ZERO); // Advance an entire window await forwardTime(60 * 511 + 1, this); @@ -146,43 +151,47 @@ contract("Coin Machine", (accounts) => { it("cannot initialise with bad arguments", async () => { await checkErrorRevert( - coinMachine.initialise(ADDRESS_ZERO, purchaseToken.address, 60, 511, 10, 10, 0, ADDRESS_ZERO), + coinMachine.initialise(ADDRESS_ZERO, purchaseToken.address, 60, 511, 10, 10, WAD, 0, ADDRESS_ZERO), "coin-machine-invalid-token" ); await checkErrorRevert( - coinMachine.initialise(token.address, purchaseToken.address, 0, 511, 10, 10, 0, ADDRESS_ZERO), + coinMachine.initialise(token.address, purchaseToken.address, 0, 511, 10, 10, WAD, 0, ADDRESS_ZERO), "coin-machine-period-too-small" ); await checkErrorRevert( - coinMachine.initialise(token.address, purchaseToken.address, 60, 0, 10, 10, 0, ADDRESS_ZERO), + coinMachine.initialise(token.address, purchaseToken.address, 60, 0, 10, 10, WAD, 0, ADDRESS_ZERO), "coin-machine-window-too-small" ); await checkErrorRevert( - coinMachine.initialise(token.address, purchaseToken.address, 60, 512, 10, 10, 0, ADDRESS_ZERO), + coinMachine.initialise(token.address, purchaseToken.address, 60, 512, 10, 10, WAD, 0, ADDRESS_ZERO), "coin-machine-window-too-large" ); await checkErrorRevert( - coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 0, 10, 0, ADDRESS_ZERO), + coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 0, 10, WAD, 0, ADDRESS_ZERO), "coin-machine-target-too-small" ); await checkErrorRevert( - coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 9, 0, ADDRESS_ZERO), + coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 9, WAD, 0, ADDRESS_ZERO), "coin-machine-max-too-small" ); + await checkErrorRevert( + coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, WAD.addn(1), 0, ADDRESS_ZERO), + "coin-machine-limit-too-large" + ); }); it("cannot initialise twice", async () => { - await coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, 0, ADDRESS_ZERO); + await coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, WAD, 0, ADDRESS_ZERO); await checkErrorRevert( - coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, 0, ADDRESS_ZERO), + coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, WAD, 0, ADDRESS_ZERO), "coin-machine-already-initialised" ); }); it("cannot initialise if not root", async () => { await checkErrorRevert( - coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, 0, ADDRESS_ZERO, { from: USER1 }), + coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, WAD, 0, ADDRESS_ZERO, { from: USER1 }), "coin-machine-caller-not-root" ); }); @@ -199,6 +208,7 @@ contract("Coin Machine", (accounts) => { 10, // number of periods for averaging WAD.muln(100), // tokens per period WAD.muln(200), // max per period + WAD, // user limit percentage WAD, // starting price ADDRESS_ZERO // whitelist address ); @@ -264,7 +274,7 @@ contract("Coin Machine", (accounts) => { await otherToken.mint(coinMachine.address, UINT128_MAX); - await coinMachine.initialise(otherToken.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, ADDRESS_ZERO); + await coinMachine.initialise(otherToken.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, WAD, ADDRESS_ZERO); await purchaseToken.mint(USER0, WAD, { from: USER0 }); await purchaseToken.approve(coinMachine.address, WAD, { from: USER0 }); @@ -283,7 +293,7 @@ contract("Coin Machine", (accounts) => { await token.mint(coinMachine.address, WAD); - await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, WAD, ADDRESS_ZERO); await purchaseToken.mint(USER0, WAD.muln(2), { from: USER0 }); await purchaseToken.approve(coinMachine.address, WAD.muln(2), { from: USER0 }); @@ -314,7 +324,7 @@ contract("Coin Machine", (accounts) => { await token.mint(coinMachine.address, WAD.muln(1000)); - await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, WAD, ADDRESS_ZERO); await purchaseToken.mint(USER1, WAD, { from: USER0 }); await purchaseToken.approve(coinMachine.address, WAD, { from: USER1 }); @@ -347,7 +357,7 @@ contract("Coin Machine", (accounts) => { await token.mint(coinMachine.address, UINT128_MAX); - await coinMachine.initialise(token.address, ADDRESS_ZERO, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, ADDRESS_ZERO, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, WAD, ADDRESS_ZERO); const currentPrice = await coinMachine.getCurrentPrice(); @@ -382,6 +392,56 @@ contract("Coin Machine", (accounts) => { expect(balance).to.eq.BN(maxPerPeriod); }); + it("cannot buy more than ther user limit allows", async () => { + await colony.uninstallExtension(COIN_MACHINE, { from: USER0 }); + await colony.installExtension(COIN_MACHINE, coinMachineVersion, { from: USER0 }); + const coinMachineAddress = await colonyNetwork.getExtensionInstallation(COIN_MACHINE, colony.address); + coinMachine = await CoinMachine.at(coinMachineAddress); + + await token.mint(coinMachine.address, WAD.muln(1000)); + + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD.divn(2), WAD, ADDRESS_ZERO); + const periodLength = await coinMachine.getPeriodLength(); + + await purchaseToken.mint(USER0, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER0 }); + + await purchaseToken.mint(USER1, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER1 }); + + await purchaseToken.mint(USER2, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER2 }); + + let maxPurchase; + + // totalSold is 0, so we use targetPerPeriod (100) as a pseudo-total + // The user can buy 100 tokens, because it thinks the total will be 200 + maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.eq.BN(WAD.muln(100)); + + await coinMachine.buyTokens(WAD.muln(100), { from: USER0 }); + await coinMachine.buyTokens(WAD.muln(100), { from: USER1 }); + + await forwardTime(periodLength.toNumber(), this); + + // Now totalSold is 200 + // Since each owns half already, neither can buy, only a new user can + // The new user can buy 200 tokens, half of 400 + maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.be.zero; + maxPurchase = await coinMachine.getMaxPurchase(USER2); + expect(maxPurchase).to.eq.BN(WAD.muln(200)); + + await coinMachine.buyTokens(WAD.muln(200), { from: USER2 }); + + await forwardTime(periodLength.toNumber(), this); + + // Now totalSold is 400 + // Original users can buy 200 tokens, owning half (300) of 600 + maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.eq.BN(WAD.muln(200)); + }); + it("can buy tokens over multiple periods", async () => { const windowSize = await coinMachine.getWindowSize(); const periodLength = await coinMachine.getPeriodLength(); @@ -454,7 +514,7 @@ contract("Coin Machine", (accounts) => { await token.mint(coinMachine.address, WAD.muln(200)); - await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD, WAD, ADDRESS_ZERO); const periodLength = await coinMachine.getPeriodLength(); const maxPerPeriod = await coinMachine.getMaxPerPeriod(); @@ -698,7 +758,7 @@ contract("Coin Machine", (accounts) => { await token.mint(coinMachine.address, UINT128_MAX); - await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD, WAD, WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD, WAD, WAD, WAD, ADDRESS_ZERO); await purchaseToken.mint(USER0, WAD, { from: USER0 }); await purchaseToken.approve(coinMachine.address, WAD, { from: USER0 }); @@ -720,7 +780,7 @@ contract("Coin Machine", (accounts) => { await token.mint(coinMachine.address, UINT128_MAX); - await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD, WAD, WAD, ADDRESS_ZERO); + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD, WAD, WAD, WAD, ADDRESS_ZERO); await purchaseToken.mint(USER0, WAD, { from: USER0 }); await purchaseToken.approve(coinMachine.address, WAD, { from: USER0 }); @@ -742,7 +802,7 @@ contract("Coin Machine", (accounts) => { await token.mint(coinMachine.address, UINT128_MAX); - await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD, WAD, WAD, whitelist.address); + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD, WAD, WAD, WAD, whitelist.address); await colony.setAdministrationRole(1, UINT256_MAX, USER1, 1, true); }); From c8fc90a35dcd469392a22dc4a57dfaf37ae79ea8 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 21 May 2021 14:15:13 -0700 Subject: [PATCH 3/6] Respond to review comments I --- contracts/extensions/CoinMachine.sol | 5 +- test/extensions/coin-machine.js | 140 +++++++++++++++++---------- 2 files changed, 93 insertions(+), 52 deletions(-) diff --git a/contracts/extensions/CoinMachine.sol b/contracts/extensions/CoinMachine.sol index 06ca3e1898..f65df4073a 100644 --- a/contracts/extensions/CoinMachine.sol +++ b/contracts/extensions/CoinMachine.sol @@ -62,7 +62,7 @@ contract CoinMachine is ColonyExtension { bool evolvePrice; // If we should evolve the price or not uint256 soldTotal; // Total tokens sold by the coin machine - uint256 userLimit; // Limit any address can buy of the total amount (as WAD percentage) + uint256 userLimit; // Limit any address can buy of the total amount (as WAD-denominated fraction) mapping(address => uint256) soldUser; // Tokens sold to a particular user @@ -154,6 +154,7 @@ contract CoinMachine is ColonyExtension { require(_targetPerPeriod > 0, "coin-machine-target-too-small"); require(_maxPerPeriod >= _targetPerPeriod, "coin-machine-max-too-small"); require(_userLimit <= WAD, "coin-machine-limit-too-large"); + require(_userLimit > 0, "coin-machine-limit-too-small"); token = _token; purchaseToken = _purchaseToken; @@ -362,7 +363,7 @@ contract CoinMachine is ColonyExtension { /// @notice Get the maximum amount of tokens a user can purchase function getMaxPurchase(address _user) public view returns (uint256) { // ((max(soldTotal, targetPerPeriod) * userLimit) - soldUser) / (1 - userLimit) - return (userLimit == WAD) ? UINT256_MAX : wdiv( + return (userLimit == WAD || whitelist == address(0x0)) ? UINT256_MAX : wdiv( sub(wmul(max(soldTotal, targetPerPeriod), userLimit), soldUser[_user]), sub(WAD, userLimit) ); diff --git a/test/extensions/coin-machine.js b/test/extensions/coin-machine.js index e96e5ae839..a6657c245c 100644 --- a/test/extensions/coin-machine.js +++ b/test/extensions/coin-machine.js @@ -178,6 +178,10 @@ contract("Coin Machine", (accounts) => { coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, WAD.addn(1), 0, ADDRESS_ZERO), "coin-machine-limit-too-large" ); + await checkErrorRevert( + coinMachine.initialise(token.address, purchaseToken.address, 60, 511, 10, 10, 0, 0, ADDRESS_ZERO), + "coin-machine-limit-too-small" + ); }); it("cannot initialise twice", async () => { @@ -392,56 +396,6 @@ contract("Coin Machine", (accounts) => { expect(balance).to.eq.BN(maxPerPeriod); }); - it("cannot buy more than ther user limit allows", async () => { - await colony.uninstallExtension(COIN_MACHINE, { from: USER0 }); - await colony.installExtension(COIN_MACHINE, coinMachineVersion, { from: USER0 }); - const coinMachineAddress = await colonyNetwork.getExtensionInstallation(COIN_MACHINE, colony.address); - coinMachine = await CoinMachine.at(coinMachineAddress); - - await token.mint(coinMachine.address, WAD.muln(1000)); - - await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD.divn(2), WAD, ADDRESS_ZERO); - const periodLength = await coinMachine.getPeriodLength(); - - await purchaseToken.mint(USER0, WAD.muln(500), { from: USER0 }); - await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER0 }); - - await purchaseToken.mint(USER1, WAD.muln(500), { from: USER0 }); - await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER1 }); - - await purchaseToken.mint(USER2, WAD.muln(500), { from: USER0 }); - await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER2 }); - - let maxPurchase; - - // totalSold is 0, so we use targetPerPeriod (100) as a pseudo-total - // The user can buy 100 tokens, because it thinks the total will be 200 - maxPurchase = await coinMachine.getMaxPurchase(USER0); - expect(maxPurchase).to.eq.BN(WAD.muln(100)); - - await coinMachine.buyTokens(WAD.muln(100), { from: USER0 }); - await coinMachine.buyTokens(WAD.muln(100), { from: USER1 }); - - await forwardTime(periodLength.toNumber(), this); - - // Now totalSold is 200 - // Since each owns half already, neither can buy, only a new user can - // The new user can buy 200 tokens, half of 400 - maxPurchase = await coinMachine.getMaxPurchase(USER0); - expect(maxPurchase).to.be.zero; - maxPurchase = await coinMachine.getMaxPurchase(USER2); - expect(maxPurchase).to.eq.BN(WAD.muln(200)); - - await coinMachine.buyTokens(WAD.muln(200), { from: USER2 }); - - await forwardTime(periodLength.toNumber(), this); - - // Now totalSold is 400 - // Original users can buy 200 tokens, owning half (300) of 600 - maxPurchase = await coinMachine.getMaxPurchase(USER0); - expect(maxPurchase).to.eq.BN(WAD.muln(200)); - }); - it("can buy tokens over multiple periods", async () => { const windowSize = await coinMachine.getWindowSize(); const periodLength = await coinMachine.getPeriodLength(); @@ -841,5 +795,91 @@ contract("Coin Machine", (accounts) => { recordedAddress = await coinMachine.getWhitelist(); expect(recordedAddress).to.equal(ADDRESS_ZERO); }); + + it("cannot buy more than ther user limit allows", async () => { + await colony.uninstallExtension(COIN_MACHINE, { from: USER0 }); + await colony.installExtension(COIN_MACHINE, coinMachineVersion, { from: USER0 }); + const coinMachineAddress = await colonyNetwork.getExtensionInstallation(COIN_MACHINE, colony.address); + coinMachine = await CoinMachine.at(coinMachineAddress); + + await token.mint(coinMachine.address, WAD.muln(1000)); + + await whitelist.approveUsers([USER0, USER1, USER2], true, { from: USER1 }); + + await coinMachine.initialise( + token.address, + purchaseToken.address, + 60 * 60, + 10, + WAD.muln(100), + WAD.muln(200), + WAD.divn(2), + WAD, + whitelist.address + ); + + const periodLength = await coinMachine.getPeriodLength(); + + await purchaseToken.mint(USER0, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER0 }); + + await purchaseToken.mint(USER1, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER1 }); + + await purchaseToken.mint(USER2, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER2 }); + + let maxPurchase; + let balance; + + // totalSold is 0, so we use targetPerPeriod (100) as a pseudo-total + // The user can buy 100 tokens, because it thinks the total will be 200 + maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.eq.BN(WAD.muln(100)); + + await coinMachine.buyTokens(WAD.muln(500), { from: USER0 }); + await coinMachine.buyTokens(WAD.muln(500), { from: USER1 }); + + // Only buys up to limit + balance = await token.balanceOf(USER0); + expect(balance).to.eq.BN(WAD.muln(100)); + balance = await token.balanceOf(USER1); + expect(balance).to.eq.BN(WAD.muln(100)); + + await forwardTime(periodLength.toNumber(), this); + + // Now totalSold is 200 + // Since each owns half already, neither can buy, only a new user can + // The new user can buy 200 tokens, half of 400 + maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.be.zero; + maxPurchase = await coinMachine.getMaxPurchase(USER2); + expect(maxPurchase).to.eq.BN(WAD.muln(200)); + + await coinMachine.buyTokens(WAD.muln(500), { from: USER2 }); + + // Only buys up to limit + balance = await token.balanceOf(USER2); + expect(balance).to.eq.BN(WAD.muln(200)); + + await forwardTime(periodLength.toNumber(), this); + + // Now totalSold is 400 + // Original users can buy 200 tokens, owning half (300) of 600 + maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.eq.BN(WAD.muln(200)); + }); + + it("cannot set a user limit without a whitelist", async () => { + await colony.uninstallExtension(COIN_MACHINE, { from: USER0 }); + await colony.installExtension(COIN_MACHINE, coinMachineVersion, { from: USER0 }); + const coinMachineAddress = await colonyNetwork.getExtensionInstallation(COIN_MACHINE, colony.address); + coinMachine = await CoinMachine.at(coinMachineAddress); + + await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD.divn(2), WAD, ADDRESS_ZERO); + + const maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.eq.BN(UINT256_MAX); + }); }); }); From 1b7d5ab8567b918e1a2aa4246db687d4ae6e863c Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 25 May 2021 15:47:59 -0700 Subject: [PATCH 4/6] Respond to review comments II --- contracts/extensions/CoinMachine.sol | 47 +++++------ test/extensions/coin-machine.js | 119 +++++++++++++++++++++------ 2 files changed, 118 insertions(+), 48 deletions(-) diff --git a/contracts/extensions/CoinMachine.sol b/contracts/extensions/CoinMachine.sol index f65df4073a..9538eeff3f 100644 --- a/contracts/extensions/CoinMachine.sol +++ b/contracts/extensions/CoinMachine.sol @@ -61,9 +61,8 @@ contract CoinMachine is ColonyExtension { bool evolvePrice; // If we should evolve the price or not + uint256 userLimitFraction; // Limit an address can buy of the total amount (in WADs) uint256 soldTotal; // Total tokens sold by the coin machine - uint256 userLimit; // Limit any address can buy of the total amount (as WAD-denominated fraction) - mapping(address => uint256) soldUser; // Tokens sold to a particular user // Modifiers @@ -138,7 +137,7 @@ contract CoinMachine is ColonyExtension { uint256 _windowSize, uint256 _targetPerPeriod, uint256 _maxPerPeriod, - uint256 _userLimit, + uint256 _userLimitFraction, uint256 _startingPrice, address _whitelist ) @@ -153,8 +152,8 @@ contract CoinMachine is ColonyExtension { require(_windowSize <= 511, "coin-machine-window-too-large"); require(_targetPerPeriod > 0, "coin-machine-target-too-small"); require(_maxPerPeriod >= _targetPerPeriod, "coin-machine-max-too-small"); - require(_userLimit <= WAD, "coin-machine-limit-too-large"); - require(_userLimit > 0, "coin-machine-limit-too-small"); + require(_userLimitFraction <= WAD, "coin-machine-limit-too-large"); + require(_userLimitFraction > 0, "coin-machine-limit-too-small"); token = _token; purchaseToken = _purchaseToken; @@ -168,7 +167,7 @@ contract CoinMachine is ColonyExtension { targetPerPeriod = _targetPerPeriod; maxPerPeriod = _maxPerPeriod; - userLimit = _userLimit; + userLimitFraction = _userLimitFraction; activePrice = _startingPrice; activePeriod = getCurrentPeriod(); @@ -200,11 +199,8 @@ contract CoinMachine is ColonyExtension { "coin-machine-unauthorised" ); - uint256 tokenBalance = getTokenBalance(); uint256 maxPurchase = getMaxPurchase(msg.sender); - uint256 remainingTokens = sub(maxPerPeriod, activeSold); - - uint256 numTokens = min(maxPurchase, min(tokenBalance, min(remainingTokens, _numTokens))); + uint256 numTokens = min(maxPurchase, _numTokens); uint256 totalCost = wmul(numTokens, activePrice); activeIntake = add(activeIntake, totalCost); @@ -212,14 +208,14 @@ contract CoinMachine is ColonyExtension { assert(activeSold <= maxPerPeriod); - // Do userLimit bookkeeping (only if needed) - if (userLimit < WAD) { + // Do userLimitFraction bookkeeping (only if needed) + if (userLimitFraction < WAD) { soldTotal = add(soldTotal, numTokens); soldUser[msg.sender] = add(soldUser[msg.sender], numTokens); } // Check if we've sold out - if (numTokens >= tokenBalance) { setPriceEvolution(false); } + if (numTokens >= getTokenBalance()) { setPriceEvolution(false); } if (purchaseToken == address(0x0)) { require(msg.value >= totalCost, "coin-machine-insufficient-funds"); @@ -353,20 +349,25 @@ contract CoinMachine is ColonyExtension { } /// @notice Get the number of remaining tokens for sale this period - function getNumAvailable() public view returns (uint256) { - return min( - ERC20(token).balanceOf(address(this)), - sub(maxPerPeriod, ((activePeriod >= getCurrentPeriod()) ? activeSold : 0)) + function getSellableTokens() public view returns (uint256) { + return sub(maxPerPeriod, ((activePeriod >= getCurrentPeriod()) ? activeSold : 0)); + } + + /// @notice Get the maximum amount of tokens a user can purchase in total + function getUserLimit(address _user) public view returns (uint256) { + // ((max(soldTotal, targetPerPeriod) * userLimitFraction) - soldUser) / (1 - userLimitFraction) + return (userLimitFraction == WAD || whitelist == address(0x0)) ? UINT256_MAX : wdiv( + sub(wmul(max(soldTotal, targetPerPeriod), userLimitFraction), soldUser[_user]), + sub(WAD, userLimitFraction) ); } - /// @notice Get the maximum amount of tokens a user can purchase + /// @notice Get the maximum amount of tokens a user can purchase in a period function getMaxPurchase(address _user) public view returns (uint256) { - // ((max(soldTotal, targetPerPeriod) * userLimit) - soldUser) / (1 - userLimit) - return (userLimit == WAD || whitelist == address(0x0)) ? UINT256_MAX : wdiv( - sub(wmul(max(soldTotal, targetPerPeriod), userLimit), soldUser[_user]), - sub(WAD, userLimit) - ); + uint256 tokenBalance = getTokenBalance(); + uint256 sellableTokens = getSellableTokens(); + uint256 userLimit = getUserLimit(_user); + return min(userLimit, min(tokenBalance, sellableTokens)); } /// @notice Get the address of the whitelist (if exists) diff --git a/test/extensions/coin-machine.js b/test/extensions/coin-machine.js index a6657c245c..7a57b5ee75 100644 --- a/test/extensions/coin-machine.js +++ b/test/extensions/coin-machine.js @@ -632,10 +632,10 @@ contract("Coin Machine", (accounts) => { const alphaAsWad = new BN(2).mul(WAD).div(windowSize.addn(1)); let currentPrice = await coinMachine.getCurrentPrice(); - let numAvailable = await coinMachine.getNumAvailable(); + let sellableTokens = await coinMachine.getSellableTokens(); expect(currentPrice).to.eq.BN(WAD); - expect(numAvailable).to.eq.BN(maxPerPeriod); + expect(sellableTokens).to.eq.BN(maxPerPeriod); await purchaseToken.mint(USER0, maxPerPeriod.muln(2), { from: USER0 }); await purchaseToken.approve(coinMachine.address, maxPerPeriod.muln(2), { from: USER0 }); @@ -644,61 +644,61 @@ contract("Coin Machine", (accounts) => { await coinMachine.buyTokens(targetPerPeriod.divn(2), { from: USER0 }); currentPrice = await coinMachine.getCurrentPrice(); - numAvailable = await coinMachine.getNumAvailable(); + sellableTokens = await coinMachine.getSellableTokens(); expect(currentPrice).to.eq.BN(WAD); - expect(numAvailable).to.eq.BN(maxPerPeriod.sub(targetPerPeriod.divn(2))); + expect(sellableTokens).to.eq.BN(maxPerPeriod.sub(targetPerPeriod.divn(2))); await coinMachine.buyTokens(targetPerPeriod.divn(2), { from: USER0 }); currentPrice = await coinMachine.getCurrentPrice(); - numAvailable = await coinMachine.getNumAvailable(); + sellableTokens = await coinMachine.getSellableTokens(); expect(currentPrice).to.eq.BN(WAD); - expect(numAvailable).to.eq.BN(maxPerPeriod.sub(targetPerPeriod)); + expect(sellableTokens).to.eq.BN(maxPerPeriod.sub(targetPerPeriod)); await coinMachine.buyTokens(targetPerPeriod, { from: USER0 }); currentPrice = await coinMachine.getCurrentPrice(); - numAvailable = await coinMachine.getNumAvailable(); + sellableTokens = await coinMachine.getSellableTokens(); expect(currentPrice).to.eq.BN(WAD); - expect(numAvailable).to.be.zero; + expect(sellableTokens).to.be.zero; // Advance to next period without calling `updatePeriod` await forwardTime(periodLength.toNumber(), this); currentPrice = await coinMachine.getCurrentPrice(); - numAvailable = await coinMachine.getNumAvailable(); + sellableTokens = await coinMachine.getSellableTokens(); let emaIntake = WAD.muln(100).mul(WAD.sub(alphaAsWad)).add(maxPerPeriod.mul(alphaAsWad)); let expectedPrice = emaIntake.div(WAD.muln(100)); // Bought maxPerPeriod tokens so price should be up expect(currentPrice).to.eq.BN(expectedPrice); - expect(numAvailable).to.eq.BN(maxPerPeriod); + expect(sellableTokens).to.eq.BN(maxPerPeriod); // Advance to next period without calling `updatePeriod` // Now we are two periods advanced from `currentPeriod` await forwardTime(periodLength.toNumber(), this); currentPrice = await coinMachine.getCurrentPrice(); - numAvailable = await coinMachine.getNumAvailable(); + sellableTokens = await coinMachine.getSellableTokens(); emaIntake = emaIntake.mul(WAD.sub(alphaAsWad)).div(WAD); expectedPrice = emaIntake.div(WAD.muln(100)); expect(currentPrice).to.eq.BN(expectedPrice); - expect(numAvailable).to.eq.BN(maxPerPeriod); + expect(sellableTokens).to.eq.BN(maxPerPeriod); // Now buy some tokens await coinMachine.buyTokens(maxPerPeriod, { from: USER0 }); // Price should be the same, but no tokens available. currentPrice = await coinMachine.getCurrentPrice(); - numAvailable = await coinMachine.getNumAvailable(); + sellableTokens = await coinMachine.getSellableTokens(); expect(currentPrice).to.eq.BN(expectedPrice); - expect(numAvailable).to.be.zero; + expect(sellableTokens).to.be.zero; }); it("can handle sales tokens with different decimals", async () => { @@ -829,13 +829,13 @@ contract("Coin Machine", (accounts) => { await purchaseToken.mint(USER2, WAD.muln(500), { from: USER0 }); await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER2 }); - let maxPurchase; + let userLimit; let balance; // totalSold is 0, so we use targetPerPeriod (100) as a pseudo-total // The user can buy 100 tokens, because it thinks the total will be 200 - maxPurchase = await coinMachine.getMaxPurchase(USER0); - expect(maxPurchase).to.eq.BN(WAD.muln(100)); + userLimit = await coinMachine.getUserLimit(USER0); + expect(userLimit).to.eq.BN(WAD.muln(100)); await coinMachine.buyTokens(WAD.muln(500), { from: USER0 }); await coinMachine.buyTokens(WAD.muln(500), { from: USER1 }); @@ -851,10 +851,10 @@ contract("Coin Machine", (accounts) => { // Now totalSold is 200 // Since each owns half already, neither can buy, only a new user can // The new user can buy 200 tokens, half of 400 - maxPurchase = await coinMachine.getMaxPurchase(USER0); - expect(maxPurchase).to.be.zero; - maxPurchase = await coinMachine.getMaxPurchase(USER2); - expect(maxPurchase).to.eq.BN(WAD.muln(200)); + userLimit = await coinMachine.getUserLimit(USER0); + expect(userLimit).to.be.zero; + userLimit = await coinMachine.getUserLimit(USER2); + expect(userLimit).to.eq.BN(WAD.muln(200)); await coinMachine.buyTokens(WAD.muln(500), { from: USER2 }); @@ -866,8 +866,8 @@ contract("Coin Machine", (accounts) => { // Now totalSold is 400 // Original users can buy 200 tokens, owning half (300) of 600 - maxPurchase = await coinMachine.getMaxPurchase(USER0); - expect(maxPurchase).to.eq.BN(WAD.muln(200)); + userLimit = await coinMachine.getUserLimit(USER0); + expect(userLimit).to.eq.BN(WAD.muln(200)); }); it("cannot set a user limit without a whitelist", async () => { @@ -878,8 +878,77 @@ contract("Coin Machine", (accounts) => { await coinMachine.initialise(token.address, purchaseToken.address, 60 * 60, 10, WAD.muln(100), WAD.muln(200), WAD.divn(2), WAD, ADDRESS_ZERO); - const maxPurchase = await coinMachine.getMaxPurchase(USER0); - expect(maxPurchase).to.eq.BN(UINT256_MAX); + const userLimit = await coinMachine.getUserLimit(USER0); + expect(userLimit).to.eq.BN(UINT256_MAX); + }); + + it("can calculate the max purchase at any given time", async () => { + await colony.uninstallExtension(COIN_MACHINE, { from: USER0 }); + await colony.installExtension(COIN_MACHINE, coinMachineVersion, { from: USER0 }); + const coinMachineAddress = await colonyNetwork.getExtensionInstallation(COIN_MACHINE, colony.address); + coinMachine = await CoinMachine.at(coinMachineAddress); + + // Initial supply of 250 tokens + await token.mint(coinMachine.address, WAD.muln(250)); + + await whitelist.approveUsers([USER0, USER1, USER2], true, { from: USER1 }); + + await coinMachine.initialise( + token.address, + purchaseToken.address, + 60 * 60, + 10, + WAD.muln(100), + WAD.muln(200), + WAD.divn(2), + WAD, + whitelist.address + ); + + const periodLength = await coinMachine.getPeriodLength(); + + await purchaseToken.mint(USER0, WAD.muln(500), { from: USER0 }); + await purchaseToken.mint(USER1, WAD.muln(500), { from: USER0 }); + await purchaseToken.mint(USER2, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER1 }); + await purchaseToken.approve(coinMachine.address, WAD.muln(500), { from: USER2 }); + + let maxPurchase; + + // User0 limited by user limit (100) + maxPurchase = await coinMachine.getMaxPurchase(USER0); + expect(maxPurchase).to.eq.BN(WAD.muln(100)); + + // 100 sold, 150 remaining + await coinMachine.buyTokens(WAD.muln(100), { from: USER0 }); + + await forwardTime(periodLength.toNumber(), this); + await coinMachine.updatePeriod(); + + // 200 sold, 50 remaining + await coinMachine.buyTokens(WAD.muln(100), { from: USER1 }); + + await forwardTime(periodLength.toNumber(), this); + await coinMachine.updatePeriod(); + + // User2 limited by token balance (50) + maxPurchase = await coinMachine.getMaxPurchase(USER2); + expect(maxPurchase).to.eq.BN(WAD.muln(50)); + + // New supply of 250 tokens + await token.mint(coinMachine.address, WAD.muln(250)); + + // User2 limited by user limit (200 of what will be 400 tokens) + maxPurchase = await coinMachine.getMaxPurchase(USER2); + expect(maxPurchase).to.eq.BN(WAD.muln(200)); + + // 350 sold, 150 remaining + await coinMachine.buyTokens(WAD.muln(150), { from: USER2 }); + + // User1 limited by max per period (50 / 200) + maxPurchase = await coinMachine.getMaxPurchase(USER1); + expect(maxPurchase).to.eq.BN(WAD.muln(50)); }); }); }); From 6d3c0006857b7b4115579cfe87cd59a511b05635 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 26 May 2021 08:33:57 -0700 Subject: [PATCH 5/6] Update smoke tests --- test-smoke/colony-storage-consistent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 36827aa53d..4c755295fb 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -154,8 +154,8 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleAccount.stateRoot.toString("hex")); console.log("tokenLockingStateHash:", tokenLockingAccount.stateRoot.toString("hex")); - expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("e10e816b235f21825b73d4e90c2114cd2fbc68f2d49d3c4081b8536747b70e24"); - expect(colonyAccount.stateRoot.toString("hex")).to.equal("76a5c2ea62ee88ed3992e3278cdbb821da482b4d5c3f40ed79ed4f3ee530520f"); + expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("2d8b407a3d5d1a48b07a00c968942ee7cb384961a5f166035a194cf7f8bd3245"); + expect(colonyAccount.stateRoot.toString("hex")).to.equal("36096952d464e6c70a0a3aa2ecf068dff7bc6af70ffe022d57d8e2342d39f989"); expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("6bee0085d000fc929e06c1281c162eaa7ac22ff9e2dcc520066e5e7720de6394"); expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("bcfee6939c375d042704e338652a1e2d1e6f7b0e69587b79e72bde08ca777927"); expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("8b9242eb6e0fd017538f71e90c6ce9793839743e869bed54030861d3594453b1"); From 4791038bcd022a81018e246d5b77f59bf8ff67a0 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 9 Jun 2021 15:11:25 -0700 Subject: [PATCH 6/6] Respond to review comments III --- contracts/extensions/CoinMachine.sol | 3 +++ test/extensions/coin-machine.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/CoinMachine.sol b/contracts/extensions/CoinMachine.sol index 9538eeff3f..155c25d80e 100644 --- a/contracts/extensions/CoinMachine.sol +++ b/contracts/extensions/CoinMachine.sol @@ -95,6 +95,7 @@ contract CoinMachine is ColonyExtension { /// @notice Called when upgrading the extension function finishUpgrade() public override auth { token = colony.getToken(); + userLimitFraction = WAD; setPriceEvolution(getTokenBalance() > 0 && !deprecated); } @@ -203,6 +204,8 @@ contract CoinMachine is ColonyExtension { uint256 numTokens = min(maxPurchase, _numTokens); uint256 totalCost = wmul(numTokens, activePrice); + if (numTokens <= 0) { return; } + activeIntake = add(activeIntake, totalCost); activeSold = add(activeSold, numTokens); diff --git a/test/extensions/coin-machine.js b/test/extensions/coin-machine.js index 7a57b5ee75..2b569dd177 100644 --- a/test/extensions/coin-machine.js +++ b/test/extensions/coin-machine.js @@ -796,7 +796,7 @@ contract("Coin Machine", (accounts) => { expect(recordedAddress).to.equal(ADDRESS_ZERO); }); - it("cannot buy more than ther user limit allows", async () => { + it("cannot buy more than their user limit allows", async () => { await colony.uninstallExtension(COIN_MACHINE, { from: USER0 }); await colony.installExtension(COIN_MACHINE, coinMachineVersion, { from: USER0 }); const coinMachineAddress = await colonyNetwork.getExtensionInstallation(COIN_MACHINE, colony.address);