Skip to content

Commit

Permalink
Merge pull request #960 from JoinColony/feat/buyer-limit
Browse files Browse the repository at this point in the history
Per-user Coin Machine limits
  • Loading branch information
area authored Jun 18, 2021
2 parents e26e8dc + 4791038 commit fffe3ab
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 47 deletions.
46 changes: 39 additions & 7 deletions contracts/extensions/CoinMachine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ 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
mapping(address => uint256) soldUser; // Tokens sold to a particular user

// Modifiers

modifier onlyRoot() {
Expand Down Expand Up @@ -91,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);
}
Expand Down Expand Up @@ -133,6 +138,7 @@ contract CoinMachine is ColonyExtension {
uint256 _windowSize,
uint256 _targetPerPeriod,
uint256 _maxPerPeriod,
uint256 _userLimitFraction,
uint256 _startingPrice,
address _whitelist
)
Expand All @@ -147,6 +153,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(_userLimitFraction <= WAD, "coin-machine-limit-too-large");
require(_userLimitFraction > 0, "coin-machine-limit-too-small");

token = _token;
purchaseToken = _purchaseToken;
Expand All @@ -160,6 +168,8 @@ contract CoinMachine is ColonyExtension {
targetPerPeriod = _targetPerPeriod;
maxPerPeriod = _maxPerPeriod;

userLimitFraction = _userLimitFraction;

activePrice = _startingPrice;
activePeriod = getCurrentPeriod();

Expand Down Expand Up @@ -190,17 +200,25 @@ contract CoinMachine is ColonyExtension {
"coin-machine-unauthorised"
);

uint256 tokenBalance = getTokenBalance();
uint256 numTokens = min(min(_numTokens, maxPerPeriod - activeSold), tokenBalance);
uint256 maxPurchase = getMaxPurchase(msg.sender);
uint256 numTokens = min(maxPurchase, _numTokens);
uint256 totalCost = wmul(numTokens, activePrice);

if (numTokens <= 0) { return; }

activeIntake = add(activeIntake, totalCost);
activeSold = add(activeSold, numTokens);

assert(activeSold <= maxPerPeriod);

// 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");
Expand Down Expand Up @@ -334,13 +352,27 @@ 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 in a period
function getMaxPurchase(address _user) public view returns (uint256) {
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)
function getWhitelist() public view returns (address) {
return whitelist;
Expand Down
2 changes: 2 additions & 0 deletions contracts/extensions/ColonyExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion contracts/extensions/FundingQueue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 0 additions & 1 deletion contracts/extensions/OneTxPayment.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 0 additions & 1 deletion contracts/extensions/VotingReputation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions test-smoke/colony-storage-consistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion test/contracts-network/colony-arbitrary-transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
Loading

0 comments on commit fffe3ab

Please sign in to comment.