tests: reorganize packages (#7)

* tests: reorganize testing packages

* gitignore and minor changes
This commit is contained in:
Federico Kunze
2021-05-11 07:54:55 -04:00
committed by GitHub
parent 117342b1b3
commit 96cad7de9c
115 changed files with 68 additions and 80 deletions
BIN
View File
Binary file not shown.
+410
View File
@@ -0,0 +1,410 @@
package importer
import (
"flag"
"fmt"
"io"
"math/big"
"os"
"os/signal"
"runtime/pprof"
"sort"
"syscall"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
sdkcodec "github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/store"
sdkstore "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
paramkeeper "github.com/cosmos/cosmos-sdk/x/params/keeper"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/cosmos/ethermint/codec"
"github.com/cosmos/ethermint/core"
"github.com/cosmos/ethermint/types"
evmkeeper "github.com/cosmos/ethermint/x/evm/keeper"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
ethcmn "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash"
ethcore "github.com/ethereum/go-ethereum/core"
ethtypes "github.com/ethereum/go-ethereum/core/types"
ethvm "github.com/ethereum/go-ethereum/core/vm"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
ethparams "github.com/ethereum/go-ethereum/params"
ethrlp "github.com/ethereum/go-ethereum/rlp"
tmlog "github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
dbm "github.com/tendermint/tm-db"
)
var (
flagDataDir string
flagBlockchain string
flagCPUProfile string
genInvestor = ethcmn.HexToAddress("0x756F45E3FA69347A9A973A725E3C98bC4db0b5a0")
logger = tmlog.NewNopLogger()
rewardBig8 = big.NewInt(8)
rewardBig32 = big.NewInt(32)
)
func init() {
flag.StringVar(&flagCPUProfile, "cpu-profile", "", "write CPU profile")
flag.StringVar(&flagDataDir, "datadir", "", "test data directory for state storage")
flag.StringVar(&flagBlockchain, "blockchain", "blockchain", "ethereum block export file (blocks to import)")
testing.Init()
flag.Parse()
}
func newTestCodec() (sdkcodec.BinaryMarshaler, *sdkcodec.LegacyAmino) {
interfaceRegistry := codectypes.NewInterfaceRegistry()
cdc := sdkcodec.NewProtoCodec(interfaceRegistry)
amino := sdkcodec.NewLegacyAmino()
sdk.RegisterLegacyAminoCodec(amino)
codec.RegisterInterfaces(interfaceRegistry)
return cdc, amino
}
func cleanup() {
fmt.Println("cleaning up test execution...")
os.RemoveAll(flagDataDir)
if flagCPUProfile != "" {
pprof.StopCPUProfile()
}
}
func trapSignals() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
cleanup()
os.Exit(1)
}()
}
// nolint: interfacer
func createAndTestGenesis(t *testing.T, cms sdk.CommitMultiStore, ak authkeeper.AccountKeeper, bk bankkeeper.Keeper, evmKeeper *evmkeeper.Keeper) {
genBlock := ethcore.DefaultGenesisBlock()
ms := cms.CacheMultiStore()
ctx := sdk.NewContext(ms, tmproto.Header{}, false, logger)
// Set the default Ethermint parameters to the parameter keeper store
evmKeeper.SetParams(ctx, evmtypes.DefaultParams())
// sort the addresses and insertion of key/value pairs matters
genAddrs := make([]string, len(genBlock.Alloc))
i := 0
for addr := range genBlock.Alloc {
genAddrs[i] = addr.String()
i++
}
sort.Strings(genAddrs)
for _, addrStr := range genAddrs {
addr := ethcmn.HexToAddress(addrStr)
acc := genBlock.Alloc[addr]
evmKeeper.AddBalance(ctx, addr, acc.Balance)
evmKeeper.SetCode(ctx, addr, acc.Code)
evmKeeper.SetNonce(ctx, addr, acc.Nonce)
for key, value := range acc.Storage {
evmKeeper.SetState(ctx, addr, key, value)
}
}
// get balance of one of the genesis account having 400 ETH
b := evmKeeper.GetBalance(ctx, genInvestor)
require.Equal(t, "200000000000000000000", b.String())
// commit the stateDB with 'false' to delete empty objects
//
// NOTE: Commit does not yet return the intra merkle root (version)
_, err := evmKeeper.Commit(ctx, false)
require.NoError(t, err)
// persist multi-store cache state
ms.Write()
// persist multi-store root state
cms.Commit()
// verify account mapper state
genAcc := ak.GetAccount(ctx, sdk.AccAddress(genInvestor.Bytes()))
require.NotNil(t, genAcc)
evmDenom := evmKeeper.GetParams(ctx).EvmDenom
balance := bk.GetBalance(ctx, genAcc.GetAddress(), evmDenom)
require.Equal(t, sdk.NewIntFromBigInt(b), balance.Amount)
}
func TestImportBlocks(t *testing.T) {
if flagDataDir == "" {
flagDataDir = os.TempDir()
}
if flagCPUProfile != "" {
f, err := os.Create(flagCPUProfile)
require.NoError(t, err, "failed to create CPU profile")
err = pprof.StartCPUProfile(f)
require.NoError(t, err, "failed to start CPU profile")
}
db, err := dbm.NewDB("state_test"+uuid.New().String(), dbm.GoLevelDBBackend, flagDataDir)
require.NoError(t, err)
defer cleanup()
trapSignals()
cdc, amino := newTestCodec()
cms := store.NewCommitMultiStore(db)
authStoreKey := sdk.NewKVStoreKey(authtypes.StoreKey)
bankStoreKey := sdk.NewKVStoreKey(banktypes.StoreKey)
evmStoreKey := sdk.NewKVStoreKey(evmtypes.StoreKey)
paramsStoreKey := sdk.NewKVStoreKey(paramtypes.StoreKey)
paramsTransientStoreKey := sdk.NewTransientStoreKey(paramtypes.TStoreKey)
// mount stores
keys := []*sdk.KVStoreKey{authStoreKey, bankStoreKey, evmStoreKey, paramsStoreKey}
for _, key := range keys {
cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, nil)
}
cms.MountStoreWithDB(paramsTransientStoreKey, sdk.StoreTypeTransient, nil)
paramsKeeper := paramkeeper.NewKeeper(cdc, amino, paramsStoreKey, paramsTransientStoreKey)
// Set specific subspaces
authSubspace := paramsKeeper.Subspace(authtypes.ModuleName)
bankSubspace := paramsKeeper.Subspace(banktypes.ModuleName)
evmSubspace := paramsKeeper.Subspace(evmtypes.ModuleName).WithKeyTable(evmtypes.ParamKeyTable())
// create keepers
ak := authkeeper.NewAccountKeeper(cdc, authStoreKey, authSubspace, types.ProtoAccount, nil)
bk := bankkeeper.NewBaseKeeper(cdc, bankStoreKey, ak, bankSubspace, nil)
evmKeeper := evmkeeper.NewKeeper(cdc, evmStoreKey, evmSubspace, ak, bk)
cms.SetPruning(sdkstore.PruneNothing)
// load latest version (root)
err = cms.LoadLatestVersion()
require.NoError(t, err)
// set and test genesis block
createAndTestGenesis(t, cms, ak, bk, evmKeeper)
// open blockchain export file
blockchainInput, err := os.Open(flagBlockchain)
require.Nil(t, err)
defer func() {
err := blockchainInput.Close()
require.NoError(t, err)
}()
// ethereum mainnet config
chainContext := core.NewChainContext()
vmConfig := ethvm.Config{}
chainConfig := ethparams.MainnetChainConfig
// create RLP stream for exported blocks
stream := ethrlp.NewStream(blockchainInput, 0)
startTime := time.Now()
var block ethtypes.Block
for {
err = stream.Decode(&block)
if err == io.EOF {
break
}
require.NoError(t, err, "failed to decode block")
var (
usedGas = new(uint64)
gp = new(ethcore.GasPool).AddGas(block.GasLimit())
)
header := block.Header()
chainContext.Coinbase = header.Coinbase
chainContext.SetHeader(block.NumberU64(), header)
// Create a cached-wrapped multi-store based on the commit multi-store and
// create a new context based off of that.
ms := cms.CacheMultiStore()
ctx := sdk.NewContext(ms, tmproto.Header{}, false, logger)
ctx = ctx.WithBlockHeight(int64(block.NumberU64()))
if chainConfig.DAOForkSupport && chainConfig.DAOForkBlock != nil && chainConfig.DAOForkBlock.Cmp(block.Number()) == 0 {
applyDAOHardFork(evmKeeper)
}
for i, tx := range block.Transactions() {
evmKeeper.Prepare(ctx, tx.Hash(), block.Hash(), i)
// evmKeeper.CommitStateDB.Set(block.Hash())
receipt, gas, err := applyTransaction(
chainConfig, chainContext, nil, gp, evmKeeper, header, tx, usedGas, vmConfig,
)
require.NoError(t, err, "failed to apply tx at block %d; tx: %X; gas %d; receipt:%v", block.NumberU64(), tx.Hash(), gas, receipt)
require.NotNil(t, receipt)
}
// apply mining rewards
accumulateRewards(chainConfig, evmKeeper, header, block.Uncles())
// commit stateDB
_, err := evmKeeper.CommitStateDB.Commit(chainConfig.IsEIP158(block.Number()))
require.NoError(t, err, "failed to commit StateDB")
// simulate BaseApp EndBlocker commitment
ms.Write()
cms.Commit()
// block debugging output
if block.NumberU64() > 0 && block.NumberU64()%1000 == 0 {
fmt.Printf("processed block: %d (time so far: %v)\n", block.NumberU64(), time.Since(startTime))
}
}
}
// accumulateRewards credits the coinbase of the given block with the mining
// reward. The total reward consists of the static block reward and rewards for
// included uncles. The coinbase of each uncle block is also rewarded.
func accumulateRewards(
config *ethparams.ChainConfig, evmKeeper *evmkeeper.Keeper,
header *ethtypes.Header, uncles []*ethtypes.Header,
) {
// select the correct block reward based on chain progression
blockReward := ethash.FrontierBlockReward
if config.IsByzantium(header.Number) {
blockReward = ethash.ByzantiumBlockReward
}
// accumulate the rewards for the miner and any included uncles
reward := new(big.Int).Set(blockReward)
r := new(big.Int)
for _, uncle := range uncles {
r.Add(uncle.Number, rewardBig8)
r.Sub(r, header.Number)
r.Mul(r, blockReward)
r.Div(r, rewardBig8)
evmKeeper.CommitStateDB.AddBalance(uncle.Coinbase, r)
r.Div(blockReward, rewardBig32)
reward.Add(reward, r)
}
evmKeeper.CommitStateDB.AddBalance(header.Coinbase, reward)
}
// ApplyDAOHardFork modifies the state database according to the DAO hard-fork
// rules, transferring all balances of a set of DAO accounts to a single refund
// contract.
// Code is pulled from go-ethereum 1.9 because the StateDB interface does not include the
// SetBalance function implementation
// Ref: https://github.com/ethereum/go-ethereum/blob/52f2461774bcb8cdd310f86b4bc501df5b783852/consensus/misc/dao.go#L74
func applyDAOHardFork(evmKeeper *evmkeeper.Keeper) {
// Retrieve the contract to refund balances into
if !evmKeeper.CommitStateDB.Exist(ethparams.DAORefundContract) {
evmKeeper.CommitStateDB.CreateAccount(ethparams.DAORefundContract)
}
// Move every DAO account and extra-balance account funds into the refund contract
for _, addr := range ethparams.DAODrainList() {
evmKeeper.CommitStateDB.AddBalance(ethparams.DAORefundContract, evmKeeper.CommitStateDB.GetBalance(addr))
evmKeeper.CommitStateDB.SetBalance(addr, new(big.Int))
}
}
// ApplyTransaction attempts to apply a transaction to the given state database
// and uses the input parameters for its environment. It returns the receipt
// for the transaction, gas used and an error if the transaction failed,
// indicating the block was invalid.
// Function is also pulled from go-ethereum 1.9 because of the incompatible usage
// Ref: https://github.com/ethereum/go-ethereum/blob/52f2461774bcb8cdd310f86b4bc501df5b783852/core/state_processor.go#L88
func applyTransaction(
config *ethparams.ChainConfig, bc ethcore.ChainContext, author *ethcmn.Address,
gp *ethcore.GasPool, evmKeeper *evmkeeper.Keeper, header *ethtypes.Header,
tx *ethtypes.Transaction, usedGas *uint64, cfg ethvm.Config,
) (*ethtypes.Receipt, uint64, error) {
msg, err := tx.AsMessage(ethtypes.MakeSigner(config, header.Number))
if err != nil {
return nil, 0, err
}
// Create a new context to be used in the EVM environment
blockCtx := ethcore.NewEVMBlockContext(header, bc, author)
txCtx := ethcore.NewEVMTxContext(msg)
// Create a new environment which holds all relevant information
// about the transaction and calling mechanisms.
vmenv := ethvm.NewEVM(blockCtx, txCtx, evmKeeper.CommitStateDB, config, cfg)
// Apply the transaction to the current state (included in the env)
execResult, err := ethcore.ApplyMessage(vmenv, msg, gp)
if err != nil {
// NOTE: ignore vm execution error (eg: tx out of gas at block 51169) as we care only about state transition errors
return &ethtypes.Receipt{}, 0, nil
}
// Update the state with pending changes
var intRoot ethcmn.Hash
if config.IsByzantium(header.Number) {
err = evmKeeper.CommitStateDB.Finalise(true)
} else {
intRoot, err = evmKeeper.CommitStateDB.IntermediateRoot(config.IsEIP158(header.Number))
}
if err != nil {
return nil, execResult.UsedGas, err
}
root := intRoot.Bytes()
*usedGas += execResult.UsedGas
// Create a new receipt for the transaction, storing the intermediate root and gas used by the tx
// based on the eip phase, we're passing whether the root touch-delete accounts.
receipt := ethtypes.NewReceipt(root, execResult.Failed(), *usedGas)
receipt.TxHash = tx.Hash()
receipt.GasUsed = execResult.UsedGas
// if the transaction created a contract, store the creation address in the receipt.
if msg.To() == nil {
receipt.ContractAddress = ethcrypto.CreateAddress(vmenv.TxContext.Origin, tx.Nonce())
}
// Set the receipt logs and create a bloom for filtering
receipt.Logs, err = evmKeeper.CommitStateDB.GetLogs(tx.Hash())
receipt.Bloom = ethtypes.CreateBloom(ethtypes.Receipts{receipt})
receipt.BlockHash = evmKeeper.CommitStateDB.BlockHash()
receipt.BlockNumber = header.Number
receipt.TransactionIndex = uint(evmKeeper.CommitStateDB.TxIndex())
return receipt, execResult.UsedGas, err
}
@@ -1,4 +1,4 @@
package tests
package rpc
import (
"encoding/json"
@@ -3,13 +3,12 @@
// To run these tests please first ensure you have the ethermintd running
//
// You can configure the desired HOST and MODE as well in integration-test-all.sh
package pending
package rpc
import (
"encoding/json"
"fmt"
"math/big"
"os"
"testing"
"github.com/ethereum/go-ethereum/common"
@@ -17,45 +16,34 @@ import (
"github.com/stretchr/testify/require"
rpctypes "github.com/cosmos/ethermint/ethereum/rpc/types"
util "github.com/cosmos/ethermint/tests"
)
const (
addrA = "0xc94770007dda54cF92009BFF0dE90c06F603a09f"
addrAStoreKey = 0
)
// func TestMain(m *testing.M) {
// if MODE != "pending" {
// _, _ = fmt.Fprintln(os.Stdout, "Skipping pending RPC test")
// return
// }
var (
MODE = os.Getenv("MODE")
from = []byte{}
)
// var err error
// from, err = GetAddress()
// if err != nil {
// fmt.Printf("failed to get account: %s\n", err)
// os.Exit(1)
// }
func TestMain(m *testing.M) {
if MODE != "pending" {
_, _ = fmt.Fprintln(os.Stdout, "Skipping pending RPC test")
return
}
var err error
from, err = util.GetAddress()
if err != nil {
fmt.Printf("failed to get account: %s\n", err)
os.Exit(1)
}
// Start all tests
code := m.Run()
os.Exit(code)
}
// // Start all tests
// code := m.Run()
// os.Exit(code)
// }
func TestEth_Pending_GetBalance(t *testing.T) {
var res hexutil.Big
rpcRes := util.Call(t, "eth_getBalance", []string{addrA, "latest"})
rpcRes := Call(t, "eth_getBalance", []string{addrA, "latest"})
err := res.UnmarshalJSON(rpcRes.Result)
require.NoError(t, err)
preTxLatestBalance := res.ToInt()
rpcRes = util.Call(t, "eth_getBalance", []string{addrA, "pending"})
rpcRes = Call(t, "eth_getBalance", []string{addrA, "pending"})
err = res.UnmarshalJSON(rpcRes.Result)
require.NoError(t, err)
preTxPendingBalance := res.ToInt()
@@ -71,13 +59,13 @@ func TestEth_Pending_GetBalance(t *testing.T) {
param[0]["gasLimit"] = "0x5208"
param[0]["gasPrice"] = "0x1"
txRes := util.Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
txRes := Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
require.Nil(t, txRes.Error)
rpcRes = util.Call(t, "eth_sendTransaction", param)
rpcRes = Call(t, "eth_sendTransaction", param)
require.Nil(t, rpcRes.Error)
rpcRes = util.Call(t, "eth_getBalance", []string{addrA, "pending"})
rpcRes = Call(t, "eth_getBalance", []string{addrA, "pending"})
err = res.UnmarshalJSON(rpcRes.Result)
require.NoError(t, err)
postTxPendingBalance := res.ToInt()
@@ -85,7 +73,7 @@ func TestEth_Pending_GetBalance(t *testing.T) {
require.Equal(t, preTxPendingBalance.Add(preTxPendingBalance, big.NewInt(10)), postTxPendingBalance)
rpcRes = util.Call(t, "eth_getBalance", []string{addrA, "latest"})
rpcRes = Call(t, "eth_getBalance", []string{addrA, "latest"})
err = res.UnmarshalJSON(rpcRes.Result)
require.NoError(t, err)
postTxLatestBalance := res.ToInt()
@@ -95,10 +83,10 @@ func TestEth_Pending_GetBalance(t *testing.T) {
}
func TestEth_Pending_GetTransactionCount(t *testing.T) {
prePendingNonce := util.GetNonce(t, "pending")
prePendingNonce := GetNonce(t, "pending")
t.Logf("Pending nonce before tx is %d", prePendingNonce)
currentNonce := util.GetNonce(t, "latest")
currentNonce := GetNonce(t, "latest")
t.Logf("Current nonce is %d", currentNonce)
require.Equal(t, prePendingNonce, currentNonce)
@@ -110,13 +98,13 @@ func TestEth_Pending_GetTransactionCount(t *testing.T) {
param[0]["gasLimit"] = "0x5208"
param[0]["gasPrice"] = "0x1"
txRes := util.Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
txRes := Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
require.Nil(t, txRes.Error)
txRes = util.Call(t, "eth_sendTransaction", param)
txRes = Call(t, "eth_sendTransaction", param)
require.Nil(t, txRes.Error)
pendingNonce := util.GetNonce(t, "pending")
latestNonce := util.GetNonce(t, "latest")
pendingNonce := GetNonce(t, "pending")
latestNonce := GetNonce(t, "latest")
t.Logf("Latest nonce is %d", latestNonce)
require.Equal(t, currentNonce+1, latestNonce)
@@ -128,13 +116,13 @@ func TestEth_Pending_GetTransactionCount(t *testing.T) {
}
func TestEth_Pending_GetBlockTransactionCountByNumber(t *testing.T) {
rpcRes := util.Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"pending"})
rpcRes := Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"pending"})
var preTxPendingTxCount hexutil.Uint
err := json.Unmarshal(rpcRes.Result, &preTxPendingTxCount)
require.NoError(t, err)
t.Logf("Pre tx pending nonce is %d", preTxPendingTxCount)
rpcRes = util.Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"latest"})
rpcRes = Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"latest"})
var preTxLatestTxCount hexutil.Uint
err = json.Unmarshal(rpcRes.Result, &preTxLatestTxCount)
require.NoError(t, err)
@@ -150,19 +138,19 @@ func TestEth_Pending_GetBlockTransactionCountByNumber(t *testing.T) {
param[0]["gasLimit"] = "0x5208"
param[0]["gasPrice"] = "0x1"
txRes := util.Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
txRes := Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
require.Nil(t, txRes.Error)
txRes = util.Call(t, "eth_sendTransaction", param)
txRes = Call(t, "eth_sendTransaction", param)
require.Nil(t, txRes.Error)
rpcRes = util.Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"pending"})
rpcRes = Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"pending"})
var postTxPendingTxCount hexutil.Uint
err = json.Unmarshal(rpcRes.Result, &postTxPendingTxCount)
require.NoError(t, err)
t.Logf("Post tx pending nonce is %d", postTxPendingTxCount)
rpcRes = util.Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"latest"})
rpcRes = Call(t, "eth_getBlockTransactionCountByNumber", []interface{}{"latest"})
var postTxLatestTxCount hexutil.Uint
err = json.Unmarshal(rpcRes.Result, &postTxLatestTxCount)
require.NoError(t, err)
@@ -175,13 +163,13 @@ func TestEth_Pending_GetBlockTransactionCountByNumber(t *testing.T) {
}
func TestEth_Pending_GetBlockByNumber(t *testing.T) {
rpcRes := util.Call(t, "eth_getBlockByNumber", []interface{}{"latest", true})
rpcRes := Call(t, "eth_getBlockByNumber", []interface{}{"latest", true})
var preTxLatestBlock map[string]interface{}
err := json.Unmarshal(rpcRes.Result, &preTxLatestBlock)
require.NoError(t, err)
preTxLatestTxs := len(preTxLatestBlock["transactions"].([]interface{}))
rpcRes = util.Call(t, "eth_getBlockByNumber", []interface{}{"pending", true})
rpcRes = Call(t, "eth_getBlockByNumber", []interface{}{"pending", true})
var preTxPendingBlock map[string]interface{}
err = json.Unmarshal(rpcRes.Result, &preTxPendingBlock)
require.NoError(t, err)
@@ -195,19 +183,19 @@ func TestEth_Pending_GetBlockByNumber(t *testing.T) {
param[0]["gasLimit"] = "0x5208"
param[0]["gasPrice"] = "0x1"
txRes := util.Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
txRes := Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
require.Nil(t, txRes.Error)
txRes = util.Call(t, "eth_sendTransaction", param)
txRes = Call(t, "eth_sendTransaction", param)
require.Nil(t, txRes.Error)
rpcRes = util.Call(t, "eth_getBlockByNumber", []interface{}{"pending", true})
rpcRes = Call(t, "eth_getBlockByNumber", []interface{}{"pending", true})
var postTxPendingBlock map[string]interface{}
err = json.Unmarshal(rpcRes.Result, &postTxPendingBlock)
require.NoError(t, err)
postTxPendingTxs := len(postTxPendingBlock["transactions"].([]interface{}))
require.Equal(t, postTxPendingTxs, preTxPendingTxs)
rpcRes = util.Call(t, "eth_getBlockByNumber", []interface{}{"latest", true})
rpcRes = Call(t, "eth_getBlockByNumber", []interface{}{"latest", true})
var postTxLatestBlock map[string]interface{}
err = json.Unmarshal(rpcRes.Result, &postTxLatestBlock)
require.NoError(t, err)
@@ -219,7 +207,7 @@ func TestEth_Pending_GetBlockByNumber(t *testing.T) {
func TestEth_Pending_GetTransactionByBlockNumberAndIndex(t *testing.T) {
var pendingTx []*rpctypes.RPCTransaction
resPendingTxs := util.Call(t, "eth_pendingTransactions", []string{})
resPendingTxs := Call(t, "eth_pendingTransactions", []string{})
err := json.Unmarshal(resPendingTxs.Result, &pendingTx)
require.NoError(t, err)
pendingTxCount := len(pendingTx)
@@ -234,9 +222,9 @@ func TestEth_Pending_GetTransactionByBlockNumberAndIndex(t *testing.T) {
param[0]["gasPrice"] = "0x1"
param[0]["data"] = data
txRes := util.Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
txRes := Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
require.Nil(t, txRes.Error)
txRes = util.Call(t, "eth_sendTransaction", param)
txRes = Call(t, "eth_sendTransaction", param)
require.Nil(t, txRes.Error)
// test will be blocked here until tx gets confirmed
@@ -244,7 +232,7 @@ func TestEth_Pending_GetTransactionByBlockNumberAndIndex(t *testing.T) {
err = json.Unmarshal(txRes.Result, &txHash)
require.NoError(t, err)
rpcRes := util.Call(t, "eth_getTransactionByBlockNumberAndIndex", []interface{}{"latest", "0x" + fmt.Sprintf("%X", pendingTxCount)})
rpcRes := Call(t, "eth_getTransactionByBlockNumberAndIndex", []interface{}{"latest", "0x" + fmt.Sprintf("%X", pendingTxCount)})
var latestBlockTx map[string]interface{}
err = json.Unmarshal(rpcRes.Result, &latestBlockTx)
require.NoError(t, err)
@@ -254,7 +242,7 @@ func TestEth_Pending_GetTransactionByBlockNumberAndIndex(t *testing.T) {
require.Equal(t, latestBlockTx["value"], "0xa")
require.Equal(t, data, latestBlockTx["input"])
rpcRes = util.Call(t, "eth_getTransactionByBlockNumberAndIndex", []interface{}{"pending", "0x" + fmt.Sprintf("%X", pendingTxCount)})
rpcRes = Call(t, "eth_getTransactionByBlockNumberAndIndex", []interface{}{"pending", "0x" + fmt.Sprintf("%X", pendingTxCount)})
var pendingBlock map[string]interface{}
err = json.Unmarshal(rpcRes.Result, &pendingBlock)
require.NoError(t, err)
@@ -274,15 +262,15 @@ func TestEth_Pending_GetTransactionByHash(t *testing.T) {
param[0]["gasPrice"] = "0x1"
param[0]["data"] = data
txRes := util.Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
txRes := Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
require.Nil(t, txRes.Error)
txRes = util.Call(t, "eth_sendTransaction", param)
txRes = Call(t, "eth_sendTransaction", param)
var txHash common.Hash
err := txHash.UnmarshalJSON(txRes.Result)
require.NoError(t, err)
rpcRes := util.Call(t, "eth_getTransactionByHash", []interface{}{txHash})
rpcRes := Call(t, "eth_getTransactionByHash", []interface{}{txHash})
var pendingBlockTx map[string]interface{}
err = json.Unmarshal(rpcRes.Result, &pendingBlockTx)
require.NoError(t, err)
@@ -295,7 +283,7 @@ func TestEth_Pending_GetTransactionByHash(t *testing.T) {
}
func TestEth_Pending_SendTransaction_PendingNonce(t *testing.T) {
currNonce := util.GetNonce(t, "latest")
currNonce := GetNonce(t, "latest")
param := make([]map[string]string, 1)
param[0] = make(map[string]string)
param[0]["from"] = "0x" + fmt.Sprintf("%x", from)
@@ -304,28 +292,28 @@ func TestEth_Pending_SendTransaction_PendingNonce(t *testing.T) {
param[0]["gasLimit"] = "0x5208"
param[0]["gasPrice"] = "0x1"
txRes := util.Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
txRes := Call(t, "personal_unlockAccount", []interface{}{param[0]["from"], ""})
require.Nil(t, txRes.Error)
// first transaction
txRes1 := util.Call(t, "eth_sendTransaction", param)
txRes1 := Call(t, "eth_sendTransaction", param)
require.Nil(t, txRes1.Error)
pendingNonce1 := util.GetNonce(t, "pending")
pendingNonce1 := GetNonce(t, "pending")
require.Greater(t, uint64(pendingNonce1), uint64(currNonce))
// second transaction
param[0]["to"] = "0x7f0f463c4d57b1bd3e3b79051e6c5ab703e803d9"
txRes2 := util.Call(t, "eth_sendTransaction", param)
txRes2 := Call(t, "eth_sendTransaction", param)
require.Nil(t, txRes2.Error)
pendingNonce2 := util.GetNonce(t, "pending")
pendingNonce2 := GetNonce(t, "pending")
require.Greater(t, uint64(pendingNonce2), uint64(currNonce))
require.Greater(t, uint64(pendingNonce2), uint64(pendingNonce1))
// third transaction
param[0]["to"] = "0x7fb24493808b3f10527e3e0870afeb8a953052d2"
txRes3 := util.Call(t, "eth_sendTransaction", param)
txRes3 := Call(t, "eth_sendTransaction", param)
require.Nil(t, txRes3.Error)
pendingNonce3 := util.GetNonce(t, "pending")
pendingNonce3 := GetNonce(t, "pending")
require.Greater(t, uint64(pendingNonce3), uint64(currNonce))
require.Greater(t, uint64(pendingNonce3), uint64(pendingNonce2))
}
+1 -1
View File
@@ -4,7 +4,7 @@
// and have started the RPC service with `injectived rest-server`.
//
// You can configure the desired HOST and MODE as well
package tests
package rpc
import (
"bytes"
+1 -1
View File
@@ -1,4 +1,4 @@
package tests
package rpc
import (
"bytes"
+1
View File
@@ -0,0 +1 @@
*.sol linguist-language=Solidity
+5
View File
@@ -0,0 +1,5 @@
# dependencies
node_modules/
# ignore package-lock files (only use yarn.lock)
package-lock.json
+102
View File
@@ -0,0 +1,102 @@
# Solidity tests
Increasingly difficult tests are provided:
- [Basic](./suites/basic): simple Counter example, for basic calls, transactions, and events
- [Initialize](./suites/initialize): initialization contract and tests from [aragonOS](https://github.com/aragon/aragonOS)
- [Initialize (Buidler)](./suites/initialize-buidler): initialization contract and tests from [aragonOS](https://github.com/aragon/aragonOS), using [buidler](https://buidler.dev/)
- [Proxy](./suites/proxy): depositable delegate proxy contract and tests from [aragonOS](https://github.com/aragon/aragonOS)
- [Staking](./suites/staking): Staking contracts and full test suite from [aragon/staking](http://github.com/aragon/staking)
### Quick start
**Prerequisite**: in the repo's root, run `make install` to install the `ethermintd` and `ethermintd` binaries. When done, come back to this directory.
**Prerequisite**: install the individual solidity packages. They're set up as individual reops in a yarn monorepo workspace. Install them all via `yarn install`.
To run the tests, start three terminals (or two, if you run `ethermintd` with `&`).
In the first, run `ethermintd`:
```sh
./init-test-node.sh
```
In the second, run `ethermintd` as mentioned in the script's output:
```sh
ethermintd rest-server --laddr "tcp://localhost:8545" --unlock-key localkey,user1,user2 --chain-id "ethermint-1337" --trace --wsport 8546
```
You will now have three ethereum accounts unlocked in the test node:
- `0x3b7252d007059ffc82d16d022da3cbf9992d2f70` (Validator)
- `0xddd64b4712f7c8f1ace3c145c950339eddaf221d` (User 1)
- `0x0f54f47bf9b8e317b214ccd6a7c3e38b893cd7f0` (user 2)
From here, in your other available terminal, go into any of the tests and run `yarn test-ethermint`. You should see `ethermintd` accepting transactions and producing blocks. You should be able to query for any transaction via:
- `ethermintd query tx <cosmos-sdk tx>`
- `curl localhost:8545 -H "Content-Type:application/json" -X POST --data '{"jsonrpc":"2.0","method":"eth_getTransactionByHash","params":["<ethereum tx>"],"id":1}'`
And obviously more, via the Ethereum JSON-RPC API).
When in doubt, you can also run the tests against a Ganache instance via `yarn test-ganache`, to make sure they are behaving correctly.
### Test node
The [`init-test-node.sh`](./init-test-node.sh) script sets up ethermint with the following accounts:
- `eth18de995q8qk0leqk3d5pzmg7tlxvj6tmsku084d` (Validator)
- `0x3b7252d007059ffc82d16d022da3cbf9992d2f70`
- `eth1mhtyk3cj7ly0rt8rc9zuj5pnnmw67gsapygwyq` (User 1)
- `0xddd64b4712f7c8f1ace3c145c950339eddaf221d`
- `eth1pa20g7lehr330vs5ent20slr3wyne4lsy8qae3` (user 2)
- `0x0f54f47bf9b8e317b214ccd6a7c3e38b893cd7f0`
Each with roughly 100 ETH available (1e18 photon).
Running `ethermintd list keys` should output:
```json
[
{
"name": "localkey",
"type": "local",
"address": "eth18de995q8qk0leqk3d5pzmg7tlxvj6tmsku084d",
"pubkey": "ethpub1pfqnmk6pq3ycjs34vv4n6rkty89f6m02qcsal3ecdzn7a3uunx0e5ly0846pzg903hxf2zp5gq4grh8jcatcemfrscdfl797zhg5crkcsx43gujzppge3n"
},
{
"name": "user1",
"type": "local",
"address": "eth1mhtyk3cj7ly0rt8rc9zuj5pnnmw67gsapygwyq",
"pubkey": "ethpub1pfqnmk6pq3wrkx6lh7uug8ss0thggact3n49m5gkmpca4vylldpur5qrept57e0rrxfmeq5mp5xt3cyf4kys53qcv66qxttv970das69hlpkf8cnyd2a2x"
},
{
"name": "user2",
"type": "local",
"address": "eth1pa20g7lehr330vs5ent20slr3wyne4lsy8qae3",
"pubkey": "ethpub1pfqnmk6pq3art9y45zw5ntyktt2qrt0skmsl0ux9qwk8458ed3d8sgnrs99zlgvj3rt2vggvkh0x56hffugwsyddwqla48npx46pglgs6xhcqpall58tgn"
}
]
```
And running:
```sh
curl localhost:8545 -H "Content-Type:application/json" -X POST --data '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}'
```
Should output:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": [
"0x3b7252d007059ffc82d16d022da3cbf9992d2f70",
"0xddd64b4712f7c8f1ace3c145c950339eddaf221d",
"0x0f54f47bf9b8e317b214ccd6a7c3e38b893cd7f0"
]
}
```
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
CHAINID="ethermint-1337"
MONIKER="localtestnet"
# localKey address 0x7cb61d4117ae31a12e393a1cfa3bac666481d02e
VAL_KEY="localkey"
VAL_MNEMONIC="gesture inject test cycle original hollow east ridge hen combine junk child bacon zero hope comfort vacuum milk pitch cage oppose unhappy lunar seat"
# user1 address 0xc6fe5d33615a1c52c08018c47e8bc53646a0e101
USER1_KEY="user1"
USER1_MNEMONIC="copper push brief egg scan entry inform record adjust fossil boss egg comic alien upon aspect dry avoid interest fury window hint race symptom"
# user2 address 0x963ebdf2e1f8db8707d05fc75bfeffba1b5bac17
USER2_KEY="user2"
USER2_MNEMONIC="maximum display century economy unlock van census kite error heart snow filter midnight usage egg venture cash kick motor survey drastic edge muffin visual"
# remove existing daemon and client
rm -rf ~/.ethermint*
# Import keys from mnemonics
echo $VAL_MNEMONIC | ethermintd keys add $VAL_KEY --recover --keyring-backend test --algo "eth_secp256k1"
echo $USER1_MNEMONIC | ethermintd keys add $USER1_KEY --recover --keyring-backend test --algo "eth_secp256k1"
echo $USER2_MNEMONIC | ethermintd keys add $USER2_KEY --recover --keyring-backend test --algo "eth_secp256k1"
ethermintd init $MONIKER --chain-id $CHAINID
# Allocate genesis accounts (cosmos formatted addresses)
ethermintd add-genesis-account "$(ethermintd keys show $VAL_KEY -a --keyring-backend test)" 1000000000000000000000aphoton,1000000000000000000stake --keyring-backend test
ethermintd add-genesis-account "$(ethermintd keys show $USER1_KEY -a --keyring-backend test)" 1000000000000000000000aphoton,1000000000000000000stake --keyring-backend test
ethermintd add-genesis-account "$(ethermintd keys show $USER2_KEY -a --keyring-backend test)" 1000000000000000000000aphoton,1000000000000000000stake --keyring-backend test
# Sign genesis transaction
ethermintd gentx $VAL_KEY 1000000000000000000stake --amount=1000000000000000000000aphoton --chain-id $CHAINID --keyring-backend test
# Collect genesis tx
ethermintd collect-gentxs
# Run this to ensure everything worked and that the genesis file is setup correctly
ethermintd validate-genesis
# Start the node (remove the --pruning=nothing flag if historical queries are not needed)
ethermintd start --pruning=nothing --rpc.unsafe --rpc-api "web3, eth, personal, net" --keyring-backend test --trace --log_level "info"
+15
View File
@@ -0,0 +1,15 @@
{
"name": "tests-solidity",
"private": true,
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"workspaces": {
"packages": [
"suites/*"
],
"nohoist": [
"**/@aragon/contract-helpers-test"
]
}
}
@@ -0,0 +1,24 @@
pragma solidity ^0.5.11;
contract Counter {
uint256 counter = 0;
string internal constant ERROR_TOO_LOW = "COUNTER_TOO_LOW";
event Changed(uint256 counter);
event Added(uint256 counter);
function add() public {
counter++;
emit Added(counter);
emit Changed(counter);
}
function subtract() public {
require(counter > 0, ERROR_TOO_LOW);
counter--;
emit Changed(counter);
}
function getCounter() public view returns (uint256) {
return counter;
}
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}
+158
View File
@@ -0,0 +1,158 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
)
var (
HOST = os.Getenv("HOST")
HOME = os.Getenv("PWD")
)
type Request struct {
Version string `json:"jsonrpc"`
Method string `json:"method"`
Params interface{} `json:"params"`
ID int `json:"id"`
}
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
type Response struct {
Error *RPCError `json:"error"`
ID int `json:"id"`
Result json.RawMessage `json:"result,omitempty"`
}
func createRequest(method string, params interface{}) Request {
return Request{
Version: "2.0",
Method: method,
Params: params,
ID: 1,
}
}
func getTransactionReceipt(hash hexutil.Bytes) (map[string]interface{}, error) {
param := []string{hash.String()}
rpcRes, err := call("eth_getTransactionReceipt", param)
if err != nil {
return nil, err
}
receipt := make(map[string]interface{})
err = json.Unmarshal(rpcRes.Result, &receipt)
if err != nil {
return nil, err
}
return receipt, nil
}
func waitForReceipt(hash hexutil.Bytes) (map[string]interface{}, error) {
for i := 0; i < 10; i++ {
receipt, err := getTransactionReceipt(hash)
if receipt != nil {
return receipt, err
} else if err != nil {
return nil, err
}
time.Sleep(time.Second)
}
return nil, errors.New("cound not find transaction on chain")
}
func call(method string, params interface{}) (*Response, error) {
var rpcRes *Response
if HOST == "" {
HOST = "http://localhost:8545"
}
req, err := json.Marshal(createRequest(method, params))
if err != nil {
return nil, err
}
time.Sleep(1000000 * time.Nanosecond)
/* #nosec */
res, err := http.Post(HOST, "application/json", bytes.NewBuffer(req))
if err != nil {
return nil, err
}
decoder := json.NewDecoder(res.Body)
rpcRes = new(Response)
if err := decoder.Decode(&rpcRes); err != nil {
return nil, err
}
if err := res.Body.Close(); err != nil {
return nil, err
}
return rpcRes, nil
}
func main() {
var hash hexutil.Bytes
dat, err := ioutil.ReadFile(HOME + "/counter/counter_sol.bin")
if err != nil {
log.Fatal(err)
}
param := make([]map[string]string, 1)
param[0] = make(map[string]string)
param[0]["from"] = os.Args[1]
param[0]["data"] = "0x" + string(dat)
txRPCRes, err := call("eth_sendTransaction", param)
if err != nil {
log.Fatal(err)
}
if err := json.Unmarshal(txRPCRes.Result, &hash); err != nil {
log.Fatal(err)
}
fmt.Println("Contract TX hash: ", hash)
receipt, err := waitForReceipt(hash)
if err != nil {
log.Fatal(err)
}
/*
//test for bad hash
testhash, err := hexutil.Decode("0xe146d95c74a48e730bf825c2a3dcbce8122b8a463bc15bcbb38b9c195402f0a5")
if err != nil {
log.Fatal(err)
}
receipt, err := waitForReceipt(testhash)
if err != nil {
log.Fatal(err)
}
*/
fmt.Println("receipt: ", receipt)
}
@@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
+17
View File
@@ -0,0 +1,17 @@
{
"name": "basic",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"truffle": "^5.1.42",
"web3": "^1.2.11"
},
"dependencies": {
"truffle": "^5.1.43"
}
}
@@ -0,0 +1,80 @@
const Counter = artifacts.require("Counter")
contract('Counter', ([one, two, three]) => {
console.log(one, two, three)
let counter
beforeEach(async() => {
counter = await Counter.new()
console.log('Counter:', counter.address)
console.log('Current eth:')
console.log(' - ', await web3.eth.getBalance(one))
console.log(' - ', await web3.eth.getBalance(two))
console.log(' - ', await web3.eth.getBalance(three))
console.log('')
})
it('should add', async() => {
const balanceOne = await web3.eth.getBalance(one)
const balanceTwo = await web3.eth.getBalance(two)
const balanceThree = await web3.eth.getBalance(three)
let count
await counter.add({ from: one })
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '1', 'Counter should be 1')
assert.notEqual(balanceOne, await web3.eth.getBalance(one), `${one}'s balance should be different`)
await counter.add({ from: two })
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '2', 'Counter should be 2')
assert.notEqual(balanceTwo, await web3.eth.getBalance(two), `${two}'s balance should be different`)
await counter.add({ from: three })
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '3', 'Counter should be 3')
assert.notEqual(balanceThree, await web3.eth.getBalance(three), `${three}'s balance should be different`)
})
it('should subtract', async() => {
let count
await counter.add()
count = await counter.getCounter()
console.log(count.toString())
assert.equal(count, '1', 'Counter should be 1')
// Use receipt to ensure logs are emitted
const receipt = await counter.subtract()
count = await counter.getCounter()
console.log(count.toString())
console.log()
console.log('Subtract tx receipt:', receipt)
assert.equal(count, '0', 'Counter should be 0')
assert.equal(receipt.logs[0].event, 'Changed', "Should have emitted 'Changed' event")
assert.equal(receipt.logs[0].args.counter, '0', "Should have emitted 'Changed' event with counter being 0")
// Check lifecycle of events
const contract = new web3.eth.Contract(counter.abi, counter.address)
const allEvents = await contract.getPastEvents("allEvents", { fromBlock: 0, toBlock: 'latest' })
const changedEvents = await contract.getPastEvents("Changed", { fromBlock: 0, toBlock: 'latest' })
console.log('allEvents', allEvents)
console.log('changedEvents', changedEvents)
assert.equal(allEvents.length, 3)
assert.equal(changedEvents.length, 2)
try {
await counter.subtract()
assert.fail('Subtracting past 0 should have reverted')
} catch (err) {
console.log()
console.log('Passed -- was expecting error')
console.log('Error:', err)
}
})
})
@@ -0,0 +1,17 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.5.17",
},
},
}
@@ -0,0 +1,3 @@
# Buidler
artifacts
cache
@@ -0,0 +1,28 @@
const { usePlugin } = require('@nomiclabs/buidler/config')
usePlugin("@nomiclabs/buidler-ganache")
usePlugin('@nomiclabs/buidler-truffle5')
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ganache: {
url: 'http://localhost:8545',
gasLimit: 5000000,
gasPrice: 1000000000, // 1 gwei (in wei)
defaultBalanceEther: 100
},
ethermint: {
url: 'http://localhost:8545',
gasLimit: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
solc: {
version: '0.4.24',
optimizer: {
enabled: true,
runs: 10000,
},
},
}
@@ -0,0 +1,59 @@
/*
* SPDX-License-Identifier: MIT
*/
pragma solidity ^0.4.24;
import "./TimeHelpers.sol";
import "./UnstructuredStorage.sol";
contract Initializable is TimeHelpers {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.initializable.initializationBlock")
bytes32 internal constant INITIALIZATION_BLOCK_POSITION = 0xebb05b386a8d34882b8711d156f463690983dc47815980fb82aeeff1aa43579e;
string private constant ERROR_ALREADY_INITIALIZED = "INIT_ALREADY_INITIALIZED";
string private constant ERROR_NOT_INITIALIZED = "INIT_NOT_INITIALIZED";
modifier onlyInit {
require(getInitializationBlock() == 0, ERROR_ALREADY_INITIALIZED);
_;
}
modifier isInitialized {
require(hasInitialized(), ERROR_NOT_INITIALIZED);
_;
}
/**
* @return Block number in which the contract was initialized
*/
function getInitializationBlock() public view returns (uint256) {
return INITIALIZATION_BLOCK_POSITION.getStorageUint256();
}
/**
* @return Whether the contract has been initialized by the time of the current block
*/
function hasInitialized() public view returns (bool) {
uint256 initializationBlock = getInitializationBlock();
return initializationBlock != 0 && getBlockNumber() >= initializationBlock;
}
/**
* @dev Function to be called by top level contract after initialization has finished.
*/
function initialized() internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(getBlockNumber());
}
/**
* @dev Function to be called by top level contract after initialization to enable the contract
* at a future block number rather than immediately.
*/
function initializedAt(uint256 _blockNumber) internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(_blockNumber);
}
}
@@ -0,0 +1,21 @@
pragma solidity ^0.4.24;
import "./Initializable.sol";
contract Petrifiable is Initializable {
// Use block UINT256_MAX (which should be never) as the initializable date
uint256 internal constant PETRIFIED_BLOCK = uint256(-1);
function isPetrified() public view returns (bool) {
return getInitializationBlock() == PETRIFIED_BLOCK;
}
/**
* @dev Function to be called by top level contract to prevent being initialized.
* Useful for freezing base contracts when they're used behind proxies.
*/
function petrify() internal onlyInit {
initializedAt(PETRIFIED_BLOCK);
}
}
@@ -0,0 +1,44 @@
pragma solidity ^0.4.24;
import "./Uint256Helpers.sol";
contract TimeHelpers {
using Uint256Helpers for uint256;
/**
* @dev Returns the current block number.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber() internal view returns (uint256) {
return block.number;
}
/**
* @dev Returns the current block number, converted to uint64.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber64() internal view returns (uint64) {
return getBlockNumber().toUint64();
}
/**
* @dev Returns the current timestamp.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp() internal view returns (uint256) {
return block.timestamp; // solium-disable-line security/no-block-members
}
/**
* @dev Returns the current timestamp, converted to uint64.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp64() internal view returns (uint64) {
return getTimestamp().toUint64();
}
}
@@ -0,0 +1,13 @@
pragma solidity ^0.4.24;
library Uint256Helpers {
uint256 private constant MAX_UINT64 = uint64(-1);
string private constant ERROR_NUMBER_TOO_BIG = "UINT64_NUMBER_TOO_BIG";
function toUint64(uint256 a) internal pure returns (uint64) {
require(a <= MAX_UINT64, ERROR_NUMBER_TOO_BIG);
return uint64(a);
}
}
@@ -0,0 +1,36 @@
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}
@@ -0,0 +1,15 @@
pragma solidity 0.4.24;
import "../Initializable.sol";
import "../Petrifiable.sol";
contract LifecycleMock is Initializable, Petrifiable {
function initializeMock() public {
initialized();
}
function petrifyMock() public {
petrify();
}
}
@@ -0,0 +1,19 @@
{
"name": "initializable-buidler",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn buidler test --network ganache",
"test-ethermint": "yarn buidler test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.1.0",
"@nomiclabs/buidler": "^1.4.3",
"@nomiclabs/buidler-ganache": "^1.3.3",
"@nomiclabs/buidler-truffle5": "^1.3.4",
"@nomiclabs/buidler-web3": "^1.3.4",
"chai": "^4.2.0",
"web3": "^1.2.11"
}
}
@@ -0,0 +1,74 @@
const { assertRevert } = require('@aragon/contract-helpers-test/src/asserts')
// Mocks
const LifecycleMock = artifacts.require('LifecycleMock')
const ERRORS = {
INIT_ALREADY_INITIALIZED: 'INIT_ALREADY_INITIALIZED',
}
contract('Lifecycle', () => {
let lifecycle
beforeEach(async () => {
lifecycle = await LifecycleMock.new()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
context('> Initialized', () => {
beforeEach(async () => {
await lifecycle.initializeMock()
})
it('is initialized', async () => {
assert.isTrue(await lifecycle.hasInitialized(), 'should be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
it('has correct initialization block', async () => {
assert.equal(await lifecycle.getInitializationBlock(), await web3.eth.getBlockNumber(), 'initialization block should be correct')
})
it('cannot be re-initialized', async () => {
await assertRevert(lifecycle.initializeMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('cannot be petrified', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
})
context('> Petrified', () => {
beforeEach(async () => {
await lifecycle.petrifyMock()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is petrified', async () => {
assert.isTrue(await lifecycle.isPetrified(), 'should be petrified')
})
it('cannot be petrified again', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('has initialization block in the future', async () => {
const petrifiedBlock = await lifecycle.getInitializationBlock()
const blockNumber = await web3.eth.getBlockNumber()
assert.isTrue(petrifiedBlock.gt(blockNumber), 'petrified block should be in the future')
})
})
})
@@ -0,0 +1,59 @@
/*
* SPDX-License-Identifier: MIT
*/
pragma solidity ^0.4.24;
import "./TimeHelpers.sol";
import "./UnstructuredStorage.sol";
contract Initializable is TimeHelpers {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.initializable.initializationBlock")
bytes32 internal constant INITIALIZATION_BLOCK_POSITION = 0xebb05b386a8d34882b8711d156f463690983dc47815980fb82aeeff1aa43579e;
string private constant ERROR_ALREADY_INITIALIZED = "INIT_ALREADY_INITIALIZED";
string private constant ERROR_NOT_INITIALIZED = "INIT_NOT_INITIALIZED";
modifier onlyInit {
require(getInitializationBlock() == 0, ERROR_ALREADY_INITIALIZED);
_;
}
modifier isInitialized {
require(hasInitialized(), ERROR_NOT_INITIALIZED);
_;
}
/**
* @return Block number in which the contract was initialized
*/
function getInitializationBlock() public view returns (uint256) {
return INITIALIZATION_BLOCK_POSITION.getStorageUint256();
}
/**
* @return Whether the contract has been initialized by the time of the current block
*/
function hasInitialized() public view returns (bool) {
uint256 initializationBlock = getInitializationBlock();
return initializationBlock != 0 && getBlockNumber() >= initializationBlock;
}
/**
* @dev Function to be called by top level contract after initialization has finished.
*/
function initialized() internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(getBlockNumber());
}
/**
* @dev Function to be called by top level contract after initialization to enable the contract
* at a future block number rather than immediately.
*/
function initializedAt(uint256 _blockNumber) internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(_blockNumber);
}
}
@@ -0,0 +1,21 @@
pragma solidity ^0.4.24;
import "./Initializable.sol";
contract Petrifiable is Initializable {
// Use block UINT256_MAX (which should be never) as the initializable date
uint256 internal constant PETRIFIED_BLOCK = uint256(-1);
function isPetrified() public view returns (bool) {
return getInitializationBlock() == PETRIFIED_BLOCK;
}
/**
* @dev Function to be called by top level contract to prevent being initialized.
* Useful for freezing base contracts when they're used behind proxies.
*/
function petrify() internal onlyInit {
initializedAt(PETRIFIED_BLOCK);
}
}
@@ -0,0 +1,44 @@
pragma solidity ^0.4.24;
import "./Uint256Helpers.sol";
contract TimeHelpers {
using Uint256Helpers for uint256;
/**
* @dev Returns the current block number.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber() internal view returns (uint256) {
return block.number;
}
/**
* @dev Returns the current block number, converted to uint64.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber64() internal view returns (uint64) {
return getBlockNumber().toUint64();
}
/**
* @dev Returns the current timestamp.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp() internal view returns (uint256) {
return block.timestamp; // solium-disable-line security/no-block-members
}
/**
* @dev Returns the current timestamp, converted to uint64.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp64() internal view returns (uint64) {
return getTimestamp().toUint64();
}
}
@@ -0,0 +1,13 @@
pragma solidity ^0.4.24;
library Uint256Helpers {
uint256 private constant MAX_UINT64 = uint64(-1);
string private constant ERROR_NUMBER_TOO_BIG = "UINT64_NUMBER_TOO_BIG";
function toUint64(uint256 a) internal pure returns (uint64) {
require(a <= MAX_UINT64, ERROR_NUMBER_TOO_BIG);
return uint64(a);
}
}
@@ -0,0 +1,36 @@
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}
@@ -0,0 +1,15 @@
pragma solidity 0.4.24;
import "../Initializable.sol";
import "../Petrifiable.sol";
contract LifecycleMock is Initializable, Petrifiable {
function initializeMock() public {
initialized();
}
function petrifyMock() public {
petrify();
}
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}
@@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
@@ -0,0 +1,19 @@
{
"name": "initializable",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"contract-compile": "yarn truffle compile",
"contract-migrate": "yarn truffle migrate --network ethermint",
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.1.0",
"chai": "^4.2.0",
"truffle": "^5.2.5",
"web3": "^1.3.4",
"sleep": "^6.3.0"
}
}
@@ -0,0 +1,74 @@
const { assertRevert } = require('@aragon/contract-helpers-test/src/asserts')
// Mocks
const LifecycleMock = artifacts.require('LifecycleMock')
const ERRORS = {
INIT_ALREADY_INITIALIZED: 'INIT_ALREADY_INITIALIZED',
}
contract('Lifecycle', () => {
let lifecycle
beforeEach(async () => {
lifecycle = await LifecycleMock.new()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
context('> Initialized', () => {
beforeEach(async () => {
await lifecycle.initializeMock()
})
it('is initialized', async () => {
assert.isTrue(await lifecycle.hasInitialized(), 'should be initialized')
})
it('is not petrified', async () => {
assert.isFalse(await lifecycle.isPetrified(), 'should not be petrified')
})
it('has correct initialization block', async () => {
assert.equal(await lifecycle.getInitializationBlock(), await web3.eth.getBlockNumber(), 'initialization block should be correct')
})
it('cannot be re-initialized', async () => {
await assertRevert(lifecycle.initializeMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('cannot be petrified', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
})
context('> Petrified', () => {
beforeEach(async () => {
await lifecycle.petrifyMock()
})
it('is not initialized', async () => {
assert.isFalse(await lifecycle.hasInitialized(), 'should not be initialized')
})
it('is petrified', async () => {
assert.isTrue(await lifecycle.isPetrified(), 'should be petrified')
})
it('cannot be petrified again', async () => {
await assertRevert(lifecycle.petrifyMock()/*, ERRORS.INIT_ALREADY_INITIALIZED*/)
})
it('has initialization block in the future', async () => {
const petrifiedBlock = await lifecycle.getInitializationBlock()
const blockNumber = await web3.eth.getBlockNumber()
assert.isTrue(petrifiedBlock.gt(blockNumber), 'petrified block should be in the future')
})
})
})
@@ -0,0 +1,23 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.4.24",
settings: {
optimizer: {
enabled: true,
runs: 10000,
},
},
},
},
}
@@ -0,0 +1,31 @@
pragma solidity 0.4.24;
import "./IsContract.sol";
import "./ERCProxy.sol";
contract DelegateProxy is ERCProxy, IsContract {
uint256 internal constant FWD_GAS_LIMIT = 10000;
/**
* @dev Performs a delegatecall and returns whatever the delegatecall returned (entire context execution will return!)
* @param _dst Destination address to perform the delegatecall
* @param _calldata Calldata for the delegatecall
*/
function delegatedFwd(address _dst, bytes _calldata) internal {
require(isContract(_dst));
uint256 fwdGasLimit = FWD_GAS_LIMIT;
assembly {
let result := delegatecall(sub(gas, fwdGasLimit), _dst, add(_calldata, 0x20), mload(_calldata), 0, 0)
let size := returndatasize
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
// revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas.
// if the call returned error data, forward it
switch result case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
@@ -0,0 +1,44 @@
pragma solidity 0.4.24;
import "./DelegateProxy.sol";
import "./DepositableStorage.sol";
contract DepositableDelegateProxy is DepositableStorage, DelegateProxy {
event ProxyDeposit(address sender, uint256 value);
function () external payable {
uint256 forwardGasThreshold = FWD_GAS_LIMIT;
bytes32 isDepositablePosition = DEPOSITABLE_POSITION;
// Optimized assembly implementation to prevent EIP-1884 from breaking deposits, reference code in Solidity:
// https://github.com/aragon/aragonOS/blob/v4.2.1/contracts/common/DepositableDelegateProxy.sol#L10-L20
assembly {
// Continue only if the gas left is lower than the threshold for forwarding to the implementation code,
// otherwise continue outside of the assembly block.
if lt(gas, forwardGasThreshold) {
// Only accept the deposit and emit an event if all of the following are true:
// the proxy accepts deposits (isDepositable), msg.data.length == 0, and msg.value > 0
if and(and(sload(isDepositablePosition), iszero(calldatasize)), gt(callvalue, 0)) {
// Equivalent Solidity code for emitting the event:
// emit ProxyDeposit(msg.sender, msg.value);
let logData := mload(0x40) // free memory pointer
mstore(logData, caller) // add 'msg.sender' to the log data (first event param)
mstore(add(logData, 0x20), callvalue) // add 'msg.value' to the log data (second event param)
// Emit an event with one topic to identify the event: keccak256('ProxyDeposit(address,uint256)') = 0x15ee...dee1
log1(logData, 0x40, 0x15eeaa57c7bd188c1388020bcadc2c436ec60d647d36ef5b9eb3c742217ddee1)
stop() // Stop. Exits execution context
}
// If any of above checks failed, revert the execution (if ETH was sent, it is returned to the sender)
revert(0, 0)
}
}
address target = implementation();
delegatedFwd(target, msg.data);
}
}
@@ -0,0 +1,19 @@
pragma solidity 0.4.24;
import "./UnstructuredStorage.sol";
contract DepositableStorage {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.depositableStorage.depositable")
bytes32 internal constant DEPOSITABLE_POSITION = 0x665fd576fbbe6f247aff98f5c94a561e3f71ec2d3c988d56f12d342396c50cea;
function isDepositable() public view returns (bool) {
return DEPOSITABLE_POSITION.getStorageBool();
}
function setDepositable(bool _depositable) internal {
DEPOSITABLE_POSITION.setStorageBool(_depositable);
}
}
@@ -0,0 +1,10 @@
pragma solidity ^0.4.24;
contract ERCProxy {
uint256 internal constant FORWARDING = 1;
uint256 internal constant UPGRADEABLE = 2;
function proxyType() public pure returns (uint256 proxyTypeId);
function implementation() public view returns (address codeAddr);
}
@@ -0,0 +1,21 @@
pragma solidity ^0.4.24;
contract IsContract {
/*
* NOTE: this should NEVER be used for authentication
* (see pitfalls: https://github.com/fergarrui/ethereum-security/tree/master/contracts/extcodesize).
*
* This is only intended to be used as a sanity check that an address is actually a contract,
* RATHER THAN an address not being a contract.
*/
function isContract(address _target) internal view returns (bool) {
if (_target == address(0)) {
return false;
}
uint256 size;
assembly { size := extcodesize(_target) }
return size > 0;
}
}
@@ -0,0 +1,36 @@
pragma solidity ^0.4.24;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}
@@ -0,0 +1,24 @@
pragma solidity 0.4.24;
import "../DepositableDelegateProxy.sol";
contract DepositableDelegateProxyMock is DepositableDelegateProxy {
address private implementationMock;
function enableDepositsOnMock() external {
setDepositable(true);
}
function setImplementationOnMock(address _implementationMock) external {
implementationMock = _implementationMock;
}
function implementation() public view returns (address) {
return implementationMock;
}
function proxyType() public pure returns (uint256 proxyTypeId) {
return UPGRADEABLE;
}
}
@@ -0,0 +1,8 @@
pragma solidity 0.4.24;
contract EthSender {
function sendEth(address to) external payable {
to.transfer(msg.value);
}
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;
modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}
@@ -0,0 +1,17 @@
pragma solidity 0.4.24;
contract ProxyTargetWithoutFallback {
event Pong();
function ping() external {
emit Pong();
}
}
contract ProxyTargetWithFallback is ProxyTargetWithoutFallback {
event ReceivedEth();
function () external payable {
emit ReceivedEth();
}
}
@@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
+16
View File
@@ -0,0 +1,16 @@
{
"name": "proxy",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.1.0",
"chai": "^4.2.0",
"truffle": "^5.1.42",
"web3": "^1.2.11"
}
}
@@ -0,0 +1,155 @@
const { bn } = require('@aragon/contract-helpers-test')
const { assertAmountOfEvents, assertEvent, assertRevert, assertOutOfGas, assertBn } = require('@aragon/contract-helpers-test/src/asserts')
// Mocks
const DepositableDelegateProxyMock = artifacts.require('DepositableDelegateProxyMock')
const EthSender = artifacts.require('EthSender')
const ProxyTargetWithoutFallback = artifacts.require('ProxyTargetWithoutFallback')
const ProxyTargetWithFallback = artifacts.require('ProxyTargetWithFallback')
const TX_BASE_GAS = 21000
const SEND_ETH_GAS = TX_BASE_GAS + 9999 // <10k gas is the threshold for depositing
const PROXY_FORWARD_GAS = TX_BASE_GAS + 2e6 // high gas amount to ensure that the proxy forwards the call
const FALLBACK_SETUP_GAS = 100 // rough estimation of how much gas it spends before executing the fallback code
const SOLIDITY_TRANSFER_GAS = 2300
contract('DepositableDelegateProxy', ([ sender ]) => {
let ethSender, proxy, target, proxyTargetWithoutFallbackBase, proxyTargetWithFallbackBase
// Initial setup
before(async () => {
ethSender = await EthSender.new()
proxyTargetWithoutFallbackBase = await ProxyTargetWithoutFallback.new()
proxyTargetWithFallbackBase = await ProxyTargetWithFallback.new()
})
beforeEach(async () => {
proxy = await DepositableDelegateProxyMock.new()
target = await ProxyTargetWithFallback.at(proxy.address)
})
const itForwardsToImplementationIfGasIsOverThreshold = () => {
context('when implementation address is set', () => {
const itSuccessfullyForwardsCall = () => {
it('forwards call with data', async () => {
const receipt = await target.ping({ gas: PROXY_FORWARD_GAS })
assertAmountOfEvents(receipt, 'Pong')
})
}
context('when implementation has a fallback', () => {
beforeEach(async () => {
await proxy.setImplementationOnMock(proxyTargetWithFallbackBase.address)
})
itSuccessfullyForwardsCall()
it('can receive ETH [@skip-on-coverage]', async () => {
const receipt = await target.sendTransaction({ value: 1, gas: SEND_ETH_GAS + FALLBACK_SETUP_GAS })
assertAmountOfEvents(receipt, 'ReceivedEth')
})
})
context('when implementation doesn\'t have a fallback', () => {
beforeEach(async () => {
await proxy.setImplementationOnMock(proxyTargetWithoutFallbackBase.address)
})
itSuccessfullyForwardsCall()
it('reverts when sending ETH', async () => {
await assertRevert(target.sendTransaction({ value: 1, gas: PROXY_FORWARD_GAS }))
})
})
})
context('when implementation address is not set', () => {
it('reverts when a function is called', async () => {
await assertRevert(target.ping({ gas: PROXY_FORWARD_GAS }))
})
it('reverts when sending ETH', async () => {
await assertRevert(target.sendTransaction({ value: 1, gas: PROXY_FORWARD_GAS }))
})
})
}
const itRevertsOnInvalidDeposits = () => {
it('reverts when call has data', async () => {
await proxy.setImplementationOnMock(proxyTargetWithoutFallbackBase.address)
await assertRevert(target.ping({ gas: SEND_ETH_GAS }))
})
it('reverts when call sends 0 value', async () => {
await assertRevert(proxy.sendTransaction({ value: 0, gas: SEND_ETH_GAS }))
})
}
context('when proxy is set as depositable', () => {
beforeEach(async () => {
await proxy.enableDepositsOnMock()
})
context('when call gas is below the forwarding threshold', () => {
const value = bn(100)
const assertSendEthToProxy = async ({ value, gas, shouldOOG }) => {
const initialBalance = bn(await web3.eth.getBalance(proxy.address))
const sendEthAction = () => proxy.sendTransaction({ from: sender, gas, value })
if (shouldOOG) {
await assertOutOfGas(sendEthAction())
assertBn(bn(await web3.eth.getBalance(proxy.address)), initialBalance, 'Target balance should be the same as before')
} else {
const receipt = await sendEthAction()
assertBn(bn(await web3.eth.getBalance(proxy.address)), initialBalance.add(value), 'Target balance should be correct')
assertAmountOfEvents(receipt, 'ProxyDeposit', { decodeForAbi: DepositableDelegateProxyMock.abi })
assertEvent(receipt, 'ProxyDeposit', { decodeForAbi: DepositableDelegateProxyMock.abi, expectedArgs: { sender, value } })
return receipt
}
}
it('can receive ETH', async () => {
await assertSendEthToProxy({ value, gas: SEND_ETH_GAS })
})
it('cannot receive ETH if sent with a small amount of gas [@skip-on-coverage]', async () => {
const oogDecrease = 250
// deposit cannot be done with this amount of gas
const gas = TX_BASE_GAS + SOLIDITY_TRANSFER_GAS - oogDecrease
await assertSendEthToProxy({ shouldOOG: true, value, gas })
})
it('can receive ETH from contract [@skip-on-coverage]', async () => {
const receipt = await ethSender.sendEth(proxy.address, { value })
assertAmountOfEvents(receipt, 'ProxyDeposit', { decodeForAbi: proxy.abi })
assertEvent(receipt, 'ProxyDeposit', { decodeForAbi: proxy.abi, expectedArgs: { sender: ethSender.address, value } })
})
itRevertsOnInvalidDeposits()
})
context('when call gas is over forwarding threshold', () => {
itForwardsToImplementationIfGasIsOverThreshold()
})
})
context('when proxy is not set as depositable', () => {
context('when call gas is below the forwarding threshold', () => {
it('reverts when depositing ETH', async () => {
await assertRevert(proxy.sendTransaction({ value: 1, gas: SEND_ETH_GAS }))
})
itRevertsOnInvalidDeposits()
})
context('when call gas is over forwarding threshold', () => {
itForwardsToImplementationIfGasIsOverThreshold()
})
})
})
@@ -0,0 +1,23 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.4.24",
settings: {
optimizer: {
enabled: true,
runs: 10000,
},
},
},
},
}
@@ -0,0 +1,28 @@
name: contracts
on:
push:
branches: master
pull_request:
branches: '*'
jobs:
CI:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install node
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install
run: yarn
- name: Lint
run: yarn lint
- name: Test
run: yarn test
- name: coverage
continue-on-error: true
run: yarn coverage
env:
CI: true
@@ -0,0 +1,648 @@
pragma solidity 0.5.17;
import "./lib/os/SafeMath.sol";
import "./lib/os/SafeERC20.sol";
import "./lib/os/IsContract.sol";
import "./lib/os/Autopetrified.sol";
import "./lib/Checkpointing.sol";
import "./standards/ERC900.sol";
import "./locking/IStakingLocking.sol";
import "./locking/ILockManager.sol";
contract Staking is Autopetrified, ERC900, IStakingLocking, IsContract {
using SafeMath for uint256;
using Checkpointing for Checkpointing.History;
using SafeERC20 for ERC20;
uint256 private constant MAX_UINT64 = uint256(uint64(-1));
string private constant ERROR_TOKEN_NOT_CONTRACT = "STAKING_TOKEN_NOT_CONTRACT";
string private constant ERROR_AMOUNT_ZERO = "STAKING_AMOUNT_ZERO";
string private constant ERROR_TOKEN_TRANSFER = "STAKING_TOKEN_TRANSFER_FAIL";
string private constant ERROR_TOKEN_DEPOSIT = "STAKING_TOKEN_DEPOSIT_FAIL";
string private constant ERROR_TOKEN_NOT_SENDER = "STAKING_TOKEN_NOT_SENDER";
string private constant ERROR_WRONG_TOKEN = "STAKING_WRONG_TOKEN";
string private constant ERROR_NOT_ENOUGH_BALANCE = "STAKING_NOT_ENOUGH_BALANCE";
string private constant ERROR_NOT_ENOUGH_ALLOWANCE = "STAKING_NOT_ENOUGH_ALLOWANCE";
string private constant ERROR_SENDER_NOT_ALLOWED = "STAKING_SENDER_NOT_ALLOWED";
string private constant ERROR_ALLOWANCE_ZERO = "STAKING_ALLOWANCE_ZERO";
string private constant ERROR_LOCK_ALREADY_EXISTS = "STAKING_LOCK_ALREADY_EXISTS";
string private constant ERROR_LOCK_DOES_NOT_EXIST = "STAKING_LOCK_DOES_NOT_EXIST";
string private constant ERROR_NOT_ENOUGH_LOCK = "STAKING_NOT_ENOUGH_LOCK";
string private constant ERROR_CANNOT_UNLOCK = "STAKING_CANNOT_UNLOCK";
string private constant ERROR_CANNOT_CHANGE_ALLOWANCE = "STAKING_CANNOT_CHANGE_ALLOWANCE";
string private constant ERROR_LOCKMANAGER_CALL_FAIL = "STAKING_LOCKMANAGER_CALL_FAIL";
string private constant ERROR_BLOCKNUMBER_TOO_BIG = "STAKING_BLOCKNUMBER_TOO_BIG";
struct Lock {
uint256 amount;
uint256 allowance; // must be greater than zero to consider the lock active, and always greater than or equal to amount
}
struct Account {
mapping (address => Lock) locks; // from manager to lock
uint256 totalLocked;
Checkpointing.History stakedHistory;
}
ERC20 internal stakingToken;
mapping (address => Account) internal accounts;
Checkpointing.History internal totalStakedHistory;
/**
* @notice Initialize Staking app with token `_stakingToken`
* @param _stakingToken ERC20 token used for staking
*/
function initialize(ERC20 _stakingToken) external {
require(isContract(address(_stakingToken)), ERROR_TOKEN_NOT_CONTRACT);
initialized();
stakingToken = _stakingToken;
}
/**
* @notice Stakes `@tokenAmount(self.token(): address, _amount)`, transferring them from `msg.sender`
* @param _amount Number of tokens staked
* @param _data Used in Staked event, to add signalling information in more complex staking applications
*/
function stake(uint256 _amount, bytes calldata _data) external isInitialized {
_stakeFor(msg.sender, msg.sender, _amount, _data);
}
/**
* @notice Stakes `@tokenAmount(self.token(): address, _amount)`, transferring them from `msg.sender`, and assigns them to `_user`
* @param _user The receiving accounts for the tokens staked
* @param _amount Number of tokens staked
* @param _data Used in Staked event, to add signalling information in more complex staking applications
*/
function stakeFor(address _user, uint256 _amount, bytes calldata _data) external isInitialized {
_stakeFor(msg.sender, _user, _amount, _data);
}
/**
* @notice Unstakes `@tokenAmount(self.token(): address, _amount)`, returning them to the user
* @param _amount Number of tokens to unstake
* @param _data Used in Unstaked event, to add signalling information in more complex staking applications
*/
function unstake(uint256 _amount, bytes calldata _data) external isInitialized {
// unstaking 0 tokens is not allowed
require(_amount > 0, ERROR_AMOUNT_ZERO);
_unstake(msg.sender, _amount, _data);
}
/**
* @notice Allow `_lockManager` to lock up to `@tokenAmount(self.token(): address, _allowance)` of `msg.sender`
* It creates a new lock, so the lock for this manager cannot exist before.
* @param _lockManager The manager entity for this particular lock
* @param _allowance Amount of tokens that the manager can lock
* @param _data Data to parametrize logic for the lock to be enforced by the manager
*/
function allowManager(address _lockManager, uint256 _allowance, bytes calldata _data) external isInitialized {
_allowManager(_lockManager, _allowance, _data);
}
/**
* @notice Lock `@tokenAmount(self.token(): address, _amount)` and assign `_lockManager` as manager with `@tokenAmount(self.token(): address, _allowance)` allowance and `_data` as data, so they can not be unstaked
* @param _amount The amount of tokens to be locked
* @param _lockManager The manager entity for this particular lock. This entity will have full control over the lock, in particular will be able to unlock it
* @param _allowance Amount of tokens that the manager can lock
* @param _data Data to parametrize logic for the lock to be enforced by the manager
*/
function allowManagerAndLock(uint256 _amount, address _lockManager, uint256 _allowance, bytes calldata _data) external isInitialized {
_allowManager(_lockManager, _allowance, _data);
_lockUnsafe(msg.sender, _lockManager, _amount);
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` to `_to`s staked balance
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function transfer(address _to, uint256 _amount) external isInitialized {
_transfer(msg.sender, _to, _amount);
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` to `_to`s external balance (i.e. unstaked)
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function transferAndUnstake(address _to, uint256 _amount) external isInitialized {
_transfer(msg.sender, _to, _amount);
_unstake(_to, _amount, new bytes(0));
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` from `_from`'s lock by `msg.sender` to `_to`
* @param _from Owner of locked tokens
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function slash(
address _from,
address _to,
uint256 _amount
)
external
isInitialized
{
_unlockUnsafe(_from, msg.sender, _amount);
_transfer(_from, _to, _amount);
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _amount)` from `_from`'s lock by `msg.sender` to `_to` (unstaked)
* @param _from Owner of locked tokens
* @param _to Recipient of the tokens
* @param _amount Number of tokens to be transferred
*/
function slashAndUnstake(
address _from,
address _to,
uint256 _amount
)
external
isInitialized
{
_unlockUnsafe(_from, msg.sender, _amount);
_transfer(_from, _to, _amount);
_unstake(_to, _amount, new bytes(0));
}
/**
* @notice Transfer `@tokenAmount(self.token(): address, _slashAmount)` from `_from`'s lock by `msg.sender` to `_to`, and decrease `@tokenAmount(self.token(): address, _unlockAmount)` from that lock
* @param _from Owner of locked tokens
* @param _to Recipient of the tokens
* @param _unlockAmount Number of tokens to be unlocked
* @param _slashAmount Number of tokens to be transferred
*/
function slashAndUnlock(
address _from,
address _to,
uint256 _unlockAmount,
uint256 _slashAmount
)
external
isInitialized
{
// No need to check that _slashAmount is positive, as _transfer will fail
// No need to check that have enough locked funds, as _unlockUnsafe will fail
require(_unlockAmount > 0, ERROR_AMOUNT_ZERO);
_unlockUnsafe(_from, msg.sender, _unlockAmount.add(_slashAmount));
_transfer(_from, _to, _slashAmount);
}
/**
* @notice Increase allowance by `@tokenAmount(self.token(): address, _allowance)` of lock manager `_lockManager` for user `msg.sender`
* @param _lockManager The manager entity for this particular lock
* @param _allowance Amount of allowed tokens increase
*/
function increaseLockAllowance(address _lockManager, uint256 _allowance) external isInitialized {
Lock storage lock_ = accounts[msg.sender].locks[_lockManager];
require(lock_.allowance > 0, ERROR_LOCK_DOES_NOT_EXIST);
_increaseLockAllowance(_lockManager, lock_, _allowance);
}
/**
* @notice Decrease allowance by `@tokenAmount(self.token(): address, _allowance)` of lock manager `_lockManager` for user `_user`
* @param _user Owner of locked tokens
* @param _lockManager The manager entity for this particular lock
* @param _allowance Amount of allowed tokens decrease
*/
function decreaseLockAllowance(address _user, address _lockManager, uint256 _allowance) external isInitialized {
// only owner and manager can decrease allowance
require(msg.sender == _user || msg.sender == _lockManager, ERROR_CANNOT_CHANGE_ALLOWANCE);
require(_allowance > 0, ERROR_AMOUNT_ZERO);
Lock storage lock_ = accounts[_user].locks[_lockManager];
uint256 newAllowance = lock_.allowance.sub(_allowance);
require(newAllowance >= lock_.amount, ERROR_NOT_ENOUGH_ALLOWANCE);
// unlockAndRemoveManager must be used for this:
require(newAllowance > 0, ERROR_ALLOWANCE_ZERO);
lock_.allowance = newAllowance;
emit LockAllowanceChanged(_user, _lockManager, _allowance, false);
}
/**
* @notice Increase locked amount by `@tokenAmount(self.token(): address, _amount)` for user `_user` by lock manager `_lockManager`
* @param _user Owner of locked tokens
* @param _lockManager The manager entity for this particular lock
* @param _amount Amount of locked tokens increase
*/
function lock(address _user, address _lockManager, uint256 _amount) external isInitialized {
// we are locking funds from owner account, so only owner or manager are allowed
require(msg.sender == _user || msg.sender == _lockManager, ERROR_SENDER_NOT_ALLOWED);
_lockUnsafe(_user, _lockManager, _amount);
}
/**
* @notice Decrease locked amount by `@tokenAmount(self.token(): address, _amount)` for user `_user` by lock manager `_lockManager`
* @param _user Owner of locked tokens
* @param _lockManager The manager entity for this particular lock
* @param _amount Amount of locked tokens decrease
*/
function unlock(address _user, address _lockManager, uint256 _amount) external isInitialized {
require(_amount > 0, ERROR_AMOUNT_ZERO);
// only manager and owner (if manager allows) can unlock
require(_canUnlockUnsafe(msg.sender, _user, _lockManager, _amount), ERROR_CANNOT_UNLOCK);
_unlockUnsafe(_user, _lockManager, _amount);
}
/**
* @notice Unlock `_user`'s lock by `_lockManager` so locked tokens can be unstaked again
* @param _user Owner of locked tokens
* @param _lockManager Manager of the lock for the given account
*/
function unlockAndRemoveManager(address _user, address _lockManager) external isInitialized {
// only manager and owner (if manager allows) can unlock
require(_canUnlockUnsafe(msg.sender, _user, _lockManager, 0), ERROR_CANNOT_UNLOCK);
Account storage account = accounts[_user];
Lock storage lock_ = account.locks[_lockManager];
uint256 amount = lock_.amount;
// update total
account.totalLocked = account.totalLocked.sub(amount);
emit LockAmountChanged(_user, _lockManager, amount, false);
emit LockManagerRemoved(_user, _lockManager);
delete account.locks[_lockManager];
}
/**
* @notice Change the manager of `_user`'s lock from `msg.sender` to `_newLockManager`
* @param _user Owner of lock
* @param _newLockManager New lock manager
*/
function setLockManager(address _user, address _newLockManager) external isInitialized {
Lock storage lock_ = accounts[_user].locks[msg.sender];
require(lock_.allowance > 0, ERROR_LOCK_DOES_NOT_EXIST);
accounts[_user].locks[_newLockManager] = lock_;
delete accounts[_user].locks[msg.sender];
emit LockManagerTransferred(_user, msg.sender, _newLockManager);
}
/**
* @dev MiniMeToken ApproveAndCallFallBack compliance
* @param _from Account approving tokens
* @param _amount Amount of `_token` tokens being approved
* @param _token MiniMeToken that is being approved and that the call comes from
* @param _data Used in Staked event, to add signalling information in more complex staking applications
*/
function receiveApproval(address _from, uint256 _amount, address _token, bytes calldata _data) external isInitialized {
require(_token == msg.sender, ERROR_TOKEN_NOT_SENDER);
require(_token == address(stakingToken), ERROR_WRONG_TOKEN);
_stakeFor(_from, _from, _amount, _data);
}
/**
* @notice Check whether it supports history of stakes
* @return Always true
*/
function supportsHistory() external pure returns (bool) {
return true;
}
/**
* @notice Get the token used by the contract for staking and locking
* @return The token used by the contract for staking and locking
*/
function token() external view isInitialized returns (address) {
return address(stakingToken);
}
/**
* @notice Get last time `_user` modified its staked balance
* @param _user Account requesting for
* @return Last block number when account's balance was modified
*/
function lastStakedFor(address _user) external view isInitialized returns (uint256) {
return accounts[_user].stakedHistory.lastUpdate();
}
/**
* @notice Get total amount of locked tokens for `_user`
* @param _user Owner of locks
* @return Total amount of locked tokens for the requested account
*/
function lockedBalanceOf(address _user) external view isInitialized returns (uint256) {
return _lockedBalanceOf(_user);
}
/**
* @notice Get details of `_user`'s lock by `_lockManager`
* @param _user Owner of lock
* @param _lockManager Manager of the lock for the given account
* @return Amount of locked tokens
* @return Amount of tokens that lock manager is allowed to lock
*/
function getLock(address _user, address _lockManager)
external
view
isInitialized
returns (
uint256 _amount,
uint256 _allowance
)
{
Lock storage lock_ = accounts[_user].locks[_lockManager];
_amount = lock_.amount;
_allowance = lock_.allowance;
}
/**
* @notice Get staked and locked balances of `_user`
* @param _user Account being requested
* @return Amount of staked tokens
* @return Amount of total locked tokens
*/
function getBalancesOf(address _user) external view isInitialized returns (uint256 staked, uint256 locked) {
staked = _totalStakedFor(_user);
locked = _lockedBalanceOf(_user);
}
/**
* @notice Get the amount of tokens staked by `_user`
* @param _user The owner of the tokens
* @return The amount of tokens staked by the given account
*/
function totalStakedFor(address _user) external view isInitialized returns (uint256) {
return _totalStakedFor(_user);
}
/**
* @notice Get the total amount of tokens staked by all users
* @return The total amount of tokens staked by all users
*/
function totalStaked() external view isInitialized returns (uint256) {
return _totalStaked();
}
/**
* @notice Get the total amount of tokens staked by `_user` at block number `_blockNumber`
* @param _user Account requesting for
* @param _blockNumber Block number at which we are requesting
* @return The amount of tokens staked by the account at the given block number
*/
function totalStakedForAt(address _user, uint256 _blockNumber) external view isInitialized returns (uint256) {
require(_blockNumber <= MAX_UINT64, ERROR_BLOCKNUMBER_TOO_BIG);
return accounts[_user].stakedHistory.get(uint64(_blockNumber));
}
/**
* @notice Get the total amount of tokens staked by all users at block number `_blockNumber`
* @param _blockNumber Block number at which we are requesting
* @return The amount of tokens staked at the given block number
*/
function totalStakedAt(uint256 _blockNumber) external view isInitialized returns (uint256) {
require(_blockNumber <= MAX_UINT64, ERROR_BLOCKNUMBER_TOO_BIG);
return totalStakedHistory.get(uint64(_blockNumber));
}
/**
* @notice Get the staked but unlocked amount of tokens by `_user`
* @param _user Owner of the staked but unlocked balance
* @return Amount of tokens staked but not locked by given account
*/
function unlockedBalanceOf(address _user) external view isInitialized returns (uint256) {
return _unlockedBalanceOf(_user);
}
/**
* @notice Check if `_sender` can unlock `_user`'s `@tokenAmount(self.token(): address, _amount)` locked by `_lockManager`
* @param _sender Account that would try to unlock tokens
* @param _user Owner of lock
* @param _lockManager Manager of the lock for the given owner
* @param _amount Amount of tokens to be potentially unlocked. If zero, it means the whole locked amount
* @return Whether given lock of given owner can be unlocked by given sender
*/
function canUnlock(address _sender, address _user, address _lockManager, uint256 _amount) external view isInitialized returns (bool) {
return _canUnlockUnsafe(_sender, _user, _lockManager, _amount);
}
function _stakeFor(address _from, address _user, uint256 _amount, bytes memory _data) internal {
// staking 0 tokens is invalid
require(_amount > 0, ERROR_AMOUNT_ZERO);
// checkpoint updated staking balance
uint256 newStake = _modifyStakeBalance(_user, _amount, true);
// checkpoint total supply
_modifyTotalStaked(_amount, true);
// pull tokens into Staking contract
require(stakingToken.safeTransferFrom(_from, address(this), _amount), ERROR_TOKEN_DEPOSIT);
emit Staked(_user, _amount, newStake, _data);
}
function _unstake(address _from, uint256 _amount, bytes memory _data) internal {
// checkpoint updated staking balance
uint256 newStake = _modifyStakeBalance(_from, _amount, false);
// checkpoint total supply
_modifyTotalStaked(_amount, false);
// transfer tokens
require(stakingToken.safeTransfer(_from, _amount), ERROR_TOKEN_TRANSFER);
emit Unstaked(_from, _amount, newStake, _data);
}
function _modifyStakeBalance(address _user, uint256 _by, bool _increase) internal returns (uint256) {
uint256 currentStake = _totalStakedFor(_user);
uint256 newStake;
if (_increase) {
newStake = currentStake.add(_by);
} else {
require(_by <= _unlockedBalanceOf(_user), ERROR_NOT_ENOUGH_BALANCE);
newStake = currentStake.sub(_by);
}
// add new value to account history
accounts[_user].stakedHistory.add(getBlockNumber64(), newStake);
return newStake;
}
function _modifyTotalStaked(uint256 _by, bool _increase) internal {
uint256 currentStake = _totalStaked();
uint256 newStake;
if (_increase) {
newStake = currentStake.add(_by);
} else {
newStake = currentStake.sub(_by);
}
// add new value to total history
totalStakedHistory.add(getBlockNumber64(), newStake);
}
function _allowManager(address _lockManager, uint256 _allowance, bytes memory _data) internal {
Lock storage lock_ = accounts[msg.sender].locks[_lockManager];
// check if lock exists
require(lock_.allowance == 0, ERROR_LOCK_ALREADY_EXISTS);
emit NewLockManager(msg.sender, _lockManager, _data);
_increaseLockAllowance(_lockManager, lock_, _allowance);
}
function _increaseLockAllowance(address _lockManager, Lock storage _lock, uint256 _allowance) internal {
require(_allowance > 0, ERROR_AMOUNT_ZERO);
_lock.allowance = _lock.allowance.add(_allowance);
emit LockAllowanceChanged(msg.sender, _lockManager, _allowance, true);
}
/**
* @dev Assumes that sender is either owner or lock manager
*/
function _lockUnsafe(address _user, address _lockManager, uint256 _amount) internal {
require(_amount > 0, ERROR_AMOUNT_ZERO);
// check enough unlocked tokens are available
require(_amount <= _unlockedBalanceOf(_user), ERROR_NOT_ENOUGH_BALANCE);
Account storage account = accounts[_user];
Lock storage lock_ = account.locks[_lockManager];
uint256 newAmount = lock_.amount.add(_amount);
// check allowance is enough, it also means that lock exists, as newAmount is greater than zero
require(newAmount <= lock_.allowance, ERROR_NOT_ENOUGH_ALLOWANCE);
lock_.amount = newAmount;
// update total
account.totalLocked = account.totalLocked.add(_amount);
emit LockAmountChanged(_user, _lockManager, _amount, true);
}
/**
* @dev Assumes `canUnlock` passes
*/
function _unlockUnsafe(address _user, address _lockManager, uint256 _amount) internal {
Account storage account = accounts[_user];
Lock storage lock_ = account.locks[_lockManager];
uint256 lockAmount = lock_.amount;
require(lockAmount >= _amount, ERROR_NOT_ENOUGH_LOCK);
// update lock amount
// No need for SafeMath: checked just above
lock_.amount = lockAmount - _amount;
// update total
account.totalLocked = account.totalLocked.sub(_amount);
emit LockAmountChanged(_user, _lockManager, _amount, false);
}
function _transfer(address _from, address _to, uint256 _amount) internal {
// transferring 0 staked tokens is invalid
require(_amount > 0, ERROR_AMOUNT_ZERO);
// update stakes
_modifyStakeBalance(_from, _amount, false);
_modifyStakeBalance(_to, _amount, true);
emit StakeTransferred(_from, _to, _amount);
}
/**
* @notice Get the amount of tokens staked by `_user`
* @param _user The owner of the tokens
* @return The amount of tokens staked by the given account
*/
function _totalStakedFor(address _user) internal view returns (uint256) {
// we assume it's not possible to stake in the future
return accounts[_user].stakedHistory.getLast();
}
/**
* @notice Get the total amount of tokens staked by all users
* @return The total amount of tokens staked by all users
*/
function _totalStaked() internal view returns (uint256) {
// we assume it's not possible to stake in the future
return totalStakedHistory.getLast();
}
/**
* @notice Get the staked but unlocked amount of tokens by `_user`
* @param _user Owner of the staked but unlocked balance
* @return Amount of tokens staked but not locked by given account
*/
function _unlockedBalanceOf(address _user) internal view returns (uint256) {
return _totalStakedFor(_user).sub(_lockedBalanceOf(_user));
}
function _lockedBalanceOf(address _user) internal view returns (uint256) {
return accounts[_user].totalLocked;
}
/**
* @notice Check if `_sender` can unlock `_user`'s `@tokenAmount(self.token(): address, _amount)` locked by `_lockManager`
* @dev If calling this from a state modifying function trying to unlock tokens, make sure first parameter is `msg.sender`
* @param _sender Account that would try to unlock tokens
* @param _user Owner of lock
* @param _lockManager Manager of the lock for the given owner
* @param _amount Amount of locked tokens to unlock. If zero, the full locked amount
* @return Whether given lock of given owner can be unlocked by given sender
*/
function _canUnlockUnsafe(address _sender, address _user, address _lockManager, uint256 _amount) internal view returns (bool) {
Lock storage lock_ = accounts[_user].locks[_lockManager];
require(lock_.allowance > 0, ERROR_LOCK_DOES_NOT_EXIST);
require(lock_.amount >= _amount, ERROR_NOT_ENOUGH_LOCK);
uint256 amount = _amount == 0 ? lock_.amount : _amount;
// If the sender is the lock manager, unlocking is allowed
if (_sender == _lockManager) {
return true;
}
// If the sender is neither the lock manager nor the owner, unlocking is not allowed
if (_sender != _user) {
return false;
}
// The sender must therefore be the owner of the tokens
// Allow unlocking if the amount of locked tokens has already been decreased to 0
if (amount == 0) {
return true;
}
// Otherwise, check whether the lock manager allows unlocking
return ILockManager(_lockManager).canUnlock(_user, amount);
}
function _toBytes4(bytes memory _data) internal pure returns (bytes4 result) {
if (_data.length < 4) {
return bytes4(0);
}
assembly { result := mload(add(_data, 0x20)) }
}
}
@@ -0,0 +1,44 @@
pragma solidity ^0.5.17;
import "./lib/os/ERC20.sol";
import "./Staking.sol";
import "./proxies/StakingProxy.sol";
contract StakingFactory {
Staking public baseImplementation;
mapping (address => address) internal instances;
event NewStaking(address indexed instance, address token);
constructor() public {
baseImplementation = new Staking();
}
function existsInstance(ERC20 token) external view returns (bool) {
return _getInstance(token) != address(0);
}
function getInstance(ERC20 token) external view returns (Staking) {
return Staking(_getInstance(token));
}
function getOrCreateInstance(ERC20 token) external returns (Staking) {
address instance = _getInstance(token);
return instance != address(0) ? Staking(instance) : _createInstance(token);
}
function _getInstance(ERC20 token) internal view returns (address) {
return instances[address(token)];
}
function _createInstance(ERC20 token) internal returns (Staking) {
StakingProxy instance = new StakingProxy(baseImplementation, token);
address tokenAddress = address(token);
address instanceAddress = address(instance);
instances[tokenAddress] = instanceAddress;
emit NewStaking(instanceAddress, tokenAddress);
return Staking(instanceAddress);
}
}
@@ -0,0 +1,155 @@
pragma solidity ^0.5.17;
/**
* @title Checkpointing - Library to handle a historic set of numeric values
*/
library Checkpointing {
uint256 private constant MAX_UINT192 = uint256(uint192(-1));
string private constant ERROR_VALUE_TOO_BIG = "CHECKPOINT_VALUE_TOO_BIG";
string private constant ERROR_CANNOT_ADD_PAST_VALUE = "CHECKPOINT_CANNOT_ADD_PAST_VALUE";
/**
* @dev To specify a value at a given point in time, we need to store two values:
* - `time`: unit-time value to denote the first time when a value was registered
* - `value`: a positive numeric value to registered at a given point in time
*
* Note that `time` does not need to refer necessarily to a timestamp value, any time unit could be used
* for it like block numbers, terms, etc.
*/
struct Checkpoint {
uint64 time;
uint192 value;
}
/**
* @dev A history simply denotes a list of checkpoints
*/
struct History {
Checkpoint[] history;
}
/**
* @dev Add a new value to a history for a given point in time. This function does not allow to add values previous
* to the latest registered value, if the value willing to add corresponds to the latest registered value, it
* will be updated.
* @param self Checkpoints history to be altered
* @param _time Point in time to register the given value
* @param _value Numeric value to be registered at the given point in time
*/
function add(History storage self, uint64 _time, uint256 _value) internal {
require(_value <= MAX_UINT192, ERROR_VALUE_TOO_BIG);
_add192(self, _time, uint192(_value));
}
/**
* TODO
*/
function lastUpdate(History storage self) internal view returns (uint256) {
uint256 length = self.history.length;
if (length > 0) {
return uint256(self.history[length - 1].time);
}
return 0;
}
/**
* @dev Fetch the latest registered value of history, it will return zero if there was no value registered
* @param self Checkpoints history to be queried
*/
function getLast(History storage self) internal view returns (uint256) {
uint256 length = self.history.length;
if (length > 0) {
return uint256(self.history[length - 1].value);
}
return 0;
}
/**
* @dev Fetch the most recent registered past value of a history based on a given point in time that is not known
* how recent it is beforehand. It will return zero if there is no registered value or if given time is
* previous to the first registered value.
* It uses a binary search.
* @param self Checkpoints history to be queried
* @param _time Point in time to query the most recent registered past value of
*/
function get(History storage self, uint64 _time) internal view returns (uint256) {
return _binarySearch(self, _time);
}
/**
* @dev Private function to add a new value to a history for a given point in time. This function does not allow to
* add values previous to the latest registered value, if the value willing to add corresponds to the latest
* registered value, it will be updated.
* @param self Checkpoints history to be altered
* @param _time Point in time to register the given value
* @param _value Numeric value to be registered at the given point in time
*/
function _add192(History storage self, uint64 _time, uint192 _value) private {
uint256 length = self.history.length;
if (length == 0 || self.history[self.history.length - 1].time < _time) {
// If there was no value registered or the given point in time is after the latest registered value,
// we can insert it to the history directly.
self.history.push(Checkpoint(_time, _value));
} else {
// If the point in time given for the new value is not after the latest registered value, we must ensure
// we are only trying to update the latest value, otherwise we would be changing past data.
Checkpoint storage currentCheckpoint = self.history[length - 1];
require(_time == currentCheckpoint.time, ERROR_CANNOT_ADD_PAST_VALUE);
currentCheckpoint.value = _value;
}
}
/**
* @dev Private function execute a binary search to find the most recent registered past value of a history based on
* a given point in time. It will return zero if there is no registered value or if given time is previous to
* the first registered value. Note that this function will be more suitable when don't know how recent the
* time used to index may be.
* @param self Checkpoints history to be queried
* @param _time Point in time to query the most recent registered past value of
*/
function _binarySearch(History storage self, uint64 _time) private view returns (uint256) {
// If there was no value registered for the given history return simply zero
uint256 length = self.history.length;
if (length == 0) {
return 0;
}
// If the requested time is equal to or after the time of the latest registered value, return latest value
uint256 lastIndex = length - 1;
if (_time >= self.history[lastIndex].time) {
return uint256(self.history[lastIndex].value);
}
// If the requested time is previous to the first registered value, return zero to denote missing checkpoint
if (_time < self.history[0].time) {
return 0;
}
// Execute a binary search between the checkpointed times of the history
uint256 low = 0;
uint256 high = lastIndex;
while (high > low) {
// No need for SafeMath: for this to overflow array size should be ~2^255
uint256 mid = (high + low + 1) / 2;
Checkpoint storage checkpoint = self.history[mid];
uint64 midTime = checkpoint.time;
if (_time > midTime) {
low = mid;
} else if (_time < midTime) {
// No need for SafeMath: high > low >= 0 => high >= 1 => mid >= 1
high = mid - 1;
} else {
return uint256(checkpoint.value);
}
}
return uint256(self.history[low].value);
}
}
@@ -0,0 +1,15 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Autopetrified.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
import "./Petrifiable.sol";
contract Autopetrified is Petrifiable {
constructor() public {
// Immediately petrify base (non-proxy) instances of inherited contracts on deploy.
// This renders them uninitializable (and unusable without a proxy).
petrify();
}
}
@@ -0,0 +1,34 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Autopetrified.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity 0.5.17;
import "./ERCProxy.sol";
import "./IsContract.sol";
contract DelegateProxy is ERCProxy, IsContract {
uint256 internal constant FWD_GAS_LIMIT = 10000;
/**
* @dev Performs a delegatecall and returns whatever the delegatecall returned (entire context execution will return!)
* @param _dst Destination address to perform the delegatecall
* @param _calldata Calldata for the delegatecall
*/
function delegatedFwd(address _dst, bytes memory _calldata) internal {
require(isContract(_dst));
uint256 fwdGasLimit = FWD_GAS_LIMIT;
assembly {
let result := delegatecall(sub(gas, fwdGasLimit), _dst, add(_calldata, 0x20), mload(_calldata), 0, 0)
let size := returndatasize
let ptr := mload(0x40)
returndatacopy(ptr, 0, size)
// revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas.
// if the call returned error data, forward it
switch result case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
@@ -0,0 +1,35 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/token/ERC20.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 {
function totalSupply() public view returns (uint256);
function balanceOf(address _who) public view returns (uint256);
function allowance(address _owner, address _spender) public view returns (uint256);
function transfer(address _to, uint256 _value) public returns (bool);
function approve(address _spender, uint256 _value) public returns (bool);
function transferFrom(address _from, address _to, uint256 _value) public returns (bool);
event Transfer(
address indexed from,
address indexed to,
uint256 value
);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}
@@ -0,0 +1,13 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/misc/ERCProxy.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
contract ERCProxy {
uint256 internal constant FORWARDING = 1;
uint256 internal constant UPGRADEABLE = 2;
function proxyType() public pure returns (uint256 proxyTypeId);
function implementation() public view returns (address codeAddr);
}
@@ -0,0 +1,58 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Initializable.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
import "./TimeHelpers.sol";
import "./UnstructuredStorage.sol";
contract Initializable is TimeHelpers {
using UnstructuredStorage for bytes32;
// keccak256("aragonOS.initializable.initializationBlock")
bytes32 internal constant INITIALIZATION_BLOCK_POSITION = 0xebb05b386a8d34882b8711d156f463690983dc47815980fb82aeeff1aa43579e;
string private constant ERROR_ALREADY_INITIALIZED = "INIT_ALREADY_INITIALIZED";
string private constant ERROR_NOT_INITIALIZED = "INIT_NOT_INITIALIZED";
modifier onlyInit {
require(getInitializationBlock() == 0, ERROR_ALREADY_INITIALIZED);
_;
}
modifier isInitialized {
require(hasInitialized(), ERROR_NOT_INITIALIZED);
_;
}
/**
* @return Block number in which the contract was initialized
*/
function getInitializationBlock() public view returns (uint256) {
return INITIALIZATION_BLOCK_POSITION.getStorageUint256();
}
/**
* @return Whether the contract has been initialized by the time of the current block
*/
function hasInitialized() public view returns (bool) {
uint256 initializationBlock = getInitializationBlock();
return initializationBlock != 0 && getBlockNumber() >= initializationBlock;
}
/**
* @dev Function to be called by top level contract after initialization has finished.
*/
function initialized() internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(getBlockNumber());
}
/**
* @dev Function to be called by top level contract after initialization to enable the contract
* at a future block number rather than immediately.
*/
function initializedAt(uint256 _blockNumber) internal onlyInit {
INITIALIZATION_BLOCK_POSITION.setStorageUint256(_blockNumber);
}
}
@@ -0,0 +1,24 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/IsContract.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
contract IsContract {
/*
* NOTE: this should NEVER be used for authentication
* (see pitfalls: https://github.com/fergarrui/ethereum-security/tree/master/contracts/extcodesize).
*
* This is only intended to be used as a sanity check that an address is actually a contract,
* RATHER THAN an address not being a contract.
*/
function isContract(address _target) internal view returns (bool) {
if (_target == address(0)) {
return false;
}
uint256 size;
assembly { size := extcodesize(_target) }
return size > 0;
}
}
@@ -0,0 +1,29 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/misc/Migrations.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
contract Migrations {
address public owner;
uint256 public lastCompletedMigration;
modifier restricted() {
if (msg.sender == owner) {
_;
}
}
constructor() public {
owner = msg.sender;
}
function setCompleted(uint256 completed) public restricted {
lastCompletedMigration = completed;
}
function upgrade(address newAddress) public restricted {
Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(lastCompletedMigration);
}
}
@@ -0,0 +1,24 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Petrifiable.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
import "./Initializable.sol";
contract Petrifiable is Initializable {
// Use block UINT256_MAX (which should be never) as the initializable date
uint256 internal constant PETRIFIED_BLOCK = uint256(-1);
function isPetrified() public view returns (bool) {
return getInitializationBlock() == PETRIFIED_BLOCK;
}
/**
* @dev Function to be called by top level contract to prevent being initialized.
* Useful for freezing base contracts when they're used behind proxies.
*/
function petrify() internal onlyInit {
initializedAt(PETRIFIED_BLOCK);
}
}
@@ -0,0 +1,91 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/SafeERC20.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
import "./ERC20.sol";
library SafeERC20 {
// Before 0.5, solidity has a mismatch between `address.transfer()` and `token.transfer()`:
// https://github.com/ethereum/solidity/issues/3544
bytes4 private constant TRANSFER_SELECTOR = 0xa9059cbb;
/**
* @dev Same as a standards-compliant ERC20.transfer() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeTransfer(ERC20 _token, address _to, uint256 _amount) internal returns (bool) {
bytes memory transferCallData = abi.encodeWithSelector(
TRANSFER_SELECTOR,
_to,
_amount
);
return invokeAndCheckSuccess(address(_token), transferCallData);
}
/**
* @dev Same as a standards-compliant ERC20.transferFrom() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeTransferFrom(ERC20 _token, address _from, address _to, uint256 _amount) internal returns (bool) {
bytes memory transferFromCallData = abi.encodeWithSelector(
_token.transferFrom.selector,
_from,
_to,
_amount
);
return invokeAndCheckSuccess(address(_token), transferFromCallData);
}
/**
* @dev Same as a standards-compliant ERC20.approve() that never reverts (returns false).
* Note that this makes an external call to the token.
*/
function safeApprove(ERC20 _token, address _spender, uint256 _amount) internal returns (bool) {
bytes memory approveCallData = abi.encodeWithSelector(
_token.approve.selector,
_spender,
_amount
);
return invokeAndCheckSuccess(address(_token), approveCallData);
}
function invokeAndCheckSuccess(address _addr, bytes memory _calldata) private returns (bool) {
bool ret;
assembly {
let ptr := mload(0x40) // free memory pointer
let success := call(
gas, // forward all gas
_addr, // address
0, // no value
add(_calldata, 0x20), // calldata start
mload(_calldata), // calldata length
ptr, // write output over free memory
0x20 // uint256 return
)
if gt(success, 0) {
// Check number of bytes returned from last function call
switch returndatasize
// No bytes returned: assume success
case 0 {
ret := 1
}
// 32 bytes returned: check if non-zero
case 0x20 {
// Only return success if returned data was true
// Already have output in ptr
ret := eq(mload(ptr), 1)
}
// Not sure what was returned: don't mark as success
default { }
}
}
return ret;
}
}
@@ -0,0 +1,73 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/math/SafeMath.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity >=0.4.24 <0.6.0;
/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {
string private constant ERROR_ADD_OVERFLOW = "MATH_ADD_OVERFLOW";
string private constant ERROR_SUB_UNDERFLOW = "MATH_SUB_UNDERFLOW";
string private constant ERROR_MUL_OVERFLOW = "MATH_MUL_OVERFLOW";
string private constant ERROR_DIV_ZERO = "MATH_DIV_ZERO";
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 _a, uint256 _b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (_a == 0) {
return 0;
}
uint256 c = _a * _b;
require(c / _a == _b, ERROR_MUL_OVERFLOW);
return c;
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 _a, uint256 _b) internal pure returns (uint256) {
require(_b > 0, ERROR_DIV_ZERO); // Solidity only automatically asserts when dividing by 0
uint256 c = _a / _b;
// assert(_a == _b * c + _a % _b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 _a, uint256 _b) internal pure returns (uint256) {
require(_b <= _a, ERROR_SUB_UNDERFLOW);
uint256 c = _a - _b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 _a, uint256 _b) internal pure returns (uint256) {
uint256 c = _a + _b;
require(c >= _a, ERROR_ADD_OVERFLOW);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0, ERROR_DIV_ZERO);
return a % b;
}
}
@@ -0,0 +1,66 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/lib/math/SafeMath64.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
/**
* @title SafeMath64
* @dev Math operations for uint64 with safety checks that revert on error
*/
library SafeMath64 {
string private constant ERROR_ADD_OVERFLOW = "MATH64_ADD_OVERFLOW";
string private constant ERROR_SUB_UNDERFLOW = "MATH64_SUB_UNDERFLOW";
string private constant ERROR_MUL_OVERFLOW = "MATH64_MUL_OVERFLOW";
string private constant ERROR_DIV_ZERO = "MATH64_DIV_ZERO";
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint64 _a, uint64 _b) internal pure returns (uint64) {
uint256 c = uint256(_a) * uint256(_b);
require(c < 0x010000000000000000, ERROR_MUL_OVERFLOW); // 2**64 (less gas this way)
return uint64(c);
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint64 _a, uint64 _b) internal pure returns (uint64) {
require(_b > 0, ERROR_DIV_ZERO); // Solidity only automatically asserts when dividing by 0
uint64 c = _a / _b;
// assert(_a == _b * c + _a % _b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint64 _a, uint64 _b) internal pure returns (uint64) {
require(_b <= _a, ERROR_SUB_UNDERFLOW);
uint64 c = _a - _b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint64 _a, uint64 _b) internal pure returns (uint64) {
uint64 c = _a + _b;
require(c >= _a, ERROR_ADD_OVERFLOW);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint64 a, uint64 b) internal pure returns (uint64) {
require(b != 0, ERROR_DIV_ZERO);
return a % b;
}
}
@@ -0,0 +1,47 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/evmscript/ScriptHelpers.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
library ScriptHelpers {
function getSpecId(bytes memory _script) internal pure returns (uint32) {
return uint32At(_script, 0);
}
function uint256At(bytes memory _data, uint256 _location) internal pure returns (uint256 result) {
assembly {
result := mload(add(_data, add(0x20, _location)))
}
}
function addressAt(bytes memory _data, uint256 _location) internal pure returns (address result) {
uint256 word = uint256At(_data, _location);
assembly {
result := div(and(word, 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000),
0x1000000000000000000000000)
}
}
function uint32At(bytes memory _data, uint256 _location) internal pure returns (uint32 result) {
uint256 word = uint256At(_data, _location);
assembly {
result := div(and(word, 0xffffffff00000000000000000000000000000000000000000000000000000000),
0x100000000000000000000000000000000000000000000000000000000)
}
}
function locationOf(bytes memory _data, uint256 _location) internal pure returns (uint256 result) {
assembly {
result := add(_data, add(0x20, _location))
}
}
function toBytes(bytes4 _sig) internal pure returns (bytes memory) {
bytes memory payload = new bytes(4);
assembly { mstore(add(payload, 0x20), _sig) }
return payload;
}
}
@@ -0,0 +1,47 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/TimeHelpers.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
import "./Uint256Helpers.sol";
contract TimeHelpers {
using Uint256Helpers for uint256;
/**
* @dev Returns the current block number.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber() internal view returns (uint256) {
return block.number;
}
/**
* @dev Returns the current block number, converted to uint64.
* Using a function rather than `block.number` allows us to easily mock the block number in
* tests.
*/
function getBlockNumber64() internal view returns (uint64) {
return getBlockNumber().toUint64();
}
/**
* @dev Returns the current timestamp.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp() internal view returns (uint256) {
return block.timestamp; // solium-disable-line security/no-block-members
}
/**
* @dev Returns the current timestamp, converted to uint64.
* Using a function rather than `block.timestamp` allows us to easily mock it in
* tests.
*/
function getTimestamp64() internal view returns (uint64) {
return getTimestamp().toUint64();
}
}
@@ -0,0 +1,23 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/Uint256Helpers.sol
// Adapted to use pragma ^0.5.8 and satisfy our linter rules
pragma solidity ^0.5.8;
library Uint256Helpers {
uint256 private constant MAX_UINT8 = uint8(-1);
uint256 private constant MAX_UINT64 = uint64(-1);
string private constant ERROR_UINT8_NUMBER_TOO_BIG = "UINT8_NUMBER_TOO_BIG";
string private constant ERROR_UINT64_NUMBER_TOO_BIG = "UINT64_NUMBER_TOO_BIG";
function toUint8(uint256 a) internal pure returns (uint8) {
require(a <= MAX_UINT8, ERROR_UINT8_NUMBER_TOO_BIG);
return uint8(a);
}
function toUint64(uint256 a) internal pure returns (uint64) {
require(a <= MAX_UINT64, ERROR_UINT64_NUMBER_TOO_BIG);
return uint64(a);
}
}
@@ -0,0 +1,39 @@
// Brought from https://github.com/aragon/aragonOS/blob/v4.3.0/contracts/common/UnstructuredStorage.sol
// Adapted to use pragma ^0.5.17 and satisfy our linter rules
pragma solidity ^0.5.17;
library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}
function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}
function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}
function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}
function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}
function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}
function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}
function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}
@@ -0,0 +1,12 @@
pragma solidity ^0.5.17;
interface ILockManager {
/**
* @notice Check if `_user`'s by `_lockManager` can be unlocked
* @param _user Owner of lock
* @param _amount Amount of locked tokens to unlock
* @return Whether given lock of given owner can be unlocked by given sender
*/
function canUnlock(address _user, uint256 _amount) external view returns (bool);
}
@@ -0,0 +1,31 @@
pragma solidity ^0.5.17;
interface IStakingLocking {
event NewLockManager(address indexed account, address indexed lockManager, bytes data);
event Unlocked(address indexed account, address indexed lockManager, uint256 amount);
event LockAmountChanged(address indexed account, address indexed lockManager, uint256 amount, bool increase);
event LockAllowanceChanged(address indexed account, address indexed lockManager, uint256 allowance, bool increase);
event LockManagerRemoved(address indexed account, address lockManager);
event LockManagerTransferred(address indexed account, address indexed oldLockManager, address newLockManager);
event StakeTransferred(address indexed from, address to, uint256 amount);
function allowManager(address _lockManager, uint256 _allowance, bytes calldata _data) external;
function allowManagerAndLock(uint256 _amount, address _lockManager, uint256 _allowance, bytes calldata _data) external;
function unlockAndRemoveManager(address _account, address _lockManager) external;
function increaseLockAllowance(address _lockManager, uint256 _allowance) external;
function decreaseLockAllowance(address _account, address _lockManager, uint256 _allowance) external;
function lock(address _account, address _lockManager, uint256 _amount) external;
function unlock(address _account, address _lockManager, uint256 _amount) external;
function setLockManager(address _account, address _newLockManager) external;
function transfer(address _to, uint256 _amount) external;
function transferAndUnstake(address _to, uint256 _amount) external;
function slash(address _account, address _to, uint256 _amount) external;
function slashAndUnstake(address _account, address _to, uint256 _amount) external;
function getLock(address _account, address _lockManager) external view returns (uint256 _amount, uint256 _allowance);
function unlockedBalanceOf(address _account) external view returns (uint256);
function lockedBalanceOf(address _user) external view returns (uint256);
function getBalancesOf(address _user) external view returns (uint256 staked, uint256 locked);
function canUnlock(address _sender, address _account, address _lockManager, uint256 _amount) external view returns (bool);
}
@@ -0,0 +1,72 @@
pragma solidity 0.5.17;
import "../lib/os/TimeHelpers.sol";
import "../lib/os/ScriptHelpers.sol";
import "../locking/ILockManager.sol";
import "../locking/IStakingLocking.sol";
/**
* Time based lock manager for Staking contract
* Allows to set a time interval, either in blocks or seconds, during which the funds are locked.
* Outside that window the owner can unlock them.
*/
contract TimeLockManager is ILockManager, TimeHelpers {
using ScriptHelpers for bytes;
string private constant ERROR_ALREADY_LOCKED = "TLM_ALREADY_LOCKED";
string private constant ERROR_WRONG_INTERVAL = "TLM_WRONG_INTERVAL";
enum TimeUnit { Blocks, Seconds }
struct TimeInterval {
uint256 unit;
uint256 start;
uint256 end;
}
mapping (address => TimeInterval) internal timeIntervals;
event LogLockCallback(uint256 amount, uint256 allowance, bytes data);
/**
* @notice Set a locked amount, along with a time interval, either in blocks or seconds during which the funds are locked.
* @param _staking The Staking contract holding the lock
* @param _owner The account owning the locked funds
* @param _amount The amount to be locked
* @param _unit Blocks or seconds, the unit for the time interval
* @param _start The start of the time interval
* @param _end The end of the time interval
*/
function lock(IStakingLocking _staking, address _owner, uint256 _amount, uint256 _unit, uint256 _start, uint256 _end) external {
require(timeIntervals[_owner].end == 0, ERROR_ALREADY_LOCKED);
require(_end > _start, ERROR_WRONG_INTERVAL);
timeIntervals[_owner] = TimeInterval(_unit, _start, _end);
_staking.lock(_owner, address(this), _amount);
}
/**
* @notice Check if the owner can unlock the funds, i.e., if current timestamp is outside the lock interval
* @param _owner Owner of the locked funds
* @return True if current timestamp is outside the lock interval
*/
function canUnlock(address _owner, uint256) external view returns (bool) {
TimeInterval storage timeInterval = timeIntervals[_owner];
uint256 comparingValue;
if (timeInterval.unit == uint256(TimeUnit.Blocks)) {
comparingValue = getBlockNumber();
} else {
comparingValue = getTimestamp();
}
return comparingValue < timeInterval.start || comparingValue > timeInterval.end;
}
function getTimeInterval(address _owner) external view returns (uint256 unit, uint256 start, uint256 end) {
TimeInterval storage timeInterval = timeIntervals[_owner];
return (timeInterval.unit, timeInterval.start, timeInterval.end);
}
}
@@ -0,0 +1,31 @@
pragma solidity ^0.5.17;
import "../lib/os/ERC20.sol";
import "../Staking.sol";
import "./ThinProxy.sol";
contract StakingProxy is ThinProxy {
// keccak256("aragon.network.staking")
bytes32 internal constant IMPLEMENTATION_SLOT = 0xbd536e2e005accda865e2f0d1827f83ec8824f3ea04ecd6131b7c10058635814;
constructor(Staking _implementation, ERC20 _token) ThinProxy(address(_implementation)) public {
bytes4 selector = _implementation.initialize.selector;
bytes memory initializeData = abi.encodeWithSelector(selector, _token);
(bool success,) = address(_implementation).delegatecall(initializeData);
if (!success) {
assembly {
let output := mload(0x40)
mstore(0x40, add(output, returndatasize))
returndatacopy(output, 0, returndatasize)
revert(output, returndatasize)
}
}
}
function _implementationSlot() internal pure returns (bytes32) {
return IMPLEMENTATION_SLOT;
}
}
@@ -0,0 +1,27 @@
pragma solidity ^0.5.17;
import "../lib/os/DelegateProxy.sol";
import "../lib/os/UnstructuredStorage.sol";
contract ThinProxy is DelegateProxy {
using UnstructuredStorage for bytes32;
constructor(address _implementation) public {
_implementationSlot().setStorageAddress(_implementation);
}
function () external {
delegatedFwd(implementation(), msg.data);
}
function proxyType() public pure returns (uint256) {
return FORWARDING;
}
function implementation() public view returns (address) {
return _implementationSlot().getStorageAddress();
}
function _implementationSlot() internal pure returns (bytes32);
}
@@ -0,0 +1,55 @@
pragma solidity ^0.5.17;
// Interface for ERC900: https://eips.ethereum.org/EIPS/eip-900
interface ERC900 {
event Staked(address indexed user, uint256 amount, uint256 total, bytes data);
event Unstaked(address indexed user, uint256 amount, uint256 total, bytes data);
/**
* @dev Stake a certain amount of tokens
* @param _amount Amount of tokens to be staked
* @param _data Optional data that can be used to add signalling information in more complex staking applications
*/
function stake(uint256 _amount, bytes calldata _data) external;
/**
* @dev Stake a certain amount of tokens in favor of someone
* @param _user Address to stake an amount of tokens to
* @param _amount Amount of tokens to be staked
* @param _data Optional data that can be used to add signalling information in more complex staking applications
*/
function stakeFor(address _user, uint256 _amount, bytes calldata _data) external;
/**
* @dev Unstake a certain amount of tokens
* @param _amount Amount of tokens to be unstaked
* @param _data Optional data that can be used to add signalling information in more complex staking applications
*/
function unstake(uint256 _amount, bytes calldata _data) external;
/**
* @dev Tell the total amount of tokens staked for an address
* @param _addr Address querying the total amount of tokens staked for
* @return Total amount of tokens staked for an address
*/
function totalStakedFor(address _addr) external view returns (uint256);
/**
* @dev Tell the total amount of tokens staked
* @return Total amount of tokens staked
*/
function totalStaked() external view returns (uint256);
/**
* @dev Tell the address of the token used for staking
* @return Address of the token used for staking
*/
function token() external view returns (address);
/*
* @dev Tell if the current registry supports historic information or not
* @return True if the optional history functions are implemented, false otherwise
*/
function supportsHistory() external pure returns (bool);
}
@@ -0,0 +1,23 @@
pragma solidity 0.5.17;
import "./lib/MiniMeToken.sol";
import "../lib/os/Migrations.sol";
// You might think this file is a bit odd, but let me explain.
// We only use some contracts in our tests, which means Truffle
// will not compile it for us, because it is from an external
// dependency.
//
// We are now left with three options:
// - Copy/paste these contracts
// - Run the tests with `truffle compile --all` on
// - Or trick Truffle by claiming we use it in a Solidity test
//
// You know which one I went for.
contract TestImports {
constructor() public {
// solium-disable-previous-line no-empty-blocks
}
}
@@ -0,0 +1,88 @@
pragma solidity 0.5.17;
import "../../Staking.sol";
import "../../lib/os/SafeMath.sol";
import "../mocks/NoApproveTokenMock.sol";
contract EchidnaStaking is Staking {
using SafeMath for uint256;
constructor() public {
stakingToken = ERC20(new NoApproveTokenMock(msg.sender, 10 ** 24));
}
// check that staked amount for an account is always >= total locked
function echidna_account_stake_locks() external view returns (bool) {
address _account = msg.sender;
Account storage account = accounts[_account];
if (_totalStakedFor(_account) < account.totalLocked) {
return false;
}
return true;
}
// TODO: delete. Fake test to check that previous echidna test works
function echidna_account_stake_locks_fake() external view returns (bool) {
address _account = msg.sender;
Account storage account = accounts[_account];
if (_totalStakedFor(_account) > account.totalLocked) {
return false;
}
return true;
}
// check that Checkpointing history arrays are ordered
function echidna_global_history_is_ordered() external view returns (bool) {
for (uint256 i = 1; i < totalStakedHistory.history.length; i++) {
if (totalStakedHistory.history[i].time <= totalStakedHistory.history[i - 1].time) {
return false;
}
}
return true;
}
function echidna_user_history_is_ordered() external view returns (bool) {
address account = msg.sender;
for (uint256 i = 1; i < accounts[account].stakedHistory.history.length; i++) {
if (accounts[account].stakedHistory.history[i].time <= accounts[account].stakedHistory.history[i - 1].time) {
return false;
}
}
return true;
}
// total staked matches less or equal than token balance
function echidna_total_staked_is_balance() external view returns (bool) {
if (_totalStaked() <= stakingToken.balanceOf(address(this))) {
return true;
}
return false;
}
function echidna_staked_ge_unlocked() external view returns (bool) {
if (_unlockedBalanceOf(msg.sender) > _totalStakedFor(msg.sender)) {
return false;
}
return true;
}
function echidna_staked_ge_locked() external view returns (bool) {
if (_lockedBalanceOf(msg.sender) > _totalStakedFor(msg.sender)) {
return false;
}
return true;
}
// sum of all account stakes should be equal to total staked and to staking token balance of staking contract, but it's hard to compute as accounts is a mapping
}
@@ -0,0 +1,27 @@
pragma solidity ^0.5.17;
/// @dev The token controller contract must implement these functions
interface ITokenController {
/// @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) external payable returns(bool);
/// @notice Notifies the controller about a token transfer allowing the
/// controller to react if desired
/// @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) external returns(bool);
/// @notice Notifies the controller about an approval allowing the
/// controller to react if desired
/// @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) external returns(bool);
}
@@ -0,0 +1,576 @@
pragma solidity ^0.5.17;
/*
Copyright 2016, Jordi Baylina
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/// @title MiniMeToken Contract
/// @author Jordi Baylina
/// @dev This token contract's goal is to make it easy for anyone to clone this
/// token using the token distribution at a given block, this will allow DAO's
/// and DApps to upgrade their features in a decentralized manner without
/// affecting the original token
/// @dev It is ERC20 compliant, but still needs to under go further testing.
import "./ITokenController.sol";
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);
_;
}
address payable public controller;
constructor() public { controller = msg.sender; }
/// @notice Changes the controller of the contract
/// @param _newController The new controller of the contract
function changeController(address payable _newController) onlyController public {
controller = _newController;
}
}
contract ApproveAndCallFallBack {
function receiveApproval(
address from,
uint256 _amount,
address _token,
bytes calldata _data
) external;
}
/// @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.1"; //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 {
// `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;
}
// `parentToken` is the Token address that was cloned to produce this token;
// it will be 0x0 for a token that was not cloned
MiniMeToken public parentToken;
// `parentSnapShotBlock` is the block number from the Parent Token that was
// used to determine the initial distribution of the Clone Token
uint public parentSnapShotBlock;
// `creationBlock` is the block number that the Clone Token was created
uint 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;
// `allowed` tracks any extra transfer rights as in all ERC20 tokens
mapping (address => mapping (address => uint256)) allowed;
// Tracks the history of the `totalSupply` of the token
Checkpoint[] totalSupplyHistory;
// Flag that determines if the token is transferable or not.
bool public transfersEnabled;
// The factory used to create new clone tokens
MiniMeTokenFactory public tokenFactory;
////////////////
// Constructor
////////////////
/// @notice Constructor to create a MiniMeToken
/// @param _tokenFactory The address of the MiniMeTokenFactory contract that
/// will create the Clone token contracts, the token factory needs to be
/// deployed first
/// @param _parentToken Address of the parent token, set to 0x0 if it is a
/// new token
/// @param _parentSnapShotBlock Block of the parent token that will
/// determine the initial distribution of the clone token, set to 0 if it
/// is a new token
/// @param _tokenName Name of the new token
/// @param _decimalUnits Number of decimals of the new token
/// @param _tokenSymbol Token Symbol for the new token
/// @param _transfersEnabled If true, tokens will be able to be transferred
constructor(
MiniMeTokenFactory _tokenFactory,
MiniMeToken _parentToken,
uint _parentSnapShotBlock,
string memory _tokenName,
uint8 _decimalUnits,
string memory _tokenSymbol,
bool _transfersEnabled
) public
{
tokenFactory = _tokenFactory;
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
///////////////////
/// @notice Send `_amount` tokens to `_to` from `msg.sender`
/// @param _to The address of the recipient
/// @param _amount The amount of tokens to be transferred
/// @return Whether the transfer was successful or not
function transfer(address _to, uint256 _amount) public returns (bool success) {
require(transfersEnabled);
return doTransfer(msg.sender, _to, _amount);
}
/// @notice Send `_amount` tokens to `_to` from `_from` on the condition it
/// is approved by `_from`
/// @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
/// @return True if the transfer was successful
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
// another open source smart contract or 0x0
if (msg.sender != controller) {
require(transfersEnabled);
// The standard ERC 20 transferFrom functionality
if (allowed[_from][msg.sender] < _amount)
return false;
allowed[_from][msg.sender] -= _amount;
}
return doTransfer(_from, _to, _amount);
}
/// @dev This is the actual transfer function in the token contract, it can
/// only be called by other functions in this contract.
/// @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
/// @return True if the transfer was successful
function doTransfer(address _from, address _to, uint _amount) internal returns(bool) {
if (_amount == 0) {
return true;
}
require(parentSnapShotBlock < block.number);
// 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 returns false
uint256 previousBalanceFrom = balanceOfAt(_from, block.number);
if (previousBalanceFrom < _amount) {
return false;
}
// Alerts the token controller of the transfer
if (isContract(controller)) {
// Adding the ` == true` makes the linter shut up so...
require(ITokenController(controller).onTransfer(_from, _to, _amount) == true);
}
// 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);
return true;
}
/// @param _owner The address that's balance is being requested
/// @return The balance of `_owner` at the current block
function balanceOf(address _owner) public view returns (uint256 balance) {
return balanceOfAt(_owner, block.number);
}
/// @notice `msg.sender` approves `_spender` to spend `_amount` tokens on
/// its behalf. This is a modified version of the ERC20 approve function
/// to be a little bit safer
/// @param _spender The address of the account able to transfer the tokens
/// @param _amount The amount of tokens to be approved for transfer
/// @return True if the approval was successful
function approve(address _spender, uint256 _amount) public returns (bool success) {
require(transfersEnabled);
// To change the approve amount you first have to reduce the addresses`
// allowance to zero by calling `approve(_spender,0)` if it is not
// already 0 to mitigate the race condition described here:
// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
require((_amount == 0) || (allowed[msg.sender][_spender] == 0));
// Alerts the token controller of the approve function call
if (isContract(controller)) {
// Adding the ` == true` makes the linter shut up so...
require(ITokenController(controller).onApprove(msg.sender, _spender, _amount) == true);
}
allowed[msg.sender][_spender] = _amount;
emit Approval(msg.sender, _spender, _amount);
return true;
}
/// @dev This function makes it easy to read the `allowed[]` map
/// @param _owner The address of the account that owns the token
/// @param _spender The address of the account able to transfer the tokens
/// @return Amount of remaining tokens of _owner that _spender is allowed
/// to spend
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
/// @notice `msg.sender` approves `_spender` to send `_amount` tokens on
/// its behalf, and then a function is triggered in the contract that is
/// being approved, `_spender`. This allows users to use their tokens to
/// interact with contracts in one function call instead of two
/// @param _spender The address of the contract able to transfer the tokens
/// @param _amount The amount of tokens to be approved for transfer
/// @return True if the function call was successful
function approveAndCall(ApproveAndCallFallBack _spender, uint256 _amount, bytes calldata _extraData) external returns (bool success) {
require(approve(address(_spender), _amount));
_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) {
return totalSupplyAt(block.number);
}
////////////////
// 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) {
// 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 (address(parentToken) != address(0)) {
return parentToken.balanceOfAt(_owner, min(_blockNumber, parentSnapShotBlock));
} else {
// Has no parent
return 0;
}
// This will return the expected balance during normal situations
} else {
return getValueAt(balances[_owner], _blockNumber);
}
}
/// @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) {
// 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 (address(parentToken) != address(0)) {
return parentToken.totalSupplyAt(min(_blockNumber, parentSnapShotBlock));
} else {
return 0;
}
// This will return the expected totalSupply during normal situations
} else {
return getValueAt(totalSupplyHistory, _blockNumber);
}
}
////////////////
// Clone Token Method
////////////////
/// @notice Creates a new clone token with the initial distribution being
/// this token at `_snapshotBlock`
/// @param _cloneTokenName Name of the clone token
/// @param _cloneDecimalUnits Number of decimals of the smallest unit
/// @param _cloneTokenSymbol Symbol of the clone token
/// @param _snapshotBlock Block when the distribution of the parent token is
/// copied to set the initial distribution of the new clone token;
/// if the block is zero than the actual block, the current block is used
/// @param _transfersEnabled True if transfers are allowed in the clone
/// @return The address of the new MiniMeToken Contract
function createCloneToken(
string calldata _cloneTokenName,
uint8 _cloneDecimalUnits,
string calldata _cloneTokenSymbol,
uint _snapshotBlock,
bool _transfersEnabled
) external returns(MiniMeToken)
{
uint256 snapshot = _snapshotBlock == 0 ? block.number - 1 : _snapshotBlock;
MiniMeToken cloneToken = tokenFactory.createCloneToken(
this,
snapshot,
_cloneTokenName,
_cloneDecimalUnits,
_cloneTokenSymbol,
_transfersEnabled
);
cloneToken.changeController(msg.sender);
// An event to make the token easy to find on the blockchain
emit NewCloneToken(address(cloneToken), snapshot);
return cloneToken;
}
////////////////
// 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) onlyController public returns (bool) {
uint curTotalSupply = totalSupply();
require(curTotalSupply + _amount >= curTotalSupply); // Check for overflow
uint previousBalanceTo = balanceOf(_owner);
require(previousBalanceTo + _amount >= previousBalanceTo); // 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();
require(curTotalSupply >= _amount);
uint previousBalanceFrom = balanceOf(_owner);
require(previousBalanceFrom >= _amount);
updateValueAtNow(totalSupplyHistory, curTotalSupply - _amount);
updateValueAtNow(balances[_owner], previousBalanceFrom - _amount);
emit Transfer(_owner, address(0), _amount);
return true;
}
////////////////
// Enable tokens transfers
////////////////
/// @notice Enables token holders to transfer their tokens freely if true
/// @param _transfersEnabled True if transfers are allowed in the clone
function enableTransfers(bool _transfersEnabled) onlyController public {
transfersEnabled = _transfersEnabled;
}
////////////////
// 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) {
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[0].fromBlock)
return 0;
// Binary search of the value in the array
uint min = 0;
uint max = checkpoints.length-1;
while (max > min) {
uint mid = (max + min + 1) / 2;
if (checkpoints[mid].fromBlock<=_block) {
min = mid;
} else {
max = mid-1;
}
}
return checkpoints[min].value;
}
/// @dev `updateValueAtNow` used to update the `balances` map and the
/// `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[checkpoints.length++];
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;
if (_addr == address(0))
return false;
assembly {
size := extcodesize(_addr)
}
return size>0;
}
/// @dev Helper function to return a min betwen the two uints
function min(uint a, uint b) pure internal returns (uint) {
return a < b ? a : b;
}
/// @notice The fallback function: If the contract's controller has not been
/// set to 0, then the `proxyPayment` method is called which relays the
/// ether and creates tokens as described in the token controller contract
function () external payable {
require(isContract(controller));
// Adding the ` == true` makes the linter shut up so...
require(ITokenController(controller).proxyPayment.value(msg.value)(msg.sender) == true);
}
//////////
// 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(address payable _token) onlyController public {
if (_token == address(0)) {
controller.transfer(address(this).balance);
return;
}
MiniMeToken token = MiniMeToken(_token);
uint balance = token.balanceOf(address(this));
token.transfer(controller, balance);
emit ClaimedTokens(_token, controller, balance);
}
////////////////
// Events
////////////////
event ClaimedTokens(address indexed _token, address indexed _controller, uint _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
);
}
////////////////
// MiniMeTokenFactory
////////////////
/// @dev This contract is used to generate clone contracts from a contract.
/// 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
/// @param _snapshotBlock Block of the parent token that will
/// determine the initial distribution of the clone token
/// @param _tokenName Name of the new token
/// @param _decimalUnits Number of decimals of the new token
/// @param _tokenSymbol Token Symbol for the new token
/// @param _transfersEnabled If true, tokens will be able to be transferred
/// @return The address of the new token contract
function createCloneToken(
MiniMeToken _parentToken,
uint _snapshotBlock,
string calldata _tokenName,
uint8 _decimalUnits,
string calldata _tokenSymbol,
bool _transfersEnabled
) external returns (MiniMeToken)
{
MiniMeToken newToken = new MiniMeToken(
this,
_parentToken,
_snapshotBlock,
_tokenName,
_decimalUnits,
_tokenSymbol,
_transfersEnabled
);
newToken.changeController(msg.sender);
return newToken;
}
}
@@ -0,0 +1,218 @@
// Copied from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
// transfer function always returns false!
pragma solidity 0.5.17;
//import "./ERC20.sol";
import "../../lib/os/ERC20.sol";
import "../../lib/os/SafeMath.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract BadTokenMock is ERC20 {
using SafeMath for uint256;
mapping (address => uint256) private balances;
mapping (address => mapping (address => uint256)) private allowed;
uint256 private totalSupply_;
constructor(address initialAccount, uint256 initialBalance) public {
balances[initialAccount] = initialBalance;
totalSupply_ = initialBalance;
}
function mint (address account, uint256 amount) public {
balances[account] = balances[account].add(amount);
totalSupply_ = totalSupply_.add(amount);
}
/**
* @dev Total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}
/**
* @dev Transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_value <= balances[msg.sender]);
require(_to != address(0));
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return false; // <--- Bad Token!!
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
require(_to != address(0));
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(
address _spender,
uint256 _addedValue
)
public
returns (bool)
{
allowed[msg.sender][_spender] = (
allowed[msg.sender][_spender].add(_addedValue));
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(
address _spender,
uint256 _subtractedValue
)
public
returns (bool)
{
uint256 oldValue = allowed[msg.sender][_spender];
if (_subtractedValue >= oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Internal function that mints an amount of the token and assigns it to
* an account. This encapsulates the modification of balances such that the
* proper events are emitted.
* @param _account The account that will receive the created tokens.
* @param _amount The amount that will be created.
*/
function _mint(address _account, uint256 _amount) internal {
require(_account != address(0));
totalSupply_ = totalSupply_.add(_amount);
balances[_account] = balances[_account].add(_amount);
emit Transfer(address(0), _account, _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burn(address _account, uint256 _amount) internal {
require(_account != address(0));
require(_amount <= balances[_account]);
totalSupply_ = totalSupply_.sub(_amount);
balances[_account] = balances[_account].sub(_amount);
emit Transfer(_account, address(0), _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account, deducting from the sender's allowance for said account. Uses the
* internal _burn function.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burnFrom(address _account, uint256 _amount) internal {
require(_amount <= allowed[_account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
allowed[_account][msg.sender] = allowed[_account][msg.sender].sub(_amount);
_burn(_account, _amount);
}
}
@@ -0,0 +1,30 @@
pragma solidity 0.5.17;
import "../../lib/Checkpointing.sol";
contract CheckpointingMock {
using Checkpointing for Checkpointing.History;
Checkpointing.History history;
function add(uint64 value, uint256 time) public {
history.add(value, time);
}
function getLast() public view returns (uint256) {
return history.getLast();
}
function get(uint64 time) public view returns (uint256) {
return history.get(time);
}
function getHistorySize() public view returns (uint256) {
return history.history.length;
}
function lastUpdate() public view returns (uint256) {
return history.lastUpdate();
}
}
@@ -0,0 +1,26 @@
pragma solidity ^0.5.17;
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 {
function totalSupply() public view returns (uint256);
function balanceOf(address _who) public view returns (uint256);
function allowance(address _owner, address _spender)
public view returns (uint256);
function transfer(address _to, uint256 _value) public returns (bool);
function approve(address _spender, uint256 _value)
public returns (bool);
function transferFrom(address _from, address _to, uint256 _value)
public returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
@@ -0,0 +1,41 @@
pragma solidity 0.5.17;
import "../../locking/ILockManager.sol";
import "../../Staking.sol";
contract LockManagerMock is ILockManager {
bool result;
function slash(Staking _staking, address _from, address _to, uint256 _amount) external {
_staking.slash(_from, _to, _amount);
}
function slashAndUnstake(Staking _staking, address _from, address _to, uint256 _amount) external {
_staking.slashAndUnstake(_from, _to, _amount);
}
function unlock(Staking _staking, address _account, uint256 _amount) external {
_staking.unlock(_account, address(this), _amount);
}
function unlockAndRemoveManager(Staking _staking, address _account) external {
_staking.unlockAndRemoveManager(_account, address(this));
}
function setLockManager(Staking _staking, address _account, ILockManager _newManager) external {
_staking.setLockManager(_account, address(_newManager));
}
function canUnlock(address, uint256) external view returns (bool) {
return result;
}
function setResult(bool _result) public {
result = _result;
}
function unlockAndRemoveManager(Staking _staking, address _account, address _manager) public {
_staking.unlockAndRemoveManager(_account, _manager);
}
}
@@ -0,0 +1,215 @@
// Copied from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
pragma solidity 0.5.17;
import "../../lib/os/ERC20.sol";
import "../../lib/os/SafeMath.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract NoApproveTokenMock is ERC20 {
using SafeMath for uint256;
mapping (address => uint256) private balances;
mapping (address => mapping (address => uint256)) private allowed;
uint256 private totalSupply_;
constructor(address initialAccount, uint256 initialBalance) public {
balances[initialAccount] = initialBalance;
totalSupply_ = initialBalance;
}
function mint (address account, uint256 amount) public {
balances[account] = balances[account].add(amount);
totalSupply_ = totalSupply_.add(amount);
}
/**
* @dev Total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}
/**
* @dev Transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_value <= balances[msg.sender]);
require(_to != address(0));
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_value <= balances[_from]);
//require(_value <= allowed[_from][msg.sender]);
require(_to != address(0));
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
//allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(
address _spender,
uint256 _addedValue
)
public
returns (bool)
{
allowed[msg.sender][_spender] = (
allowed[msg.sender][_spender].add(_addedValue));
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(
address _spender,
uint256 _subtractedValue
)
public
returns (bool)
{
uint256 oldValue = allowed[msg.sender][_spender];
if (_subtractedValue >= oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Internal function that mints an amount of the token and assigns it to
* an account. This encapsulates the modification of balances such that the
* proper events are emitted.
* @param _account The account that will receive the created tokens.
* @param _amount The amount that will be created.
*/
function _mint(address _account, uint256 _amount) internal {
require(_account != address(0));
totalSupply_ = totalSupply_.add(_amount);
balances[_account] = balances[_account].add(_amount);
emit Transfer(address(0), _account, _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burn(address _account, uint256 _amount) internal {
require(_account != address(0));
require(_amount <= balances[_account]);
totalSupply_ = totalSupply_.sub(_amount);
balances[_account] = balances[_account].sub(_amount);
emit Transfer(_account, address(0), _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account, deducting from the sender's allowance for said account. Uses the
* internal _burn function.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burnFrom(address _account, uint256 _amount) internal {
require(_amount <= allowed[_account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
allowed[_account][msg.sender] = allowed[_account][msg.sender].sub(_amount);
_burn(_account, _amount);
}
}
@@ -0,0 +1,54 @@
pragma solidity 0.5.17;
import "../../Staking.sol";
import "../../lib/os/SafeMath.sol";
import "./TimeHelpersMock.sol";
contract StakingMock is Staking, TimeHelpersMock {
using SafeMath for uint256;
event LogGas(uint256 gas);
string private constant ERROR_TOKEN_NOT_CONTRACT = "STAKING_TOKEN_NOT_CONTRACT";
uint64 private constant MAX_UINT64 = uint64(-1);
modifier measureGas {
uint256 initialGas = gasleft();
_;
emit LogGas(initialGas - gasleft());
}
constructor(ERC20 _stakingToken) public {
require(isContract(address(_stakingToken)), ERROR_TOKEN_NOT_CONTRACT);
initialized();
stakingToken = _stakingToken;
}
function unlockedBalanceOfGas() external returns (uint256) {
uint256 initialGas = gasleft();
_unlockedBalanceOf(msg.sender);
uint256 gasConsumed = initialGas - gasleft();
emit LogGas(gasConsumed);
return gasConsumed;
}
function transferGas(address _to, address, uint256 _amount) external measureGas {
// have enough unlocked funds
require(_amount <= _unlockedBalanceOf(msg.sender));
_transfer(msg.sender, _to, _amount);
}
function setBlockNumber(uint64 _mockedBlockNumber) public {
mockedBlockNumber = _mockedBlockNumber;
}
// Override petrify functions to allow mocking the initialization process
function petrify() internal onlyInit {
// solium-disable-previous-line no-empty-blocks
// initializedAt(PETRIFIED_BLOCK);
}
}
@@ -0,0 +1,215 @@
// Copied from https://github.com/OpenZeppelin/openzeppelin-solidity/blob/a9f910d34f0ab33a1ae5e714f69f9596a02b4d91/contracts/token/ERC20/StandardToken.sol
pragma solidity 0.5.17;
import "../../lib/os/ERC20.sol";
import "../../lib/os/SafeMath.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract StandardTokenMock is ERC20 {
using SafeMath for uint256;
mapping (address => uint256) private balances;
mapping (address => mapping (address => uint256)) private allowed;
uint256 private totalSupply_;
constructor(address initialAccount, uint256 initialBalance) public {
balances[initialAccount] = initialBalance;
totalSupply_ = initialBalance;
}
function mint (address account, uint256 amount) public {
balances[account] = balances[account].add(amount);
totalSupply_ = totalSupply_.add(amount);
}
/**
* @dev Total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}
/**
* @dev Transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_value <= balances[msg.sender]);
require(_to != address(0));
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
require(_to != address(0));
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(
address _spender,
uint256 _addedValue
)
public
returns (bool)
{
allowed[msg.sender][_spender] = (
allowed[msg.sender][_spender].add(_addedValue));
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(
address _spender,
uint256 _subtractedValue
)
public
returns (bool)
{
uint256 oldValue = allowed[msg.sender][_spender];
if (_subtractedValue >= oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Internal function that mints an amount of the token and assigns it to
* an account. This encapsulates the modification of balances such that the
* proper events are emitted.
* @param _account The account that will receive the created tokens.
* @param _amount The amount that will be created.
*/
function _mint(address _account, uint256 _amount) internal {
require(_account != address(0));
totalSupply_ = totalSupply_.add(_amount);
balances[_account] = balances[_account].add(_amount);
emit Transfer(address(0), _account, _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burn(address _account, uint256 _amount) internal {
require(_account != address(0));
require(_amount <= balances[_account]);
totalSupply_ = totalSupply_.sub(_amount);
balances[_account] = balances[_account].sub(_amount);
emit Transfer(_account, address(0), _amount);
}
/**
* @dev Internal function that burns an amount of the token of a given
* account, deducting from the sender's allowance for said account. Uses the
* internal _burn function.
* @param _account The account whose tokens will be burnt.
* @param _amount The amount that will be burnt.
*/
function _burnFrom(address _account, uint256 _amount) internal {
require(_amount <= allowed[_account][msg.sender]);
// Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
// this function needs to emit an event with the updated approval.
allowed[_account][msg.sender] = allowed[_account][msg.sender].sub(_amount);
_burn(_account, _amount);
}
}
@@ -0,0 +1,75 @@
pragma solidity ^0.5.17;
import "../../lib/os/TimeHelpers.sol";
import "../..//lib/os/SafeMath.sol";
import "../..//lib/os/SafeMath64.sol";
contract TimeHelpersMock is TimeHelpers {
using SafeMath for uint256;
using SafeMath64 for uint64;
uint256 public mockedTimestamp;
uint256 public mockedBlockNumber;
/**
* @dev Sets a mocked timestamp value, used only for testing purposes
*/
function mockSetTimestamp(uint256 _timestamp) external {
mockedTimestamp = _timestamp;
}
/**
* @dev Increases the mocked timestamp value, used only for testing purposes
*/
function mockIncreaseTime(uint256 _seconds) external {
if (mockedTimestamp != 0) mockedTimestamp = mockedTimestamp.add(_seconds);
else mockedTimestamp = block.timestamp.add(_seconds);
}
/**
* @dev Decreases the mocked timestamp value, used only for testing purposes
*/
function mockDecreaseTime(uint256 _seconds) external {
if (mockedTimestamp != 0) mockedTimestamp = mockedTimestamp.sub(_seconds);
else mockedTimestamp = block.timestamp.sub(_seconds);
}
/**
* @dev Advances the mocked block number value, used only for testing purposes
*/
function mockAdvanceBlocks(uint256 _number) external {
if (mockedBlockNumber != 0) mockedBlockNumber = mockedBlockNumber.add(_number);
else mockedBlockNumber = block.number.add(_number);
}
/**
* @dev Returns the mocked timestamp value
*/
function getTimestampPublic() external view returns (uint64) {
return getTimestamp64();
}
/**
* @dev Returns the mocked block number value
*/
function getBlockNumberPublic() external view returns (uint256) {
return getBlockNumber();
}
/**
* @dev Returns the mocked timestamp if it was set, or current `block.timestamp`
*/
function getTimestamp() internal view returns (uint256) {
if (mockedTimestamp != 0) return mockedTimestamp;
return super.getTimestamp();
}
/**
* @dev Returns the mocked block number if it was set, or current `block.number`
*/
function getBlockNumber() internal view returns (uint256) {
if (mockedBlockNumber != 0) return mockedBlockNumber;
return super.getBlockNumber();
}
}
@@ -0,0 +1,36 @@
pragma solidity 0.5.17;
import "../../locking/TimeLockManager.sol";
import "../../Staking.sol";
contract TimeLockManagerMock is TimeLockManager {
uint64 public constant MAX_UINT64 = uint64(-1);
uint256 _mockTime = now;
uint256 _mockBlockNumber = block.number;
function getTimestampExt() external view returns (uint256) {
return getTimestamp();
}
function getBlockNumberExt() external view returns (uint256) {
return getBlockNumber();
}
function setTimestamp(uint256 i) public {
_mockTime = i;
}
function setBlockNumber(uint256 i) public {
_mockBlockNumber = i;
}
function getTimestamp() internal view returns (uint256) {
return _mockTime;
}
function getBlockNumber() internal view returns (uint256) {
return _mockBlockNumber;
}
}
@@ -0,0 +1,18 @@
{
"name": "staking",
"version": "1.0.0",
"author": "Aragon Association <contact@aragon.org>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"@aragon/contract-helpers-test": "^0.0.3",
"chai": "^4.2.0",
"ganache-cli": "^6.1.0",
"truffle": "^5.1.42",
"web3-eth-abi": "^1.2.11",
"web3-utils": "^1.2.11"
}
}
@@ -0,0 +1,52 @@
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
const { bn, assertBn } = require('@aragon/contract-helpers-test/numbers')
const { DEFAULT_STAKE_AMOUNT, EMPTY_DATA, ZERO_ADDRESS } = require('./helpers/constants')
const { STAKING_ERRORS } = require('./helpers/errors')
const StakingMock = artifacts.require('StakingMock')
const MiniMeToken = artifacts.require('MiniMeToken')
contract('Staking app, Approve and call fallback', ([owner, user]) => {
let staking, token, stakingAddress, tokenAddress
beforeEach(async () => {
const initialAmount = DEFAULT_STAKE_AMOUNT.mul(bn(1000))
const tokenContract = await MiniMeToken.new(ZERO_ADDRESS, ZERO_ADDRESS, 0, 'Test Token', 18, 'TT', true)
token = tokenContract
tokenAddress = tokenContract.address
await token.generateTokens(user, DEFAULT_STAKE_AMOUNT)
const stakingContract = await StakingMock.new(tokenAddress)
staking = stakingContract
stakingAddress = stakingContract.address
})
it('stakes through approveAndCall', async () => {
const initialUserBalance = await token.balanceOf(user)
const initialStakingBalance = await token.balanceOf(stakingAddress)
await token.approveAndCall(stakingAddress, DEFAULT_STAKE_AMOUNT, EMPTY_DATA, { from: user })
const finalUserBalance = await token.balanceOf(user)
const finalStakingBalance = await token.balanceOf(stakingAddress)
assertBn(finalUserBalance, initialUserBalance.sub(DEFAULT_STAKE_AMOUNT), "user balance should match")
assertBn(finalStakingBalance, initialStakingBalance.add(DEFAULT_STAKE_AMOUNT), "Staking app balance should match")
assertBn(await staking.totalStakedFor(user), DEFAULT_STAKE_AMOUNT, "staked value should match")
// total stake
assertBn(await staking.totalStaked(), DEFAULT_STAKE_AMOUNT, "Total stake should match")
})
it('fails staking 0 amount through approveAndCall', async () => {
await assertRevert(token.approveAndCall(stakingAddress, 0, EMPTY_DATA, { from: user })/*, STAKING_ERRORS.ERROR_AMOUNT_ZERO*/)
})
it('fails calling approveAndCall on a different token', async () => {
const token2 = await MiniMeToken.new(ZERO_ADDRESS, ZERO_ADDRESS, 0, 'Test Token 2', 18, 'TT2', true)
await token2.generateTokens(user, DEFAULT_STAKE_AMOUNT)
await assertRevert(token2.approveAndCall(stakingAddress, 0, EMPTY_DATA, { from: user })/*, STAKING_ERRORS.ERROR_WRONG_TOKEN*/)
})
it('fails calling receiveApproval from a different account than the token', async () => {
await assertRevert(staking.receiveApproval(user, DEFAULT_STAKE_AMOUNT, tokenAddress, EMPTY_DATA)/*, STAKING_ERRORS.ERROR_TOKEN_NOT_SENDER*/)
})
})
+83
View File
@@ -0,0 +1,83 @@
const { MAX_UINT64 } = require('@aragon/contract-helpers-test/numbers')
const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] }
const { deploy } = require('./helpers/deploy')(artifacts)
const { DEFAULT_STAKE_AMOUNT, DEFAULT_LOCK_AMOUNT, EMPTY_DATA, ZERO_ADDRESS, ACTIVATED_LOCK } = require('./helpers/constants')
contract.skip('Staking app, gas measures', accounts => {
let staking, token, lockManager, stakingAddress, tokenAddress, lockManagerAddress
let owner, user1, user2
const approveAndStake = async (amount = DEFAULT_STAKE_AMOUNT, from = owner) => {
await token.approve(stakingAddress, amount, { from })
await staking.stake(amount, EMPTY_DATA, { from })
}
const approveStakeAndLock = async (
manager,
lockAmount = DEFAULT_LOCK_AMOUNT,
stakeAmount = DEFAULT_STAKE_AMOUNT,
from = owner
) => {
await approveAndStake(stakeAmount, from)
await staking.allowManagerAndLock(lockAmount, manager, lockAmount, ACTIVATED_LOCK, { from })
}
before(async () => {
owner = accounts[0]
user1 = accounts[1]
user2 = accounts[2]
})
beforeEach(async () => {
const deployment = await deploy(owner)
token = deployment.token
staking = deployment.staking
lockManager = deployment.lockManager
stakingAddress = staking.address
tokenAddress = token.address
lockManagerAddress = lockManager.address
})
// increases 1185 gas for each lock
it('measures unlockedBalanceOf gas', async () => {
await approveStakeAndLock(lockManagerAddress)
const r = await staking.unlockedBalanceOfGas()
const gas = getEvent(r, 'LogGas', 'gas')
console.log(`unlockedBalanceOf gas: ${gas.toNumber()}`)
})
// 110973 gas
/*
it('measures lock gas', async () => {
await approveAndStake()
const r = await staking.lockGas(DEFAULT_LOCK_AMOUNT, lockManagerAddress, ACTIVATED_LOCK, { from: owner })
const gas = getEvent(r, 'LogGas', 'gas')
console.log('lock gas:', gas.toNumber())
})
*/
// 27601 gas
it('measures transfer gas', async () => {
await approveStakeAndLock(lockManagerAddress)
const r = await staking.transferGas(owner, lockManagerAddress, DEFAULT_LOCK_AMOUNT)
const gas = getEvent(r, 'LogGas', 'gas')
console.log('transfer gas:', gas.toNumber())
})
/*
it('measures unlock gas', async () => {
await approveStakeAndLock(user1)
const r = await staking.unlockGas(owner, user1, { from: user1 })
const gas = getEvent(r, 'LogGas', 'gas')
console.log(`unlock gas: ${gas.toNumber()}`)
await approveStakeAndLock(lockManagerAddress)
})
*/
})
@@ -0,0 +1,10 @@
const { bn, bigExp } = require('@aragon/contract-helpers-test/numbers')
const DEFAULT_STAKE_AMOUNT = bigExp(120, 18)
module.exports = {
DEFAULT_STAKE_AMOUNT,
DEFAULT_LOCK_AMOUNT: DEFAULT_STAKE_AMOUNT.div(bn(3)),
EMPTY_DATA: '0x',
ZERO_ADDRESS: '0x' + '0'.repeat(40),
ACTIVATED_LOCK: '0x01'
}
@@ -0,0 +1,35 @@
const { bn } = require('@aragon/contract-helpers-test/numbers')
const { DEFAULT_STAKE_AMOUNT } = require('./constants')
module.exports = (artifacts) => {
const StakingFactory = artifacts.require('StakingFactory')
const Staking = artifacts.require('Staking')
const StandardTokenMock = artifacts.require('StandardTokenMock')
const LockManagerMock = artifacts.require('LockManagerMock')
const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event === event)[0].args[arg] }
const deploy = async (owner, initialAmount = DEFAULT_STAKE_AMOUNT.mul(bn(1000))) => {
const token = await StandardTokenMock.new(owner, initialAmount)
const staking = await deployStaking(token)
const lockManager = await LockManagerMock.new()
return { token, staking, lockManager }
}
const deployStaking = async (token) => {
const factory = await StakingFactory.new()
const receipt = await factory.getOrCreateInstance(token.address)
const stakingAddress = getEvent(receipt, 'NewStaking', 'instance')
const staking = await Staking.at(stakingAddress)
return staking
}
return {
deploy
}
}
@@ -0,0 +1,35 @@
const CHECKPOINT_ERRORS = {
ERROR_VALUE_TOO_BIG: 'CHECKPOINT_VALUE_TOO_BIG',
ERROR_CANNOT_ADD_PAST_VALUE: 'CHECKPOINT_CANNOT_ADD_PAST_VALUE',
}
const STAKING_ERRORS = {
ERROR_TOKEN_NOT_CONTRACT: 'STAKING_TOKEN_NOT_CONTRACT',
ERROR_AMOUNT_ZERO: 'STAKING_AMOUNT_ZERO',
ERROR_TOKEN_TRANSFER: 'STAKING_TOKEN_TRANSFER_FAIL',
ERROR_TOKEN_DEPOSIT: 'STAKING_TOKEN_DEPOSIT_FAIL',
ERROR_TOKEN_NOT_SENDER: 'STAKING_TOKEN_NOT_SENDER',
ERROR_WRONG_TOKEN: 'STAKING_WRONG_TOKEN',
ERROR_NOT_ENOUGH_BALANCE: 'STAKING_NOT_ENOUGH_BALANCE',
ERROR_NOT_ENOUGH_ALLOWANCE: 'STAKING_NOT_ENOUGH_ALLOWANCE',
ERROR_SENDER_NOT_ALLOWED: 'STAKING_SENDER_NOT_ALLOWED',
ERROR_ALLOWANCE_ZERO: 'STAKING_ALLOWANCE_ZERO',
ERROR_LOCK_ALREADY_EXISTS: 'STAKING_LOCK_ALREADY_EXISTS',
ERROR_LOCK_DOES_NOT_EXIST: 'STAKING_LOCK_DOES_NOT_EXIST',
ERROR_NOT_ENOUGH_LOCK: 'STAKING_NOT_ENOUGH_LOCK',
ERROR_CANNOT_UNLOCK: 'STAKING_CANNOT_UNLOCK',
ERROR_CANNOT_CHANGE_ALLOWANCE: 'STAKING_CANNOT_CHANGE_ALLOWANCE',
ERROR_LOCKMANAGER_CALL_FAIL: 'STAKING_LOCKMANAGER_CALL_FAIL',
ERROR_BLOCKNUMBER_TOO_BIG: 'STAKING_BLOCKNUMBER_TOO_BIG',
}
const TIME_LOCK_MANAGER_ERRORS = {
ERROR_ALREADY_LOCKED: 'TLM_ALREADY_LOCKED',
ERROR_WRONG_INTERVAL: 'TLM_WRONG_INTERVAL',
}
module.exports = {
CHECKPOINT_ERRORS,
STAKING_ERRORS,
TIME_LOCK_MANAGER_ERRORS,
}

Some files were not shown because too many files have changed in this diff Show More