c9639c3860
* tests: add solidity test suite * tests: remove require strings * Update tests-solidity/init-test-node.sh * Update tests-solidity/init-test-node.sh Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
284 lines
11 KiB
JavaScript
284 lines
11 KiB
JavaScript
const { assertRevert } = require('@aragon/contract-helpers-test/assertThrow')
|
||
const { bn, assertBn } = require('@aragon/contract-helpers-test/numbers')
|
||
|
||
const { DEFAULT_STAKE_AMOUNT, DEFAULT_LOCK_AMOUNT, EMPTY_DATA, ZERO_ADDRESS } = require('./constants')
|
||
const { STAKING_ERRORS } = require('../helpers/errors')
|
||
|
||
module.exports = (artifacts) => {
|
||
const StandardTokenMock = artifacts.require('StandardTokenMock')
|
||
const LockManagerMock = artifacts.require('LockManagerMock')
|
||
|
||
const approveAndStake = async ({ staking, amount = DEFAULT_STAKE_AMOUNT, from }) => {
|
||
const token = await StandardTokenMock.at(await staking.token())
|
||
await token.approve(staking.address, amount, { from })
|
||
await staking.stake(amount, EMPTY_DATA, { from })
|
||
}
|
||
|
||
const approveStakeAndLock = async ({
|
||
staking,
|
||
manager,
|
||
allowanceAmount = DEFAULT_LOCK_AMOUNT,
|
||
lockAmount = DEFAULT_LOCK_AMOUNT,
|
||
stakeAmount = DEFAULT_STAKE_AMOUNT,
|
||
data = EMPTY_DATA,
|
||
from
|
||
}) => {
|
||
await approveAndStake({ staking, stake: stakeAmount, from })
|
||
const receipt = await staking.allowManagerAndLock(lockAmount, manager, allowanceAmount, data, { from })
|
||
|
||
return receipt
|
||
}
|
||
|
||
// funds flows helpers
|
||
|
||
function UserState(address, walletBalance) {
|
||
this.address = address
|
||
this.walletBalance = walletBalance
|
||
this.stakedBalance = bn(0)
|
||
this.lockedBalance = bn(0)
|
||
|
||
this.walletAdd = (amount) => { this.walletBalance = this.walletBalance.add(amount) }
|
||
this.walletSub = (amount) => { this.walletBalance = this.walletBalance.sub(amount) }
|
||
|
||
this.stakedAdd = (amount) => { this.stakedBalance = this.stakedBalance.add(amount) }
|
||
this.stakedSub = (amount) => { this.stakedBalance = this.stakedBalance.sub(amount) }
|
||
|
||
this.lockedAdd = (amount) => { this.lockedBalance = this.lockedBalance.add(amount) }
|
||
this.lockedSub = (amount) => { this.lockedBalance = this.lockedBalance.sub(amount) }
|
||
|
||
this.totalBalance = () => this.walletBalance.add(this.stakedBalance)
|
||
}
|
||
|
||
const approveAndStakeWithState = async ({ staking, amount = DEFAULT_STAKE_AMOUNT, user }) => {
|
||
await approveAndStake({ staking, amount, from: user.address })
|
||
user.walletSub(amount)
|
||
user.stakedAdd(amount)
|
||
}
|
||
|
||
const approveStakeAndLockWithState = async ({
|
||
staking,
|
||
manager,
|
||
allowanceAmount = DEFAULT_LOCK_AMOUNT,
|
||
lockAmount = DEFAULT_LOCK_AMOUNT,
|
||
stakeAmount = DEFAULT_STAKE_AMOUNT,
|
||
data = EMPTY_DATA,
|
||
user
|
||
}) => {
|
||
await approveStakeAndLock({ staking, manager, allowanceAmount, lockAmount, stakeAmount, data, from: user.address })
|
||
user.walletSub(stakeAmount)
|
||
user.stakedAdd(stakeAmount)
|
||
user.lockedAdd(lockAmount)
|
||
}
|
||
|
||
const unstakeWithState = async ({ staking, unstakeAmount, user }) => {
|
||
await staking.unstake(unstakeAmount, EMPTY_DATA, { from: user.address })
|
||
user.walletAdd(unstakeAmount)
|
||
user.stakedSub(unstakeAmount)
|
||
}
|
||
|
||
const unlockWithState = async ({ staking, managerAddress, unlockAmount, user }) => {
|
||
await staking.unlock(user.address, managerAddress, unlockAmount, { from: user.address })
|
||
user.lockedSub(unlockAmount)
|
||
}
|
||
|
||
const unlockFromManagerWithState = async ({ staking, lockManager, unlockAmount, user }) => {
|
||
await lockManager.unlock(staking.address, user.address, unlockAmount, { from: user.address })
|
||
user.lockedSub(unlockAmount)
|
||
}
|
||
|
||
const transferWithState = async ({ staking, transferAmount, userFrom, userTo }) => {
|
||
await staking.transfer(userTo.address, transferAmount, { from: userFrom.address })
|
||
userTo.stakedAdd(transferAmount)
|
||
userFrom.stakedSub(transferAmount)
|
||
}
|
||
|
||
const transferAndUnstakeWithState = async ({ staking, transferAmount, userFrom, userTo }) => {
|
||
await staking.transferAndUnstake(userTo.address, transferAmount, { from: userFrom.address })
|
||
userTo.walletAdd(transferAmount)
|
||
userFrom.stakedSub(transferAmount)
|
||
}
|
||
|
||
const slashWithState = async ({ staking, slashAmount, userFrom, userTo, managerAddress }) => {
|
||
await staking.slash(userFrom.address, userTo.address, slashAmount, { from: managerAddress })
|
||
userTo.stakedAdd(slashAmount)
|
||
userFrom.stakedSub(slashAmount)
|
||
userFrom.lockedSub(slashAmount)
|
||
}
|
||
|
||
const slashAndUnstakeWithState = async ({ staking, slashAmount, userFrom, userTo, managerAddress }) => {
|
||
await staking.slashAndUnstake(userFrom.address, userTo.address, slashAmount, { from: managerAddress })
|
||
userTo.walletAdd(slashAmount)
|
||
userFrom.stakedSub(slashAmount)
|
||
userFrom.lockedSub(slashAmount)
|
||
}
|
||
|
||
const slashFromContractWithState = async ({ staking, slashAmount, userFrom, userTo, lockManager }) => {
|
||
await lockManager.slash(staking.address, userFrom.address, userTo.address, slashAmount)
|
||
userTo.stakedAdd(slashAmount)
|
||
userFrom.stakedSub(slashAmount)
|
||
userFrom.lockedSub(slashAmount)
|
||
}
|
||
|
||
const slashAndUnstakeFromContractWithState = async ({ staking, slashAmount, userFrom, userTo, lockManager }) => {
|
||
await lockManager.slashAndUnstake(staking.address, userFrom.address, userTo.address, slashAmount)
|
||
userTo.walletAdd(slashAmount)
|
||
userFrom.stakedSub(slashAmount)
|
||
userFrom.lockedSub(slashAmount)
|
||
}
|
||
|
||
// check that real user balances (token in external wallet, staked and locked) match with accounted in state
|
||
const checkUserBalances = async ({ staking, users }) => {
|
||
const token = await StandardTokenMock.at(await staking.token())
|
||
await Promise.all(
|
||
users.map(async (user) => {
|
||
assertBn(user.walletBalance, await token.balanceOf(user.address), 'token balance doesn’t match')
|
||
const balances = await staking.getBalancesOf(user.address)
|
||
assertBn(user.stakedBalance, balances.staked, 'staked balance doesn’t match')
|
||
assertBn(user.lockedBalance, balances.locked, 'locked balance doesn’t match')
|
||
})
|
||
)
|
||
}
|
||
|
||
// check that Staking contract total staked matches with:
|
||
// - total staked by users in state (must go in combination with checkUserBalances, to make sure this is legit)
|
||
// - token balance of staking app
|
||
const checkTotalStaked = async ({ staking, users }) => {
|
||
const totalStaked = await staking.totalStaked()
|
||
const totalStakedState = users.reduce((total, user) => total.add(user.stakedBalance), bn(0))
|
||
assertBn(totalStaked, totalStakedState, 'total staked doesn’t match')
|
||
const token = await StandardTokenMock.at(await staking.token())
|
||
const stakingTokenBalance = await token.balanceOf(staking.address)
|
||
assertBn(totalStaked, stakingTokenBalance, 'Staking token balance doesn’t match')
|
||
}
|
||
|
||
// check that staked balance is greater than locked balance for all users
|
||
// uses local state for efficiency, so it must go with checkUserBalances
|
||
const checkStakeAndLock = ({ staking, users }) => {
|
||
users.map(user => assert.isTrue(user.stakedBalance.gte(user.lockedBalance)))
|
||
}
|
||
|
||
// check that allowed balance is always greater than locked balance, for all pairs of owner-manager
|
||
const checkAllowanceAndLock = async ({ staking, users, managers }) => {
|
||
await Promise.all(
|
||
users.map(async (user) => await Promise.all(
|
||
managers.map(async (manager) => {
|
||
const lock = await staking.getLock(user.address, manager)
|
||
assert.isTrue(lock._amount.lte(lock._allowance))
|
||
})
|
||
))
|
||
)
|
||
}
|
||
|
||
// check that users can’t unstake more than unlocked balance
|
||
const checkOverUnstaking = async ({ staking, users }) => {
|
||
await Promise.all(
|
||
users.map(async (user) => {
|
||
await assertRevert(
|
||
staking.unstake(user.stakedBalance.sub(user.lockedBalance).add(bn(1)), EMPTY_DATA, { from: user.address })/*,
|
||
STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE
|
||
*/
|
||
)
|
||
})
|
||
)
|
||
}
|
||
|
||
// check that users can’t unlock more than locked balance
|
||
const checkOverUnlocking = async ({ staking, users, managers }) => {
|
||
await Promise.all(
|
||
users.map(async (user) => await Promise.all(
|
||
managers.map(async (manager) => {
|
||
const lock = await staking.getLock(user.address, manager)
|
||
// const errorMessage = lock._allowance.gt(bn(0)) ? STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK : STAKING_ERRORS.ERROR_LOCK_DOES_NOT_EXIST
|
||
await assertRevert(
|
||
staking.unlock(user.address, manager, user.lockedBalance.add(bn(1)), { from: user.address })/*,
|
||
errorMessage
|
||
*/
|
||
)
|
||
})
|
||
))
|
||
)
|
||
}
|
||
|
||
// check that users can’t transfer more than unlocked balance
|
||
const checkOverTransferring = async ({ staking, users }) => {
|
||
await Promise.all(
|
||
users.map(async (user) => {
|
||
const to = user.address === users[0].address ? users[1].address : users[0].address
|
||
await assertRevert(
|
||
staking.transfer(to, user.stakedBalance.sub(user.lockedBalance).add(bn(1)), { from: user.address })/*,
|
||
STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE
|
||
*/
|
||
)
|
||
await assertRevert(
|
||
staking.transferAndUnstake(to, user.stakedBalance.sub(user.lockedBalance).add(bn(1)), { from: user.address })/*,
|
||
STAKING_ERRORS.ERROR_NOT_ENOUGH_BALANCE
|
||
*/
|
||
)
|
||
})
|
||
)
|
||
}
|
||
|
||
// check that managers can’t slash more than locked balance
|
||
const checkOverSlashing = async ({ staking, users, managers }) => {
|
||
await Promise.all(
|
||
users.map(async (user) => {
|
||
const to = user.address === users[0].address ? users[1].address : users[0].address
|
||
for (let i = 0; i < managers.length - 1; i++) {
|
||
await assertRevert(
|
||
staking.slash(user.address, to, user.lockedBalance.add(bn(1)), { from: managers[i] })/*,
|
||
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
|
||
*/
|
||
)
|
||
await assertRevert(
|
||
staking.slashAndUnstake(user.address, to, user.lockedBalance.add(bn(1)), { from: managers[i] }),/*
|
||
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
|
||
*/
|
||
)
|
||
}
|
||
// last in the array is a contract
|
||
const lockManagerAddress = managers[managers.length - 1]
|
||
const lockManager = await LockManagerMock.at(lockManagerAddress)
|
||
await assertRevert(
|
||
lockManager.slash(staking.address, user.address, to, user.lockedBalance.add(bn(1))),/*
|
||
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
|
||
*/
|
||
)
|
||
await assertRevert(
|
||
lockManager.slashAndUnstake(staking.address, user.address, to, user.lockedBalance.add(bn(1))),/*
|
||
STAKING_ERRORS.ERROR_NOT_ENOUGH_LOCK
|
||
*/
|
||
)
|
||
})
|
||
)
|
||
}
|
||
|
||
const checkInvariants = async ({ staking, users, managers }) => {
|
||
await checkUserBalances({ staking, users })
|
||
await checkTotalStaked({ staking, users })
|
||
checkStakeAndLock({ staking, users })
|
||
await checkAllowanceAndLock({ staking, users, managers })
|
||
await checkOverUnstaking({ staking, users })
|
||
await checkOverUnlocking({ staking, users, managers })
|
||
await checkOverTransferring({ staking, users })
|
||
await checkOverSlashing({ staking, users, managers })
|
||
}
|
||
|
||
return {
|
||
approveAndStake,
|
||
approveStakeAndLock,
|
||
UserState,
|
||
approveAndStakeWithState,
|
||
approveStakeAndLockWithState,
|
||
unstakeWithState,
|
||
unlockWithState,
|
||
unlockFromManagerWithState,
|
||
transferWithState,
|
||
transferAndUnstakeWithState,
|
||
slashWithState,
|
||
slashAndUnstakeWithState,
|
||
slashFromContractWithState,
|
||
slashAndUnstakeFromContractWithState,
|
||
checkInvariants,
|
||
}
|
||
}
|