tests: port existing JS tests to foundry tests (#9)

This commit is contained in:
r4bbit 2023-09-12 17:08:59 +02:00 committed by GitHub
parent 2a8505f3b2
commit 5386b09f55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 489 additions and 265 deletions

View File

@ -6,7 +6,9 @@ The MiniMeToken contract is a standard ERC20 token with extra functionality:
### The token is easy to clone! ### The token is easy to clone!
Anybody can create a new clone token from any token using this contract with an initial distribution identical to the original token at a specified block. The address calling the `createCloneToken` function will become the token controller and the token's default settings can be specified in the function call. Anybody can create a new clone token from any token using this contract with an initial distribution identical to the
original token at a specified block. The address calling the `createCloneToken` function will become the token
controller and the token's default settings can be specified in the function call.
function createCloneToken( function createCloneToken(
string _cloneTokenName, string _cloneTokenName,
@ -20,7 +22,8 @@ Once the clone token is created, it acts as a completely independent token, with
### Balance history is registered and available to be queried ### Balance history is registered and available to be queried
All MiniMe Tokens maintain a history of the balance changes that occur during each block. Two calls are introduced to read the totalSupply and the balance of any address at any block in the past. All MiniMe Tokens maintain a history of the balance changes that occur during each block. Two calls are introduced to
read the totalSupply and the balance of any address at any block in the past.
function totalSupplyAt(uint _blockNumber) constant returns(uint) function totalSupplyAt(uint _blockNumber) constant returns(uint)
@ -28,9 +31,14 @@ All MiniMe Tokens maintain a history of the balance changes that occur during ea
### Optional token controller ### Optional token controller
The controller of the contract can generate/destroy/transfer tokens at its own discretion. The controller can be a regular account, but the intention is for the controller to be another contract that imposes transparent rules on the token's issuance and functionality. The Token Controller is not required for the MiniMe token to function, if there is no reason to generate/destroy/transfer tokens, the token controller can be set to 0x0 and this functionality will be disabled. The controller of the contract can generate/destroy/transfer tokens at its own discretion. The controller can be a
regular account, but the intention is for the controller to be another contract that imposes transparent rules on the
token's issuance and functionality. The Token Controller is not required for the MiniMe token to function, if there is
no reason to generate/destroy/transfer tokens, the token controller can be set to 0x0 and this functionality will be
disabled.
For example, a Token Creation contract can be set as the controller of the MiniMe Token and at the end of the token creation period, the controller can be transferred to the 0x0 address, to guarantee that no new tokens will be created. For example, a Token Creation contract can be set as the controller of the MiniMe Token and at the end of the token
creation period, the controller can be transferred to the 0x0 address, to guarantee that no new tokens will be created.
To create and destroy tokens, these two functions are introduced: To create and destroy tokens, these two functions are introduced:
@ -40,15 +48,18 @@ To create and destroy tokens, these two functions are introduced:
### The Token's Controller can freeze transfers. ### The Token's Controller can freeze transfers.
If transfersEnabled == false, tokens cannot be transferred by the users, however they can still be created, destroyed, and transferred by the controller. The controller can also toggle this flag. If transfersEnabled == false, tokens cannot be transferred by the users, however they can still be created, destroyed,
and transferred by the controller. The controller can also toggle this flag.
// Allows tokens to be transferred if true or frozen if false // Allows tokens to be transferred if true or frozen if false
function enableTransfers(bool _transfersEnabled) onlyController function enableTransfers(bool _transfersEnabled) onlyController
## Applications ## Applications
If this token contract is used as the base token, then clones of itself can be easily generated at any given block number, this allows for incredibly powerful functionality, effectively the ability for anyone to give extra features to the token holders without having to migrate to a new contract. Some of the applications that the MiniMe token contract can be used for are: If this token contract is used as the base token, then clones of itself can be easily generated at any given block
number, this allows for incredibly powerful functionality, effectively the ability for anyone to give extra features to
the token holders without having to migrate to a new contract. Some of the applications that the MiniMe token contract
can be used for are:
1. Generating a voting token that is burned when you vote. 1. Generating a voting token that is burned when you vote.
2. Generating a discount "coupon" that is redeemed when you use it. 2. Generating a discount "coupon" that is redeemed when you use it.
@ -59,7 +70,9 @@ If this token contract is used as the base token, then clones of itself can be e
7. Generating token that allows a central party complete control to transfer/generate/destroy tokens at will. 7. Generating token that allows a central party complete control to transfer/generate/destroy tokens at will.
8. Lots of other applications including all the applications the standard ERC 20 token can be used for. 8. Lots of other applications including all the applications the standard ERC 20 token can be used for.
All these applications and more are enabled by the MiniMe Token Contract. The most amazing part being that anyone that wants to add these features can, in a permissionless yet safe manner without affecting the parent token's intended functionality. All these applications and more are enabled by the MiniMe Token Contract. The most amazing part being that anyone that
wants to add these features can, in a permissionless yet safe manner without affecting the parent token's intended
functionality.
# How to deploy a campaign # How to deploy a campaign
@ -67,5 +80,3 @@ All these applications and more are enabled by the MiniMe Token Contract. The mo
2. Deploy the MinimeToken 2. Deploy the MinimeToken
3. Deploy the campaign 3. Deploy the campaign
4. Assign the controller of the MinimeToken to the campaign. 4. Assign the controller of the MinimeToken to the campaign.

View File

@ -4,11 +4,16 @@ pragma solidity ^0.8.0;
contract Controlled { contract Controlled {
/// @notice The address of the controller is the only address that can call /// @notice The address of the controller is the only address that can call
/// a function with this modifier /// a function with this modifier
modifier onlyController { require(msg.sender == controller); _; } modifier onlyController() {
require(msg.sender == controller);
_;
}
address payable public controller; address payable public controller;
constructor() { controller = payable(msg.sender);} constructor() {
controller = payable(msg.sender);
}
/// @notice Changes the controller of the contract /// @notice Changes the controller of the contract
/// @param _newController The new controller of the contract /// @param _newController The new controller of the contract

View File

@ -30,28 +30,24 @@ import "./Controlled.sol";
import "./TokenController.sol"; import "./TokenController.sol";
abstract contract ApproveAndCallFallBack { abstract contract ApproveAndCallFallBack {
function receiveApproval(address from, uint256 _amount, address _token, bytes memory _data) virtual public; function receiveApproval(address from, uint256 _amount, address _token, bytes memory _data) public virtual;
} }
/// @dev The actual token contract, the default controller is the msg.sender /// @dev The actual token contract, the default controller is the msg.sender
/// that deploys the contract, so usually this token will be deployed by a /// that deploys the contract, so usually this token will be deployed by a
/// token controller contract, which Giveth will call a "Campaign" /// token controller contract, which Giveth will call a "Campaign"
contract MiniMeToken is Controlled { contract MiniMeToken is Controlled {
string public name; //The Token's name: e.g. DigixDAO Tokens
string public name; //The Token's name: e.g. DigixDAO Tokens uint8 public decimals; //Number of decimals of the smallest unit
uint8 public decimals; //Number of decimals of the smallest unit string public symbol; //An identifier: e.g. REP
string public symbol; //An identifier: e.g. REP string public version = "MMT_0.2"; //An arbitrary versioning scheme
string public version = 'MMT_0.2'; //An arbitrary versioning scheme
/// @dev `Checkpoint` is the structure that attaches a block number to a /// @dev `Checkpoint` is the structure that attaches a block number to a
/// given value, the block number attached is the one that last changed the /// given value, the block number attached is the one that last changed the
/// value /// value
struct Checkpoint { struct Checkpoint {
// `fromBlock` is the block number that the value was generated from // `fromBlock` is the block number that the value was generated from
uint128 fromBlock; uint128 fromBlock;
// `value` is the amount of tokens at a specific block number // `value` is the amount of tokens at a specific block number
uint128 value; uint128 value;
} }
@ -62,18 +58,18 @@ contract MiniMeToken is Controlled {
// `parentSnapShotBlock` is the block number from the Parent Token that was // `parentSnapShotBlock` is the block number from the Parent Token that was
// used to determine the initial distribution of the Clone Token // used to determine the initial distribution of the Clone Token
uint public parentSnapShotBlock; uint256 public parentSnapShotBlock;
// `creationBlock` is the block number that the Clone Token was created // `creationBlock` is the block number that the Clone Token was created
uint public creationBlock; uint256 public creationBlock;
// `balances` is the map that tracks the balance of each address, in this // `balances` is the map that tracks the balance of each address, in this
// contract when the balance changes the block number that the change // contract when the balance changes the block number that the change
// occurred is also included in the map // occurred is also included in the map
mapping (address => Checkpoint[]) balances; mapping(address => Checkpoint[]) balances;
// `allowed` tracks any extra transfer rights as in all ERC20 tokens // `allowed` tracks any extra transfer rights as in all ERC20 tokens
mapping (address => mapping (address => uint256)) allowed; mapping(address => mapping(address => uint256)) allowed;
// Tracks the history of the `totalSupply` of the token // Tracks the history of the `totalSupply` of the token
Checkpoint[] totalSupplyHistory; Checkpoint[] totalSupplyHistory;
@ -84,9 +80,9 @@ contract MiniMeToken is Controlled {
// The factory used to create new clone tokens // The factory used to create new clone tokens
MiniMeTokenFactory public tokenFactory; MiniMeTokenFactory public tokenFactory;
//////////////// ////////////////
// Constructor // Constructor
//////////////// ////////////////
/// @notice Constructor to create a MiniMeToken /// @notice Constructor to create a MiniMeToken
/// @param _tokenFactory The address of the MiniMeTokenFactory contract that /// @param _tokenFactory The address of the MiniMeTokenFactory contract that
@ -104,26 +100,25 @@ contract MiniMeToken is Controlled {
constructor( constructor(
MiniMeTokenFactory _tokenFactory, MiniMeTokenFactory _tokenFactory,
MiniMeToken _parentToken, MiniMeToken _parentToken,
uint _parentSnapShotBlock, uint256 _parentSnapShotBlock,
string memory _tokenName, string memory _tokenName,
uint8 _decimalUnits, uint8 _decimalUnits,
string memory _tokenSymbol, string memory _tokenSymbol,
bool _transfersEnabled bool _transfersEnabled
) { ) {
tokenFactory = _tokenFactory; tokenFactory = _tokenFactory;
name = _tokenName; // Set the name name = _tokenName; // Set the name
decimals = _decimalUnits; // Set the decimals decimals = _decimalUnits; // Set the decimals
symbol = _tokenSymbol; // Set the symbol symbol = _tokenSymbol; // Set the symbol
parentToken = _parentToken; parentToken = _parentToken;
parentSnapShotBlock = _parentSnapShotBlock; parentSnapShotBlock = _parentSnapShotBlock;
transfersEnabled = _transfersEnabled; transfersEnabled = _transfersEnabled;
creationBlock = block.number; creationBlock = block.number;
} }
///////////////////
/////////////////// // ERC20 Methods
// ERC20 Methods ///////////////////
///////////////////
/// @notice Send `_amount` tokens to `_to` from `msg.sender` /// @notice Send `_amount` tokens to `_to` from `msg.sender`
/// @param _to The address of the recipient /// @param _to The address of the recipient
@ -141,9 +136,7 @@ contract MiniMeToken is Controlled {
/// @param _to The address of the recipient /// @param _to The address of the recipient
/// @param _amount The amount of tokens to be transferred /// @param _amount The amount of tokens to be transferred
/// @return success True if the transfer was successful /// @return success True if the transfer was successful
function transferFrom(address _from, address _to, uint256 _amount function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success) {
) public returns (bool success) {
// The controller of this contract can move tokens around at will, // The controller of this contract can move tokens around at will,
// this is important to recognize! Confirm that you trust the // this is important to recognize! Confirm that you trust the
// controller of this contract, which in most situations should be // controller of this contract, which in most situations should be
@ -164,43 +157,40 @@ contract MiniMeToken is Controlled {
/// @param _from The address holding the tokens being transferred /// @param _from The address holding the tokens being transferred
/// @param _to The address of the recipient /// @param _to The address of the recipient
/// @param _amount The amount of tokens to be transferred /// @param _amount The amount of tokens to be transferred
function doTransfer(address _from, address _to, uint _amount function doTransfer(address _from, address _to, uint256 _amount) internal {
) internal { if (_amount == 0) {
emit Transfer(_from, _to, _amount); // Follow the spec to louch the event when transfer 0
return;
}
if (_amount == 0) { require(parentSnapShotBlock < block.number);
emit Transfer(_from, _to, _amount); // Follow the spec to louch the event when transfer 0
return;
}
require(parentSnapShotBlock < block.number); // Do not allow transfer to 0x0 or the token contract itself
require((_to != address(0)) && (_to != address(this)));
// Do not allow transfer to 0x0 or the token contract itself // If the amount being transfered is more than the balance of the
require((_to != address(0)) && (_to != address(this))); // account the transfer throws
uint256 previousBalanceFrom = balanceOfAt(_from, block.number);
// If the amount being transfered is more than the balance of the require(previousBalanceFrom >= _amount);
// account the transfer throws
uint256 previousBalanceFrom = balanceOfAt(_from, block.number);
require(previousBalanceFrom >= _amount); // Alerts the token controller of the transfer
if (isContract(controller)) {
require(TokenController(controller).onTransfer(_from, _to, _amount));
}
// Alerts the token controller of the transfer // First update the balance array with the new value for the address
if (isContract(controller)) { // sending the tokens
require(TokenController(controller).onTransfer(_from, _to, _amount)); updateValueAtNow(balances[_from], previousBalanceFrom - _amount);
}
// First update the balance array with the new value for the address // Then update the balance array with the new value for the address
// sending the tokens // receiving the tokens
updateValueAtNow(balances[_from], previousBalanceFrom - _amount); uint256 previousBalanceTo = balanceOfAt(_to, block.number);
require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow
// Then update the balance array with the new value for the address updateValueAtNow(balances[_to], previousBalanceTo + _amount);
// receiving the tokens
uint256 previousBalanceTo = balanceOfAt(_to, block.number);
require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow
updateValueAtNow(balances[_to], previousBalanceTo + _amount);
// An event to make the transfer easy to find on the blockchain
emit Transfer(_from, _to, _amount);
// An event to make the transfer easy to find on the blockchain
emit Transfer(_from, _to, _amount);
} }
/// @param _owner The address that's balance is being requested /// @param _owner The address that's balance is being requested
@ -239,8 +229,7 @@ contract MiniMeToken is Controlled {
/// @param _spender The address of the account able to transfer the tokens /// @param _spender The address of the account able to transfer the tokens
/// @return remaining Amount of remaining tokens of _owner that _spender is allowed /// @return remaining Amount of remaining tokens of _owner that _spender is allowed
/// to spend /// to spend
function allowance(address _owner, address _spender function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
) public view returns (uint256 remaining) {
return allowed[_owner][_spender]; return allowed[_owner][_spender];
} }
@ -251,45 +240,35 @@ contract MiniMeToken is Controlled {
/// @param _spender The address of the contract able to transfer the tokens /// @param _spender The address of the contract able to transfer the tokens
/// @param _amount The amount of tokens to be approved for transfer /// @param _amount The amount of tokens to be approved for transfer
/// @return success True if the function call was successful /// @return success True if the function call was successful
function approveAndCall(address _spender, uint256 _amount, bytes memory _extraData function approveAndCall(address _spender, uint256 _amount, bytes memory _extraData) public returns (bool success) {
) public returns (bool success) {
require(approve(_spender, _amount)); require(approve(_spender, _amount));
ApproveAndCallFallBack(_spender).receiveApproval( ApproveAndCallFallBack(_spender).receiveApproval(msg.sender, _amount, address(this), _extraData);
msg.sender,
_amount,
address(this),
_extraData
);
return true; return true;
} }
/// @dev This function makes it easy to get the total number of tokens /// @dev This function makes it easy to get the total number of tokens
/// @return The total number of tokens /// @return The total number of tokens
function totalSupply() public view returns (uint) { function totalSupply() public view returns (uint256) {
return totalSupplyAt(block.number); return totalSupplyAt(block.number);
} }
////////////////
//////////////// // Query balance and totalSupply in History
// Query balance and totalSupply in History ////////////////
////////////////
/// @dev Queries the balance of `_owner` at a specific `_blockNumber` /// @dev Queries the balance of `_owner` at a specific `_blockNumber`
/// @param _owner The address from which the balance will be retrieved /// @param _owner The address from which the balance will be retrieved
/// @param _blockNumber The block number when the balance is queried /// @param _blockNumber The block number when the balance is queried
/// @return The balance at `_blockNumber` /// @return The balance at `_blockNumber`
function balanceOfAt(address _owner, uint _blockNumber) public view function balanceOfAt(address _owner, uint256 _blockNumber) public view returns (uint256) {
returns (uint) {
// These next few lines are used when the balance of the token is // These next few lines are used when the balance of the token is
// requested before a check point was ever created for this token, it // requested before a check point was ever created for this token, it
// requires that the `parentToken.balanceOfAt` be queried at the // requires that the `parentToken.balanceOfAt` be queried at the
// genesis block for that token as this contains initial balance of // genesis block for that token as this contains initial balance of
// this token // this token
if ((balances[_owner].length == 0) if ((balances[_owner].length == 0) || (balances[_owner][0].fromBlock > _blockNumber)) {
|| (balances[_owner][0].fromBlock > _blockNumber)) {
if (address(parentToken) != address(0)) { if (address(parentToken) != address(0)) {
return parentToken.balanceOfAt(_owner, min(_blockNumber, parentSnapShotBlock)); return parentToken.balanceOfAt(_owner, min(_blockNumber, parentSnapShotBlock));
} else { } else {
@ -297,7 +276,7 @@ contract MiniMeToken is Controlled {
return 0; return 0;
} }
// This will return the expected balance during normal situations // This will return the expected balance during normal situations
} else { } else {
return getValueAt(balances[_owner], _blockNumber); return getValueAt(balances[_owner], _blockNumber);
} }
@ -306,30 +285,28 @@ contract MiniMeToken is Controlled {
/// @notice Total amount of tokens at a specific `_blockNumber`. /// @notice Total amount of tokens at a specific `_blockNumber`.
/// @param _blockNumber The block number when the totalSupply is queried /// @param _blockNumber The block number when the totalSupply is queried
/// @return The total amount of tokens at `_blockNumber` /// @return The total amount of tokens at `_blockNumber`
function totalSupplyAt(uint _blockNumber) public view returns(uint) { function totalSupplyAt(uint256 _blockNumber) public view returns (uint256) {
// These next few lines are used when the totalSupply of the token is // These next few lines are used when the totalSupply of the token is
// requested before a check point was ever created for this token, it // requested before a check point was ever created for this token, it
// requires that the `parentToken.totalSupplyAt` be queried at the // requires that the `parentToken.totalSupplyAt` be queried at the
// genesis block for this token as that contains totalSupply of this // genesis block for this token as that contains totalSupply of this
// token at this block number. // token at this block number.
if ((totalSupplyHistory.length == 0) if ((totalSupplyHistory.length == 0) || (totalSupplyHistory[0].fromBlock > _blockNumber)) {
|| (totalSupplyHistory[0].fromBlock > _blockNumber)) {
if (address(parentToken) != address(0)) { if (address(parentToken) != address(0)) {
return parentToken.totalSupplyAt(min(_blockNumber, parentSnapShotBlock)); return parentToken.totalSupplyAt(min(_blockNumber, parentSnapShotBlock));
} else { } else {
return 0; return 0;
} }
// This will return the expected totalSupply during normal situations // This will return the expected totalSupply during normal situations
} else { } else {
return getValueAt(totalSupplyHistory, _blockNumber); return getValueAt(totalSupplyHistory, _blockNumber);
} }
} }
//////////////// ////////////////
// Clone Token Method // Clone Token Method
//////////////// ////////////////
/// @notice Creates a new clone token with the initial distribution being /// @notice Creates a new clone token with the initial distribution being
/// this token at `_snapshotBlock` /// this token at `_snapshotBlock`
@ -345,18 +322,16 @@ contract MiniMeToken is Controlled {
string memory _cloneTokenName, string memory _cloneTokenName,
uint8 _cloneDecimalUnits, uint8 _cloneDecimalUnits,
string memory _cloneTokenSymbol, string memory _cloneTokenSymbol,
uint _snapshotBlock, uint256 _snapshotBlock,
bool _transfersEnabled bool _transfersEnabled
) public returns(address) { )
public
returns (address)
{
if (_snapshotBlock == 0) _snapshotBlock = block.number; if (_snapshotBlock == 0) _snapshotBlock = block.number;
MiniMeToken cloneToken = tokenFactory.createCloneToken( MiniMeToken cloneToken = tokenFactory.createCloneToken(
this, this, _snapshotBlock, _cloneTokenName, _cloneDecimalUnits, _cloneTokenSymbol, _transfersEnabled
_snapshotBlock, );
_cloneTokenName,
_cloneDecimalUnits,
_cloneTokenSymbol,
_transfersEnabled
);
cloneToken.changeController(payable(msg.sender)); cloneToken.changeController(payable(msg.sender));
@ -365,36 +340,33 @@ contract MiniMeToken is Controlled {
return address(cloneToken); return address(cloneToken);
} }
//////////////// ////////////////
// Generate and destroy tokens // Generate and destroy tokens
//////////////// ////////////////
/// @notice Generates `_amount` tokens that are assigned to `_owner` /// @notice Generates `_amount` tokens that are assigned to `_owner`
/// @param _owner The address that will be assigned the new tokens /// @param _owner The address that will be assigned the new tokens
/// @param _amount The quantity of tokens generated /// @param _amount The quantity of tokens generated
/// @return True if the tokens are generated correctly /// @return True if the tokens are generated correctly
function generateTokens(address _owner, uint _amount function generateTokens(address _owner, uint256 _amount) public onlyController returns (bool) {
) public onlyController returns (bool) { uint256 curTotalSupply = totalSupply();
uint curTotalSupply = totalSupply(); require(curTotalSupply + _amount >= curTotalSupply, "OVERFLOW"); // Check for overflow
require(curTotalSupply + _amount >= curTotalSupply); // Check for overflow uint256 previousBalanceTo = balanceOf(_owner);
uint previousBalanceTo = balanceOf(_owner); require(previousBalanceTo + _amount >= previousBalanceTo, "OVERFLOW 2"); // Check for overflow
require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow
updateValueAtNow(totalSupplyHistory, curTotalSupply + _amount); updateValueAtNow(totalSupplyHistory, curTotalSupply + _amount);
updateValueAtNow(balances[_owner], previousBalanceTo + _amount); updateValueAtNow(balances[_owner], previousBalanceTo + _amount);
emit Transfer(address(0), _owner, _amount); emit Transfer(address(0), _owner, _amount);
return true; return true;
} }
/// @notice Burns `_amount` tokens from `_owner` /// @notice Burns `_amount` tokens from `_owner`
/// @param _owner The address that will lose the tokens /// @param _owner The address that will lose the tokens
/// @param _amount The quantity of tokens to burn /// @param _amount The quantity of tokens to burn
/// @return True if the tokens are burned correctly /// @return True if the tokens are burned correctly
function destroyTokens(address _owner, uint _amount function destroyTokens(address _owner, uint256 _amount) public onlyController returns (bool) {
) onlyController public returns (bool) { uint256 curTotalSupply = totalSupply();
uint curTotalSupply = totalSupply();
require(curTotalSupply >= _amount); require(curTotalSupply >= _amount);
uint previousBalanceFrom = balanceOf(_owner); uint256 previousBalanceFrom = balanceOf(_owner);
require(previousBalanceFrom >= _amount); require(previousBalanceFrom >= _amount);
updateValueAtNow(totalSupplyHistory, curTotalSupply - _amount); updateValueAtNow(totalSupplyHistory, curTotalSupply - _amount);
updateValueAtNow(balances[_owner], previousBalanceFrom - _amount); updateValueAtNow(balances[_owner], previousBalanceFrom - _amount);
@ -402,10 +374,9 @@ contract MiniMeToken is Controlled {
return true; return true;
} }
//////////////// ////////////////
// Enable tokens transfers // Enable tokens transfers
//////////////// ////////////////
/// @notice Enables token holders to transfer their tokens freely if true /// @notice Enables token holders to transfer their tokens freely if true
/// @param _transfersEnabled True if transfers are allowed in the clone /// @param _transfersEnabled True if transfers are allowed in the clone
@ -413,32 +384,32 @@ contract MiniMeToken is Controlled {
transfersEnabled = _transfersEnabled; transfersEnabled = _transfersEnabled;
} }
//////////////// ////////////////
// Internal helper functions to query and set a value in a snapshot array // Internal helper functions to query and set a value in a snapshot array
//////////////// ////////////////
/// @dev `getValueAt` retrieves the number of tokens at a given block number /// @dev `getValueAt` retrieves the number of tokens at a given block number
/// @param checkpoints The history of values being queried /// @param checkpoints The history of values being queried
/// @param _block The block number to retrieve the value at /// @param _block The block number to retrieve the value at
/// @return The number of tokens being queried /// @return The number of tokens being queried
function getValueAt(Checkpoint[] storage checkpoints, uint _block function getValueAt(Checkpoint[] storage checkpoints, uint256 _block) internal view returns (uint256) {
) view internal returns (uint) {
if (checkpoints.length == 0) return 0; if (checkpoints.length == 0) return 0;
// Shortcut for the actual value // Shortcut for the actual value
if (_block >= checkpoints[checkpoints.length-1].fromBlock) if (_block >= checkpoints[checkpoints.length - 1].fromBlock) {
return checkpoints[checkpoints.length-1].value; return checkpoints[checkpoints.length - 1].value;
}
if (_block < checkpoints[0].fromBlock) return 0; if (_block < checkpoints[0].fromBlock) return 0;
// Binary search of the value in the array // Binary search of the value in the array
uint min = 0; uint256 min = 0;
uint max = checkpoints.length-1; uint256 max = checkpoints.length - 1;
while (max > min) { while (max > min) {
uint mid = (max + min + 1)/ 2; uint256 mid = (max + min + 1) / 2;
if (checkpoints[mid].fromBlock<=_block) { if (checkpoints[mid].fromBlock <= _block) {
min = mid; min = mid;
} else { } else {
max = mid-1; max = mid - 1;
} }
} }
return checkpoints[min].value; return checkpoints[min].value;
@ -448,33 +419,31 @@ contract MiniMeToken is Controlled {
/// `totalSupplyHistory` /// `totalSupplyHistory`
/// @param checkpoints The history of data being updated /// @param checkpoints The history of data being updated
/// @param _value The new number of tokens /// @param _value The new number of tokens
function updateValueAtNow(Checkpoint[] storage checkpoints, uint _value function updateValueAtNow(Checkpoint[] storage checkpoints, uint256 _value) internal {
) internal { if ((checkpoints.length == 0) || (checkpoints[checkpoints.length - 1].fromBlock < block.number)) {
if ((checkpoints.length == 0) Checkpoint storage newCheckPoint = checkpoints.push();
|| (checkpoints[checkpoints.length -1].fromBlock < block.number)) { newCheckPoint.fromBlock = uint128(block.number);
Checkpoint storage newCheckPoint = checkpoints.push(); newCheckPoint.value = uint128(_value);
newCheckPoint.fromBlock = uint128(block.number); } else {
newCheckPoint.value = uint128(_value); Checkpoint storage oldCheckPoint = checkpoints[checkpoints.length - 1];
} else { oldCheckPoint.value = uint128(_value);
Checkpoint storage oldCheckPoint = checkpoints[checkpoints.length-1]; }
oldCheckPoint.value = uint128(_value);
}
} }
/// @dev Internal function to determine if an address is a contract /// @dev Internal function to determine if an address is a contract
/// @param _addr The address being queried /// @param _addr The address being queried
/// @return True if `_addr` is a contract /// @return True if `_addr` is a contract
function isContract(address _addr) view internal returns(bool) { function isContract(address _addr) internal view returns (bool) {
uint size; uint256 size;
if (_addr == address(0)) return false; if (_addr == address(0)) return false;
assembly { assembly {
size := extcodesize(_addr) size := extcodesize(_addr)
} }
return size>0; return size > 0;
} }
/// @dev Helper function to return a min betwen the two uints /// @dev Helper function to return a min betwen the two uints
function min(uint a, uint b) pure internal returns (uint) { function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b; return a < b ? a : b;
} }
@ -483,44 +452,39 @@ contract MiniMeToken is Controlled {
/// ether and creates tokens as described in the token controller contract /// ether and creates tokens as described in the token controller contract
receive() external payable { receive() external payable {
require(isContract(controller)); require(isContract(controller));
require(TokenController(controller).proxyPayment{value: msg.value}(msg.sender)); require(TokenController(controller).proxyPayment{ value: msg.value }(msg.sender));
} }
////////// //////////
// Safety Methods // Safety Methods
////////// //////////
/// @notice This method can be used by the controller to extract mistakenly /// @notice This method can be used by the controller to extract mistakenly
/// sent tokens to this contract. /// sent tokens to this contract.
/// @param _token The address of the token contract that you want to recover /// @param _token The address of the token contract that you want to recover
/// set to 0 in case you want to extract ether. /// set to 0 in case you want to extract ether.
function claimTokens(MiniMeToken _token) public onlyController { //TODO: change is to generic ERC20 interface function claimTokens(MiniMeToken _token) public onlyController {
//TODO: change is to generic ERC20 interface
if (address(_token) == address(0)) { if (address(_token) == address(0)) {
controller.transfer(address(this).balance); controller.transfer(address(this).balance);
return; return;
} }
MiniMeToken token = _token; MiniMeToken token = _token;
uint balance = token.balanceOf(address(this)); uint256 balance = token.balanceOf(address(this));
token.transfer(controller, balance); token.transfer(controller, balance);
emit ClaimedTokens(address(_token), controller, balance); emit ClaimedTokens(address(_token), controller, balance);
} }
//////////////// ////////////////
// Events // Events
//////////////// ////////////////
event ClaimedTokens(address indexed _token, address indexed _controller, uint _amount); event ClaimedTokens(address indexed _token, address indexed _controller, uint256 _amount);
event Transfer(address indexed _from, address indexed _to, uint256 _amount); event Transfer(address indexed _from, address indexed _to, uint256 _amount);
event NewCloneToken(address indexed _cloneToken, uint _snapshotBlock); event NewCloneToken(address indexed _cloneToken, uint256 _snapshotBlock);
event Approval( event Approval(address indexed _owner, address indexed _spender, uint256 _amount);
address indexed _owner,
address indexed _spender,
uint256 _amount
);
} }
//////////////// ////////////////
// MiniMeTokenFactory // MiniMeTokenFactory
//////////////// ////////////////
@ -529,7 +493,6 @@ contract MiniMeToken is Controlled {
/// In solidity this is the way to create a contract from a contract of the /// In solidity this is the way to create a contract from a contract of the
/// same class /// same class
contract MiniMeTokenFactory { contract MiniMeTokenFactory {
/// @notice Update the DApp by creating a new token with new functionalities /// @notice Update the DApp by creating a new token with new functionalities
/// the msg.sender becomes the controller of this clone token /// the msg.sender becomes the controller of this clone token
/// @param _parentToken Address of the token being cloned /// @param _parentToken Address of the token being cloned
@ -542,12 +505,15 @@ contract MiniMeTokenFactory {
/// @return The address of the new token contract /// @return The address of the new token contract
function createCloneToken( function createCloneToken(
MiniMeToken _parentToken, MiniMeToken _parentToken,
uint _snapshotBlock, uint256 _snapshotBlock,
string memory _tokenName, string memory _tokenName,
uint8 _decimalUnits, uint8 _decimalUnits,
string memory _tokenSymbol, string memory _tokenSymbol,
bool _transfersEnabled bool _transfersEnabled
) public returns (MiniMeToken) { )
public
returns (MiniMeToken)
{
MiniMeToken newToken = new MiniMeToken( MiniMeToken newToken = new MiniMeToken(
this, this,
_parentToken, _parentToken,

View File

@ -27,18 +27,22 @@ pragma solidity ^0.8.0;
import "./MiniMeToken.sol"; import "./MiniMeToken.sol";
/// @dev `Owned` is a base level contract that assigns an `owner` that can be /// @dev `Owned` is a base level contract that assigns an `owner` that can be
/// later changed /// later changed
contract Owned { contract Owned {
/// @dev `owner` is the only address that can call a function with this /// @dev `owner` is the only address that can call a function with this
/// modifier /// modifier
modifier onlyOwner { require (msg.sender == owner); _; } modifier onlyOwner() {
require(msg.sender == owner);
_;
}
address public owner; address public owner;
/// @notice The Constructor assigns the message sender to be `owner` /// @notice The Constructor assigns the message sender to be `owner`
constructor() { owner = msg.sender;} constructor() {
owner = msg.sender;
}
/// @notice `owner` can step down and assign some other address to this role /// @notice `owner` can step down and assign some other address to this role
/// @param _newOwner The address of the new owner. 0x0 can be used to create /// @param _newOwner The address of the new owner. 0x0 can be used to create
@ -48,139 +52,131 @@ contract Owned {
} }
} }
/// @dev This is designed to control the issuance of a MiniMe Token for a /// @dev This is designed to control the issuance of a MiniMe Token for a
/// non-profit Campaign. This contract effectively dictates the terms of the /// non-profit Campaign. This contract effectively dictates the terms of the
/// funding round. /// funding round.
contract Campaign is TokenController, Owned { contract Campaign is TokenController, Owned {
uint256 public startFundingTime; // In UNIX Time Format
uint256 public endFundingTime; // In UNIX Time Format
uint256 public maximumFunding; // In wei
uint256 public totalCollected; // In wei
MiniMeToken public tokenContract; // The new token for this Campaign
address payable public vaultAddress; // The address to hold the funds donated
uint public startFundingTime; // In UNIX Time Format /// @notice 'Campaign()' initiates the Campaign by setting its funding
uint public endFundingTime; // In UNIX Time Format /// parameters
uint public maximumFunding; // In wei /// @dev There are several checks to make sure the parameters are acceptable
uint public totalCollected; // In wei /// @param _startFundingTime The UNIX time that the Campaign will be able to
MiniMeToken public tokenContract; // The new token for this Campaign /// start receiving funds
address payable public vaultAddress; // The address to hold the funds donated /// @param _endFundingTime The UNIX time that the Campaign will stop being able
/// to receive funds
/// @notice 'Campaign()' initiates the Campaign by setting its funding /// @param _maximumFunding In wei, the Maximum amount that the Campaign can
/// parameters /// receive (currently the max is set at 10,000 ETH for the beta)
/// @dev There are several checks to make sure the parameters are acceptable /// @param _vaultAddress The address that will store the donated funds
/// @param _startFundingTime The UNIX time that the Campaign will be able to /// @param _tokenAddress Address of the token contract this contract controls
/// start receiving funds
/// @param _endFundingTime The UNIX time that the Campaign will stop being able
/// to receive funds
/// @param _maximumFunding In wei, the Maximum amount that the Campaign can
/// receive (currently the max is set at 10,000 ETH for the beta)
/// @param _vaultAddress The address that will store the donated funds
/// @param _tokenAddress Address of the token contract this contract controls
constructor( constructor(
uint _startFundingTime, uint256 _startFundingTime,
uint _endFundingTime, uint256 _endFundingTime,
uint _maximumFunding, uint256 _maximumFunding,
address payable _vaultAddress, address payable _vaultAddress,
MiniMeToken _tokenAddress MiniMeToken _tokenAddress
) { ) {
require ((_endFundingTime >= block.timestamp) && // Cannot end in the past require(
(_endFundingTime > _startFundingTime) && (_endFundingTime >= block.timestamp) // Cannot end in the past
(_maximumFunding <= 10000 ether) && // The Beta is limited && (_endFundingTime > _startFundingTime) && (_maximumFunding <= 10_000 ether) // The Beta is limited
(_vaultAddress != address(0))); // To prevent burning ETH && (_vaultAddress != address(0))
); // To prevent burning ETH
startFundingTime = _startFundingTime; startFundingTime = _startFundingTime;
endFundingTime = _endFundingTime; endFundingTime = _endFundingTime;
maximumFunding = _maximumFunding; maximumFunding = _maximumFunding;
tokenContract = _tokenAddress;// The Deployed Token Contract tokenContract = _tokenAddress; // The Deployed Token Contract
vaultAddress = _vaultAddress; vaultAddress = _vaultAddress;
} }
/// @dev The fallback function is called when ether is sent to the contract, it /// @dev The fallback function is called when ether is sent to the contract, it
/// simply calls `doPayment()` with the address that sent the ether as the /// simply calls `doPayment()` with the address that sent the ether as the
/// `_owner`. Payable is a required solidity modifier for functions to receive /// `_owner`. Payable is a required solidity modifier for functions to receive
/// ether, without this modifier functions will throw if ether is sent to them /// ether, without this modifier functions will throw if ether is sent to them
receive() external payable { receive() external payable {
doPayment(msg.sender); doPayment(msg.sender);
} }
///////////////// /////////////////
// TokenController interface // TokenController interface
///////////////// /////////////////
/// @notice `proxyPayment()` allows the caller to send ether to the Campaign and /// @notice `proxyPayment()` allows the caller to send ether to the Campaign and
/// have the tokens created in an address of their choosing /// have the tokens created in an address of their choosing
/// @param _owner The address that will hold the newly created tokens /// @param _owner The address that will hold the newly created tokens
function proxyPayment(address _owner) public override payable returns(bool) { function proxyPayment(address _owner) public payable override returns (bool) {
doPayment(_owner); doPayment(_owner);
return true; return true;
} }
/// @notice Notifies the controller about a transfer, for this Campaign all /// @notice Notifies the controller about a transfer, for this Campaign all
/// transfers are allowed by default and no extra notifications are needed /// transfers are allowed by default and no extra notifications are needed
/// @param _from The origin of the transfer /// @param _from The origin of the transfer
/// @param _to The destination of the transfer /// @param _to The destination of the transfer
/// @param _amount The amount of the transfer /// @param _amount The amount of the transfer
/// @return False if the controller does not authorize the transfer /// @return False if the controller does not authorize the transfer
function onTransfer(address _from, address _to, uint _amount) public override returns(bool) { function onTransfer(address _from, address _to, uint256 _amount) public override returns (bool) {
return true; return true;
} }
/// @notice Notifies the controller about an approval, for this Campaign all /// @notice Notifies the controller about an approval, for this Campaign all
/// approvals are allowed by default and no extra notifications are needed /// approvals are allowed by default and no extra notifications are needed
/// @param _owner The address that calls `approve()` /// @param _owner The address that calls `approve()`
/// @param _spender The spender in the `approve()` call /// @param _spender The spender in the `approve()` call
/// @param _amount The amount in the `approve()` call /// @param _amount The amount in the `approve()` call
/// @return False if the controller does not authorize the approval /// @return False if the controller does not authorize the approval
function onApprove(address _owner, address _spender, uint _amount) public override function onApprove(address _owner, address _spender, uint256 _amount) public override returns (bool) {
returns(bool)
{
return true; return true;
} }
/// @dev `doPayment()` is an internal function that sends the ether that this
/// @dev `doPayment()` is an internal function that sends the ether that this /// contract receives to the `vault` and creates tokens in the address of the
/// contract receives to the `vault` and creates tokens in the address of the /// `_owner` assuming the Campaign is still accepting funds
/// `_owner` assuming the Campaign is still accepting funds /// @param _owner The address that will hold the newly created tokens
/// @param _owner The address that will hold the newly created tokens
function doPayment(address _owner) internal { function doPayment(address _owner) internal {
// First check that the Campaign is allowed to receive this donation
require(
(block.timestamp >= startFundingTime) && (block.timestamp <= endFundingTime)
&& (tokenContract.controller() != address(0)) // Extra check
&& (msg.value != 0) && (totalCollected + msg.value <= maximumFunding)
);
// First check that the Campaign is allowed to receive this donation //Track how much the Campaign has collected
require ((block.timestamp >= startFundingTime) &&
(block.timestamp <= endFundingTime) &&
(tokenContract.controller() != address(0)) && // Extra check
(msg.value != 0) &&
(totalCollected + msg.value <= maximumFunding));
//Track how much the Campaign has collected
totalCollected += msg.value; totalCollected += msg.value;
//Send the ether to the vault //Send the ether to the vault
require (vaultAddress.send(msg.value)); require(vaultAddress.send(msg.value));
// Creates an equal amount of tokens as ether sent. The new tokens are created // Creates an equal amount of tokens as ether sent. The new tokens are created
// in the `_owner` address // in the `_owner` address
require (tokenContract.generateTokens(_owner, msg.value)); require(tokenContract.generateTokens(_owner, msg.value));
return; return;
} }
/// @notice `finalizeFunding()` ends the Campaign by calling setting the /// @notice `finalizeFunding()` ends the Campaign by calling setting the
/// controller to 0, thereby ending the issuance of new tokens and stopping the /// controller to 0, thereby ending the issuance of new tokens and stopping the
/// Campaign from receiving more ether /// Campaign from receiving more ether
/// @dev `finalizeFunding()` can only be called after the end of the funding period. /// @dev `finalizeFunding()` can only be called after the end of the funding period.
function finalizeFunding() external { function finalizeFunding() external {
require(block.timestamp >= endFundingTime); require(block.timestamp >= endFundingTime);
tokenContract.changeController(payable(address(0))); tokenContract.changeController(payable(address(0)));
} }
/// @notice `onlyOwner` changes the location that ether is sent
/// @notice `onlyOwner` changes the location that ether is sent /// @param _newVaultAddress The address that will receive the ether sent to this
/// @param _newVaultAddress The address that will receive the ether sent to this /// Campaign
/// Campaign
function setVault(address payable _newVaultAddress) external onlyOwner { function setVault(address payable _newVaultAddress) external onlyOwner {
vaultAddress = _newVaultAddress; vaultAddress = _newVaultAddress;
} }
} }

View File

@ -6,7 +6,7 @@ abstract contract TokenController {
/// @notice Called when `_owner` sends ether to the MiniMe Token contract /// @notice Called when `_owner` sends ether to the MiniMe Token contract
/// @param _owner The address that sent the ether to create tokens /// @param _owner The address that sent the ether to create tokens
/// @return True if the ether is accepted, false if it throws /// @return True if the ether is accepted, false if it throws
function proxyPayment(address _owner) virtual public payable returns(bool); function proxyPayment(address _owner) public payable virtual returns (bool);
/// @notice Notifies the controller about a token transfer allowing the /// @notice Notifies the controller about a token transfer allowing the
/// controller to react if desired /// controller to react if desired
@ -14,7 +14,7 @@ abstract contract TokenController {
/// @param _to The destination of the transfer /// @param _to The destination of the transfer
/// @param _amount The amount of the transfer /// @param _amount The amount of the transfer
/// @return False if the controller does not authorize the transfer /// @return False if the controller does not authorize the transfer
function onTransfer(address _from, address _to, uint _amount) virtual public returns(bool); function onTransfer(address _from, address _to, uint256 _amount) public virtual returns (bool);
/// @notice Notifies the controller about an approval allowing the /// @notice Notifies the controller about an approval allowing the
/// controller to react if desired /// controller to react if desired
@ -22,6 +22,5 @@ abstract contract TokenController {
/// @param _spender The spender in the `approve()` call /// @param _spender The spender in the `approve()` call
/// @param _amount The amount in the `approve()` call /// @param _amount The amount in the `approve()` call
/// @return False if the controller does not authorize the approval /// @return False if the controller does not authorize the approval
function onApprove(address _owner, address _spender, uint _amount) virtual public function onApprove(address _owner, address _spender, uint256 _amount) public virtual returns (bool);
returns(bool);
} }

View File

@ -4,8 +4,36 @@ pragma solidity >=0.8.19 <=0.9.0;
import { BaseScript } from "./Base.s.sol"; import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol"; import { DeploymentConfig } from "./DeploymentConfig.s.sol";
import { MiniMeToken } from "../contracts/MiniMeToken.sol";
import { MiniMeTokenFactory } from "../contracts/MiniMeToken.sol";
contract Deploy is BaseScript { contract Deploy is BaseScript {
function run() public returns (DeploymentConfig deploymentConfig) { function run()
public
returns (DeploymentConfig deploymentConfig, MiniMeTokenFactory minimeFactory, MiniMeToken minimeToken)
{
deploymentConfig = new DeploymentConfig(broadcaster); deploymentConfig = new DeploymentConfig(broadcaster);
(
,
address parentToken,
uint256 parentSnapShotBlock,
string memory name,
uint8 decimals,
string memory symbol,
bool transferEnabled
) = deploymentConfig.activeNetworkConfig();
vm.startBroadcast(broadcaster);
minimeFactory = new MiniMeTokenFactory();
minimeToken = new MiniMeToken(
minimeFactory,
MiniMeToken(payable(parentToken)),
parentSnapShotBlock,
name,
decimals,
symbol,
transferEnabled
);
vm.stopBroadcast();
} }
} }

View File

@ -10,6 +10,12 @@ contract DeploymentConfig is Script {
struct NetworkConfig { struct NetworkConfig {
address deployer; address deployer;
address parentToken;
uint256 parentSnapShotBlock;
string name;
uint8 decimals;
string symbol;
bool transferEnabled;
} }
NetworkConfig public activeNetworkConfig; NetworkConfig public activeNetworkConfig;
@ -17,17 +23,25 @@ contract DeploymentConfig is Script {
address private deployer; address private deployer;
constructor(address _broadcaster) { constructor(address _broadcaster) {
deployer = _broadcaster;
if (block.chainid == 31_337) { if (block.chainid == 31_337) {
activeNetworkConfig = getOrCreateAnvilEthConfig(); activeNetworkConfig = getOrCreateAnvilEthConfig();
} else { } else {
revert DeploymentConfig_NoConfigForChain(block.chainid); revert DeploymentConfig_NoConfigForChain(block.chainid);
} }
if (_broadcaster == address(0)) revert DeploymentConfig_InvalidDeployerAddress(); if (_broadcaster == address(0)) revert DeploymentConfig_InvalidDeployerAddress();
deployer = _broadcaster;
} }
function getOrCreateAnvilEthConfig() public view returns (NetworkConfig memory) { function getOrCreateAnvilEthConfig() public view returns (NetworkConfig memory) {
return NetworkConfig({ deployer: deployer }); return NetworkConfig({
deployer: deployer,
parentToken: address(0),
parentSnapShotBlock: 0,
name: "MiniMe Test Token",
decimals: 18,
symbol: "MMT",
transferEnabled: true
});
} }
// This function is a hack to have it excluded by `forge coverage` until // This function is a hack to have it excluded by `forge coverage` until

205
test/MiniMeToken.t.sol Normal file
View File

@ -0,0 +1,205 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import { Test } from "forge-std/Test.sol";
import { Deploy } from "../script/Deploy.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import { MiniMeToken } from "../contracts/MiniMeToken.sol";
import { MiniMeTokenFactory } from "../contracts/MiniMeToken.sol";
contract MiniMeTokenTest is Test {
DeploymentConfig internal deploymentConfig;
MiniMeTokenFactory internal minimeTokenFactory;
MiniMeToken internal minimeToken;
address internal deployer;
address[] internal accounts;
function setUp() public virtual {
Deploy deployment = new Deploy();
(deploymentConfig, minimeTokenFactory, minimeToken) = deployment.run();
(deployer,,,,,,) = deploymentConfig.activeNetworkConfig();
accounts = new address[](4);
accounts[0] = makeAddr("account0");
accounts[1] = makeAddr("account1");
accounts[2] = makeAddr("account2");
accounts[3] = makeAddr("account3");
}
function testDeployment() public {
(, address parentToken, uint256 parentSnapShotBlock, string memory name, uint8 decimals, string memory symbol,)
= deploymentConfig.activeNetworkConfig();
assertEq(minimeToken.name(), name);
assertEq(minimeToken.symbol(), symbol);
assertEq(minimeToken.decimals(), decimals);
assertEq(minimeToken.controller(), deployer);
assertEq(address(minimeToken.parentToken()), parentToken);
assertEq(minimeToken.parentSnapShotBlock(), parentSnapShotBlock);
}
function _generateTokens(address to, uint256 amount) internal {
vm.prank(deployer);
minimeToken.generateTokens(to, amount);
}
}
contract GenerateTokensTest is MiniMeTokenTest {
function setUp() public virtual override {
MiniMeTokenTest.setUp();
}
function test_RevertWhen_SenderIsNotController() public {
vm.expectRevert();
minimeToken.generateTokens(accounts[0], 10);
}
function testGenerateTokens() public {
_generateTokens(accounts[0], 10);
assertEq(minimeToken.totalSupply(), 10);
assertEq(minimeToken.balanceOf(accounts[0]), 10);
}
}
contract TransferTest is MiniMeTokenTest {
function setUp() public virtual override {
MiniMeTokenTest.setUp();
}
function testTransfer() public {
uint256 currentBlock = block.number;
uint256 nextBlock = currentBlock + 1;
_generateTokens(accounts[0], 10);
// enforce the next block
vm.roll(nextBlock);
vm.prank(accounts[0]);
minimeToken.transfer(accounts[1], 2);
assertEq(minimeToken.totalSupply(), 10);
assertEq(minimeToken.balanceOf(accounts[0]), 8);
assertEq(minimeToken.balanceOf(accounts[1]), 2);
// check balance at original block
assertEq(minimeToken.balanceOfAt(accounts[0], currentBlock), 10);
}
}
contract AllowanceTest is MiniMeTokenTest {
function setUp() public virtual override {
MiniMeTokenTest.setUp();
}
function testAllowance() public {
vm.prank(accounts[0]);
minimeToken.approve(accounts[1], 2);
uint256 allowed = minimeToken.allowance(accounts[0], accounts[1]);
assertEq(allowed, 2);
uint256 currentBlock = block.number;
uint256 nextBlock = currentBlock + 1;
// ensure `accounts[0]` has tokens
_generateTokens(accounts[0], 10);
// enforce the next block
vm.roll(nextBlock);
vm.prank(accounts[1]);
minimeToken.transferFrom(accounts[0], accounts[2], 1);
allowed = minimeToken.allowance(accounts[0], accounts[1]);
assertEq(allowed, 1);
assertEq(minimeToken.totalSupply(), 10);
assertEq(minimeToken.balanceOf(accounts[0]), 9);
assertEq(minimeToken.balanceOf(accounts[2]), 1);
// check balance at blocks
assertEq(minimeToken.balanceOfAt(accounts[0], currentBlock), 10);
assertEq(minimeToken.balanceOfAt(accounts[0], nextBlock), 9);
assertEq(minimeToken.balanceOfAt(accounts[2], nextBlock), 1);
}
}
contract DestroyTokensTest is MiniMeTokenTest {
function setUp() public virtual override {
MiniMeTokenTest.setUp();
}
function testDestroyTokens() public {
// ensure `accounts[0]` has tokens
_generateTokens(accounts[0], 10);
vm.prank(deployer);
minimeToken.destroyTokens(accounts[0], 3);
assertEq(minimeToken.totalSupply(), 7);
assertEq(minimeToken.balanceOf(accounts[0]), 7);
}
}
contract CreateCloneTokenTest is MiniMeTokenTest {
function setUp() public virtual override {
MiniMeTokenTest.setUp();
}
function _createClone() internal returns (MiniMeToken) {
address cloneAddress = minimeToken.createCloneToken("Clone Token 1", 18, "MMTc", 0, true);
MiniMeToken clone = MiniMeToken(payable(cloneAddress));
return clone;
}
function testCreateCloneToken() public {
// fund some accounts to later check if cloned token has same balances
uint256 currentBlock = block.number;
_generateTokens(accounts[0], 7);
uint256 nextBlock = block.number + 1;
vm.roll(nextBlock);
_generateTokens(accounts[1], 3);
uint256 secondNextBlock = block.number + 2;
vm.roll(secondNextBlock);
_generateTokens(accounts[2], 5);
MiniMeToken clone = _createClone();
assertEq(address(clone.parentToken()), address(minimeToken));
assertEq(clone.parentSnapShotBlock(), block.number);
assertEq(clone.totalSupply(), 15);
assertEq(clone.balanceOf(accounts[0]), 7);
assertEq(clone.balanceOf(accounts[1]), 3);
assertEq(clone.balanceOf(accounts[2]), 5);
assertEq(clone.totalSupplyAt(currentBlock), 7);
assertEq(clone.totalSupplyAt(nextBlock), 10);
assertEq(clone.balanceOfAt(accounts[0], currentBlock), 7);
assertEq(clone.balanceOfAt(accounts[1], currentBlock), 0);
assertEq(clone.balanceOfAt(accounts[2], currentBlock), 0);
assertEq(clone.balanceOfAt(accounts[0], nextBlock), 7);
assertEq(clone.balanceOfAt(accounts[1], nextBlock), 3);
assertEq(clone.balanceOfAt(accounts[2], nextBlock), 0);
assertEq(clone.balanceOfAt(accounts[0], secondNextBlock), 7);
assertEq(clone.balanceOfAt(accounts[1], secondNextBlock), 3);
assertEq(clone.balanceOfAt(accounts[2], secondNextBlock), 5);
}
function testGenerateTokens() public {
_generateTokens(accounts[0], 10);
vm.prank(deployer);
MiniMeToken clone = _createClone();
assertEq(clone.totalSupply(), 10);
vm.prank(deployer);
clone.generateTokens(accounts[0], 5);
}
}