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!
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(
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
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)
@ -28,9 +31,14 @@ All MiniMe Tokens maintain a history of the balance changes that occur during ea
### 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:
@ -40,15 +48,18 @@ To create and destroy tokens, these two functions are introduced:
### 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
function enableTransfers(bool _transfersEnabled) onlyController
## 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.
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.
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
@ -67,5 +80,3 @@ All these applications and more are enabled by the MiniMe Token Contract. The mo
2. Deploy the MinimeToken
3. Deploy 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 {
/// @notice The address of the controller is the only address that can call
/// a function with this modifier
modifier onlyController { require(msg.sender == controller); _; }
modifier onlyController() {
require(msg.sender == controller);
_;
}
address payable public controller;
constructor() { controller = payable(msg.sender);}
constructor() {
controller = payable(msg.sender);
}
/// @notice Changes the controller of the contract
/// @param _newController The new controller of the contract

View File

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

View File

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

View File

@ -6,7 +6,7 @@ abstract contract TokenController {
/// @notice Called when `_owner` sends ether to the MiniMe Token contract
/// @param _owner The address that sent the ether to create tokens
/// @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
/// controller to react if desired
@ -14,7 +14,7 @@ abstract contract TokenController {
/// @param _to The destination of the transfer
/// @param _amount The amount of 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
/// controller to react if desired
@ -22,6 +22,5 @@ abstract contract TokenController {
/// @param _spender The spender in the `approve()` call
/// @param _amount The amount in the `approve()` call
/// @return False if the controller does not authorize the approval
function onApprove(address _owner, address _spender, uint _amount) virtual public
returns(bool);
function onApprove(address _owner, address _spender, uint256 _amount) public virtual returns (bool);
}

View File

@ -4,8 +4,36 @@ pragma solidity >=0.8.19 <=0.9.0;
import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";
import { MiniMeToken } from "../contracts/MiniMeToken.sol";
import { MiniMeTokenFactory } from "../contracts/MiniMeToken.sol";
contract Deploy is BaseScript {
function run() public returns (DeploymentConfig deploymentConfig) {
function run()
public
returns (DeploymentConfig deploymentConfig, MiniMeTokenFactory minimeFactory, MiniMeToken minimeToken)
{
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 {
address deployer;
address parentToken;
uint256 parentSnapShotBlock;
string name;
uint8 decimals;
string symbol;
bool transferEnabled;
}
NetworkConfig public activeNetworkConfig;
@ -17,17 +23,25 @@ contract DeploymentConfig is Script {
address private deployer;
constructor(address _broadcaster) {
deployer = _broadcaster;
if (block.chainid == 31_337) {
activeNetworkConfig = getOrCreateAnvilEthConfig();
} else {
revert DeploymentConfig_NoConfigForChain(block.chainid);
}
if (_broadcaster == address(0)) revert DeploymentConfig_InvalidDeployerAddress();
deployer = _broadcaster;
}
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

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);
}
}