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,10 +58,10 @@ 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
@ -104,7 +100,7 @@ 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,
@ -120,7 +116,6 @@ contract MiniMeToken is Controlled {
creationBlock = block.number; creationBlock = block.number;
} }
/////////////////// ///////////////////
// ERC20 Methods // ERC20 Methods
/////////////////// ///////////////////
@ -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,9 +157,7 @@ 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) { if (_amount == 0) {
emit Transfer(_from, _to, _amount); // Follow the spec to louch the event when transfer 0 emit Transfer(_from, _to, _amount); // Follow the spec to louch the event when transfer 0
return; return;
@ -200,7 +191,6 @@ contract MiniMeToken is Controlled {
// An event to make the transfer easy to find on the blockchain // An event to make the transfer easy to find on the blockchain
emit Transfer(_from, _to, _amount); 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,27 +240,20 @@ 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
//////////////// ////////////////
@ -280,16 +262,13 @@ contract MiniMeToken is Controlled {
/// @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 {
@ -306,15 +285,13 @@ 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 {
@ -345,17 +322,15 @@ 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));
@ -373,28 +348,25 @@ contract MiniMeToken is Controlled {
/// @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);
@ -406,7 +378,6 @@ contract MiniMeToken is Controlled {
// 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
function enableTransfers(bool _transfersEnabled) public onlyController { function enableTransfers(bool _transfersEnabled) public onlyController {
@ -421,20 +392,20 @@ contract MiniMeToken is Controlled {
/// @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 {
@ -448,10 +419,8 @@ 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)
|| (checkpoints[checkpoints.length -1].fromBlock < block.number)) {
Checkpoint storage newCheckPoint = checkpoints.push(); Checkpoint storage newCheckPoint = checkpoints.push();
newCheckPoint.fromBlock = uint128(block.number); newCheckPoint.fromBlock = uint128(block.number);
newCheckPoint.value = uint128(_value); newCheckPoint.value = uint128(_value);
@ -464,8 +433,8 @@ contract MiniMeToken is Controlled {
/// @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)
@ -474,7 +443,7 @@ contract MiniMeToken is Controlled {
} }
/// @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;
} }
@ -494,14 +463,15 @@ contract MiniMeToken is Controlled {
/// 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);
} }
@ -509,18 +479,12 @@ contract MiniMeToken is Controlled {
//////////////// ////////////////
// 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,17 +52,15 @@ 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
uint public startFundingTime; // In UNIX Time Format uint256 public endFundingTime; // In UNIX Time Format
uint public endFundingTime; // In UNIX Time Format uint256 public maximumFunding; // In wei
uint public maximumFunding; // In wei uint256 public totalCollected; // In wei
uint public totalCollected; // In wei
MiniMeToken public tokenContract; // The new token for this Campaign MiniMeToken public tokenContract; // The new token for this Campaign
address payable public vaultAddress; // The address to hold the funds donated address payable public vaultAddress; // The address to hold the funds donated
@ -75,17 +77,17 @@ contract Campaign is TokenController, Owned {
/// @param _tokenAddress Address of the token contract this contract controls /// @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;
@ -110,7 +112,7 @@ contract Campaign is TokenController, Owned {
/// 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;
} }
@ -121,7 +123,7 @@ contract Campaign is TokenController, Owned {
/// @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;
} }
@ -131,26 +133,22 @@ contract Campaign is TokenController, Owned {
/// @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 // First check that the Campaign is allowed to receive this donation
require ((block.timestamp >= startFundingTime) && require(
(block.timestamp <= endFundingTime) && (block.timestamp >= startFundingTime) && (block.timestamp <= endFundingTime)
(tokenContract.controller() != address(0)) && // Extra check && (tokenContract.controller() != address(0)) // Extra check
(msg.value != 0) && && (msg.value != 0) && (totalCollected + msg.value <= maximumFunding)
(totalCollected + msg.value <= maximumFunding)); );
//Track how much the Campaign has collected //Track how much the Campaign has collected
totalCollected += msg.value; totalCollected += msg.value;
@ -175,12 +173,10 @@ contract Campaign is TokenController, Owned {
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);
}
}