316 lines
12 KiB
JavaScript
316 lines
12 KiB
JavaScript
// Required for opengsn proper work
|
|
require('array-flat-polyfill')
|
|
|
|
const XDaiForeignBridgeMock = artifacts.require('XDaiForeignBridgeMock.sol')
|
|
const BridgeValidators = artifacts.require('BridgeValidators.sol')
|
|
const ERC677BridgeToken = artifacts.require('ERC677BridgeToken.sol')
|
|
|
|
const UniswapRouterMock = artifacts.require('UniswapRouterMock.sol')
|
|
const TokenPaymaster = artifacts.require('TokenPaymaster.sol')
|
|
|
|
// GSN
|
|
const { RelayProvider } = require('@opengsn/provider')
|
|
const { GsnTestEnvironment } = require('@opengsn/cli/dist/GsnTestEnvironment')
|
|
|
|
const { toBN, ERROR_MSG, ZERO_ADDRESS } = require('../setup')
|
|
const {
|
|
createMessage,
|
|
sign,
|
|
signatureToVRS,
|
|
ether,
|
|
packSignatures,
|
|
evalMetrics,
|
|
paymasterError,
|
|
deployProxy
|
|
} = require('../helpers/helpers')
|
|
const getCompoundContracts = require('../compound/contracts')
|
|
|
|
const requireBlockConfirmations = 8
|
|
const gasPrice = web3.utils.toWei('1', 'gwei')
|
|
const homeDailyLimit = ether('1001')
|
|
const homeMaxPerTx = ether('1000')
|
|
const maxPerTx = homeMaxPerTx
|
|
const minPerTx = ether('0.01')
|
|
const dailyLimit = homeDailyLimit
|
|
const ZERO = toBN(0)
|
|
const decimalShiftZero = 0
|
|
const HALF_ETHER = ether('0.5')
|
|
const FIVE_ETHER = ether('5')
|
|
const GSNGasLimit = 500000
|
|
|
|
function createEmptyAccount(relayer) {
|
|
const GSNUser = web3.eth.accounts.create()
|
|
relayer.addAccount(GSNUser.privateKey)
|
|
return GSNUser.address
|
|
}
|
|
|
|
contract('ForeignBridge_ERC20_to_Native_GSN', async accounts => {
|
|
const faucet = accounts[6] // account where all Compound-related DAIs where minted
|
|
|
|
let validatorContract
|
|
let authorities
|
|
let owner
|
|
let token
|
|
let foreignBridge
|
|
let otherSideBridge
|
|
|
|
let router
|
|
let paymaster
|
|
let RelayHubAddress
|
|
let ForwarderAddress
|
|
before(async () => {
|
|
// Deploy GSN
|
|
const env = await GsnTestEnvironment.startGsn(web3.currentProvider.host)
|
|
RelayHubAddress = env.contractsDeployment.relayHubAddress
|
|
ForwarderAddress = env.contractsDeployment.forwarderAddress
|
|
|
|
validatorContract = await deployProxy(BridgeValidators)
|
|
|
|
authorities = [accounts[1], accounts[2]]
|
|
owner = accounts[0]
|
|
await validatorContract.initialize(1, authorities, owner)
|
|
otherSideBridge = '0x1234567812345678123456781234567812345678'
|
|
|
|
const contracts = await getCompoundContracts()
|
|
token = contracts.dai
|
|
})
|
|
after(async () => {
|
|
await GsnTestEnvironment.stopGsn()
|
|
})
|
|
describe('#initialize', async () => {
|
|
it('should initialize paymaster', async () => {
|
|
router = await UniswapRouterMock.new()
|
|
foreignBridge = await deployProxy(XDaiForeignBridgeMock)
|
|
|
|
paymaster = await TokenPaymaster.new(
|
|
RelayHubAddress,
|
|
ForwarderAddress,
|
|
ZERO_ADDRESS,
|
|
router.address,
|
|
ZERO_ADDRESS,
|
|
{ from: owner }
|
|
)
|
|
|
|
await paymaster.setToken(token.address, { from: accounts[1] }).should.be.rejected
|
|
|
|
await paymaster.setToken(token.address, { from: owner }).should.be.fulfilled
|
|
await paymaster.setRouter(router.address, { from: owner }).should.be.fulfilled
|
|
await paymaster.setBridge(foreignBridge.address, { from: owner }).should.be.fulfilled
|
|
})
|
|
})
|
|
|
|
describe('#executeSignaturesGSN', async () => {
|
|
const BRIDGE_TOKENS = ether('300')
|
|
const REQUESTED_TOKENS = BRIDGE_TOKENS
|
|
|
|
let GSNRelayer
|
|
let GSNSigner
|
|
beforeEach(async () => {
|
|
XDaiForeignBridgeMock.web3.setProvider(web3.currentProvider)
|
|
foreignBridge = await deployProxy(XDaiForeignBridgeMock)
|
|
await foreignBridge.initialize(
|
|
validatorContract.address,
|
|
token.address,
|
|
requireBlockConfirmations,
|
|
gasPrice,
|
|
[dailyLimit, maxPerTx, minPerTx],
|
|
[homeDailyLimit, homeMaxPerTx],
|
|
owner,
|
|
decimalShiftZero,
|
|
otherSideBridge
|
|
)
|
|
|
|
router = await UniswapRouterMock.new()
|
|
paymaster = await TokenPaymaster.new(
|
|
RelayHubAddress,
|
|
ForwarderAddress,
|
|
token.address,
|
|
router.address,
|
|
foreignBridge.address
|
|
)
|
|
await paymaster.setPostGasUsage(250000)
|
|
|
|
await foreignBridge.setTrustedForwarder(ForwarderAddress)
|
|
await foreignBridge.setPayMaster(paymaster.address)
|
|
|
|
await token.transfer(foreignBridge.address, BRIDGE_TOKENS, { from: faucet })
|
|
|
|
// Give Router 1 ether
|
|
await web3.eth.sendTransaction({
|
|
from: accounts[0],
|
|
to: router.address,
|
|
value: ether('1')
|
|
})
|
|
// Give Paymaster 1 ether
|
|
await web3.eth.sendTransaction({
|
|
from: accounts[0],
|
|
to: paymaster.address,
|
|
value: ether('1')
|
|
})
|
|
|
|
// GSN configuration
|
|
GSNRelayer = await RelayProvider.newProvider({
|
|
provider: web3.currentProvider,
|
|
config: {
|
|
loggerConfigration: {
|
|
logLevel: 'debug'
|
|
},
|
|
paymasterAddress: paymaster.address
|
|
}
|
|
}).init()
|
|
GSNSigner = createEmptyAccount(GSNRelayer)
|
|
|
|
// From now on all calls will be relayed through GSN.
|
|
// If you want to omit GSN specify
|
|
// { useGSN: false } in transaction details
|
|
XDaiForeignBridgeMock.web3.setProvider(GSNRelayer)
|
|
})
|
|
it('should allow to executeSignaturesGSN', async () => {
|
|
const recipientAccount = GSNSigner
|
|
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
|
|
|
|
const MAX_COMMISSION = FIVE_ETHER
|
|
const [
|
|
userTokenBalanceBefore,
|
|
userEthBalanceBefore,
|
|
,
|
|
pmTokenBalanceBefore,
|
|
pmDepositBalanceBefore,
|
|
|
|
userTokenBalanceAfter,
|
|
,
|
|
bridgeTokenBalanceAfter,
|
|
pmTokenBalanceAfter,
|
|
pmDepositBalanceAfter
|
|
] = await evalMetrics(
|
|
async () => {
|
|
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
|
|
const signature = await sign(authorities[0], message)
|
|
const oneSignature = packSignatures([signatureToVRS(signature)])
|
|
|
|
await foreignBridge.executeSignaturesGSN(message, oneSignature, MAX_COMMISSION, {
|
|
from: recipientAccount,
|
|
gas: GSNGasLimit
|
|
})
|
|
},
|
|
() => token.balanceOf(recipientAccount),
|
|
() => web3.eth.getBalance(recipientAccount).then(toBN),
|
|
() => token.balanceOf(foreignBridge.address),
|
|
() => token.balanceOf(paymaster.address),
|
|
() => paymaster.getRelayHubDeposit()
|
|
)
|
|
|
|
userEthBalanceBefore.should.be.bignumber.equal(ZERO)
|
|
userTokenBalanceBefore.should.be.bignumber.equal(ZERO)
|
|
userTokenBalanceAfter.should.be.bignumber.gte(REQUESTED_TOKENS.sub(MAX_COMMISSION))
|
|
|
|
pmDepositBalanceAfter.should.be.bignumber.gte(pmDepositBalanceBefore)
|
|
|
|
pmTokenBalanceBefore.should.be.bignumber.equal(ZERO)
|
|
pmTokenBalanceAfter.should.be.bignumber.equal(ZERO)
|
|
|
|
bridgeTokenBalanceAfter.should.be.bignumber.equal(BRIDGE_TOKENS.sub(REQUESTED_TOKENS))
|
|
true.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
|
|
})
|
|
it('should reject insufficient fee', async () => {
|
|
const from = createEmptyAccount(GSNRelayer)
|
|
|
|
const recipientAccount = from
|
|
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
|
|
|
|
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
|
|
const signature = await sign(authorities[0], message)
|
|
const oneSignature = packSignatures([signatureToVRS(signature)])
|
|
|
|
const err = await foreignBridge.executeSignaturesGSN(message, oneSignature, ZERO, {
|
|
from: recipientAccount,
|
|
gas: GSNGasLimit
|
|
}).should.be.rejected
|
|
// NOTE: we don't use `err.reason` because after transaction failed
|
|
// truffle contract makes eth_call to get error reason and it makes this call
|
|
// not through GSN but directly with web3 provider.
|
|
// `err.reason` is always the same - 'invalid forwarder'
|
|
err.message.should.include(paymasterError("tokens fee can't cover expenses"))
|
|
|
|
false.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
|
|
})
|
|
it('should not allow second withdraw (replay attack) with same transactionHash but different recipient', async () => {
|
|
// tx 1
|
|
const from = createEmptyAccount(GSNRelayer)
|
|
const transactionHash = '0x35d3818e50234655f6aebb2a1cfbf30f59568d8a4ec72066fac5a25dbe7b8121'
|
|
const message = createMessage(from, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
|
|
const signature = await sign(authorities[0], message)
|
|
const oneSignature = packSignatures([signatureToVRS(signature)])
|
|
false.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
|
|
|
|
// Check if from not equal tokens reciever
|
|
await foreignBridge.executeSignaturesGSN(message, oneSignature, FIVE_ETHER, { from, gas: GSNGasLimit }).should.be
|
|
.fulfilled
|
|
|
|
// tx 2
|
|
await token.transfer(foreignBridge.address, BRIDGE_TOKENS, { from: faucet })
|
|
const from2 = createEmptyAccount(GSNRelayer)
|
|
const message2 = createMessage(from2, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
|
|
const signature2 = await sign(authorities[0], message2)
|
|
const oneSignature2 = packSignatures([signatureToVRS(signature2)])
|
|
true.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
|
|
|
|
const pmDepositBefore = await paymaster.getRelayHubDeposit()
|
|
await foreignBridge.executeSignaturesGSN(message2, oneSignature2, FIVE_ETHER, { from: from2, gas: GSNGasLimit })
|
|
.should.be.rejected
|
|
|
|
const pmDepositAfter = await paymaster.getRelayHubDeposit()
|
|
pmDepositAfter.should.be.bignumber.equal(pmDepositBefore)
|
|
})
|
|
it('should reject calls to other functions', async () => {
|
|
const recipientAccount = createEmptyAccount(GSNRelayer)
|
|
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
|
|
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
|
|
const signature = await sign(authorities[0], message)
|
|
const oneSignature = packSignatures([signatureToVRS(signature)])
|
|
|
|
const err = await foreignBridge.executeSignatures(message, oneSignature, {
|
|
from: recipientAccount,
|
|
gas: GSNGasLimit
|
|
}).should.be.rejected
|
|
err.message.should.include(paymasterError('not allowed target'))
|
|
})
|
|
it('should reject not GSN calls', async () => {
|
|
const recipientAccount = accounts[0]
|
|
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
|
|
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
|
|
const signature = await sign(authorities[0], message)
|
|
const oneSignature = packSignatures([signatureToVRS(signature)])
|
|
|
|
await foreignBridge
|
|
.executeSignaturesGSN(message, oneSignature, FIVE_ETHER, { from: recipientAccount, useGSN: false })
|
|
.should.be.rejectedWith(`${ERROR_MSG} invalid forwarder`)
|
|
})
|
|
})
|
|
|
|
describe('#claimTokens', async () => {
|
|
it('can send erc20', async () => {
|
|
router = await UniswapRouterMock.new()
|
|
paymaster = await TokenPaymaster.new(
|
|
RelayHubAddress,
|
|
ForwarderAddress,
|
|
ZERO_ADDRESS,
|
|
router.address,
|
|
ZERO_ADDRESS,
|
|
{ from: owner }
|
|
)
|
|
|
|
const token = await ERC677BridgeToken.new('Some ERC20', 'RSZT', 18)
|
|
await token.mint(accounts[1], HALF_ETHER).should.be.fulfilled
|
|
await token.transfer(paymaster.address, HALF_ETHER, { from: accounts[1] })
|
|
|
|
await paymaster.claimTokens(token.address, accounts[3], { from: accounts[3] }).should.be.rejectedWith(ERROR_MSG)
|
|
await paymaster.claimTokens(token.address, accounts[3], { from: owner })
|
|
const pmBalance = await token.balanceOf(paymaster.address)
|
|
const accBalance = await token.balanceOf(accounts[3])
|
|
pmBalance.should.be.bignumber.equal(ZERO)
|
|
accBalance.should.be.bignumber.equal(HALF_ETHER)
|
|
})
|
|
})
|
|
})
|